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}