Skip to main content

floem/views/
svg.rs

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                // self.id.request_style();
272            }
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(), // Fallback for unsupported gradient types
396            }
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    // CSS angles are measured clockwise from the positive y-axis
406    let mut angle_deg = 90.0 - angle_rad.to_degrees();
407
408    // Normalize to 0-360 range
409    if angle_deg < 0.0 {
410        angle_deg += 360.0;
411    }
412
413    angle_deg
414}