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:
parent
7fb6000189
commit
785df6a5ff
6 changed files with 20 additions and 423 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue