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),
}
})
}