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