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