Skip to main content

gpui/elements/
svg.rs

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