1use floem_reactive::Effect;
2use floem_renderer::{
3 Renderer,
4 usvg::{self, Tree},
5};
6use peniko::{
7 Brush, GradientKind, LinearGradientPosition,
8 kurbo::{Point, Size},
9};
10use sha2::{Digest, Sha256};
11
12use crate::{
13 prop, prop_extractor,
14 style::{Style, TextColor},
15 style_class,
16 view::View,
17 view::ViewId,
18};
19
20use super::Decorators;
21
22prop!(pub SvgColor: Option<Brush> {} = None);
23
24prop_extractor! {
25 SvgStyle {
26 svg_color: SvgColor,
27 text_color: TextColor,
28 }
29}
30
31pub struct Svg {
32 id: ViewId,
33 svg_tree: Option<Tree>,
34 svg_hash: Option<Vec<u8>>,
35 svg_style: SvgStyle,
36 svg_string: String,
37 svg_css: Option<String>,
38 css_prop: Option<Box<dyn SvgCssPropExtractor>>,
39 aspect_ratio: f32,
40}
41
42style_class!(pub SvgClass);
43
44pub struct SvgStrFn {
45 str_fn: Box<dyn Fn() -> String>,
46}
47
48impl<T, F> From<F> for SvgStrFn
49where
50 F: Fn() -> T + 'static,
51 T: Into<String>,
52{
53 fn from(value: F) -> Self {
54 SvgStrFn {
55 str_fn: Box::new(move || value().into()),
56 }
57 }
58}
59
60impl From<String> for SvgStrFn {
61 fn from(value: String) -> Self {
62 SvgStrFn {
63 str_fn: Box::new(move || value.clone()),
64 }
65 }
66}
67
68impl From<&str> for SvgStrFn {
69 fn from(value: &str) -> Self {
70 let value = value.to_string();
71 SvgStrFn {
72 str_fn: Box::new(move || value.clone()),
73 }
74 }
75}
76
77pub trait SvgCssPropExtractor {
78 fn read_custom(&mut self, cx: &mut crate::context::StyleCx) -> bool;
79 fn css_string(&self) -> String;
80}
81
82#[derive(Debug, Clone)]
83pub enum SvgOrStyle {
84 Svg(String),
85 Style(String),
86}
87
88impl Svg {
89 pub fn update_value<S: Into<String>>(self, svg_str: impl Fn() -> S + 'static) -> Self {
90 let id = self.id;
91 Effect::new(move |_| {
92 let new_svg_str = svg_str();
93 id.update_state(SvgOrStyle::Svg(new_svg_str.into()));
94 });
95 self
96 }
97
98 pub fn set_css_extractor(mut self, css: impl SvgCssPropExtractor + 'static) -> Self {
99 self.css_prop = Some(Box::new(css));
100 self
101 }
102}
103
104pub fn svg(svg_str_fn: impl Into<SvgStrFn> + 'static) -> Svg {
105 let id = ViewId::new();
106 let svg_str_fn: SvgStrFn = svg_str_fn.into();
107 Effect::new(move |_| {
108 let new_svg_str = (svg_str_fn.str_fn)();
109 id.update_state(SvgOrStyle::Svg(new_svg_str));
110 });
111 Svg {
112 id,
113 svg_tree: None,
114 svg_hash: None,
115 svg_style: Default::default(),
116 svg_string: Default::default(),
117 css_prop: None,
118 svg_css: None,
119 aspect_ratio: 1.,
120 }
121 .class(SvgClass)
122}
123
124impl View for Svg {
125 fn id(&self) -> ViewId {
126 self.id
127 }
128
129 fn view_style(&self) -> Option<crate::style::Style> {
130 if !self.aspect_ratio.is_nan() {
131 Some(Style::new().aspect_ratio(self.aspect_ratio))
132 } else {
133 None
134 }
135 }
136
137 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
138 let style = cx.style();
139 self.svg_style.read_style(cx, &style);
140 if let Some(tree) = &self.svg_tree {
141 let size = tree.size();
142 let aspect_ratio = size.width() / size.height();
143 if self.aspect_ratio != aspect_ratio {
144 self.aspect_ratio = aspect_ratio;
145 self.id.request_style();
146 }
147 }
148 if let Some(prop_reader) = &mut self.css_prop {
149 if prop_reader.read_custom(cx) {
150 self.id
151 .update_state(SvgOrStyle::Style(prop_reader.css_string()));
152 }
153 }
154 }
155
156 fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
157 if let Ok(state) = state.downcast::<SvgOrStyle>() {
158 let (text, style) = match *state {
159 SvgOrStyle::Svg(text) => {
160 self.svg_string = text;
161 (&self.svg_string, self.svg_css.clone())
162 }
163 SvgOrStyle::Style(css) => {
164 self.svg_css = Some(css);
165 (&self.svg_string, self.svg_css.clone())
166 }
167 };
168
169 self.svg_tree = Tree::from_str(
170 text,
171 &usvg::Options {
172 style_sheet: style,
173 ..Default::default()
174 },
175 )
176 .ok();
177
178 let mut hasher = Sha256::new();
179 hasher.update(text);
180 let hash = hasher.finalize().to_vec();
181 self.svg_hash = Some(hash);
182
183 self.id.request_layout();
184 }
185 }
186
187 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
188 if let Some(tree) = self.svg_tree.as_ref() {
189 let hash = self.svg_hash.as_ref().unwrap();
190 let layout = self.id.get_layout().unwrap_or_default();
191 let rect = Size::new(layout.size.width as f64, layout.size.height as f64).to_rect();
192 let color = if let Some(brush) = self.svg_style.svg_color() {
193 Some(brush)
194 } else {
195 self.svg_style.text_color().map(Brush::Solid)
196 };
197 cx.draw_svg(crate::RendererSvg { tree, hash }, rect, color.as_ref());
198 }
199 }
200}
201
202pub fn brush_to_css_string(brush: &Brush) -> String {
203 match brush {
204 Brush::Solid(color) => {
205 let r = (color.components[0] * 255.0).round() as u8;
206 let g = (color.components[1] * 255.0).round() as u8;
207 let b = (color.components[2] * 255.0).round() as u8;
208 let a = color.components[3];
209
210 if a < 1.0 {
211 format!("rgba({r}, {g}, {b}, {a})")
212 } else {
213 format!("#{r:02x}{g:02x}{b:02x}")
214 }
215 }
216 Brush::Gradient(gradient) => {
217 match &gradient.kind {
218 GradientKind::Linear(LinearGradientPosition { start, end }) => {
219 let angle_degrees = calculate_angle(start, end);
220
221 let mut css = format!("linear-gradient({angle_degrees}deg, ");
222
223 for (i, stop) in gradient.stops.iter().enumerate() {
224 let color = &stop.color;
225 let r = (color.components[0] * 255.0).round() as u8;
226 let g = (color.components[1] * 255.0).round() as u8;
227 let b = (color.components[2] * 255.0).round() as u8;
228 let a = color.components[3];
229
230 let color_str = if a < 1.0 {
231 format!("rgba({r}, {g}, {b}, {a})")
232 } else {
233 format!("#{r:02x}{g:02x}{b:02x}")
234 };
235
236 css.push_str(&format!("{} {}%", color_str, (stop.offset * 100.0).round()));
237
238 if i < gradient.stops.len() - 1 {
239 css.push_str(", ");
240 }
241 }
242
243 css.push(')');
244 css
245 }
246
247 _ => "currentColor".to_string(), }
249 }
250 Brush::Image(_) => "currentColor".to_string(),
251 }
252}
253
254fn calculate_angle(start: &Point, end: &Point) -> f64 {
255 let angle_rad = (end.y - start.y).atan2(end.x - start.x);
256
257 let mut angle_deg = 90.0 - angle_rad.to_degrees();
259
260 if angle_deg < 0.0 {
262 angle_deg += 360.0;
263 }
264
265 angle_deg
266}