Skip to content

Commit 7cc6c22

Browse files
authored
Svg css reader (#797)
1 parent 08aa75b commit 7cc6c22

File tree

2 files changed

+122
-7
lines changed

2 files changed

+122
-7
lines changed

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ pub use clipboard::{Clipboard, ClipboardError};
225225
pub use floem_reactive as reactive;
226226
pub use floem_renderer::text;
227227
pub use floem_renderer::Renderer;
228+
pub use floem_renderer::Svg as RendererSvg;
228229
pub use id::ViewId;
229230
pub use peniko;
230231
pub use peniko::kurbo;

src/views/svg.rs

+121-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use floem_renderer::{
33
usvg::{self, Tree},
44
Renderer,
55
};
6-
use peniko::{kurbo::Size, Brush};
6+
use peniko::{
7+
kurbo::{Point, Size},
8+
Brush, GradientKind,
9+
};
710
use sha2::{Digest, Sha256};
811

912
use crate::{id::ViewId, prop, prop_extractor, style::TextColor, style_class, view::View};
@@ -24,6 +27,9 @@ pub struct Svg {
2427
svg_tree: Option<Tree>,
2528
svg_hash: Option<Vec<u8>>,
2629
svg_style: SvgStyle,
30+
svg_string: String,
31+
svg_css: Option<String>,
32+
css_prop: Option<Box<dyn SvgCssPropExtractor>>,
2733
}
2834

2935
style_class!(pub SvgClass);
@@ -61,29 +67,48 @@ impl From<&str> for SvgStrFn {
6167
}
6268
}
6369

70+
pub trait SvgCssPropExtractor {
71+
fn read_custom(&mut self, cx: &mut crate::context::StyleCx) -> bool;
72+
fn css_string(&self) -> String;
73+
}
74+
75+
#[derive(Debug, Clone)]
76+
pub enum SvgOrStyle {
77+
Svg(String),
78+
Style(String),
79+
}
80+
6481
impl Svg {
6582
pub fn update_value<S: Into<String>>(self, svg_str: impl Fn() -> S + 'static) -> Self {
6683
let id = self.id;
6784
create_effect(move |_| {
6885
let new_svg_str = svg_str();
69-
id.update_state(new_svg_str.into());
86+
id.update_state(SvgOrStyle::Svg(new_svg_str.into()));
7087
});
7188
self
7289
}
90+
91+
pub fn set_css_extractor(mut self, css: impl SvgCssPropExtractor + 'static) -> Self {
92+
self.css_prop = Some(Box::new(css));
93+
self
94+
}
7395
}
7496

7597
pub fn svg(svg_str_fn: impl Into<SvgStrFn> + 'static) -> Svg {
7698
let id = ViewId::new();
7799
let svg_str_fn: SvgStrFn = svg_str_fn.into();
78100
create_effect(move |_| {
79101
let new_svg_str = (svg_str_fn.str_fn)();
80-
id.update_state(new_svg_str);
102+
id.update_state(SvgOrStyle::Svg(new_svg_str));
81103
});
82104
Svg {
83105
id,
84106
svg_tree: None,
85107
svg_hash: None,
86108
svg_style: Default::default(),
109+
svg_string: Default::default(),
110+
css_prop: None,
111+
svg_css: None,
87112
}
88113
.class(SvgClass)
89114
}
@@ -95,12 +120,35 @@ impl View for Svg {
95120

96121
fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
97122
self.svg_style.read(cx);
123+
if let Some(prop_reader) = &mut self.css_prop {
124+
if prop_reader.read_custom(cx) {
125+
self.id
126+
.update_state(SvgOrStyle::Style(prop_reader.css_string()));
127+
}
128+
}
98129
}
99130

100131
fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
101-
if let Ok(state) = state.downcast::<String>() {
102-
let text = &*state;
103-
self.svg_tree = Tree::from_str(text, &usvg::Options::default()).ok();
132+
if let Ok(state) = state.downcast::<SvgOrStyle>() {
133+
let (text, style) = match *state {
134+
SvgOrStyle::Svg(text) => {
135+
self.svg_string = text;
136+
(&self.svg_string, self.svg_css.clone())
137+
}
138+
SvgOrStyle::Style(css) => {
139+
self.svg_css = Some(css);
140+
(&self.svg_string, self.svg_css.clone())
141+
}
142+
};
143+
144+
self.svg_tree = Tree::from_str(
145+
text,
146+
&usvg::Options {
147+
style_sheet: style,
148+
..Default::default()
149+
},
150+
)
151+
.ok();
104152

105153
let mut hasher = Sha256::new();
106154
hasher.update(text);
@@ -121,7 +169,73 @@ impl View for Svg {
121169
} else {
122170
self.svg_style.text_color().map(Brush::Solid)
123171
};
124-
cx.draw_svg(floem_renderer::Svg { tree, hash }, rect, color.as_ref());
172+
cx.draw_svg(crate::RendererSvg { tree, hash }, rect, color.as_ref());
173+
}
174+
}
175+
}
176+
177+
pub fn brush_to_css_string(brush: &Brush) -> String {
178+
match brush {
179+
Brush::Solid(color) => {
180+
let r = (color.components[0] * 255.0).round() as u8;
181+
let g = (color.components[1] * 255.0).round() as u8;
182+
let b = (color.components[2] * 255.0).round() as u8;
183+
let a = color.components[3];
184+
185+
if a < 1.0 {
186+
format!("rgba({}, {}, {}, {})", r, g, b, a)
187+
} else {
188+
format!("#{:02x}{:02x}{:02x}", r, g, b)
189+
}
190+
}
191+
Brush::Gradient(gradient) => {
192+
match &gradient.kind {
193+
GradientKind::Linear { start, end } => {
194+
let angle_degrees = calculate_angle(start, end);
195+
196+
let mut css = format!("linear-gradient({}deg, ", angle_degrees);
197+
198+
for (i, stop) in gradient.stops.iter().enumerate() {
199+
let color = &stop.color;
200+
let r = (color.components[0] * 255.0).round() as u8;
201+
let g = (color.components[1] * 255.0).round() as u8;
202+
let b = (color.components[2] * 255.0).round() as u8;
203+
let a = color.components[3];
204+
205+
let color_str = if a < 1.0 {
206+
format!("rgba({}, {}, {}, {})", r, g, b, a)
207+
} else {
208+
format!("#{:02x}{:02x}{:02x}", r, g, b)
209+
};
210+
211+
css.push_str(&format!("{} {}%", color_str, (stop.offset * 100.0).round()));
212+
213+
if i < gradient.stops.len() - 1 {
214+
css.push_str(", ");
215+
}
216+
}
217+
218+
css.push(')');
219+
css
220+
}
221+
222+
_ => "currentColor".to_string(), // Fallback for unsupported gradient types
223+
}
125224
}
225+
Brush::Image(_) => "currentColor".to_string(),
126226
}
127227
}
228+
229+
fn calculate_angle(start: &Point, end: &Point) -> f64 {
230+
let angle_rad = (end.y - start.y).atan2(end.x - start.x);
231+
232+
// CSS angles are measured clockwise from the positive y-axis
233+
let mut angle_deg = 90.0 - angle_rad.to_degrees();
234+
235+
// Normalize to 0-360 range
236+
if angle_deg < 0.0 {
237+
angle_deg += 360.0;
238+
}
239+
240+
angle_deg
241+
}

0 commit comments

Comments
 (0)