svg.rs

  1use crate::{
  2    geometry::Negate as _, point, px, radians, size, Bounds, Element, ElementContext, Hitbox,
  3    InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString,
  4    Size, StyleRefinement, Styled, TransformationMatrix,
  5};
  6use util::ResultExt;
  7
  8/// An SVG element.
  9pub struct Svg {
 10    interactivity: Interactivity,
 11    transformation: Option<Transformation>,
 12    path: Option<SharedString>,
 13}
 14
 15/// Create a new SVG element.
 16pub fn svg() -> Svg {
 17    Svg {
 18        interactivity: Interactivity::default(),
 19        transformation: None,
 20        path: None,
 21    }
 22}
 23
 24impl Svg {
 25    /// Set the path to the SVG file for this element.
 26    pub fn path(mut self, path: impl Into<SharedString>) -> Self {
 27        self.path = Some(path.into());
 28        self
 29    }
 30
 31    /// Transform the SVG element with the given transformation.
 32    /// Note that this won't effect the hitbox or layout of the element, only the rendering.
 33    pub fn with_transformation(mut self, transformation: Transformation) -> Self {
 34        self.transformation = Some(transformation);
 35        self
 36    }
 37}
 38
 39impl Element for Svg {
 40    type RequestLayoutState = ();
 41    type PrepaintState = Option<Hitbox>;
 42
 43    fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
 44        let layout_id = self
 45            .interactivity
 46            .request_layout(cx, |style, cx| cx.request_layout(&style, None));
 47        (layout_id, ())
 48    }
 49
 50    fn prepaint(
 51        &mut self,
 52        bounds: Bounds<Pixels>,
 53        _request_layout: &mut Self::RequestLayoutState,
 54        cx: &mut ElementContext,
 55    ) -> Option<Hitbox> {
 56        self.interactivity
 57            .prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
 58    }
 59
 60    fn paint(
 61        &mut self,
 62        bounds: Bounds<Pixels>,
 63        _request_layout: &mut Self::RequestLayoutState,
 64        hitbox: &mut Option<Hitbox>,
 65        cx: &mut ElementContext,
 66    ) where
 67        Self: Sized,
 68    {
 69        self.interactivity
 70            .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
 71                if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
 72                    let transformation = self
 73                        .transformation
 74                        .as_ref()
 75                        .map(|transformation| {
 76                            transformation.into_matrix(bounds.center(), cx.scale_factor())
 77                        })
 78                        .unwrap_or_default();
 79
 80                    cx.paint_svg(bounds, path.clone(), transformation, color)
 81                        .log_err();
 82                }
 83            })
 84    }
 85}
 86
 87impl IntoElement for Svg {
 88    type Element = Self;
 89
 90    fn into_element(self) -> Self::Element {
 91        self
 92    }
 93}
 94
 95impl Styled for Svg {
 96    fn style(&mut self) -> &mut StyleRefinement {
 97        &mut self.interactivity.base_style
 98    }
 99}
100
101impl InteractiveElement for Svg {
102    fn interactivity(&mut self) -> &mut Interactivity {
103        &mut self.interactivity
104    }
105}
106
107/// A transformation to apply to an SVG element.
108#[derive(Clone, Copy, Debug, PartialEq)]
109pub struct Transformation {
110    scale: Size<f32>,
111    translate: Point<Pixels>,
112    rotate: Radians,
113}
114
115impl Default for Transformation {
116    fn default() -> Self {
117        Self {
118            scale: size(1.0, 1.0),
119            translate: point(px(0.0), px(0.0)),
120            rotate: radians(0.0),
121        }
122    }
123}
124
125impl Transformation {
126    /// Create a new Transformation with the specified scale along each axis.
127    pub fn scale(scale: Size<f32>) -> Self {
128        Self {
129            scale,
130            translate: point(px(0.0), px(0.0)),
131            rotate: radians(0.0),
132        }
133    }
134
135    /// Create a new Transformation with the specified translation.
136    pub fn translate(translate: Point<Pixels>) -> Self {
137        Self {
138            scale: size(1.0, 1.0),
139            translate,
140            rotate: radians(0.0),
141        }
142    }
143
144    /// Create a new Transformation with the specified rotation in radians.
145    pub fn rotate(rotate: impl Into<Radians>) -> Self {
146        let rotate = rotate.into();
147        Self {
148            scale: size(1.0, 1.0),
149            translate: point(px(0.0), px(0.0)),
150            rotate,
151        }
152    }
153
154    /// Update the scaling factor of this transformation.
155    pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
156        self.scale = scale;
157        self
158    }
159
160    /// Update the translation value of this transformation.
161    pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
162        self.translate = translate;
163        self
164    }
165
166    /// Update the rotation angle of this transformation.
167    pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
168        self.rotate = rotate.into();
169        self
170    }
171
172    fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
173        //Note: if you read this as a sequence of matrix mulitplications, start from the bottom
174        TransformationMatrix::unit()
175            .translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
176            .rotate(self.rotate)
177            .scale(self.scale)
178            .translate(center.scale(scale_factor).negate())
179    }
180}