use crate::{ AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image, decode_static_image_from_decoder, px,};use anyhow::Result; use futures::Future;use gpui_util::ResultExt;use image::{ AnimationDecoder, ImageError, ImageFormat, Rgba, codecs::{gif::GifDecoder, webp::WebPDecoder},};use scheduler::Instant;use smallvec::SmallVec;use std::{ fs, io::{self, Cursor}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, str::FromStr, sync::Arc, time::Duration,};use thiserror::Error; use super::{Stateful, StatefulInteractiveElement}; /// The delay before showing the loading state.pub const LOADING_DELAY: Duration = Duration::from_millis(200); /// A type alias to the resource loader that the `img()` element uses.////// Note: that this is only for Resources, like URLs or file paths./// Custom loaders, or external images will not use this asset loaderpub type ImgResourceLoader = AssetLogger<ImageAssetLoader>; /// A source of image content.#[derive(Clone)]pub enum ImageSource { /// The image content will be loaded from some resource location Resource(Resource), /// Cached image data Render(Arc<RenderImage>), /// Cached image data Image(Arc<Image>), /// A custom loading function to use Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),} fn is_uri(uri: &str) -> bool { url::Url::from_str(uri).is_ok()} impl From<SharedUri> for ImageSource { fn from(value: SharedUri) -> Self { Self::Resource(Resource::Uri(value)) }} impl<'a> From<&'a str> for ImageSource { fn from(s: &'a str) -> Self { if is_uri(s) { Self::Resource(Resource::Uri(s.to_string().into())) } else { Self::Resource(Resource::Embedded(s.to_string().into())) } }} impl From<String> for ImageSource { fn from(s: String) -> Self { if is_uri(&s) { Self::Resource(Resource::Uri(s.into())) } else { Self::Resource(Resource::Embedded(s.into())) } }} impl From<SharedString> for ImageSource { fn from(s: SharedString) -> Self { s.as_ref().into() }} impl From<&Path> for ImageSource { fn from(value: &Path) -> Self { Self::Resource(value.to_path_buf().into()) }} impl From<Arc<Path>> for ImageSource { fn from(value: Arc<Path>) -> Self { Self::Resource(value.into()) }} impl From<PathBuf> for ImageSource { fn from(value: PathBuf) -> Self { Self::Resource(value.into()) }} impl From<Arc<RenderImage>> for ImageSource { fn from(value: Arc<RenderImage>) -> Self { Self::Render(value) }} impl From<Arc<Image>> for ImageSource { fn from(value: Arc<Image>) -> Self { Self::Image(value) }} impl<F> From<F> for ImageSourcewhere F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,{ fn from(value: F) -> Self { Self::Custom(Arc::new(value)) }} /// The style of an image element.pub struct ImageStyle { grayscale: bool, object_fit: ObjectFit, loading: Option<Box<dyn Fn() -> AnyElement>>, fallback: Option<Box<dyn Fn() -> AnyElement>>,} impl Default for ImageStyle { fn default() -> Self { Self { grayscale: false, object_fit: ObjectFit::Contain, loading: None, fallback: None, } }} /// Style an image element.pub trait StyledImage: Sized { /// Get a mutable [ImageStyle] from the element. fn image_style(&mut self) -> &mut ImageStyle; /// Set the image to be displayed in grayscale. fn grayscale(mut self, grayscale: bool) -> Self { self.image_style().grayscale = grayscale; self } /// Set the object fit for the image. fn object_fit(mut self, object_fit: ObjectFit) -> Self { self.image_style().object_fit = object_fit; self } /// Set a fallback function that will be invoked to render an error view should /// the image fail to load. fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self { self.image_style().fallback = Some(Box::new(fallback)); self } /// Set a fallback function that will be invoked to render a view while the image /// is still being loaded. fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self { self.image_style().loading = Some(Box::new(loading)); self }} impl StyledImage for Img { fn image_style(&mut self) -> &mut ImageStyle { &mut self.style }} impl StyledImage for Stateful<Img> { fn image_style(&mut self) -> &mut ImageStyle { &mut self.element.style }} /// An image element.pub struct Img { interactivity: Interactivity, source: ImageSource, style: ImageStyle, image_cache: Option<AnyImageCache>,} /// Create a new image element.#[track_caller]pub fn img(source: impl Into<ImageSource>) -> Img { Img { interactivity: Interactivity::new(), source: source.into(), style: ImageStyle::default(), image_cache: None, }} impl Img { /// A list of all format extensions currently supported by this img element pub fn extensions() -> &'static [&'static str] { // This is the list in [image::ImageFormat::from_extension] + `svg` &[ "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico", "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", ] } /// Sets the image cache for the current node. /// /// If the `image_cache` is not explicitly provided, the function will determine the image cache by: /// /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used. /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback. /// /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed, /// while ensuring a default behavior when no cache is explicitly specified. #[inline] pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self { Self { image_cache: Some(image_cache.clone().into()), ..self } }} impl Deref for Stateful<Img> { type Target = Img; fn deref(&self) -> &Self::Target { &self.element }} impl DerefMut for Stateful<Img> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.element }} /// The image state between framesstruct ImgState { frame_index: usize, last_frame_time: Option<Instant>, started_loading: Option<(Instant, Task<()>)>,} /// The image layout state between framespub struct ImgLayoutState { frame_index: usize, replacement: Option<AnyElement>,} impl Element for Img { type RequestLayoutState = ImgLayoutState; type PrepaintState = Option<Hitbox>; fn id(&self) -> Option<ElementId> { self.interactivity.element_id.clone() } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { self.interactivity.source_location() } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut layout_state = ImgLayoutState { frame_index: 0, replacement: None, }; window.with_optional_element_state(global_id, |state, window| { let mut state = state.map(|state| { state.unwrap_or(ImgState { frame_index: 0, last_frame_time: None, started_loading: None, }) }); let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0); let layout_id = self.interactivity.request_layout( global_id, inspector_id, window, cx, |mut style, window, cx| { let mut replacement_id = None; match self.source.use_data( self.image_cache .clone() .or_else(|| window.image_cache_stack.last().cloned()), window, cx, ) { Some(Ok(data)) => { let frame_count = data.frame_count(); let max_frame_index = frame_count.saturating_sub(1); if let Some(state) = &mut state { state.frame_index = state.frame_index.min(max_frame_index); if frame_count > 1 && !cx.reduce_motion() { if window.is_window_active() { let current_time = Instant::now(); if let Some(last_frame_time) = state.last_frame_time { let elapsed = current_time - last_frame_time; let frame_duration = Duration::from(data.delay(state.frame_index)); if elapsed >= frame_duration { state.frame_index = (state.frame_index + 1) % frame_count; state.last_frame_time = Some(current_time - (elapsed - frame_duration)); } } else { state.last_frame_time = Some(current_time); } } else { state.last_frame_time = None; } } else { state.last_frame_time = None; } state.started_loading = None; frame_index = state.frame_index; } let image_size = data.render_size(frame_index); style.aspect_ratio = Some(image_size.width / image_size.height); if let Length::Auto = style.size.width { style.size.width = match style.size.height { Length::Definite(DefiniteLength::Absolute(abs_length)) => { let height_px = abs_length.to_pixels(window.rem_size()); Length::Definite( px(image_size.width.0 * height_px.0 / image_size.height.0) .into(), ) } _ => Length::Definite(image_size.width.into()), }; } if let Length::Auto = style.size.height { style.size.height = match style.size.width { Length::Definite(DefiniteLength::Absolute(abs_length)) => { let width_px = abs_length.to_pixels(window.rem_size()); Length::Definite( px(image_size.height.0 * width_px.0 / image_size.width.0) .into(), ) } _ => Length::Definite(image_size.height.into()), }; } if global_id.is_some() && data.frame_count() > 1 && window.is_window_active() && !cx.reduce_motion() { window.request_animation_frame(); } } Some(_err) => { if let Some(fallback) = self.style.fallback.as_ref() { let mut element = fallback(); replacement_id = Some(element.request_layout(window, cx)); layout_state.replacement = Some(element); } if let Some(state) = &mut state { state.started_loading = None; } } None => { if let Some(state) = &mut state { if let Some((started_loading, _)) = state.started_loading { if started_loading.elapsed() > LOADING_DELAY && let Some(loading) = self.style.loading.as_ref() { let mut element = loading(); replacement_id = Some(element.request_layout(window, cx)); layout_state.replacement = Some(element); } } else { let current_view = window.current_view(); let task = window.spawn(cx, async move |cx| { cx.background_executor().timer(LOADING_DELAY).await; cx.update(move |_, cx| { cx.notify(current_view); }) .ok(); }); state.started_loading = Some((Instant::now(), task)); } } } } window.request_layout(style, replacement_id, cx) }, ); layout_state.frame_index = frame_index; ((layout_id, layout_state), state) }) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds<Pixels>, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { self.interactivity.prepaint( global_id, inspector_id, bounds, bounds.size, window, cx, |_, _, hitbox, window, cx| { if let Some(replacement) = &mut request_layout.replacement { replacement.prepaint(window, cx); } hitbox }, ) } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds<Pixels>, layout_state: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let source = self.source.clone(); self.interactivity.paint( global_id, inspector_id, bounds, hitbox.as_ref(), window, cx, |style, window, cx| { if let Some(Ok(data)) = source.use_data( self.image_cache .clone() .or_else(|| window.image_cache_stack.last().cloned()), window, cx, ) { if data.frame_count() == 0 { return; } let new_bounds = self .style .object_fit .get_bounds(bounds, data.size(layout_state.frame_index)); let corner_radii = style.corner_radii.to_pixels(window.rem_size()); window .paint_image( bounds, new_bounds, corner_radii, data, layout_state.frame_index, self.style.grayscale, )Lines 1–500 of 931. Load more
use crate::{ AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource, SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image, decode_static_image_from_decoder, px, }; use anyhow::Result; use futures::Future; use gpui_util::ResultExt; use image::{ AnimationDecoder, ImageError, ImageFormat, Rgba, codecs::{gif::GifDecoder, webp::WebPDecoder}, }; use scheduler::Instant; use smallvec::SmallVec; use std::{ fs, io::{self, Cursor}, ops::{Deref, DerefMut}, path::{Path, PathBuf}, str::FromStr, sync::Arc, time::Duration, }; use thiserror::Error; use super::{Stateful, StatefulInteractiveElement}; /// The delay before showing the loading state. pub const LOADING_DELAY: Duration = Duration::from_millis(200); /// A type alias to the resource loader that the `img()` element uses. /// /// Note: that this is only for Resources, like URLs or file paths. /// Custom loaders, or external images will not use this asset loader pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>; /// A source of image content. #[derive(Clone)] pub enum ImageSource { /// The image content will be loaded from some resource location Resource(Resource), /// Cached image data Render(Arc<RenderImage>), /// Cached image data Image(Arc<Image>), /// A custom loading function to use Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>), } fn is_uri(uri: &str) -> bool { url::Url::from_str(uri).is_ok() } impl From<SharedUri> for ImageSource { fn from(value: SharedUri) -> Self { Self::Resource(Resource::Uri(value)) } } impl<'a> From<&'a str> for ImageSource { fn from(s: &'a str) -> Self { if is_uri(s) { Self::Resource(Resource::Uri(s.to_string().into())) } else { Self::Resource(Resource::Embedded(s.to_string().into())) } } } impl From<String> for ImageSource { fn from(s: String) -> Self { if is_uri(&s) { Self::Resource(Resource::Uri(s.into())) } else { Self::Resource(Resource::Embedded(s.into())) } } } impl From<SharedString> for ImageSource { fn from(s: SharedString) -> Self { s.as_ref().into() } } impl From<&Path> for ImageSource { fn from(value: &Path) -> Self { Self::Resource(value.to_path_buf().into()) } } impl From<Arc<Path>> for ImageSource { fn from(value: Arc<Path>) -> Self { Self::Resource(value.into()) } } impl From<PathBuf> for ImageSource { fn from(value: PathBuf) -> Self { Self::Resource(value.into()) } } impl From<Arc<RenderImage>> for ImageSource { fn from(value: Arc<RenderImage>) -> Self { Self::Render(value) } } impl From<Arc<Image>> for ImageSource { fn from(value: Arc<Image>) -> Self { Self::Image(value) } } impl<F> From<F> for ImageSource where F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static, { fn from(value: F) -> Self { Self::Custom(Arc::new(value)) } } /// The style of an image element. pub struct ImageStyle { grayscale: bool, object_fit: ObjectFit, loading: Option<Box<dyn Fn() -> AnyElement>>, fallback: Option<Box<dyn Fn() -> AnyElement>>, } impl Default for ImageStyle { fn default() -> Self { Self { grayscale: false, object_fit: ObjectFit::Contain, loading: None, fallback: None, } } } /// Style an image element. pub trait StyledImage: Sized { /// Get a mutable [ImageStyle] from the element. fn image_style(&mut self) -> &mut ImageStyle; /// Set the image to be displayed in grayscale. fn grayscale(mut self, grayscale: bool) -> Self { self.image_style().grayscale = grayscale; self } /// Set the object fit for the image. fn object_fit(mut self, object_fit: ObjectFit) -> Self { self.image_style().object_fit = object_fit; self } /// Set a fallback function that will be invoked to render an error view should /// the image fail to load. fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self { self.image_style().fallback = Some(Box::new(fallback)); self } /// Set a fallback function that will be invoked to render a view while the image /// is still being loaded. fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self { self.image_style().loading = Some(Box::new(loading)); self } } impl StyledImage for Img { fn image_style(&mut self) -> &mut ImageStyle { &mut self.style } } impl StyledImage for Stateful<Img> { fn image_style(&mut self) -> &mut ImageStyle { &mut self.element.style } } /// An image element. pub struct Img { interactivity: Interactivity, source: ImageSource, style: ImageStyle, image_cache: Option<AnyImageCache>, } /// Create a new image element. #[track_caller] pub fn img(source: impl Into<ImageSource>) -> Img { Img { interactivity: Interactivity::new(), source: source.into(), style: ImageStyle::default(), image_cache: None, } } impl Img { /// A list of all format extensions currently supported by this img element pub fn extensions() -> &'static [&'static str] { // This is the list in [image::ImageFormat::from_extension] + `svg` &[ "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico", "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg", ] } /// Sets the image cache for the current node. /// /// If the `image_cache` is not explicitly provided, the function will determine the image cache by: /// /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used. /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback. /// /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed, /// while ensuring a default behavior when no cache is explicitly specified. #[inline] pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self { Self { image_cache: Some(image_cache.clone().into()), ..self } } } impl Deref for Stateful<Img> { type Target = Img; fn deref(&self) -> &Self::Target { &self.element } } impl DerefMut for Stateful<Img> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.element } } /// The image state between frames struct ImgState { frame_index: usize, last_frame_time: Option<Instant>, started_loading: Option<(Instant, Task<()>)>, } /// The image layout state between frames pub struct ImgLayoutState { frame_index: usize, replacement: Option<AnyElement>, } impl Element for Img { type RequestLayoutState = ImgLayoutState; type PrepaintState = Option<Hitbox>; fn id(&self) -> Option<ElementId> { self.interactivity.element_id.clone() } fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { self.interactivity.source_location() } fn request_layout( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut layout_state = ImgLayoutState { frame_index: 0, replacement: None, }; window.with_optional_element_state(global_id, |state, window| { let mut state = state.map(|state| { state.unwrap_or(ImgState { frame_index: 0, last_frame_time: None, started_loading: None, }) }); let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0); let layout_id = self.interactivity.request_layout( global_id, inspector_id, window, cx, |mut style, window, cx| { let mut replacement_id = None; match self.source.use_data( self.image_cache .clone() .or_else(|| window.image_cache_stack.last().cloned()), window, cx, ) { Some(Ok(data)) => { let frame_count = data.frame_count(); let max_frame_index = frame_count.saturating_sub(1); if let Some(state) = &mut state { state.frame_index = state.frame_index.min(max_frame_index); if frame_count > 1 && !cx.reduce_motion() { if window.is_window_active() { let current_time = Instant::now(); if let Some(last_frame_time) = state.last_frame_time { let elapsed = current_time - last_frame_time; let frame_duration = Duration::from(data.delay(state.frame_index)); if elapsed >= frame_duration { state.frame_index = (state.frame_index + 1) % frame_count; state.last_frame_time = Some(current_time - (elapsed - frame_duration)); } } else { state.last_frame_time = Some(current_time); } } else { state.last_frame_time = None; } } else { state.last_frame_time = None; } state.started_loading = None; frame_index = state.frame_index; } let image_size = data.render_size(frame_index); style.aspect_ratio = Some(image_size.width / image_size.height); if let Length::Auto = style.size.width { style.size.width = match style.size.height { Length::Definite(DefiniteLength::Absolute(abs_length)) => { let height_px = abs_length.to_pixels(window.rem_size()); Length::Definite( px(image_size.width.0 * height_px.0 / image_size.height.0) .into(), ) } _ => Length::Definite(image_size.width.into()), }; } if let Length::Auto = style.size.height { style.size.height = match style.size.width { Length::Definite(DefiniteLength::Absolute(abs_length)) => { let width_px = abs_length.to_pixels(window.rem_size()); Length::Definite( px(image_size.height.0 * width_px.0 / image_size.width.0) .into(), ) } _ => Length::Definite(image_size.height.into()), }; } if global_id.is_some() && data.frame_count() > 1 && window.is_window_active() && !cx.reduce_motion() { window.request_animation_frame(); } } Some(_err) => { if let Some(fallback) = self.style.fallback.as_ref() { let mut element = fallback(); replacement_id = Some(element.request_layout(window, cx)); layout_state.replacement = Some(element); } if let Some(state) = &mut state { state.started_loading = None; } } None => { if let Some(state) = &mut state { if let Some((started_loading, _)) = state.started_loading { if started_loading.elapsed() > LOADING_DELAY && let Some(loading) = self.style.loading.as_ref() { let mut element = loading(); replacement_id = Some(element.request_layout(window, cx)); layout_state.replacement = Some(element); } } else { let current_view = window.current_view(); let task = window.spawn(cx, async move |cx| { cx.background_executor().timer(LOADING_DELAY).await; cx.update(move |_, cx| { cx.notify(current_view); }) .ok(); }); state.started_loading = Some((Instant::now(), task)); } } } } window.request_layout(style, replacement_id, cx) }, ); layout_state.frame_index = frame_index; ((layout_id, layout_state), state) }) } fn prepaint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds<Pixels>, request_layout: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { self.interactivity.prepaint( global_id, inspector_id, bounds, bounds.size, window, cx, |_, _, hitbox, window, cx| { if let Some(replacement) = &mut request_layout.replacement { replacement.prepaint(window, cx); } hitbox }, ) } fn paint( &mut self, global_id: Option<&GlobalElementId>, inspector_id: Option<&InspectorElementId>, bounds: Bounds<Pixels>, layout_state: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { let source = self.source.clone(); self.interactivity.paint( global_id, inspector_id, bounds, hitbox.as_ref(), window, cx, |style, window, cx| { if let Some(Ok(data)) = source.use_data( self.image_cache .clone() .or_else(|| window.image_cache_stack.last().cloned()), window, cx, ) { if data.frame_count() == 0 { return; } let new_bounds = self .style .object_fit .get_bounds(bounds, data.size(layout_state.frame_index)); let corner_radii = style.corner_radii.to_pixels(window.rem_size()); window .paint_image( bounds, new_bounds, corner_radii, data, layout_state.frame_index, self.style.grayscale, )