1use std::{cell::RefCell, rc::Rc};
2
3use floem_reactive::Effect;
4use floem_renderer::{
5 Renderer,
6 usvg::{self, Tree},
7};
8use peniko::{
9 Brush, GradientKind, LinearGradientPosition,
10 kurbo::{Point, Size},
11};
12use sha2::{Digest, Sha256};
13
14use crate::{
15 prop, prop_extractor,
16 style::{Style, TextColor},
17 style_class,
18 view::ViewId,
19 view::{LayoutNodeCx, MeasureFn, View},
20};
21
22use super::Decorators;
23
24prop!(pub SvgColor: Option<Brush> {} = None);
25
26prop_extractor! {
27 SvgStyle {
28 svg_color: SvgColor,
29 text_color: TextColor,
30 }
31}
32
33#[derive(Clone)]
34struct SvgLayoutData {
35 natural_width: f32,
36 natural_height: f32,
37}
38
39impl SvgLayoutData {
40 fn new() -> Self {
41 Self {
42 natural_width: 0.0,
43 natural_height: 0.0,
44 }
45 }
46
47 fn set_size(&mut self, width: f32, height: f32) {
48 self.natural_width = width;
49 self.natural_height = height;
50 }
51
52 fn aspect_ratio(&self) -> f32 {
53 if self.natural_height == 0.0 {
54 1.0
55 } else {
56 self.natural_width / self.natural_height
57 }
58 }
59
60 fn create_taffy_layout_fn(layout_data: Rc<RefCell<Self>>) -> Box<MeasureFn> {
61 Box::new(
62 move |known_dimensions, available_space, _node_id, style, _measure_ctx| {
63 use taffy::*;
64
65 let data = layout_data.borrow();
66 let natural_width = data.natural_width;
67 let natural_height = data.natural_height;
68 let natural_aspect_ratio = data.aspect_ratio();
69 let explicit_aspect_ratio = style
70 .aspect_ratio
71 .filter(|ratio| ratio.is_finite() && *ratio > 0.0)
72 .unwrap_or(natural_aspect_ratio);
73
74 if let (Some(width), Some(height)) =
75 (known_dimensions.width, known_dimensions.height)
76 {
77 return Size { width, height };
78 }
79
80 if let Some(width) = known_dimensions.width {
81 let height = known_dimensions
82 .height
83 .unwrap_or_else(|| width / explicit_aspect_ratio);
84 return Size { width, height };
85 }
86
87 if let Some(height) = known_dimensions.height {
88 let width = if explicit_aspect_ratio == 0.0 {
89 0.0
90 } else {
91 height * explicit_aspect_ratio
92 };
93 return Size { width, height };
94 }
95
96 if natural_width > 0.0 && natural_height > 0.0 {
97 return Size {
98 width: natural_width,
99 height: natural_height,
100 };
101 }
102
103 match (available_space.width, available_space.height) {
104 (AvailableSpace::Definite(width), _) => Size {
105 width: if natural_width == 0.0 {
106 width
107 } else {
108 natural_width
109 },
110 height: if natural_width > 0.0 && explicit_aspect_ratio > 0.0 {
111 natural_width / explicit_aspect_ratio
112 } else {
113 0.0
114 },
115 },
116 (_, AvailableSpace::Definite(height)) => Size {
117 width: if natural_height > 0.0 && explicit_aspect_ratio > 0.0 {
118 height * explicit_aspect_ratio
119 } else {
120 0.0
121 },
122 height: if natural_height == 0.0 {
123 height
124 } else {
125 natural_height
126 },
127 },
128 _ => Size {
129 width: natural_width,
130 height: natural_height,
131 },
132 }
133 },
134 )
135 }
136}
137
138pub struct Svg {
139 id: ViewId,
140 svg_tree: Option<Tree>,
141 svg_hash: Option<Vec<u8>>,
142 svg_style: SvgStyle,
143 svg_string: String,
144 svg_css: Option<String>,
145 css_prop: Option<Box<dyn SvgCssPropExtractor>>,
146 aspect_ratio: f32,
147 layout_data: Rc<RefCell<SvgLayoutData>>,
148}
149
150style_class!(pub SvgClass);
151
152pub struct SvgStrFn {
153 str_fn: Box<dyn Fn() -> String>,
154}
155
156impl<T, F> From<F> for SvgStrFn
157where
158 F: Fn() -> T + 'static,
159 T: Into<String>,
160{
161 fn from(value: F) -> Self {
162 SvgStrFn {
163 str_fn: Box::new(move || value().into()),
164 }
165 }
166}
167
168impl From<String> for SvgStrFn {
169 fn from(value: String) -> Self {
170 SvgStrFn {
171 str_fn: Box::new(move || value.clone()),
172 }
173 }
174}
175
176impl From<&str> for SvgStrFn {
177 fn from(value: &str) -> Self {
178 let value = value.to_string();
179 SvgStrFn {
180 str_fn: Box::new(move || value.clone()),
181 }
182 }
183}
184
185pub trait SvgCssPropExtractor {
186 fn read_custom(&mut self, cx: &mut crate::context::StyleCx) -> bool;
187 fn css_string(&self) -> String;
188}
189
190#[derive(Debug, Clone)]
191pub enum SvgOrStyle {
192 Svg(String),
193 Style(String),
194}
195
196impl Svg {
197 pub fn update_value<S: Into<String>>(self, svg_str: impl Fn() -> S + 'static) -> Self {
198 let id = self.id;
199 Effect::new(move |_| {
200 let new_svg_str = svg_str();
201 id.update_state(SvgOrStyle::Svg(new_svg_str.into()));
202 });
203 self
204 }
205
206 pub fn set_css_extractor(mut self, css: impl SvgCssPropExtractor + 'static) -> Self {
207 self.css_prop = Some(Box::new(css));
208 self
209 }
210}
211
212pub fn svg(svg_str_fn: impl Into<SvgStrFn> + 'static) -> Svg {
213 let id = ViewId::new();
214 let svg_str_fn: SvgStrFn = svg_str_fn.into();
215 Effect::new(move |_| {
216 let new_svg_str = (svg_str_fn.str_fn)();
217 id.update_state(SvgOrStyle::Svg(new_svg_str));
218 });
219 let layout_data = Rc::new(RefCell::new(SvgLayoutData::new()));
220 let mut svg = Svg {
221 id,
222 svg_tree: None,
223 svg_hash: None,
224 svg_style: Default::default(),
225 svg_string: Default::default(),
226 css_prop: None,
227 svg_css: None,
228 aspect_ratio: 1.,
229 layout_data,
230 };
231 svg.set_taffy_layout();
232 svg.class(SvgClass)
233}
234
235impl Svg {
236 fn set_taffy_layout(&mut self) {
237 let taffy_node = self.id.taffy_node();
238 let taffy = self.id.taffy();
239 let layout_fn = SvgLayoutData::create_taffy_layout_fn(self.layout_data.clone());
240 let _ = taffy.borrow_mut().set_node_context(
241 taffy_node,
242 Some(LayoutNodeCx::Custom {
243 measure: layout_fn,
244 finalize: None,
245 }),
246 );
247 }
248}
249
250impl View for Svg {
251 fn id(&self) -> ViewId {
252 self.id
253 }
254
255 fn view_style(&self) -> Option<crate::style::Style> {
256 if !self.aspect_ratio.is_nan() {
257 Some(Style::new().aspect_ratio(self.aspect_ratio))
258 } else {
259 None
260 }
261 }
262
263 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
264 let style = cx.style();
265 self.svg_style.read_style(cx, &style);
266 if let Some(tree) = &self.svg_tree {
267 let size = tree.size();
268 let aspect_ratio = size.width() / size.height();
269 if self.aspect_ratio != aspect_ratio {
270 self.aspect_ratio = aspect_ratio;
271 }
273 }
274 if let Some(prop_reader) = &mut self.css_prop
275 && prop_reader.read_custom(cx)
276 {
277 self.id
278 .update_state(SvgOrStyle::Style(prop_reader.css_string()));
279 }
280 }
281
282 fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
283 if let Ok(state) = state.downcast::<SvgOrStyle>() {
284 let (text, style) = match *state {
285 SvgOrStyle::Svg(text) => (text, self.svg_css.clone()),
286 SvgOrStyle::Style(css) => (self.svg_string.clone(), Some(css)),
287 };
288
289 if text == self.svg_string && style == self.svg_css {
290 return;
291 }
292
293 self.svg_string = text.clone();
294 self.svg_css = style.clone();
295
296 let svg_tree = Tree::from_str(
297 text.as_str(),
298 &usvg::Options {
299 style_sheet: style,
300 ..Default::default()
301 },
302 )
303 .ok();
304 {
305 let mut layout_data = self.layout_data.borrow_mut();
306 if let Some(tree) = svg_tree.as_ref() {
307 let size = tree.size();
308 layout_data.set_size(size.width(), size.height());
309 } else {
310 layout_data.set_size(0.0, 0.0);
311 }
312 }
313 self.aspect_ratio = svg_tree.as_ref().map_or(f32::NAN, |tree| {
314 let size = tree.size();
315 let width = size.width();
316 let height = size.height();
317 if height == 0.0 {
318 f32::NAN
319 } else {
320 width / height
321 }
322 });
323 self.svg_tree = svg_tree;
324
325 let mut hasher = Sha256::new();
326 hasher.update(text);
327 let hash = hasher.finalize().to_vec();
328 self.svg_hash = Some(hash);
329
330 self.id.request_layout();
331 self.id.request_paint();
332 }
333 }
334
335 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
336 if let Some(tree) = self.svg_tree.as_ref() {
337 let hash = self.svg_hash.as_ref().unwrap();
338 let layout = self.id.get_layout().unwrap_or_default();
339 let rect = Size::new(layout.size.width as f64, layout.size.height as f64).to_rect();
340 let color = if let Some(brush) = self.svg_style.svg_color() {
341 Some(brush)
342 } else {
343 self.svg_style.text_color().map(Brush::Solid)
344 };
345 cx.draw_svg(crate::RendererSvg { tree, hash }, rect, color.as_ref());
346 }
347 }
348}
349
350pub fn brush_to_css_string(brush: &Brush) -> String {
351 match brush {
352 Brush::Solid(color) => {
353 let r = (color.components[0] * 255.0).round() as u8;
354 let g = (color.components[1] * 255.0).round() as u8;
355 let b = (color.components[2] * 255.0).round() as u8;
356 let a = color.components[3];
357
358 if a < 1.0 {
359 format!("rgba({r}, {g}, {b}, {a})")
360 } else {
361 format!("#{r:02x}{g:02x}{b:02x}")
362 }
363 }
364 Brush::Gradient(gradient) => {
365 match &gradient.kind {
366 GradientKind::Linear(LinearGradientPosition { start, end }) => {
367 let angle_degrees = calculate_angle(start, end);
368
369 let mut css = format!("linear-gradient({angle_degrees}deg, ");
370
371 for (i, stop) in gradient.stops.iter().enumerate() {
372 let color = &stop.color;
373 let r = (color.components[0] * 255.0).round() as u8;
374 let g = (color.components[1] * 255.0).round() as u8;
375 let b = (color.components[2] * 255.0).round() as u8;
376 let a = color.components[3];
377
378 let color_str = if a < 1.0 {
379 format!("rgba({r}, {g}, {b}, {a})")
380 } else {
381 format!("#{r:02x}{g:02x}{b:02x}")
382 };
383
384 css.push_str(&format!("{} {}%", color_str, (stop.offset * 100.0).round()));
385
386 if i < gradient.stops.len() - 1 {
387 css.push_str(", ");
388 }
389 }
390
391 css.push(')');
392 css
393 }
394
395 _ => "currentColor".to_string(), }
397 }
398 Brush::Image(_) => "currentColor".to_string(),
399 }
400}
401
402fn calculate_angle(start: &Point, end: &Point) -> f64 {
403 let angle_rad = (end.y - start.y).atan2(end.x - start.x);
404
405 let mut angle_deg = 90.0 - angle_rad.to_degrees();
407
408 if angle_deg < 0.0 {
410 angle_deg += 360.0;
411 }
412
413 angle_deg
414}