gpui: Add support for rendering SVG from external files (#42024)
Release Notes: - N/A --------- Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
co-authored by
Mikayla Maki
parent
4724aa5cb8
commit
feed34cafe
@@ -2150,7 +2150,7 @@ impl AgentPanel {
|
||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
.child(Icon::from_path(icon_path).color(Color::Muted))
|
||||
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{fs, path::Path, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
||||
App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
|
||||
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
|
||||
radians, size,
|
||||
@@ -11,6 +13,7 @@ pub struct Svg {
|
||||
interactivity: Interactivity,
|
||||
transformation: Option<Transformation>,
|
||||
path: Option<SharedString>,
|
||||
external_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// Create a new SVG element.
|
||||
@@ -20,6 +23,7 @@ pub fn svg() -> Svg {
|
||||
interactivity: Interactivity::new(),
|
||||
transformation: None,
|
||||
path: None,
|
||||
external_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +34,12 @@ impl Svg {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the path to the SVG file for this element.
|
||||
pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.external_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Transform the SVG element with the given transformation.
|
||||
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
|
||||
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
|
||||
@@ -117,7 +127,35 @@ impl Element for Svg {
|
||||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.paint_svg(bounds, path.clone(), transformation, color, cx)
|
||||
.paint_svg(bounds, path.clone(), None, transformation, color, cx)
|
||||
.log_err();
|
||||
} else if let Some((path, color)) =
|
||||
self.external_path.as_ref().zip(style.text.color)
|
||||
{
|
||||
let Some(bytes) = window
|
||||
.use_asset::<SvgAsset>(path, cx)
|
||||
.and_then(|asset| asset.log_err())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let transformation = self
|
||||
.transformation
|
||||
.as_ref()
|
||||
.map(|transformation| {
|
||||
transformation.into_matrix(bounds.center(), window.scale_factor())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.paint_svg(
|
||||
bounds,
|
||||
path.clone(),
|
||||
Some(&bytes),
|
||||
transformation,
|
||||
color,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
@@ -219,3 +257,21 @@ impl Transformation {
|
||||
.translate(center.scale(scale_factor).negate())
|
||||
}
|
||||
}
|
||||
|
||||
enum SvgAsset {}
|
||||
|
||||
impl Asset for SvgAsset {
|
||||
type Source = SharedString;
|
||||
type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
_cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
async move {
|
||||
let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
|
||||
let bytes = Arc::from(bytes);
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,27 +95,34 @@ impl SvgRenderer {
|
||||
pub(crate) fn render_alpha_mask(
|
||||
&self,
|
||||
params: &RenderSvgParams,
|
||||
bytes: Option<&[u8]>,
|
||||
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
|
||||
anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
|
||||
|
||||
// Load the tree.
|
||||
let Some(bytes) = self.asset_source.load(¶ms.path)? else {
|
||||
return Ok(None);
|
||||
let render_pixmap = |bytes| {
|
||||
let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?;
|
||||
|
||||
// Convert the pixmap's pixels into an alpha mask.
|
||||
let size = Size::new(
|
||||
DevicePixels(pixmap.width() as i32),
|
||||
DevicePixels(pixmap.height() as i32),
|
||||
);
|
||||
let alpha_mask = pixmap
|
||||
.pixels()
|
||||
.iter()
|
||||
.map(|p| p.alpha())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Some((size, alpha_mask)))
|
||||
};
|
||||
|
||||
let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
|
||||
|
||||
// Convert the pixmap's pixels into an alpha mask.
|
||||
let size = Size::new(
|
||||
DevicePixels(pixmap.width() as i32),
|
||||
DevicePixels(pixmap.height() as i32),
|
||||
);
|
||||
let alpha_mask = pixmap
|
||||
.pixels()
|
||||
.iter()
|
||||
.map(|p| p.alpha())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Some((size, alpha_mask)))
|
||||
if let Some(bytes) = bytes {
|
||||
render_pixmap(bytes)
|
||||
} else if let Some(bytes) = self.asset_source.load(¶ms.path)? {
|
||||
render_pixmap(&bytes)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
||||
|
||||
@@ -3084,6 +3084,7 @@ impl Window {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
path: SharedString,
|
||||
mut data: Option<&[u8]>,
|
||||
transformation: TransformationMatrix,
|
||||
color: Hsla,
|
||||
cx: &App,
|
||||
@@ -3104,7 +3105,8 @@ impl Window {
|
||||
let Some(tile) =
|
||||
self.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms)? else {
|
||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some((size, Cow::Owned(bytes))))
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct ContextMenuEntry {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
custom_icon_path: Option<SharedString>,
|
||||
custom_icon_svg: Option<SharedString>,
|
||||
icon_position: IconPosition,
|
||||
icon_size: IconSize,
|
||||
icon_color: Option<Color>,
|
||||
@@ -68,6 +69,7 @@ impl ContextMenuEntry {
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::Start,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -94,7 +96,15 @@ impl ContextMenuEntry {
|
||||
|
||||
pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.custom_icon_path = Some(path.into());
|
||||
self.icon = None; // Clear IconName if custom path is set
|
||||
self.custom_icon_svg = None; // Clear other icon sources if custom path is set
|
||||
self.icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn custom_icon_svg(mut self, svg: impl Into<SharedString>) -> Self {
|
||||
self.custom_icon_svg = Some(svg.into());
|
||||
self.custom_icon_path = None; // Clear other icon sources if custom path is set
|
||||
self.icon = None;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -396,6 +406,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -425,6 +436,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -454,6 +466,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -482,6 +495,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| handler(window, cx)),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: position,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -541,6 +555,7 @@ impl ContextMenu {
|
||||
}),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_position: IconPosition::End,
|
||||
icon_size: IconSize::Small,
|
||||
icon_color: None,
|
||||
@@ -572,6 +587,7 @@ impl ContextMenu {
|
||||
}),
|
||||
icon: None,
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_size: IconSize::Small,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
@@ -593,6 +609,7 @@ impl ContextMenu {
|
||||
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
|
||||
icon: Some(IconName::ArrowUpRight),
|
||||
custom_icon_path: None,
|
||||
custom_icon_svg: None,
|
||||
icon_size: IconSize::XSmall,
|
||||
icon_position: IconPosition::End,
|
||||
icon_color: None,
|
||||
@@ -913,6 +930,7 @@ impl ContextMenu {
|
||||
handler,
|
||||
icon,
|
||||
custom_icon_path,
|
||||
custom_icon_svg,
|
||||
icon_position,
|
||||
icon_size,
|
||||
icon_color,
|
||||
@@ -965,6 +983,28 @@ impl ContextMenu {
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else if let Some(custom_icon_svg) = custom_icon_svg {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.when(
|
||||
*icon_position == IconPosition::Start && toggle.is_none(),
|
||||
|flex| {
|
||||
flex.child(
|
||||
Icon::from_external_svg(custom_icon_svg.clone())
|
||||
.size(*icon_size)
|
||||
.color(icon_color),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(Label::new(label.clone()).color(label_color).truncate())
|
||||
.when(*icon_position == IconPosition::End, |flex| {
|
||||
flex.child(
|
||||
Icon::from_external_svg(custom_icon_svg.clone())
|
||||
.size(*icon_size)
|
||||
.color(icon_color),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
} else if let Some(icon_name) = icon {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
|
||||
@@ -115,24 +115,24 @@ impl From<IconName> for Icon {
|
||||
/// The source of an icon.
|
||||
enum IconSource {
|
||||
/// An SVG embedded in the Zed binary.
|
||||
Svg(SharedString),
|
||||
Embedded(SharedString),
|
||||
/// An image file located at the specified path.
|
||||
///
|
||||
/// Currently our SVG renderer is missing support for the following features:
|
||||
/// 1. Loading SVGs from external files.
|
||||
/// 2. Rendering polychrome SVGs.
|
||||
/// Currently our SVG renderer is missing support for rendering polychrome SVGs.
|
||||
///
|
||||
/// In order to support icon themes, we render the icons as images instead.
|
||||
Image(Arc<Path>),
|
||||
External(Arc<Path>),
|
||||
/// An SVG not embedded in the Zed binary.
|
||||
ExternalSvg(SharedString),
|
||||
}
|
||||
|
||||
impl IconSource {
|
||||
fn from_path(path: impl Into<SharedString>) -> Self {
|
||||
let path = path.into();
|
||||
if path.starts_with("icons/") {
|
||||
Self::Svg(path)
|
||||
Self::Embedded(path)
|
||||
} else {
|
||||
Self::Image(Arc::from(PathBuf::from(path.as_ref())))
|
||||
Self::External(Arc::from(PathBuf::from(path.as_ref())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ pub struct Icon {
|
||||
impl Icon {
|
||||
pub fn new(icon: IconName) -> Self {
|
||||
Self {
|
||||
source: IconSource::Svg(icon.path().into()),
|
||||
source: IconSource::Embedded(icon.path().into()),
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
transformation: Transformation::default(),
|
||||
@@ -164,6 +164,15 @@ impl Icon {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_external_svg(svg: SharedString) -> Self {
|
||||
Self {
|
||||
source: IconSource::ExternalSvg(svg),
|
||||
color: Color::default(),
|
||||
size: IconSize::default().rems(),
|
||||
transformation: Transformation::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
@@ -193,14 +202,21 @@ impl Transformable for Icon {
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
match self.source {
|
||||
IconSource::Svg(path) => svg()
|
||||
IconSource::Embedded(path) => svg()
|
||||
.with_transformation(self.transformation)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.path(path)
|
||||
.text_color(self.color.color(cx))
|
||||
.into_any_element(),
|
||||
IconSource::Image(path) => img(path)
|
||||
IconSource::ExternalSvg(path) => svg()
|
||||
.external_path(path)
|
||||
.with_transformation(self.transformation)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.text_color(self.color.color(cx))
|
||||
.into_any_element(),
|
||||
IconSource::External(path) => img(path)
|
||||
.size(self.size)
|
||||
.flex_none()
|
||||
.text_color(self.color.color(cx))
|
||||
|
||||
Reference in New Issue
Block a user