floem/views/
svg.rs

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(), // Fallback for unsupported gradient types
248            }
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    // CSS angles are measured clockwise from the positive y-axis
258    let mut angle_deg = 90.0 - angle_rad.to_degrees();
259
260    // Normalize to 0-360 range
261    if angle_deg < 0.0 {
262        angle_deg += 360.0;
263    }
264
265    angle_deg
266}