From 785df6a5ffe48fcc2be966cf438352bbb13b6f6f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 29 Aug 2025 17:37:11 +0800 Subject: [PATCH] chore: Remove SvgImg element. (#1187) ## Break Change - The `SvgImg` element has been removed. This is a special implementation and should not be included in the GPUI Component. --- Cargo.lock | 4 - crates/story/src/assets.rs | 10 +- crates/story/src/image_story.rs | 46 +--- crates/ui/Cargo.toml | 6 - crates/ui/src/lib.rs | 2 - crates/ui/src/svg_img.rs | 375 -------------------------------- 6 files changed, 20 insertions(+), 423 deletions(-) delete mode 100644 crates/ui/src/svg_img.rs diff --git a/Cargo.lock b/Cargo.lock index 3ac14f38..dc3f8b36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3156,11 +3156,9 @@ dependencies = [ "anyhow", "chrono", "enum-iterator", - "futures-util", "gpui", "gpui-component-macros", "html5ever 0.27.0", - "image", "indexset", "indoc", "itertools 0.13.0", @@ -3171,7 +3169,6 @@ dependencies = [ "once_cell", "paste", "regex", - "resvg", "rust-i18n", "rust_decimal", "schemars", @@ -3212,7 +3209,6 @@ dependencies = [ "tree-sitter-yaml", "tree-sitter-zig", "unicode-segmentation", - "usvg", "uuid", "wry", ] diff --git a/crates/story/src/assets.rs b/crates/story/src/assets.rs index 57ca5efd..8df62393 100644 --- a/crates/story/src/assets.rs +++ b/crates/story/src/assets.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, path::PathBuf}; use anyhow::{anyhow, Result}; @@ -19,6 +19,14 @@ impl AssetSource for Assets { Self::get(path) .map(|f| Some(f.data)) + .or_else(|| { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let path = manifest_dir.join(path); + + std::fs::read(path) + .map(|data| Some(std::borrow::Cow::Owned(data))) + .ok() + }) .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } diff --git a/crates/story/src/image_story.rs b/crates/story/src/image_story.rs index e09434ee..53020bd9 100644 --- a/crates/story/src/image_story.rs +++ b/crates/story/src/image_story.rs @@ -1,18 +1,11 @@ -use gpui::{ - img, App, AppContext, ClickEvent, ElementId, Entity, FocusHandle, Focusable, - ParentElement as _, Render, Styled, Window, -}; -use gpui_component::{button::Button, dock::PanelControl, v_flex, SvgImg}; - use crate::section; - -const SVG_ITEMS: &[&str] = &[ - include_str!("./fixtures/google.svg"), - include_str!("./fixtures/color-wheel.svg"), -]; +use gpui::{ + img, App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement as _, + Render, Styled, Window, +}; +use gpui_component::{dock::PanelControl, v_flex}; pub struct ImageStory { - svg_index: usize, focus_handle: gpui::FocusHandle, } @@ -37,7 +30,6 @@ 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(), } } @@ -45,10 +37,6 @@ 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 { @@ -58,29 +46,17 @@ impl Focusable for ImageStory { } impl Render for ImageStory { - fn render( - &mut self, - _window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + // The svg file are from Assets + // See: crates/story/src/assets.rs#L21 v_flex() .gap_4() .size_full() + .child(section("SVG 160px").child(img("src/fixtures/google.svg").size_40().flex_grow())) .child( - Button::new("switch") - .outline() - .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(); - })), + section("SVG 80px") + .child(img("src/fixtures/color-wheel.svg").size_20().flex_grow()), ) - .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(), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 681e7e7c..853ddafe 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -62,7 +62,6 @@ anyhow.workspace = true notify.workspace = true enum-iterator = "2.1.0" -futures-util = "0.3.31" itertools = "0.13.0" once_cell = "1.19.0" paste = "1" @@ -73,11 +72,6 @@ uuid = "1.10" # WebView wry = { version = "0.48.0", optional = true } -# SvgImg -image = { version = "0.25.1", default-features = false, features = ["png"] } -resvg = { version = "0.45.0", default-features = false, features = ["text"] } -usvg = { version = "0.45.0", default-features = false, features = ["text"] } - # Chart num-traits = "0.2" rust_decimal = { version = "1.37.0", optional = true } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 115869bd..dbc4c60b 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -9,7 +9,6 @@ mod kbd; mod menu; mod root; mod styled; -mod svg_img; mod time; mod title_bar; mod virtual_list; @@ -85,7 +84,6 @@ pub use window_border::{window_border, window_paddings, WindowBorder}; pub use icon::*; pub use kbd::*; -pub use svg_img::*; pub use theme::*; use std::ops::Deref; diff --git a/crates/ui/src/svg_img.rs b/crates/ui/src/svg_img.rs deleted file mode 100644 index 890a3256..00000000 --- a/crates/ui/src/svg_img.rs +++ /dev/null @@ -1,375 +0,0 @@ -use std::{ - hash::Hash, - ops::Deref, - sync::{Arc, LazyLock}, -}; - -use gpui::{ - hash, px, App, Asset, AssetLogger, Bounds, Element, ElementId, GlobalElementId, Hitbox, - ImageCacheError, InteractiveElement, Interactivity, IntoElement, Pixels, RenderImage, - SharedString, StyleRefinement, Styled, Task, Window, -}; -use image::Frame; -use smallvec::SmallVec; - -use futures_util::{future::Shared, FutureExt}; -use image::ImageBuffer; - -const SCALE: f32 = 2.; - -static OPTIONS: LazyLock = LazyLock::new(|| { - let mut options = usvg::Options::default(); - options.fontdb_mut().load_system_fonts(); - options -}); - -#[derive(Debug, Clone, Hash)] -pub enum SvgSource { - /// A svg bytes - Data(Arc<[u8]>), - /// An asset path - Path(SharedString), -} - -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 { - id: self.id.clone(), - interactivity: Interactivity::default(), - source: self.source.clone(), - } - } -} - -enum SvgImageLoader {} - -#[derive(Debug, Clone)] -pub struct ImageSource { - source: SvgSource, -} - -impl Hash for ImageSource { - /// Hash to to control the Asset cache - fn hash(&self, state: &mut H) { - self.source.hash(state); - } -} - -impl Asset for SvgImageLoader { - type Source = ImageSource; - type Output = Result, ImageCacheError>; - - fn load( - source: Self::Source, - cx: &mut App, - ) -> impl std::future::Future + Send + 'static { - let asset_source = cx.asset_source().clone(); - - async move { - let bytes = match source.source.clone() { - 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 tree = usvg::Tree::from_data(&bytes, &OPTIONS)?; - - // 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 transform = resvg::tiny_skia::Transform::from_scale(SCALE, SCALE); - - 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 with premultiplied alpha to BGRA with straight alpha. - for pixel in buffer.chunks_exact_mut(4) { - pixel.swap(0, 2); - if pixel[3] > 0 { - let a = pixel[3] as f32 / 255.; - pixel[0] = (pixel[0] as f32 / a) as u8; - pixel[1] = (pixel[1] as f32 / a) as u8; - pixel[2] = (pixel[2] as f32 / a) as u8; - } - } - - let image = Arc::new(RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))); - Ok(image) - } - } -} - -pub struct SvgImg { - id: ElementId, - interactivity: Interactivity, - source: ImageSource, -} - -impl SvgImg { - /// Create a new svg image element. - /// - /// 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: ImageSource { - source: source.into(), - }, - } - } - - /// Get the source of the svg image. - pub fn source(&self) -> &ImageSource { - &self.source - } -} - -impl IntoElement for SvgImg { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -fn load_svg( - source: &ImageSource, - window: &mut Window, - cx: &mut App, -) -> Shared, ImageCacheError>>> { - let fut = AssetLogger::::load(source.clone(), cx); - let task = cx.background_executor().spawn(fut).shared(); - - let entity = window.current_view(); - window - .spawn(cx, { - let task = task.clone(); - async move |cx| { - _ = task.await; - cx.on_next_frame(move |_, cx| { - cx.notify(entity); - }); - } - }) - .detach(); - task -} - -struct SvgImgState { - hash: u64, - image: Option>, - task: Shared, ImageCacheError>>>, -} - -impl Element for SvgImg { - type RequestLayoutState = Option>; - type PrepaintState = (Option, Option>); - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |style, window, cx| window.request_layout(style, None, cx), - ); - - let global_id = global_id.unwrap(); - let source = &self.source; - let source_hash = hash(source); - - window.with_element_state::, _>(global_id, |state, window| { - match state { - Some(state) => { - // Try to keep the previous image if it's still loading. - let mut prev_image = None; - if let Some(mut state) = state { - prev_image = state.image.clone(); - if source_hash == state.hash { - state.image = state - .task - .clone() - .now_or_never() - .transpose() - .ok() - .flatten() - .or(state.image); - - return ((layout_id, state.image.clone()), Some(state)); - } - } - - let task = load_svg(source, window, cx); - let mut image = task.clone().now_or_never().transpose().ok().flatten(); - if let Some(new_image) = image.as_ref() { - _ = window.drop_image(new_image.clone()); - } else { - image = prev_image; - } - - ( - (layout_id, image.clone()), - Some(SvgImgState { - hash: source_hash, - image, - task, - }), - ) - } - None => { - let task = load_svg(source, window, cx); - ( - (layout_id, None), - Some(SvgImgState { - hash: source_hash, - image: None, - task, - }), - ) - } - } - }) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let hitbox = self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - bounds.size, - window, - cx, - |_, _, hitbox, _, _| hitbox, - ); - - (hitbox, state.clone()) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - state: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let hitbox = state.0.as_ref(); - let Some(image) = state.1.take() else { - return; - }; - let size = image.size(0).map(|x| x.0 as f32); - - self.interactivity.paint( - global_id, - inspector_id, - bounds, - hitbox, - window, - cx, - |_, window, _| { - // 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.0.min(1.0); - let new_size = size.map(|dim| px(dim) * 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 img_bounds = Bounds { - origin: new_origin.map(|origin| origin.floor()), - size: new_size.map(|size| size.ceil()), - }; - - if let Err(err) = window.paint_image(img_bounds, px(0.).into(), image, 0, false) { - eprintln!("failed to paint svg image: {:?}", err); - } - }, - ) - } -} - -impl Styled for SvgImg { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.interactivity.base_style - } -} - -impl InteractiveElement for SvgImg { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -}