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