diff --git a/CHANGELOG.md b/CHANGELOG.md index 71991e5d0..79572c5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he - **core**: Added `QueryId` as a replacement for `TypeId` to facilitate querying types by Provider across different binaries. (#656 @M-Adoo) - **core**: Added `OverrideClass` to override a single class within a subtree. (#657 @M-Adoo) - **widgets**: Added `LinearProgress` and `SpinnerProgress` widgets along with their respective material themes. (#630 @wjian23 @M-Adoo) +- **painter**: SVG now supports switching the default color, allowing for icon color changes. (#pr @M-Adoo) ### Changed diff --git a/core/src/builtin_widgets/svg.rs b/core/src/builtin_widgets/svg.rs index de3afa681..7d35f6316 100644 --- a/core/src/builtin_widgets/svg.rs +++ b/core/src/builtin_widgets/svg.rs @@ -2,7 +2,7 @@ use crate::prelude::*; impl Render for Svg { #[inline] - fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size { clamp.clamp(self.size) } + fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size { clamp.clamp(self.size()) } fn paint(&self, ctx: &mut PaintingCtx) { let painter = ctx.painter(); diff --git a/gpu/src/gpu_backend.rs b/gpu/src/gpu_backend.rs index 447a8b08c..d933cd0ca 100644 --- a/gpu/src/gpu_backend.rs +++ b/gpu/src/gpu_backend.rs @@ -805,8 +805,7 @@ mod tests { }) .collect(); - let svg = Svg { size: Size::new(512., 512.), commands: Resource::new(commands) }; - painter.draw_svg(&svg); + painter.draw_bundle_commands(Rect::from_size(Size::new(512., 512.)), Resource::new(commands)); painter } painter_backend_eq_image_test!(draw_bundle_svg, comparison = 0.001); diff --git a/painter/src/color.rs b/painter/src/color.rs index 8714bd460..96f8ca64c 100644 --- a/painter/src/color.rs +++ b/painter/src/color.rs @@ -71,13 +71,13 @@ impl Color { } #[inline] - pub fn from_u32(rgba: u32) -> Self { + pub const fn from_u32(rgba: u32) -> Self { let bytes = rgba.to_be_bytes(); Self { red: bytes[0], green: bytes[1], blue: bytes[2], alpha: bytes[3] } } #[inline] - pub fn into_u32(self) -> u32 { + pub const fn into_u32(self) -> u32 { let Self { red, green, blue, alpha } = self; u32::from_be_bytes([red, green, blue, alpha]) } diff --git a/painter/src/painter.rs b/painter/src/painter.rs index ffef16a8f..b1148bccd 100644 --- a/painter/src/painter.rs +++ b/painter/src/painter.rs @@ -569,6 +569,7 @@ impl Painter { pub fn draw_svg(&mut self, svg: &Svg) -> &mut Self { invisible_return!(self); + let commands = svg.commands(self.fill_brush(), self.stroke_brush()); // For a large number of path commands (more than 16), bundle them // together as a single resource. This allows the backend to cache @@ -576,11 +577,11 @@ impl Painter { // For a small number of path commands (less than 16), store them // individually as multiple resources. This means the backend doesn't // need to perform a single draw operation for an SVG. - if svg.commands.len() <= 16 { + if commands.len() <= 16 { let transform = *self.transform(); let alpha = self.alpha(); - for cmd in svg.commands.iter() { + for cmd in commands.iter() { let cmd = match cmd.clone() { PaintCommand::Path(mut path) => { path.transform(&transform); @@ -600,8 +601,8 @@ impl Painter { self.commands.push(cmd); } } else { - let rect = Rect::from_size(svg.size); - self.draw_bundle_commands(rect, svg.commands.clone()); + let rect = Rect::from_size(svg.size()); + self.draw_bundle_commands(rect, commands.clone()); } self @@ -662,7 +663,7 @@ impl Painter { .map(|h| h as f32 / face.units_per_em() as f32) .unwrap_or(1.) .max(1.); - let size = svg.size; + let size = svg.size(); let bound_size = bounds.size; let scale = (bound_size.width / size.width).min(bound_size.height / size.height) / grid_scale; self diff --git a/painter/src/svg.rs b/painter/src/svg.rs index e8fa9f693..7e4a1ae8c 100644 --- a/painter/src/svg.rs +++ b/painter/src/svg.rs @@ -1,4 +1,4 @@ -use std::{error::Error, io::Read, vec}; +use std::{cell::RefCell, error::Error, io::Read, vec}; use ribir_algo::Resource; use ribir_geom::{Point, Rect, Size, Transform}; @@ -6,7 +6,8 @@ use serde::{Deserialize, Serialize}; use usvg::{Options, Stop, Tree}; use crate::{ - Brush, Color, GradientStop, LineCap, LineJoin, PaintCommand, Path, StrokeOptions, + Brush, Color, CommandBrush, GradientStop, LineCap, LineJoin, PaintCommand, PaintPathAction, Path, + StrokeOptions, color::{LinearGradient, RadialGradient}, }; @@ -14,17 +15,33 @@ use crate::{ /// currently quite simple and primarily used for Ribir icons. More features /// will be added as needed. -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize)] pub struct Svg { - pub size: Size, - pub commands: Resource>, + size: Size, + commands: Resource>, + + used_fill_fallback: bool, + used_stroke_fallback: bool, + #[serde(skip)] + last: RefCell>, +} + +#[derive(Clone)] +struct StaticSvg { + fill: Brush, + stroke: Brush, + commands: Resource>, } -// todo: we need to support currentColor to change svg color. // todo: share fontdb impl Svg { + // FIXME: This is a temporary workaround. Utilize the magic color as the default + // color for the SVG, and replace it with the actual color when rendering. + const DYNAMIC_COLOR: Color = Color::from_u32(0x191B1901); + const INJECTED_STYLE: &str = "svg { fill: #191B1901; stroke: #191B1901; }"; + pub fn parse_from_bytes(svg_data: &[u8]) -> Result> { - let opt = Options { ..<_>::default() }; + let opt = Options { style_sheet: Some(Self::INJECTED_STYLE.to_string()), ..<_>::default() }; let tree = Tree::from_data(svg_data, &opt).unwrap(); let size = tree.size(); @@ -34,13 +51,46 @@ impl Svg { paint_group(tree.root(), &mut painter); let paint_commands = painter.finish().to_owned().into_boxed_slice(); + let (used_fill_fallback, used_stroke_fallback) = fallback_color_check(&paint_commands); Ok(Svg { size: Size::new(size.width(), size.height()), commands: Resource::new(paint_commands), + used_fill_fallback, + used_stroke_fallback, + last: RefCell::new(None), }) } + pub fn size(&self) -> Size { self.size } + + pub fn commands( + &self, fill_brush: &Brush, stroke_brush: &Brush, + ) -> Resource> { + if !self.used_fill_fallback && !self.used_stroke_fallback { + self.commands.clone() + } else { + let mut last = self.last.borrow_mut(); + if let Some(last) = last + .as_ref() + .filter(|last| &last.fill == fill_brush && &last.stroke == stroke_brush) + { + last.commands.clone() + } else { + let commands = brush_replace(&self.commands, fill_brush, stroke_brush); + let commands = Resource::new(commands); + *last = Some(StaticSvg { + fill: fill_brush.clone(), + stroke: stroke_brush.clone(), + commands: commands.clone(), + }); + commands + } + } + } + + pub fn command_size(&self) -> usize { self.commands.len() } + pub fn open>(path: P) -> Result> { let mut file = std::fs::File::open(path)?; let mut bytes = vec![]; @@ -181,6 +231,63 @@ fn brush_from_usvg_paint(paint: &usvg::Paint, opacity: usvg::Opacity) -> (Brush, } } +fn fallback_color_check(cmds: &[PaintCommand]) -> (bool, bool) { + let mut fill_fallback = false; + let mut stroke_fallback = false; + for c in cmds { + if fill_fallback && stroke_fallback { + break; + } + + match c { + PaintCommand::Path(p) => { + if let PaintPathAction::Paint { painting_style, brush, .. } = &p.action { + if matches!(brush, CommandBrush::Color(c) if c == &Svg::DYNAMIC_COLOR) { + match painting_style { + crate::PaintingStyle::Fill => fill_fallback = true, + crate::PaintingStyle::Stroke(_) => stroke_fallback = true, + } + } + } + } + PaintCommand::PopClip => {} + PaintCommand::Bundle { cmds, .. } => { + let (f, s) = fallback_color_check(cmds); + fill_fallback = f; + stroke_fallback |= s; + } + } + } + (fill_fallback, stroke_fallback) +} + +fn brush_replace(cmds: &[PaintCommand], fill: &Brush, stroke: &Brush) -> Box<[PaintCommand]> { + cmds + .iter() + .map(|c| match c { + PaintCommand::Path(p) => { + let mut p = p.clone(); + if let PaintPathAction::Paint { painting_style, brush, .. } = &mut p.action { + if matches!(brush, CommandBrush::Color(c) if c == &Svg::DYNAMIC_COLOR) { + match painting_style { + crate::PaintingStyle::Fill => *brush = fill.clone().into(), + crate::PaintingStyle::Stroke(_) => *brush = stroke.clone().into(), + } + } + } + PaintCommand::Path(p) + } + PaintCommand::PopClip => PaintCommand::PopClip, + PaintCommand::Bundle { transform, opacity, bounds, cmds } => { + let cmds = brush_replace(cmds, fill, stroke); + let cmds = Resource::new(cmds); + + PaintCommand::Bundle { transform: *transform, opacity: *opacity, bounds: *bounds, cmds } + } + }) + .collect() +} + fn convert_to_gradient_stops(stops: &[Stop]) -> Vec { assert!(!stops.is_empty()); @@ -230,3 +337,15 @@ impl From for LineJoin { } } } + +impl Clone for Svg { + fn clone(&self) -> Self { + Svg { + size: self.size, + commands: self.commands.clone(), + used_fill_fallback: self.used_fill_fallback, + used_stroke_fallback: self.used_stroke_fallback, + last: RefCell::new(self.last.borrow().clone()), + } + } +} diff --git a/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png b/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png index f4d7ac8e4..18c4d93ef 100644 Binary files a/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png and b/test_cases/ribir_widgets/icon/tests/icons_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png b/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png index e5945be9f..fa4b3efdc 100644 Binary files a/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png and b/test_cases/ribir_widgets/icon/tests/icons_with_material_by_wgpu.png differ diff --git a/tests/include_svg_test.rs b/tests/include_svg_test.rs index cfcb31c43..f0e998143 100644 --- a/tests/include_svg_test.rs +++ b/tests/include_svg_test.rs @@ -4,7 +4,7 @@ use ribir_dev_helper::*; #[test] fn include_svg() { let svg: Svg = include_crate_svg!("./assets/test1.svg"); - assert_eq!(svg.commands.len(), 2); + assert_eq!(svg.command_size(), 2); } fn fix_draw_svg_not_apply_alpha() -> Painter { diff --git a/widgets/src/icon.rs b/widgets/src/icon.rs index d3cb379b4..626ee8a4e 100644 --- a/widgets/src/icon.rs +++ b/widgets/src/icon.rs @@ -113,11 +113,17 @@ mod tests { widget_image_tests!( icons, WidgetTester::new(row! { - @Icon { @ { svgs::DELETE }} - @Icon { @ { "search" } } + @Icon { + foreground: Color::BLUE, + @ { svgs::DELETE } + } + @Icon { + foreground: Color::RED, + @ { "search" } + } @Icon { @SpinnerProgress { value: Some(0.8) }} }) - .with_wnd_size(Size::new(300., 200.)) + .with_wnd_size(Size::new(128., 64.)) .with_env_init(|| { let mut theme = AppCtx::app_theme().write(); // Specify the icon font.