1use crate::{
2 px, swap_rgba_pa_to_bgra, AbsoluteLength, AnyElement, AppContext, Asset, AssetLogger, Bounds,
3 DefiniteLength, Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement,
4 Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
5 SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, WindowContext,
6};
7use anyhow::{anyhow, Result};
8
9use futures::{AsyncReadExt, Future};
10use image::{
11 codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
12};
13use smallvec::SmallVec;
14use std::{
15 fs,
16 io::{self, Cursor},
17 ops::{Deref, DerefMut},
18 path::{Path, PathBuf},
19 str::FromStr,
20 sync::Arc,
21 time::{Duration, Instant},
22};
23use thiserror::Error;
24use util::ResultExt;
25
26use super::{FocusableElement, Stateful, StatefulInteractiveElement};
27
28/// The delay before showing the loading state.
29pub const LOADING_DELAY: Duration = Duration::from_millis(200);
30
31/// A type alias to the resource loader that the `img()` element uses.
32///
33/// Note: that this is only for Resources, like URLs or file paths.
34/// Custom loaders, or external images will not use this asset loader
35pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
36
37/// A source of image content.
38#[derive(Clone)]
39pub enum ImageSource {
40 /// The image content will be loaded from some resource location
41 Resource(Resource),
42 /// Cached image data
43 Render(Arc<RenderImage>),
44 /// Cached image data
45 Image(Arc<Image>),
46 /// A custom loading function to use
47 Custom(Arc<dyn Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
48}
49
50fn is_uri(uri: &str) -> bool {
51 http_client::Uri::from_str(uri).is_ok()
52}
53
54impl From<SharedUri> for ImageSource {
55 fn from(value: SharedUri) -> Self {
56 Self::Resource(Resource::Uri(value))
57 }
58}
59
60impl<'a> From<&'a str> for ImageSource {
61 fn from(s: &'a str) -> Self {
62 if is_uri(s) {
63 Self::Resource(Resource::Uri(s.to_string().into()))
64 } else {
65 Self::Resource(Resource::Embedded(s.to_string().into()))
66 }
67 }
68}
69
70impl From<String> for ImageSource {
71 fn from(s: String) -> Self {
72 if is_uri(&s) {
73 Self::Resource(Resource::Uri(s.into()))
74 } else {
75 Self::Resource(Resource::Embedded(s.into()))
76 }
77 }
78}
79
80impl From<SharedString> for ImageSource {
81 fn from(s: SharedString) -> Self {
82 s.as_ref().into()
83 }
84}
85
86impl From<&Path> for ImageSource {
87 fn from(value: &Path) -> Self {
88 Self::Resource(value.to_path_buf().into())
89 }
90}
91
92impl From<Arc<Path>> for ImageSource {
93 fn from(value: Arc<Path>) -> Self {
94 Self::Resource(value.into())
95 }
96}
97
98impl From<PathBuf> for ImageSource {
99 fn from(value: PathBuf) -> Self {
100 Self::Resource(value.into())
101 }
102}
103
104impl From<Arc<RenderImage>> for ImageSource {
105 fn from(value: Arc<RenderImage>) -> Self {
106 Self::Render(value)
107 }
108}
109
110impl From<Arc<Image>> for ImageSource {
111 fn from(value: Arc<Image>) -> Self {
112 Self::Image(value)
113 }
114}
115
116impl<F: Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static>
117 From<F> for ImageSource
118{
119 fn from(value: F) -> Self {
120 Self::Custom(Arc::new(value))
121 }
122}
123
124/// The style of an image element.
125pub struct ImageStyle {
126 grayscale: bool,
127 object_fit: ObjectFit,
128 loading: Option<Box<dyn Fn() -> AnyElement>>,
129 fallback: Option<Box<dyn Fn() -> AnyElement>>,
130}
131
132impl Default for ImageStyle {
133 fn default() -> Self {
134 Self {
135 grayscale: false,
136 object_fit: ObjectFit::Contain,
137 loading: None,
138 fallback: None,
139 }
140 }
141}
142
143/// Style an image element.
144pub trait StyledImage: Sized {
145 /// Get a mutable [ImageStyle] from the element.
146 fn image_style(&mut self) -> &mut ImageStyle;
147
148 /// Set the image to be displayed in grayscale.
149 fn grayscale(mut self, grayscale: bool) -> Self {
150 self.image_style().grayscale = grayscale;
151 self
152 }
153
154 /// Set the object fit for the image.
155 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
156 self.image_style().object_fit = object_fit;
157 self
158 }
159
160 /// Set the object fit for the image.
161 fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
162 self.image_style().fallback = Some(Box::new(fallback));
163 self
164 }
165
166 /// Set the object fit for the image.
167 fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
168 self.image_style().loading = Some(Box::new(loading));
169 self
170 }
171}
172
173impl StyledImage for Img {
174 fn image_style(&mut self) -> &mut ImageStyle {
175 &mut self.style
176 }
177}
178
179impl StyledImage for Stateful<Img> {
180 fn image_style(&mut self) -> &mut ImageStyle {
181 &mut self.element.style
182 }
183}
184
185/// An image element.
186pub struct Img {
187 interactivity: Interactivity,
188 source: ImageSource,
189 style: ImageStyle,
190}
191
192/// Create a new image element.
193pub fn img(source: impl Into<ImageSource>) -> Img {
194 Img {
195 interactivity: Interactivity::default(),
196 source: source.into(),
197 style: ImageStyle::default(),
198 }
199}
200
201impl Img {
202 /// A list of all format extensions currently supported by this img element
203 pub fn extensions() -> &'static [&'static str] {
204 // This is the list in [image::ImageFormat::from_extension] + `svg`
205 &[
206 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
207 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
208 ]
209 }
210}
211
212impl Deref for Stateful<Img> {
213 type Target = Img;
214
215 fn deref(&self) -> &Self::Target {
216 &self.element
217 }
218}
219
220impl DerefMut for Stateful<Img> {
221 fn deref_mut(&mut self) -> &mut Self::Target {
222 &mut self.element
223 }
224}
225
226/// The image state between frames
227struct ImgState {
228 frame_index: usize,
229 last_frame_time: Option<Instant>,
230 started_loading: Option<(Instant, Task<()>)>,
231}
232
233/// The image layout state between frames
234pub struct ImgLayoutState {
235 frame_index: usize,
236 replacement: Option<AnyElement>,
237}
238
239impl Element for Img {
240 type RequestLayoutState = ImgLayoutState;
241 type PrepaintState = Option<Hitbox>;
242
243 fn id(&self) -> Option<ElementId> {
244 self.interactivity.element_id.clone()
245 }
246
247 fn request_layout(
248 &mut self,
249 global_id: Option<&GlobalElementId>,
250 cx: &mut WindowContext,
251 ) -> (LayoutId, Self::RequestLayoutState) {
252 let mut layout_state = ImgLayoutState {
253 frame_index: 0,
254 replacement: None,
255 };
256
257 cx.with_optional_element_state(global_id, |state, cx| {
258 let mut state = state.map(|state| {
259 state.unwrap_or(ImgState {
260 frame_index: 0,
261 last_frame_time: None,
262 started_loading: None,
263 })
264 });
265
266 let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
267
268 let layout_id = self
269 .interactivity
270 .request_layout(global_id, cx, |mut style, cx| {
271 let mut replacement_id = None;
272
273 match self.source.use_data(cx) {
274 Some(Ok(data)) => {
275 if let Some(state) = &mut state {
276 let frame_count = data.frame_count();
277 if frame_count > 1 {
278 let current_time = Instant::now();
279 if let Some(last_frame_time) = state.last_frame_time {
280 let elapsed = current_time - last_frame_time;
281 let frame_duration =
282 Duration::from(data.delay(state.frame_index));
283
284 if elapsed >= frame_duration {
285 state.frame_index =
286 (state.frame_index + 1) % frame_count;
287 state.last_frame_time =
288 Some(current_time - (elapsed - frame_duration));
289 }
290 } else {
291 state.last_frame_time = Some(current_time);
292 }
293 }
294 state.started_loading = None;
295 }
296
297 let image_size = data.size(frame_index);
298
299 if let Length::Auto = style.size.width {
300 style.size.width = match style.size.height {
301 Length::Definite(DefiniteLength::Absolute(
302 AbsoluteLength::Pixels(height),
303 )) => Length::Definite(
304 px(image_size.width.0 as f32 * height.0
305 / image_size.height.0 as f32)
306 .into(),
307 ),
308 _ => Length::Definite(px(image_size.width.0 as f32).into()),
309 };
310 }
311
312 if let Length::Auto = style.size.height {
313 style.size.height = match style.size.width {
314 Length::Definite(DefiniteLength::Absolute(
315 AbsoluteLength::Pixels(width),
316 )) => Length::Definite(
317 px(image_size.height.0 as f32 * width.0
318 / image_size.width.0 as f32)
319 .into(),
320 ),
321 _ => Length::Definite(px(image_size.height.0 as f32).into()),
322 };
323 }
324
325 if global_id.is_some() && data.frame_count() > 1 {
326 cx.request_animation_frame();
327 }
328 }
329 Some(_err) => {
330 if let Some(fallback) = self.style.fallback.as_ref() {
331 let mut element = fallback();
332 replacement_id = Some(element.request_layout(cx));
333 layout_state.replacement = Some(element);
334 }
335 if let Some(state) = &mut state {
336 state.started_loading = None;
337 }
338 }
339 None => {
340 if let Some(state) = &mut state {
341 if let Some((started_loading, _)) = state.started_loading {
342 if started_loading.elapsed() > LOADING_DELAY {
343 if let Some(loading) = self.style.loading.as_ref() {
344 let mut element = loading();
345 replacement_id = Some(element.request_layout(cx));
346 layout_state.replacement = Some(element);
347 }
348 }
349 } else {
350 let parent_view_id = cx.parent_view_id();
351 let task = cx.spawn(|mut cx| async move {
352 cx.background_executor().timer(LOADING_DELAY).await;
353 cx.update(|cx| {
354 cx.notify(parent_view_id);
355 })
356 .ok();
357 });
358 state.started_loading = Some((Instant::now(), task));
359 }
360 }
361 }
362 }
363
364 cx.request_layout(style, replacement_id)
365 });
366
367 layout_state.frame_index = frame_index;
368
369 ((layout_id, layout_state), state)
370 })
371 }
372
373 fn prepaint(
374 &mut self,
375 global_id: Option<&GlobalElementId>,
376 bounds: Bounds<Pixels>,
377 request_layout: &mut Self::RequestLayoutState,
378 cx: &mut WindowContext,
379 ) -> Self::PrepaintState {
380 self.interactivity
381 .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
382 if let Some(replacement) = &mut request_layout.replacement {
383 replacement.prepaint(cx);
384 }
385
386 hitbox
387 })
388 }
389
390 fn paint(
391 &mut self,
392 global_id: Option<&GlobalElementId>,
393 bounds: Bounds<Pixels>,
394 layout_state: &mut Self::RequestLayoutState,
395 hitbox: &mut Self::PrepaintState,
396 cx: &mut WindowContext,
397 ) {
398 let source = self.source.clone();
399 self.interactivity
400 .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
401 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
402
403 if let Some(Ok(data)) = source.use_data(cx) {
404 let new_bounds = self
405 .style
406 .object_fit
407 .get_bounds(bounds, data.size(layout_state.frame_index));
408 cx.paint_image(
409 new_bounds,
410 corner_radii,
411 data.clone(),
412 layout_state.frame_index,
413 self.style.grayscale,
414 )
415 .log_err();
416 } else if let Some(replacement) = &mut layout_state.replacement {
417 replacement.paint(cx);
418 }
419 })
420 }
421}
422
423impl Styled for Img {
424 fn style(&mut self) -> &mut StyleRefinement {
425 &mut self.interactivity.base_style
426 }
427}
428
429impl InteractiveElement for Img {
430 fn interactivity(&mut self) -> &mut Interactivity {
431 &mut self.interactivity
432 }
433}
434
435impl IntoElement for Img {
436 type Element = Self;
437
438 fn into_element(self) -> Self::Element {
439 self
440 }
441}
442
443impl FocusableElement for Img {}
444
445impl StatefulInteractiveElement for Img {}
446
447impl ImageSource {
448 pub(crate) fn use_data(
449 &self,
450 cx: &mut WindowContext,
451 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
452 match self {
453 ImageSource::Resource(resource) => cx.use_asset::<ImgResourceLoader>(&resource),
454 ImageSource::Custom(loading_fn) => loading_fn(cx),
455 ImageSource::Render(data) => Some(Ok(data.to_owned())),
456 ImageSource::Image(data) => cx.use_asset::<AssetLogger<ImageDecoder>>(data),
457 }
458 }
459}
460
461#[derive(Clone)]
462enum ImageDecoder {}
463
464impl Asset for ImageDecoder {
465 type Source = Arc<Image>;
466 type Output = Result<Arc<RenderImage>, ImageCacheError>;
467
468 fn load(
469 source: Self::Source,
470 cx: &mut AppContext,
471 ) -> impl Future<Output = Self::Output> + Send + 'static {
472 let renderer = cx.svg_renderer();
473 async move { source.to_image_data(renderer).map_err(Into::into) }
474 }
475}
476
477/// An image loader for the GPUI asset system
478#[derive(Clone)]
479pub enum ImageAssetLoader {}
480
481impl Asset for ImageAssetLoader {
482 type Source = Resource;
483 type Output = Result<Arc<RenderImage>, ImageCacheError>;
484
485 fn load(
486 source: Self::Source,
487 cx: &mut AppContext,
488 ) -> impl Future<Output = Self::Output> + Send + 'static {
489 let client = cx.http_client();
490 // TODO: Can we make SVGs always rescale?
491 // let scale_factor = cx.scale_factor();
492 let svg_renderer = cx.svg_renderer();
493 let asset_source = cx.asset_source().clone();
494 async move {
495 let bytes = match source.clone() {
496 Resource::Path(uri) => fs::read(uri.as_ref())?,
497 Resource::Uri(uri) => {
498 let mut response = client
499 .get(uri.as_ref(), ().into(), true)
500 .await
501 .map_err(|e| anyhow!(e))?;
502 let mut body = Vec::new();
503 response.body_mut().read_to_end(&mut body).await?;
504 if !response.status().is_success() {
505 let mut body = String::from_utf8_lossy(&body).into_owned();
506 let first_line = body.lines().next().unwrap_or("").trim_end();
507 body.truncate(first_line.len());
508 return Err(ImageCacheError::BadStatus {
509 uri,
510 status: response.status(),
511 body,
512 });
513 }
514 body
515 }
516 Resource::Embedded(path) => {
517 let data = asset_source.load(&path).ok().flatten();
518 if let Some(data) = data {
519 data.to_vec()
520 } else {
521 return Err(ImageCacheError::Asset(
522 format!("Embedded resource not found: {}", path).into(),
523 ));
524 }
525 }
526 };
527
528 let data = if let Ok(format) = image::guess_format(&bytes) {
529 let data = match format {
530 ImageFormat::Gif => {
531 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
532 let mut frames = SmallVec::new();
533
534 for frame in decoder.into_frames() {
535 let mut frame = frame?;
536 // Convert from RGBA to BGRA.
537 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
538 pixel.swap(0, 2);
539 }
540 frames.push(frame);
541 }
542
543 frames
544 }
545 _ => {
546 let mut data =
547 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
548
549 // Convert from RGBA to BGRA.
550 for pixel in data.chunks_exact_mut(4) {
551 pixel.swap(0, 2);
552 }
553
554 SmallVec::from_elem(Frame::new(data), 1)
555 }
556 };
557
558 RenderImage::new(data)
559 } else {
560 let pixmap =
561 // TODO: Can we make svgs always rescale?
562 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
563
564 let mut buffer =
565 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
566
567 for pixel in buffer.chunks_exact_mut(4) {
568 swap_rgba_pa_to_bgra(pixel);
569 }
570
571 RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
572 };
573
574 Ok(Arc::new(data))
575 }
576 }
577}
578
579/// An error that can occur when interacting with the image cache.
580#[derive(Debug, Error, Clone)]
581pub enum ImageCacheError {
582 /// Some other kind of error occurred
583 #[error("error: {0}")]
584 Other(#[from] Arc<anyhow::Error>),
585 /// An error that occurred while reading the image from disk.
586 #[error("IO error: {0}")]
587 Io(Arc<std::io::Error>),
588 /// An error that occurred while processing an image.
589 #[error("unexpected http status for {uri}: {status}, body: {body}")]
590 BadStatus {
591 /// The URI of the image.
592 uri: SharedUri,
593 /// The HTTP status code.
594 status: http_client::StatusCode,
595 /// The HTTP response body.
596 body: String,
597 },
598 /// An error that occurred while processing an asset.
599 #[error("asset error: {0}")]
600 Asset(SharedString),
601 /// An error that occurred while processing an image.
602 #[error("image error: {0}")]
603 Image(Arc<ImageError>),
604 /// An error that occurred while processing an SVG.
605 #[error("svg error: {0}")]
606 Usvg(Arc<usvg::Error>),
607}
608
609impl From<anyhow::Error> for ImageCacheError {
610 fn from(value: anyhow::Error) -> Self {
611 Self::Other(Arc::new(value))
612 }
613}
614
615impl From<io::Error> for ImageCacheError {
616 fn from(value: io::Error) -> Self {
617 Self::Io(Arc::new(value))
618 }
619}
620
621impl From<usvg::Error> for ImageCacheError {
622 fn from(value: usvg::Error) -> Self {
623 Self::Usvg(Arc::new(value))
624 }
625}
626
627impl From<image::ImageError> for ImageCacheError {
628 fn from(value: image::ImageError) -> Self {
629 Self::Image(Arc::new(value))
630 }
631}