Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svg css reader #797

Merged
merged 1 commit into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub use clipboard::{Clipboard, ClipboardError};
pub use floem_reactive as reactive;
pub use floem_renderer::text;
pub use floem_renderer::Renderer;
pub use floem_renderer::Svg as RendererSvg;
pub use id::ViewId;
pub use peniko;
pub use peniko::kurbo;
Expand Down
128 changes: 121 additions & 7 deletions src/views/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use floem_renderer::{
usvg::{self, Tree},
Renderer,
};
use peniko::{kurbo::Size, Brush};
use peniko::{
kurbo::{Point, Size},
Brush, GradientKind,
};
use sha2::{Digest, Sha256};

use crate::{id::ViewId, prop, prop_extractor, style::TextColor, style_class, view::View};
Expand All @@ -24,6 +27,9 @@ pub struct Svg {
svg_tree: Option<Tree>,
svg_hash: Option<Vec<u8>>,
svg_style: SvgStyle,
svg_string: String,
svg_css: Option<String>,
css_prop: Option<Box<dyn SvgCssPropExtractor>>,
}

style_class!(pub SvgClass);
Expand Down Expand Up @@ -61,29 +67,48 @@ impl From<&str> for SvgStrFn {
}
}

pub trait SvgCssPropExtractor {
fn read_custom(&mut self, cx: &mut crate::context::StyleCx) -> bool;
fn css_string(&self) -> String;
}

#[derive(Debug, Clone)]
pub enum SvgOrStyle {
Svg(String),
Style(String),
}

impl Svg {
pub fn update_value<S: Into<String>>(self, svg_str: impl Fn() -> S + 'static) -> Self {
let id = self.id;
create_effect(move |_| {
let new_svg_str = svg_str();
id.update_state(new_svg_str.into());
id.update_state(SvgOrStyle::Svg(new_svg_str.into()));
});
self
}

pub fn set_css_extractor(mut self, css: impl SvgCssPropExtractor + 'static) -> Self {
self.css_prop = Some(Box::new(css));
self
}
}

pub fn svg(svg_str_fn: impl Into<SvgStrFn> + 'static) -> Svg {
let id = ViewId::new();
let svg_str_fn: SvgStrFn = svg_str_fn.into();
create_effect(move |_| {
let new_svg_str = (svg_str_fn.str_fn)();
id.update_state(new_svg_str);
id.update_state(SvgOrStyle::Svg(new_svg_str));
});
Svg {
id,
svg_tree: None,
svg_hash: None,
svg_style: Default::default(),
svg_string: Default::default(),
css_prop: None,
svg_css: None,
}
.class(SvgClass)
}
Expand All @@ -95,12 +120,35 @@ impl View for Svg {

fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
self.svg_style.read(cx);
if let Some(prop_reader) = &mut self.css_prop {
if prop_reader.read_custom(cx) {
self.id
.update_state(SvgOrStyle::Style(prop_reader.css_string()));
}
}
}

fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
if let Ok(state) = state.downcast::<String>() {
let text = &*state;
self.svg_tree = Tree::from_str(text, &usvg::Options::default()).ok();
if let Ok(state) = state.downcast::<SvgOrStyle>() {
let (text, style) = match *state {
SvgOrStyle::Svg(text) => {
self.svg_string = text;
(&self.svg_string, self.svg_css.clone())
}
SvgOrStyle::Style(css) => {
self.svg_css = Some(css);
(&self.svg_string, self.svg_css.clone())
}
};

self.svg_tree = Tree::from_str(
text,
&usvg::Options {
style_sheet: style,
..Default::default()
},
)
.ok();

let mut hasher = Sha256::new();
hasher.update(text);
Expand All @@ -121,7 +169,73 @@ impl View for Svg {
} else {
self.svg_style.text_color().map(Brush::Solid)
};
cx.draw_svg(floem_renderer::Svg { tree, hash }, rect, color.as_ref());
cx.draw_svg(crate::RendererSvg { tree, hash }, rect, color.as_ref());
}
}
}

pub fn brush_to_css_string(brush: &Brush) -> String {
match brush {
Brush::Solid(color) => {
let r = (color.components[0] * 255.0).round() as u8;
let g = (color.components[1] * 255.0).round() as u8;
let b = (color.components[2] * 255.0).round() as u8;
let a = color.components[3];

if a < 1.0 {
format!("rgba({}, {}, {}, {})", r, g, b, a)
} else {
format!("#{:02x}{:02x}{:02x}", r, g, b)
}
}
Brush::Gradient(gradient) => {
match &gradient.kind {
GradientKind::Linear { start, end } => {
let angle_degrees = calculate_angle(start, end);

let mut css = format!("linear-gradient({}deg, ", angle_degrees);

for (i, stop) in gradient.stops.iter().enumerate() {
let color = &stop.color;
let r = (color.components[0] * 255.0).round() as u8;
let g = (color.components[1] * 255.0).round() as u8;
let b = (color.components[2] * 255.0).round() as u8;
let a = color.components[3];

let color_str = if a < 1.0 {
format!("rgba({}, {}, {}, {})", r, g, b, a)
} else {
format!("#{:02x}{:02x}{:02x}", r, g, b)
};

css.push_str(&format!("{} {}%", color_str, (stop.offset * 100.0).round()));

if i < gradient.stops.len() - 1 {
css.push_str(", ");
}
}

css.push(')');
css
}

_ => "currentColor".to_string(), // Fallback for unsupported gradient types
}
}
Brush::Image(_) => "currentColor".to_string(),
}
}

fn calculate_angle(start: &Point, end: &Point) -> f64 {
let angle_rad = (end.y - start.y).atan2(end.x - start.x);

// CSS angles are measured clockwise from the positive y-axis
let mut angle_deg = 90.0 - angle_rad.to_degrees();

// Normalize to 0-360 range
if angle_deg < 0.0 {
angle_deg += 360.0;
}

angle_deg
}