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