Skip to main content

gpui_authoring/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, InteractiveElement, Interactivity,
10    IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size, StyleRefinement, Styled,
11    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        window: &mut Window,
88        cx: &mut App,
89    ) -> (LayoutId, Self::RequestLayoutState) {
90        let layout_id =
91            self.interactivity
92                .request_layout(global_id, window, cx, |style, window, cx| {
93                    window.request_layout(style, None, cx)
94                });
95        (layout_id, ())
96    }
97
98    fn prepaint(
99        &mut self,
100        global_id: Option<&GlobalElementId>,
101        bounds: Bounds<Pixels>,
102        _request_layout: &mut Self::RequestLayoutState,
103        window: &mut Window,
104        cx: &mut App,
105    ) -> Option<Hitbox> {
106        self.interactivity.prepaint(
107            global_id,
108            bounds,
109            bounds.size,
110            window,
111            cx,
112            |_, _, hitbox, _, _| hitbox,
113        )
114    }
115
116    fn paint(
117        &mut self,
118        global_id: Option<&GlobalElementId>,
119        bounds: Bounds<Pixels>,
120        _request_layout: &mut Self::RequestLayoutState,
121        hitbox: &mut Option<Hitbox>,
122        window: &mut Window,
123        cx: &mut App,
124    ) where
125        Self: Sized,
126    {
127        self.interactivity.paint(
128            global_id,
129            bounds,
130            hitbox.as_ref(),
131            window,
132            cx,
133            |style, window, cx| {
134                let transformation = self
135                    .transformation
136                    .as_ref()
137                    .map(|transformation| {
138                        transformation.into_matrix(bounds.center(), window.scale_factor())
139                    })
140                    .unwrap_or_default();
141
142                if let Some((data, path)) = self.data.as_ref().zip(self.data_path.as_ref()) {
143                    if let Some(color) = style.text.color {
144                        window
145                            .paint_svg(
146                                bounds,
147                                path.clone(),
148                                Some(&**data),
149                                transformation,
150                                color,
151                                cx,
152                            )
153                            .log_err();
154                    }
155                } else if let Some((path, color)) =
156                    self.external_path.as_ref().zip(style.text.color)
157                {
158                    let Some(bytes) = window
159                        .use_asset::<SvgAsset>(path, cx)
160                        .and_then(|asset| asset.log_err())
161                    else {
162                        return;
163                    };
164
165                    window
166                        .paint_svg(
167                            bounds,
168                            path.clone(),
169                            Some(&bytes),
170                            transformation,
171                            color,
172                            cx,
173                        )
174                        .log_err();
175                } else if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
176                    window
177                        .paint_svg(bounds, path.clone(), None, transformation, color, cx)
178                        .log_err();
179                }
180            },
181        )
182    }
183}
184
185impl IntoElement for Svg {
186    type Element = Self;
187
188    fn into_element(self) -> Self::Element {
189        self
190    }
191}
192
193impl Styled for Svg {
194    fn style(&mut self) -> &mut StyleRefinement {
195        &mut self.interactivity.base_style
196    }
197}
198
199impl InteractiveElement for Svg {
200    fn interactivity(&mut self) -> &mut Interactivity {
201        &mut self.interactivity
202    }
203}
204
205/// A transformation to apply to an SVG element.
206#[derive(Clone, Copy, Debug, PartialEq)]
207pub struct Transformation {
208    scale: Size<f32>,
209    translate: Point<Pixels>,
210    rotate: Radians,
211}
212
213impl Default for Transformation {
214    fn default() -> Self {
215        Self {
216            scale: size(1.0, 1.0),
217            translate: point(px(0.0), px(0.0)),
218            rotate: radians(0.0),
219        }
220    }
221}
222
223impl Transformation {
224    /// Create a new Transformation with the specified scale along each axis.
225    pub fn scale(scale: Size<f32>) -> Self {
226        Self {
227            scale,
228            translate: point(px(0.0), px(0.0)),
229            rotate: radians(0.0),
230        }
231    }
232
233    /// Create a new Transformation with the specified translation.
234    pub fn translate(translate: Point<Pixels>) -> Self {
235        Self {
236            scale: size(1.0, 1.0),
237            translate,
238            rotate: radians(0.0),
239        }
240    }
241
242    /// Create a new Transformation with the specified rotation in radians.
243    pub fn rotate(rotate: impl Into<Radians>) -> Self {
244        let rotate = rotate.into();
245        Self {
246            scale: size(1.0, 1.0),
247            translate: point(px(0.0), px(0.0)),
248            rotate,
249        }
250    }
251
252    /// Update the scaling factor of this transformation.
253    pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
254        self.scale = scale;
255        self
256    }
257
258    /// Update the translation value of this transformation.
259    pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
260        self.translate = translate;
261        self
262    }
263
264    /// Update the rotation angle of this transformation.
265    pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
266        self.rotate = rotate.into();
267        self
268    }
269
270    fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
271        //Note: if you read this as a sequence of matrix multiplications, start from the bottom
272        TransformationMatrix::unit()
273            .translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
274            .rotate(self.rotate)
275            .scale(self.scale)
276            .translate(center.scale(-scale_factor))
277    }
278}
279
280enum SvgAsset {}
281
282impl Asset for SvgAsset {
283    type Source = SharedString;
284    type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
285
286    fn load(
287        source: Self::Source,
288        _cx: &mut App,
289    ) -> impl Future<Output = Self::Output> + Send + 'static {
290        async move {
291            let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
292            let bytes = Arc::from(bytes);
293            Ok(bytes)
294        }
295    }
296}