Skip to main content

gpui/elements/
svg.rs

1use std::{
2    fs,
3    hash::{Hash, Hasher},
4    path::Path,
5    sync::Arc,
6};
7
8use crate::{
9    App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
10    Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
11    StyleRefinement, Styled, TransformationMatrix, Window, point, px, radians, size,
12};
13use gpui_util::ResultExt;
14
15/// An element that draws an SVG as a single-color mask, filled with the text color.
16///
17/// Create one with [`svg`]. The SVG's alpha channel becomes the shape, and its own colors are
18/// dropped. That suits icons that follow the theme. To keep an SVG's colors, use [`crate::img`] instead.
19///
20/// Nothing is drawn until the element has a source: [`Svg::path`], [`Svg::external_path`] or
21/// [`Svg::data`].
22#[derive(derive_setters::Setters)]
23#[setters(generate = false)]
24pub struct Svg {
25    interactivity: Interactivity,
26    /// Rotates, scales or moves the rendered SVG around the center of the element.
27    ///
28    /// The transformation changes only what is drawn. The layout and the hitbox stay the same, so
29    /// the element still takes its full box and receives clicks there. See [`Transformation`].
30    #[setters(generate, rename = "with_transformation", strip_option)]
31    transformation: Option<Transformation>,
32    /// Sets the SVG to load from the app's [`crate::AssetRegistry`], by `path`, such as `"icons/search.svg"`.
33    ///
34    /// The registry is read when the icon is first drawn at a given size, and each rasterized size
35    /// is then cached. If the registry has no such path, nothing is drawn.
36    ///
37    /// When several sources are set, [`Svg::data`] wins, then [`Svg::external_path`], then this
38    /// one.
39    #[setters(generate, strip_option, into)]
40    path: Option<SharedString>,
41    /// Sets the SVG to load from a file on disk, relative to the working directory.
42    ///
43    /// The file is read once, in the background, and cached under `path`. Nothing is drawn until it
44    /// loads. A read error is logged, and nothing is drawn. The cache keeps the entry, so later
45    /// changes to the file are not picked up until the entry is removed.
46    ///
47    /// When several sources are set, [`Svg::data`] wins over this one.
48    #[setters(generate, strip_option, into)]
49    external_path: Option<SharedString>,
50    data: Option<Arc<[u8]>>,
51    data_path: Option<SharedString>,
52}
53
54/// Creates an [`Svg`] element with no source yet.
55///
56/// Give it a source with [`Svg::path`], [`Svg::external_path`] or [`Svg::data`]. The icon is filled
57/// with the text color, so set that with [`Styled::text_color`].
58///
59/// # Examples
60///
61/// ```
62/// # use gpui::*;
63/// const STAR: &[u8] = b"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 8 8\"><rect width=\"8\" height=\"8\"/></svg>";
64///
65/// let star = svg().data(STAR).size(px(16.)).text_color(rgb(0xf59e0b));
66/// ```
67#[track_caller]
68pub fn svg() -> Svg {
69    Svg {
70        interactivity: Interactivity::new(),
71        transformation: None,
72        path: None,
73        external_path: None,
74        data: None,
75        data_path: None,
76    }
77}
78
79impl Svg {
80    /// Sets the SVG from bytes that are already in memory, such as one included with `include_bytes!`.
81    ///
82    /// The bytes are copied into the element. The rasterized icon is cached under a hash of the
83    /// bytes, so the same data is rasterized once. Calling this again replaces the bytes. When
84    /// several sources are set, this one wins.
85    pub fn data(mut self, data: &[u8]) -> Self {
86        // Generate a unique deterministic path based on the data hash for caching
87        let mut hasher = std::collections::hash_map::DefaultHasher::new();
88        data.hash(&mut hasher);
89        let hash = hasher.finish();
90        let path = SharedString::from(format!("__binary_svg__{}", hash));
91        self.data = Some(Arc::from(data));
92        self.data_path = Some(path);
93        self
94    }
95}
96
97impl Element for Svg {
98    type RequestLayoutState = ();
99    type PrepaintState = Option<Hitbox>;
100
101    fn id(&self) -> Option<crate::ElementId> {
102        self.interactivity.element_id.clone()
103    }
104
105    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
106        self.interactivity.source_location()
107    }
108
109    fn request_layout(
110        &mut self,
111        global_id: Option<&GlobalElementId>,
112        inspector_id: Option<&InspectorElementId>,
113        window: &mut Window,
114        cx: &mut App,
115    ) -> (LayoutId, Self::RequestLayoutState) {
116        let layout_id = self.interactivity.request_layout(
117            global_id,
118            inspector_id,
119            window,
120            cx,
121            |style, window, cx| window.request_layout(style, None, cx),
122        );
123        (layout_id, ())
124    }
125
126    fn prepaint(
127        &mut self,
128        global_id: Option<&GlobalElementId>,
129        inspector_id: Option<&InspectorElementId>,
130        bounds: Bounds<Pixels>,
131        _request_layout: &mut Self::RequestLayoutState,
132        window: &mut Window,
133        cx: &mut App,
134    ) -> Option<Hitbox> {
135        self.interactivity.prepaint(
136            global_id,
137            inspector_id,
138            bounds,
139            bounds.size,
140            window,
141            cx,
142            |_, _, hitbox, _, _| hitbox,
143        )
144    }
145
146    fn paint(
147        &mut self,
148        global_id: Option<&GlobalElementId>,
149        inspector_id: Option<&InspectorElementId>,
150        bounds: Bounds<Pixels>,
151        _request_layout: &mut Self::RequestLayoutState,
152        hitbox: &mut Option<Hitbox>,
153        window: &mut Window,
154        cx: &mut App,
155    ) where
156        Self: Sized,
157    {
158        self.interactivity.paint(
159            global_id,
160            inspector_id,
161            bounds,
162            hitbox.as_ref(),
163            window,
164            cx,
165            |style, window, cx| {
166                let transformation = self
167                    .transformation
168                    .as_ref()
169                    .map(|transformation| {
170                        transformation.into_matrix(bounds.center(), window.scale_factor())
171                    })
172                    .unwrap_or_default();
173
174                if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) {
175                    if let Some(color) = style.text.color {
176                        window
177                            .paint_svg(
178                                bounds,
179                                path.clone(),
180                                Some(&**data),
181                                transformation,
182                                color,
183                                cx,
184                            )
185                            .log_err();
186                    }
187                } else if let Some((path, color)) =
188                    self.external_path.as_ref().zip(style.text.color)
189                {
190                    let Some(bytes) = window
191                        .use_asset::<SvgAsset>(path, cx)
192                        .and_then(|asset| asset.log_err())
193                    else {
194                        return;
195                    };
196
197                    window
198                        .paint_svg(
199                            bounds,
200                            path.clone(),
201                            Some(&bytes),
202                            transformation,
203                            color,
204                            cx,
205                        )
206                        .log_err();
207                } else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
208                    window
209                        .paint_svg(bounds, path.clone(), None, transformation, color, cx)
210                        .log_err();
211                }
212            },
213        )
214    }
215}
216
217impl IntoElement for Svg {
218    type Element = Self;
219
220    fn into_element(self) -> Self::Element {
221        self
222    }
223}
224
225impl Styled for Svg {
226    fn style(&mut self) -> &mut StyleRefinement {
227        &mut self.interactivity.base_style
228    }
229}
230
231impl InteractiveElement for Svg {
232    fn interactivity(&mut self) -> &mut Interactivity {
233        &mut self.interactivity
234    }
235}
236
237/// A rotation, scale and translation applied to an [`Svg`] when it is drawn.
238///
239/// Build one with [`Transformation::scale`], [`Transformation::rotate`] or
240/// [`Transformation::translate`], then chain the `with_*` methods to set the other parts. The
241/// transformation turns around the center of the element's bounds. Translation is in logical
242/// pixels, and rotation is in radians.
243///
244/// # Examples
245///
246/// ```
247/// # use gpui::*;
248/// let rotated = Transformation::rotate(radians(std::f32::consts::FRAC_PI_4));
249/// let shrunk = Transformation::scale(size(0.5, 0.5));
250/// assert_eq!(shrunk, Transformation::default().with_scaling(size(0.5, 0.5)));
251///
252/// let icon = svg()
253///     .path("icons/chevron.svg")
254///     .size(px(16.))
255///     .with_transformation(rotated);
256/// ```
257#[derive(Clone, Copy, Debug, PartialEq, derive_setters::Setters)]
258#[setters(generate = false)]
259pub struct Transformation {
260    /// Returns this transformation with its scale set to `scale`.
261    #[setters(generate, rename = "with_scaling")]
262    scale: Size<f32>,
263    /// Returns this transformation with its translation set to `translate`, in logical pixels.
264    #[setters(generate, rename = "with_translation")]
265    translate: Point<Pixels>,
266    /// Returns this transformation with its rotation set to `rotate`.
267    #[setters(generate, rename = "with_rotation", into)]
268    rotate: Radians,
269}
270
271impl Default for Transformation {
272    fn default() -> Self {
273        Self {
274            scale: size(1.0, 1.0),
275            translate: point(px(0.0), px(0.0)),
276            rotate: radians(0.0),
277        }
278    }
279}
280
281impl Transformation {
282    /// Creates a transformation that scales the SVG by `scale` on each axis. `1.0` keeps the size.
283    pub fn scale(scale: Size<f32>) -> Self {
284        Self {
285            scale,
286            translate: point(px(0.0), px(0.0)),
287            rotate: radians(0.0),
288        }
289    }
290
291    /// Creates a transformation that moves the SVG by `translate`, in logical pixels.
292    pub fn translate(translate: Point<Pixels>) -> Self {
293        Self {
294            scale: size(1.0, 1.0),
295            translate,
296            rotate: radians(0.0),
297        }
298    }
299
300    /// Creates a transformation that rotates the SVG around its center by `rotate`.
301    pub fn rotate(rotate: impl Into<Radians>) -> Self {
302        let rotate = rotate.into();
303        Self {
304            scale: size(1.0, 1.0),
305            translate: point(px(0.0), px(0.0)),
306            rotate,
307        }
308    }
309
310    fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
311        //Note: if you read this as a sequence of matrix multiplications, start from the bottom
312        TransformationMatrix::unit()
313            .translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
314            .rotate(self.rotate)
315            .scale(self.scale)
316            .translate(center.scale(-scale_factor))
317    }
318}
319
320enum SvgAsset {}
321
322impl Asset for SvgAsset {
323    type Source = SharedString;
324    type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
325
326    fn load(
327        source: Self::Source,
328        _cx: &mut App,
329    ) -> impl Future<Output = Self::Output> + Send + 'static {
330        async move {
331            let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
332            let bytes = Arc::from(bytes);
333            Ok(bytes)
334        }
335    }
336}