svg.rs

  1use crate::{
  2    geometry::Negate as _, point, px, radians, size, Bounds, Element, GlobalElementId, Hitbox,
  3    InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString,
  4    Size, StyleRefinement, Styled, TransformationMatrix, WindowContext,
  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 id(&self) -> Option<crate::ElementId> {
 44        self.interactivity.element_id.clone()
 45    }
 46
 47    fn request_layout(
 48        &mut self,
 49        global_id: Option<&GlobalElementId>,
 50        cx: &mut WindowContext,
 51    ) -> (LayoutId, Self::RequestLayoutState) {
 52        let layout_id = self
 53            .interactivity
 54            .request_layout(global_id, cx, |style, cx| cx.request_layout(style, None));
 55        (layout_id, ())
 56    }
 57
 58    fn prepaint(
 59        &mut self,
 60        global_id: Option<&GlobalElementId>,
 61        bounds: Bounds<Pixels>,
 62        _request_layout: &mut Self::RequestLayoutState,
 63        cx: &mut WindowContext,
 64    ) -> Option<Hitbox> {
 65        self.interactivity
 66            .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
 67    }
 68
 69    fn paint(
 70        &mut self,
 71        global_id: Option<&GlobalElementId>,
 72        bounds: Bounds<Pixels>,
 73        _request_layout: &mut Self::RequestLayoutState,
 74        hitbox: &mut Option<Hitbox>,
 75        cx: &mut WindowContext,
 76    ) where
 77        Self: Sized,
 78    {
 79        self.interactivity
 80            .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
 81                if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
 82                    let transformation = self
 83                        .transformation
 84                        .as_ref()
 85                        .map(|transformation| {
 86                            transformation.into_matrix(bounds.center(), cx.scale_factor())
 87                        })
 88                        .unwrap_or_default();
 89
 90                    cx.paint_svg(bounds, path.clone(), transformation, color)
 91                        .log_err();
 92                }
 93            })
 94    }
 95}
 96
 97impl IntoElement for Svg {
 98    type Element = Self;
 99
100    fn into_element(self) -> Self::Element {
101        self
102    }
103}
104
105impl Styled for Svg {
106    fn style(&mut self) -> &mut StyleRefinement {
107        &mut self.interactivity.base_style
108    }
109}
110
111impl InteractiveElement for Svg {
112    fn interactivity(&mut self) -> &mut Interactivity {
113        &mut self.interactivity
114    }
115}
116
117/// A transformation to apply to an SVG element.
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct Transformation {
120    scale: Size<f32>,
121    translate: Point<Pixels>,
122    rotate: Radians,
123}
124
125impl Default for Transformation {
126    fn default() -> Self {
127        Self {
128            scale: size(1.0, 1.0),
129            translate: point(px(0.0), px(0.0)),
130            rotate: radians(0.0),
131        }
132    }
133}
134
135impl Transformation {
136    /// Create a new Transformation with the specified scale along each axis.
137    pub fn scale(scale: Size<f32>) -> Self {
138        Self {
139            scale,
140            translate: point(px(0.0), px(0.0)),
141            rotate: radians(0.0),
142        }
143    }
144
145    /// Create a new Transformation with the specified translation.
146    pub fn translate(translate: Point<Pixels>) -> Self {
147        Self {
148            scale: size(1.0, 1.0),
149            translate,
150            rotate: radians(0.0),
151        }
152    }
153
154    /// Create a new Transformation with the specified rotation in radians.
155    pub fn rotate(rotate: impl Into<Radians>) -> Self {
156        let rotate = rotate.into();
157        Self {
158            scale: size(1.0, 1.0),
159            translate: point(px(0.0), px(0.0)),
160            rotate,
161        }
162    }
163
164    /// Update the scaling factor of this transformation.
165    pub fn with_scaling(mut self, scale: Size<f32>) -> Self {
166        self.scale = scale;
167        self
168    }
169
170    /// Update the translation value of this transformation.
171    pub fn with_translation(mut self, translate: Point<Pixels>) -> Self {
172        self.translate = translate;
173        self
174    }
175
176    /// Update the rotation angle of this transformation.
177    pub fn with_rotation(mut self, rotate: impl Into<Radians>) -> Self {
178        self.rotate = rotate.into();
179        self
180    }
181
182    fn into_matrix(self, center: Point<Pixels>, scale_factor: f32) -> TransformationMatrix {
183        //Note: if you read this as a sequence of matrix multiplications, start from the bottom
184        TransformationMatrix::unit()
185            .translate(center.scale(scale_factor) + self.translate.scale(scale_factor))
186            .rotate(self.rotate)
187            .scale(self.scale)
188            .translate(center.scale(scale_factor).negate())
189    }
190}