From 6465969c5aa0c4cd3388c3a724995db45bce545d Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 30 Jul 2024 19:54:27 +0800 Subject: [PATCH] Improve `svg_img` to use cached assets. (#87) --- assets/icons/close.svg | 4 - crates/story/src/image_story.rs | 23 ++-- crates/ui/src/svg_img.rs | 233 +++++++++++++++++++++----------- 3 files changed, 166 insertions(+), 94 deletions(-) delete mode 100644 assets/icons/close.svg diff --git a/assets/icons/close.svg b/assets/icons/close.svg deleted file mode 100644 index f7a6e18b..00000000 --- a/assets/icons/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/crates/story/src/image_story.rs b/crates/story/src/image_story.rs index 368f7f36..6c6865c7 100644 --- a/crates/story/src/image_story.rs +++ b/crates/story/src/image_story.rs @@ -1,5 +1,5 @@ use gpui::{px, ParentElement as _, Render, Styled, View, VisualContext as _, WindowContext}; -use ui::{h_flex, v_flex, SvgImg}; +use ui::{h_flex, svg_img, v_flex, SvgImg}; const GOOGLE_LOGO: &str = include_str!("./fixtures/google.svg"); const PIE_JSON: &str = include_str!("./fixtures/pie.json"); @@ -7,19 +7,17 @@ const PIE_JSON: &str = include_str!("./fixtures/pie.json"); pub struct ImageStory { google_logo: SvgImg, pie_chart: SvgImg, + inbox_img: SvgImg, } impl ImageStory { - pub fn new(cx: &WindowContext) -> Self { + pub fn new(_: &WindowContext) -> Self { let chart = charts_rs::PieChart::from_json(PIE_JSON).unwrap(); Self { - google_logo: SvgImg::new(800, 800) - .svg(GOOGLE_LOGO.as_bytes(), cx) - .unwrap(), - pie_chart: SvgImg::new(400, 300) - .svg(chart.svg().unwrap().as_bytes(), cx) - .unwrap(), + google_logo: svg_img().source(GOOGLE_LOGO.as_bytes(), px(300.), px(300.)), + pie_chart: svg_img().source(chart.svg().unwrap().as_bytes(), px(400.), px(400.)), + inbox_img: svg_img().source("icons/inbox.svg", px(300.), px(300.)), } } @@ -37,12 +35,13 @@ impl Render for ImageStory { .child( h_flex() .size_full() + .child(self.google_logo.clone().size(px(300.)).flex_grow()) .child(self.google_logo.clone().w(px(300.)).h(px(300.)).flex_grow()) - .child(self.google_logo.clone().w(px(300.)).h(px(300.)).flex_grow()) - .child(self.google_logo.clone().w(px(300.)).h(px(300.)).flex_grow()) - .child(self.google_logo.clone().w(px(300.)).h(px(300.)).flex_grow()) - .child(self.google_logo.clone().w(px(300.)).h(px(300.)).flex_grow()), + .child(self.google_logo.clone().size_80().flex_grow()) + .child(self.google_logo.clone().size_12().flex_grow()) + .child(self.google_logo.clone().w(px(300.)).h(px(300.))), ) + .child(self.inbox_img.clone().w(px(80.)).h(px(80.))) .child(self.pie_chart.clone().size_full()) } } diff --git a/crates/ui/src/svg_img.rs b/crates/ui/src/svg_img.rs index 5decd31e..e3cf8cb3 100644 --- a/crates/ui/src/svg_img.rs +++ b/crates/ui/src/svg_img.rs @@ -1,107 +1,170 @@ -use std::sync::Arc; +use std::{hash::Hash, ops::Deref, sync::Arc}; -use anyhow::Context; use gpui::{ - px, Bounds, Element, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, - SharedString, StyleRefinement, Styled, WindowContext, + px, size, Asset, Bounds, Element, Hitbox, ImageCacheError, ImageData, InteractiveElement, + Interactivity, IntoElement, IsZero, Pixels, SharedString, Size, StyleRefinement, Styled, + WindowContext, }; use image::ImageBuffer; -#[derive(Clone, Copy, Debug)] -struct SvgSize { - width: u32, - height: u32, +#[derive(Debug, Clone, Hash)] +pub enum SvgSource { + /// A svg bytes + Data(Arc<[u8]>), + /// An asset path + Path(SharedString), } -pub struct SvgImg { - interactivity: Interactivity, - size: SvgSize, - data: Option>, +impl From<&[u8]> for SvgSource { + fn from(data: &[u8]) -> Self { + Self::Data(data.into()) + } +} + +impl From> for SvgSource { + fn from(data: Arc<[u8]>) -> Self { + Self::Data(data) + } +} + +impl From for SvgSource { + fn from(path: SharedString) -> Self { + Self::Path(path) + } +} + +impl From<&'static str> for SvgSource { + fn from(path: &'static str) -> Self { + Self::Path(path.into()) + } } impl Clone for SvgImg { fn clone(&self) -> Self { Self { interactivity: Interactivity::default(), + source: self.source.clone(), size: self.size, - data: self.data.clone(), + } + } +} + +enum Image {} + +#[derive(Debug, Clone)] +struct ImageSource { + source: SvgSource, + size: Size, +} + +impl Hash for ImageSource { + /// Hash to to control the Asset cache + fn hash(&self, state: &mut H) { + self.source.hash(state); + } +} + +impl Asset for Image { + type Source = ImageSource; + type Output = Result, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut WindowContext, + ) -> impl std::future::Future + Send + 'static { + let scale = cx.scale_factor(); + 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, + height: size.height * scale, + }; + + let bytes = match source.source { + SvgSource::Data(data) => data, + SvgSource::Path(path) => { + if let Ok(Some(data)) = asset_source.load(&path) { + data.deref().to_vec().into() + } else { + Err(std::io::Error::other(format!( + "failed to load svg image from path: {}", + path + ))) + .map_err(|e| ImageCacheError::Io(Arc::new(e)))? + } + } + }; + + let options = usvg::Options { + ..Default::default() + }; + 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)?; + + let transform = tree.view_box().to_transform( + resvg::tiny_skia::Size::from_wh(size.width.0, size.height.0) + .ok_or(usvg::Error::InvalidSize)?, + ); + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + let mut buffer = ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) + .expect("invalid svg image buffer"); + + // Convert from RGBA to BGRA. + for pixel in buffer.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + + Ok(Arc::new(ImageData::new(buffer))) } } } /// An SVG image element. -pub fn svg_img(width: usize, height: usize) -> SvgImg { - SvgImg::new(width, height) +pub fn svg_img() -> SvgImg { + SvgImg::new() +} + +pub struct SvgImg { + interactivity: Interactivity, + source: Option, + size: Size, } 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(src_width: usize, src_height: usize) -> Self { + pub fn new() -> Self { Self { interactivity: Interactivity::default(), - size: SvgSize { - width: src_width as u32, - height: src_height as u32, - }, - data: None, + source: None, + size: Size::default(), } } /// Set the path of the svg image from the asset. - pub fn path(self, path: impl Into, cx: &WindowContext) -> anyhow::Result { - let svg = cx - .asset_source() - .load(&path.into()) - .expect("failed to load svg from asset") - .expect("failed to load svg from asset, return none"); - - self.svg(&svg, cx) - } - - /// Set the svg image from the bytes. - pub fn svg(mut self, svg: &[u8], cx: &WindowContext) -> anyhow::Result { - let data = self.to_image_data(svg, cx)?; - self.data = Some(Arc::new(data)); - Ok(self) - } - - pub fn to_image_data(&self, bytes: &[u8], cx: &WindowContext) -> anyhow::Result { - if self.size.width == 0 || self.size.height == 0 { - return Err(usvg::Error::InvalidSize.into()); - } - - let scale = cx.scale_factor() as u32; - let size = SvgSize { - width: self.size.width * scale, - height: self.size.height * scale, - }; - - let options = usvg::Options { - ..Default::default() - }; - let tree = usvg::Tree::from_data(&bytes, &options)?; - - let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width, size.height) - .ok_or(usvg::Error::InvalidSize)?; - - let transform = tree.view_box().to_transform( - resvg::tiny_skia::Size::from_wh(size.width as f32, size.height as f32) - .ok_or(usvg::Error::InvalidSize)?, - ); - resvg::render(&tree, transform, &mut pixmap.as_mut()); - - let mut buffer = ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()) - .context("invalid svg image buffer")?; - - // Convert from RGBA to BGRA. - for pixel in buffer.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - - Ok(ImageData::new(buffer)) + /// + /// 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 { + self.source = Some(source.into()); + self.size = size(width.into(), height.into()); + self } } @@ -129,6 +192,7 @@ impl Element for SvgImg { let layout_id = self .interactivity .request_layout(global_id, cx, |style, cx| cx.request_layout(style, None)); + (layout_id, ()) } @@ -151,23 +215,36 @@ impl Element for SvgImg { hitbox: &mut Self::PrepaintState, cx: &mut WindowContext, ) { + let source = self.source.clone(); + self.interactivity .paint(global_id, bounds, hitbox.as_ref(), cx, |_style, cx| { - if let Some(data) = self.data.as_ref() { + let size = self.size; + + let data = if let Some(source) = source { + match cx.use_cached_asset::(&ImageSource { source, size }) { + Some(Ok(data)) => Some(data), + _ => None, + } + } else { + None + }; + + 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.0 / self.size.width as f32 + bounds.size.width / size.width } else { - bounds.size.height.0 / self.size.height as f32 + bounds.size.height / size.height }; let ratio = ratio.min(1.0); let new_size = gpui::Size { - width: px(self.size.width as f32 * ratio), - height: px(self.size.height as f32 * ratio), + 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()), @@ -180,7 +257,7 @@ impl Element for SvgImg { origin: new_origin, }; - match cx.paint_image(img_bounds, px(0.).into(), data.clone(), false) { + match cx.paint_image(img_bounds, px(0.).into(), data, false) { Ok(_) => {} Err(err) => eprintln!("failed to paint svg image: {:?}", err), }