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 SVG element.
16pub struct Svg {
17    interactivity: Interactivity,
18    transformation: Option<Transformation>,
19    path: Option<SharedString>,
20    external_path: Option<SharedString>,
21    data: Option<Arc<[u8]>>,
22    data_path: Option<SharedString>,
23}
24
25/// Create a new SVG element.
26#[track_caller]
27pub fn svg() -> Svg {
28    Svg {
29        interactivity: Interactivity::new(),
30        transformation: None,
31        path: None,
32        external_path: None,
33        data: None,
34        data_path: None,
35    }
36}
37
38impl Svg {
39    /// Set the path to the SVG file for this element.
40    pub fn path(mut self, path: impl Into<SharedString>) -> Self {
41        self.path = Some(path.into());
42        self
43    }
44
45    /// Set the path to the SVG file for this element.
46    pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
47        self.external_path = Some(path.into());
48        self
49    }
50
51    /// Set the raw SVG data for this element.
52    /// The SVG will be rendered directly from the provided bytes.
53    pub fn data(mut self, data: &[u8]) -> Self {
54        // Generate a unique deterministic path based on the data hash for caching
55        let mut hasher = std::collections::hash_map::DefaultHasher::new();
56        data.hash(&mut hasher);
57        let hash = hasher.finish();
58        let path = SharedString::from(format!("__binary_svg__{}", hash));
59        self.data = Some(Arc::from(data));
60        self.data_path = Some(path);
61        self
62    }
63
64    /// Transform the SVG element with the given transformation.
65    /// Note that this won't effect the hitbox or layout of the element, only the rendering.
66    pub fn with_transformation(mut self, transformation: Transformation) -> Self {
67        self.transformation = Some(transformation);
68        self
69    }
70}
71
72impl Element for Svg {
73    type RequestLayoutState = ();
74    type PrepaintState = Option<Hitbox>;
75
76    fn id(&self) -> Option<crate::ElementId> {
77        self.interactivity.element_id.clone()
78    }
79
80    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
81        self.interactivity.source_location()
82    }
83
84    fn request_layout(
85        &mut self,
86        global_id: Option<&GlobalElementId>,
87        inspector_id: Option<&InspectorElementId>,
88        window: &mut Window,
89        cx: &mut App,
90    ) -> (LayoutId, Self::RequestLayoutState) {
91        let layout_id = self.interactivity.request_layout(
92            global_id,
93            inspector_id,
94            window,
95            cx,
96            |style, window, cx| window.request_layout(style, None, cx),
97        );
98        (layout_id, ())
99    }
100
101    fn prepaint(
102        &mut self,
103        global_id: Option<&GlobalElementId>,
104        inspector_id: Option<&InspectorElementId>,
105        bounds: Bounds<Pixels>,
106        _request_layout: &mut Self::RequestLayoutState,
107        window: &mut Window,
108        cx: &mut App,
109    ) -> Option<Hitbox> {
110        self.interactivity.prepaint(
111            global_id,
112            inspector_id,
113            bounds,
114            bounds.size,
115            window,
116            cx,
117            |_, _, hitbox, _, _| hitbox,
118        )
119    }
120
121    fn paint(
122        &mut self,
123        global_id: Option<&GlobalElementId>,
124        inspector_id: Option<&InspectorElementId>,
125        bounds: Bounds<Pixels>,
126        _request_layout: &mut Self::RequestLayoutState,
127        hitbox: &mut Option<Hitbox>,
128        window: &mut Window,
129        cx: &mut App,
130    ) where
131        Self: Sized,
132    {
133        self.interactivity.paint(
134            global_id,
135            inspector_id,
136            bounds,
137            hitbox.as_ref(),
138            window,
139            cx,
140            |style, window, cx| {
141                let transformation = self
142                    .transformation
143                    .as_ref()
144                    .map(|transformation| {
145                        transformation.into_matrix(bounds.center(), window.scale_factor())
146                    })
147                    .unwrap_or_default();
148
149                if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) {
150                    if let Some(color) = style.text.color {
151                        window
152                            .paint_svg(
153                                bounds,
154                                path.clone(),
155                                Some(&**data),
156                                transformation,
157                                color,
158                                cx,
159                            )
160                            .log_err();
161                    }
162                } else if let Some((path, color)) =
163                    self.external_path.as_ref().zip(style.text.color)
164                {
165                    let Some(bytes) = window
166                        .use_asset::<SvgAsset>(path, cx)
167                        .and_then(|asset| asset.log_err())
168                    else {
169                        return;
170                    };
171
172                    window
173                        .paint_svg(
174                            bounds,
175                            path.clone(),
176                            Some(&bytes),
177                            transformation,
178                            color,
179                            cx,
180                        )
181                        .log_err();
182                } else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
183                    window
184                        .paint_svg(bounds, path.clone(), None, transformation, color, cx)
185                        .log_err();
186                }
187            },
188        )
189    }
190}
191
192impl IntoElement for Svg {
193    type Element = Self;
194
195    fn into_element(self) -> Self::Element {
196        self
197    }
198}
199
200impl Styled for Svg {
201    fn style(&mut self) -> &mut StyleRefinement {
202        &mut self.interactivity.base_style
203    }
204}
205
206impl InteractiveElement for Svg {
207    fn interactivity(&mut self) -> &mut Interactivity {
208        &mut self.interactivity
209    }
210}
211
212/// A transformation to apply to an SVG element.
213#[derive(Clone, Copy, Debug, PartialEq)]
214pub struct Transformation {
215    scale: Size<f32>,
216    translate: Point<Pixels>,
217    rotate: Radians,
218}
219
220impl Default for Transformation {
221    fn default() -> Self {
222        Self {
223            scale: size(1.0, 1.0),
224            translate: point(px(0.0), px(0.0)),
225            rotate: radians(0.0),
226        }
227    }
228}
229
230impl Transformation {
231    /// Create a new Transformation with the specified scale along each axis.
232    pub fn scale(scale: Size<f32>) -> Self {
233        Self {
234            scale,
235            translate: point(px(0.0), px(0.0)),
236            rotate: radians(0.0),
237        }
238    }
239
240    /// Create a new Transformation with the specified translation.
241    pub fn translate(translate: Point<Pixels>) -> Self {
242        Self {
243            scale: size(1.0, 1.0),
244            translate,
245            rotate: radians(0.0),
246        }
247    }
248
249    /// Create a new Transformation with the specified rotation in radians.
250    pub fn rotate(rotate: impl Into<Radians>) -> Self {
251        let rotate = rotate.into();
252        Self {
253            scale: size(1.0, 1.0),
254            translate: point(px(0.0), px(0.0)),
255            rotate,
256        }
257    }
258
259    /// Update the scaling factor of this transformation.
260    pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
261        self.scale = scale;
262        self
263    }
264
265    /// Update the translation value of this transformation.
266    pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
267        self.translate = translate;
268        self
269    }
270
271    /// Update the rotation angle of this transformation.
272    pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
273        self.rotate = rotate.into();
274        self
275    }
276
277    fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
278        //Note: if you read this as a sequence of matrix multiplications, start from the bottom
279        TransformationMatrix::unit()
280            .translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
281            .rotate(self.rotate)
282            .scale(self.scale)
283            .translate(center.scale(-scale_factor))
284    }
285}
286
287enum SvgAsset {}
288
289impl Asset for SvgAsset {
290    type Source = SharedString;
291    type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
292
293    fn load(
294        source: Self::Source,
295        _cx: &mut App,
296    ) -> impl Future<Output = Self::Output> + Send + 'static {
297        async move {
298            let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
299            let bytes = Arc::from(bytes);
300            Ok(bytes)
301        }
302    }
303}