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.
This commit is contained in:
Jason Lee 2025-08-29 17:37:11 +08:00 committed by GitHub
parent 7fb6000189
commit 785df6a5ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 20 additions and 423 deletions

4
Cargo.lock generated
View file

@ -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",
]

View file

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

View file

@ -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<Self> {
cx.new(|cx| Self::new(window, cx))
}
fn svg_img(&self, id: impl Into<ElementId>) -> 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<Self>,
) -> impl gpui::IntoElement {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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(),

View file

@ -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 }

View file

@ -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;

View file

@ -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<usvg::Options> = 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<Arc<[u8]>> for SvgSource {
fn from(data: Arc<[u8]>) -> Self {
Self::Data(data)
}
}
impl From<SharedString> 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<H: std::hash::Hasher>(&self, state: &mut H) {
self.source.hash(state);
}
}
impl Asset for SvgImageLoader {
type Source = ImageSource;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut App,
) -> impl std::future::Future<Output = Self::Output> + 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<ElementId>, source: impl Into<SvgSource>) -> 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<Task<Result<Arc<RenderImage>, ImageCacheError>>> {
let fut = AssetLogger::<SvgImageLoader>::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<Arc<RenderImage>>,
task: Shared<Task<Result<Arc<RenderImage>, ImageCacheError>>>,
}
impl Element for SvgImg {
type RequestLayoutState = Option<Arc<RenderImage>>;
type PrepaintState = (Option<Hitbox>, Option<Arc<RenderImage>>);
fn id(&self) -> Option<ElementId> {
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::<Option<SvgImgState>, _>(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<Pixels>,
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<Pixels>,
_: &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
}
}