From eae5e941e2afb51c3b0407b9c2902a844528b718 Mon Sep 17 00:00:00 2001 From: Adoo Date: Tue, 19 Nov 2024 19:32:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(painter):=20=F0=9F=8E=B8=20SVG=20support?= =?UTF-8?q?=20switch=20default=20color?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + core/src/builtin_widgets/svg.rs | 2 +- gpu/src/gpu_backend.rs | 3 +- painter/src/color.rs | 4 +- painter/src/painter.rs | 11 +- painter/src/svg.rs | 133 +++++++++++++++++- .../icon/tests/icons_with_default_by_wgpu.png | Bin 3093 -> 1354 bytes .../tests/icons_with_material_by_wgpu.png | Bin 3568 -> 1913 bytes tests/include_svg_test.rs | 2 +- widgets/src/icon.rs | 12 +- 10 files changed, 147 insertions(+), 21 deletions(-) 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 f4d7ac8e481af388daadb90a55fd45d607d846a5..18c4d93efae535cfe19059bc217e53371501e6c6 100644 GIT binary patch literal 1354 zcmd6n|5K6&9LLe7m3={LMyJhZWvh#7t!%NKC9_G6SW?U4cBb^@s%?EC>C2-Io+ojI z%sI?f=CORTTDG{scGry~5U~7)Xy|p+))znpbTRb-Ngnd~GTiNt*xl#z{`~U!;dSr# z=RS(4nMWMjJar$=4(R@@^tl)T(&wMX{g3Z&cIK__~ZV%uug$*GQV7n^kw8|cE@1X{Sja9G*s1KQT??GWZt@{v~utrq$ zrWs$kSyl&^j^7_1g69%$dW{RM#-vW=&~o@MfkFrI2Gpk7i79$V56jgWwKm2w#SN~#PO?vXLL=X!hWAh z6s3?cT3AvZn!#Q<8jcF&mdn;t%(DCQ(FbV{&BKlKHl=iJNDf=f4ui>`8N6QhN&qL5 zNIIrHWq*P0U(;UYSxhf4{^nD1blC5EXv+*zi(25H3ew)txgMHtzaL`3 z)!>=8Ww4r0dX$4x2yyzTJJ@*<#?;8V;KgQ};kt<0O>+3qyyvGGYZ=5EG&#AQaP^A|RU5 zF_D`-dE)748hO`lY0A6Nl$DHReGK(69+3lDD}e6{j+ zvY+Ksvl{>yUtXh9wRS-3FK{87uGj&mFxSZJ#NS(<1Xdw9*IEc>&m?z7DzdKpxMg5I zyS%1TMDEtS=4X33=<%`rX~W6F7QC*~0t9w=#2m@v!-=`Mo-o7~5b_(rSllO4@Ufe2 zPM`x`vSERbUBwPu?0j5lCyvH;juDXNHJ#(tFJjL{$e*n`NdbBoezzzepnkK$%iyl2 z+SF9EMt;Hpf3W_SEhilmn6J5)aC^_`zBN7A^Qyd2Huy|RA*?BVNK literal 3093 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 Ycox6Wn^=hdw 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.