diff --git a/crates/story/src/fixtures/color-wheel.svg b/crates/story/src/fixtures/color-wheel.svg new file mode 100644 index 00000000..8d632d1d --- /dev/null +++ b/crates/story/src/fixtures/color-wheel.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + diff --git a/crates/story/src/image_story.rs b/crates/story/src/image_story.rs index f1af6a87..02351690 100644 --- a/crates/story/src/image_story.rs +++ b/crates/story/src/image_story.rs @@ -1,14 +1,18 @@ use gpui::{ - px, App, AppContext, ElementId, Entity, FocusHandle, Focusable, ParentElement as _, Render, - Styled, Window, + img, App, AppContext, ClickEvent, ElementId, Entity, FocusHandle, Focusable, + ParentElement as _, Render, Styled, Window, }; -use gpui_component::{dock::PanelControl, v_flex, SvgImg}; +use gpui_component::{button::Button, dock::PanelControl, v_flex, SvgImg}; use crate::section; -const GOOGLE_LOGO: &str = include_str!("./fixtures/google.svg"); +const SVG_ITEMS: &[&str] = &[ + include_str!("./fixtures/google.svg"), + include_str!("./fixtures/color-wheel.svg"), +]; pub struct ImageStory { + svg_index: usize, focus_handle: gpui::FocusHandle, } @@ -33,6 +37,7 @@ impl super::Story for ImageStory { impl ImageStory { pub fn new(_: &mut Window, cx: &mut App) -> Self { Self { + svg_index: 0, focus_handle: cx.focus_handle(), } } @@ -40,6 +45,10 @@ impl ImageStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } + + fn svg_img(&self, id: impl Into) -> SvgImg { + SvgImg::new(id, SVG_ITEMS[self.svg_index].as_bytes()) + } } impl Focusable for ImageStory { @@ -52,19 +61,29 @@ impl Render for ImageStory { fn render( &mut self, _window: &mut gpui::Window, - _: &mut gpui::Context, + cx: &mut gpui::Context, ) -> impl gpui::IntoElement { - v_flex().gap_4().size_full().child( - section("SVG Image") - .child(svg_img("logo1").size(px(100.)).flex_grow()) - .child(svg_img("logo2").size(px(100.)).flex_grow()) - .child(svg_img("logo3").size_80().flex_grow()) - .child(svg_img("logo4").size_12().flex_grow()) - .child(svg_img("logo5").size(px(100.))), - ) + v_flex() + .gap_4() + .size_full() + .child( + Button::new("switch") + .label("Switch SVG") + .on_click(cx.listener(|this, _: &ClickEvent, _, cx| { + this.svg_index += 1; + if this.svg_index >= SVG_ITEMS.len() { + this.svg_index = 0; + } + cx.notify(); + })), + ) + .child(section("SVG 160px").child(self.svg_img("logo1").size_40().flex_grow())) + .child(section("SVG 80px").child(self.svg_img("logo3").size_20().flex_grow())) + .child(section("SVG 48px").child(self.svg_img("logo4").size_12().flex_grow())) + .child( + section("SVG from img 40px").child( + img("https://pub.lbkrs.com/files/202503/vEnnmgUM6bo362ya/sdk.svg").h_24(), + ), + ) } } - -fn svg_img(id: impl Into) -> SvgImg { - SvgImg::new(id).source(GOOGLE_LOGO.as_bytes(), px(300.), px(300.)) -} diff --git a/crates/ui/src/svg_img.rs b/crates/ui/src/svg_img.rs index 4d96e565..266dc890 100644 --- a/crates/ui/src/svg_img.rs +++ b/crates/ui/src/svg_img.rs @@ -1,13 +1,14 @@ use std::{ + collections::HashSet, hash::Hash, ops::Deref, - sync::{Arc, LazyLock}, + sync::{Arc, LazyLock, Mutex}, }; use gpui::{ - hash, px, size, App, Asset, Bounds, Element, ElementId, GlobalElementId, Hitbox, - ImageCacheError, InteractiveElement, Interactivity, IntoElement, IsZero, Pixels, RenderImage, - SharedString, Size, StyleRefinement, Styled, Window, + hash, px, App, Asset, Bounds, Element, ElementId, GlobalElementId, Hitbox, ImageCacheError, + InteractiveElement, Interactivity, IntoElement, Pixels, RenderImage, SharedString, Size, + StyleRefinement, Styled, Window, }; use image::Frame; use smallvec::SmallVec; @@ -16,8 +17,6 @@ use image::ImageBuffer; const SCALE: f32 = 2.; -struct SvgImgState(Option<(u64, Arc)>); - static OPTIONS: LazyLock = LazyLock::new(|| { let mut options = usvg::Options::default(); options.fontdb_mut().load_system_fonts(); @@ -62,17 +61,15 @@ impl Clone for SvgImg { id: self.id.clone(), interactivity: Interactivity::default(), source: self.source.clone(), - size: self.size, } } } -pub enum Image {} +enum SvgImageLoader {} #[derive(Debug, Clone)] pub struct ImageSource { source: SvgSource, - size: Size, } impl Hash for ImageSource { @@ -82,9 +79,43 @@ impl Hash for ImageSource { } } -impl Asset for Image { +#[derive(Debug, Clone)] +pub struct ImageData { + image: Arc, + size: Size, + source: ImageSource, + uses: Arc>>, +} + +impl ImageData { + fn new(source: ImageSource, image: Arc, size: Size) -> Self { + Self { + source, + image, + size, + uses: Arc::new(Mutex::new(HashSet::new())), + } + } + + fn use_image(&self, global_id: &GlobalElementId) -> Arc { + self.uses.lock().unwrap().insert(hash(global_id)); + self.image.clone() + } + + fn remove_use(&mut self, global_id: &GlobalElementId, window: &mut Window, cx: &mut App) { + let mut uses = self.uses.lock().unwrap(); + uses.remove(&hash(global_id)); + if uses.len() == 0 { + // println!("dropping image: {:?}", self.image); + cx.remove_asset::(&self.source); + _ = window.drop_image(self.image.clone()); + } + } +} + +impl Asset for SvgImageLoader { type Source = ImageSource; - type Output = Result, ImageCacheError>; + type Output = Result; fn load( source: Self::Source, @@ -93,16 +124,7 @@ impl Asset for Image { let asset_source = cx.asset_source().clone(); async move { - let size = source.size; - if size.width.is_zero() || size.height.is_zero() { - return Err(usvg::Error::InvalidSize.into()); - } - let size = Size { - width: (size.width * SCALE).ceil(), - height: (size.height * SCALE).ceil(), - }; - - let bytes = match source.source { + let bytes = match source.source.clone() { SvgSource::Data(data) => data, SvgSource::Path(path) => { if let Ok(Some(data)) = asset_source.load(&path) { @@ -119,10 +141,15 @@ impl Asset for Image { let tree = usvg::Tree::from_data(&bytes, &OPTIONS)?; - let mut pixmap = - resvg::tiny_skia::Pixmap::new(size.width.0 as u32, size.height.0 as u32) - .ok_or(usvg::Error::InvalidSize)?; + // Get svg size + let svg_size = tree.size(); + let mut pixmap = resvg::tiny_skia::Pixmap::new( + (svg_size.width() * SCALE) as u32, + (svg_size.height() * SCALE) as u32, + ) + .ok_or(usvg::Error::InvalidSize)?; + let img_size = gpui::size(px(pixmap.width() as f32), px(pixmap.height() as f32)); let transform = resvg::tiny_skia::Transform::from_scale(SCALE, SCALE); resvg::render(&tree, transform, &mut pixmap.as_mut()); @@ -141,10 +168,8 @@ impl Asset for Image { } } - Ok(Arc::new(RenderImage::new(SmallVec::from_elem( - Frame::new(buffer), - 1, - )))) + let image = Arc::new(RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))); + Ok(ImageData::new(source, image, img_size)) } } } @@ -152,44 +177,26 @@ impl Asset for Image { pub struct SvgImg { id: ElementId, interactivity: Interactivity, - source: Option, - size: Size, + source: ImageSource, } impl SvgImg { /// Create a new svg image element. /// - /// The `src_width` and `src_height` are the original width and height of the svg image. - pub fn new(id: impl Into) -> Self { + /// The `source` can be a string of SVG XML data or a Asset Path. + pub fn new(id: impl Into, source: impl Into) -> Self { Self { id: id.into(), interactivity: Interactivity::default(), - source: None, - size: Size::default(), + source: ImageSource { + source: source.into(), + }, } } - /// Set the path of the svg image from the asset. - /// - /// The `size` argument is the size of the original svg image. - #[must_use] - pub fn source( - mut self, - source: impl Into, - width: impl Into, - height: impl Into, - ) -> Self { - let size = size(width.into(), height.into()); - self.size = size; - self.source = Some(ImageSource { - source: source.into(), - size, - }); - self - } - - pub fn get_source(&self) -> Option<&ImageSource> { - self.source.as_ref() + /// Get the source of the svg image. + pub fn source(&self) -> &ImageSource { + &self.source } } @@ -201,9 +208,11 @@ impl IntoElement for SvgImg { } } +struct SvgImgState(Option<(u64, ImageData)>); + impl Element for SvgImg { - type RequestLayoutState = Option>; - type PrepaintState = (Option, Option>); + type RequestLayoutState = Option; + type PrepaintState = (Option, Option); fn id(&self) -> Option { Some(self.id.clone()) @@ -215,40 +224,45 @@ impl Element for SvgImg { window: &mut Window, cx: &mut App, ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let layout_id = - self.interactivity - .request_layout(global_id, window, cx, |style, window, cx| { - window.request_layout(style, None, cx) - }); let global_id = global_id.unwrap(); + let layout_id = + self.interactivity + .request_layout(Some(global_id), window, cx, |style, window, cx| { + window.request_layout(style, None, cx) + }); + + let source = &self.source; + let source_hash = hash(source); + window.with_element_state::(global_id, |state, window| { - match (state, &self.source) { - (_, None) => ((layout_id, None), SvgImgState(None)), - (Some(SvgImgState(Some((prev_hash, image)))), Some(source)) - if hash(source) == prev_hash => - { - ( - (layout_id, Some(image.clone())), - SvgImgState(Some((prev_hash, image))), - ) - } - (state, Some(source)) => { - if let Some(SvgImgState(Some((_, prev_image)))) = state { - // Drop the previous image from the cache - _ = window.drop_image(prev_image); + match state { + Some(mut state) => { + if let Some((prev_hash, mut prev_image)) = state.0.take() { + if source_hash == prev_hash { + return ( + (layout_id, Some(prev_image.clone())), + SvgImgState(Some((prev_hash, prev_image))), + ); + } else { + // Drop the previous image from the cache. + // Here can't remove directly, because same the image is being used by another element. + prev_image.remove_use(global_id, window, cx); + } } let image = window - .use_asset::(&source, cx) + .use_asset::(&source, cx) .transpose() .ok() .flatten(); + ( (layout_id, image.clone()), - SvgImgState(image.map(|image| (hash(source), image))), + SvgImgState(image.map(|image| (source_hash, image))), ) } + None => ((layout_id, None), SvgImgState(None)), } }) } @@ -282,43 +296,45 @@ impl Element for SvgImg { window: &mut Window, cx: &mut App, ) { - let size = self.size; let hitbox = state.0.as_ref(); - let data = state.1.clone(); + let Some(image_data) = state.1.take() else { + return; + }; + let size = image_data.size; self.interactivity .paint(global_id, bounds, hitbox, window, cx, |_, window, _| { - if let Some(data) = data { - // To calculate the ratio of the original image size to the container bounds size. - // Scale by shortest side (width or height) to get a fit image. - // And center the image in the container bounds. - let ratio = if bounds.size.width < bounds.size.height { - bounds.size.width / size.width - } else { - bounds.size.height / size.height - }; + // To calculate the ratio of the original image size to the container bounds size. + // Scale by shortest side (width or height) to get a fit image. + // And center the image in the container bounds. + let ratio = if bounds.size.width < bounds.size.height { + bounds.size.width / size.width + } else { + bounds.size.height / size.height + }; - let ratio = ratio.min(1.0); + let ratio = ratio.min(1.0); + let new_size = size.map(|dim| dim * ratio); - let new_size = gpui::Size { - width: size.width * ratio, - height: size.height * ratio, - }; - let new_origin = gpui::Point { - x: bounds.origin.x + px(((bounds.size.width - new_size.width) / 2.).into()), - y: bounds.origin.y - + px(((bounds.size.height - new_size.height) / 2.).into()), - }; + let new_origin = gpui::Point { + x: bounds.origin.x + px(((bounds.size.width - new_size.width) / 2.).into()), + y: bounds.origin.y + px(((bounds.size.height - new_size.height) / 2.).into()), + }; - let img_bounds = Bounds { - origin: new_origin.map(|origin| origin.floor()), - size: new_size.map(|size| size.ceil()), - }; + let img_bounds = Bounds { + origin: new_origin.map(|origin| origin.floor()), + size: new_size.map(|size| size.ceil()), + }; - match window.paint_image(img_bounds, px(0.).into(), data, 0, false) { - Ok(_) => {} - Err(err) => eprintln!("failed to paint svg image: {:?}", err), - } + match window.paint_image( + img_bounds, + px(0.).into(), + image_data.use_image(&global_id.unwrap()), + 0, + false, + ) { + Ok(_) => {} + Err(err) => eprintln!("failed to paint svg image: {:?}", err), } }) }