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