Skip to content

Commit

Permalink
feat(painter): 🎸 SVG support switch default color
Browse files Browse the repository at this point in the history
  • Loading branch information
M-Adoo authored and rchangelog[bot] committed Nov 19, 2024
1 parent 1505f59 commit 5efac14
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. (#661 @M-Adoo)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion core/src/builtin_widgets/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 1 addition & 2 deletions gpu/src/gpu_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions painter/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down
11 changes: 6 additions & 5 deletions painter/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,18 +569,19 @@ 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
// them collectively.
// 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);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
133 changes: 126 additions & 7 deletions painter/src/svg.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
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};
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},
};

/// This is a basic SVG support designed for rendering to Ribir painter. It is
/// 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<Box<[PaintCommand]>>,
size: Size,
commands: Resource<Box<[PaintCommand]>>,

used_fill_fallback: bool,
used_stroke_fallback: bool,
#[serde(skip)]
last: RefCell<Option<StaticSvg>>,
}

#[derive(Clone)]
struct StaticSvg {
fill: Brush,
stroke: Brush,
commands: Resource<Box<[PaintCommand]>>,
}

// 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<Self, Box<dyn Error>> {
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();
Expand All @@ -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<Box<[PaintCommand]>> {
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<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Box<dyn Error>> {
let mut file = std::fs::File::open(path)?;
let mut bytes = vec![];
Expand Down Expand Up @@ -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<GradientStop> {
assert!(!stops.is_empty());

Expand Down Expand Up @@ -230,3 +337,15 @@ impl From<usvg::LineJoin> 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()),
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/include_svg_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 9 additions & 3 deletions widgets/src/icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 5efac14

Please sign in to comment.