diff --git a/CHANGELOG.md b/CHANGELOG.md index 71991e5d0..91a18ecae 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. (#661 @M-Adoo) ### Changed diff --git a/Cargo.toml b/Cargo.toml index 9f955fc80..15aabfc7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,17 +47,17 @@ colored = "2.1.0" derive_more = "1.0.0" dssim-core="3.2.9" env_logger = "0.7.1" -euclid = "0.22.6" +euclid = "0.22.11" fontdb = "0.23.0" futures = "0.3.26" guillotiere = "0.6.0" image = { version = "0.24.5" } indextree = "4.7.3" log = "0.4.14" -lyon_algorithms = "1.0.1" -lyon_geom = "1.0.1" -lyon_path = "1.0.1" -lyon_tessellation = "1.0.1" +lyon_algorithms = "1.0.4" +lyon_geom = "1.0.6" +lyon_path = "1.0.6" +lyon_tessellation = "1.0.15" material-color-utilities-rs = "0.2.1" icrate = "0.0.4" paste = "1.0" diff --git a/core/src/builtin_widgets/clip.rs b/core/src/builtin_widgets/clip.rs index 4308a826d..25820e544 100644 --- a/core/src/builtin_widgets/clip.rs +++ b/core/src/builtin_widgets/clip.rs @@ -20,7 +20,7 @@ impl Render for Clip { let child_size = ctx.assert_perform_single_child_layout(clamp); match self.clip { ClipType::Auto => child_size, - ClipType::Path(ref path) => path.bounds().max().to_tuple().into(), + ClipType::Path(ref path) => path.bounds(None).max().to_tuple().into(), } } diff --git a/core/src/builtin_widgets/painting_style.rs b/core/src/builtin_widgets/painting_style.rs index 4e1c1e338..ef0d9f502 100644 --- a/core/src/builtin_widgets/painting_style.rs +++ b/core/src/builtin_widgets/painting_style.rs @@ -1,16 +1,5 @@ use crate::{prelude::*, wrap_render::*}; -/// Explain the method for rendering shapes and paths, including filling or -/// stroking them. -#[derive(Clone, Debug, Default)] -pub enum PaintingStyle { - /// Fill the path. - #[default] - Fill, - /// Stroke path with line width. - Stroke(StrokeOptions), -} - /// A widget that sets the strategies for painting shapes and paths . It's can /// be inherited by its descendants. #[derive(Default)] @@ -24,11 +13,30 @@ impl Declare for PaintingStyleWidget { fn declarer() -> Self::Builder { FatObj::new(()) } } -impl_compose_child_for_wrap_render!(PaintingStyleWidget); +impl<'c> ComposeChild<'c> for PaintingStyleWidget { + type Child = Widget<'c>; + + fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { + // We need to provide the text style for the children to access. + match this.try_into_value() { + Ok(this) => { + let style = this.painting_style.clone(); + WrapRender::combine_child(State::value(this), child).attach_data(Box::new(Queryable(style))) + } + Err(this) => { + let style = this.map_reader(|w| PartData::from_ref(&w.painting_style)); + WrapRender::combine_child(this, child).attach_data(Box::new(style)) + } + } + } +} impl WrapRender for PaintingStyleWidget { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { - host.perform_layout(clamp, ctx) + let old = ctx.set_painting_style(self.painting_style.clone()); + let size = host.perform_layout(clamp, ctx); + ctx.set_painting_style(old); + size } fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { diff --git a/core/src/builtin_widgets/svg.rs b/core/src/builtin_widgets/svg.rs index de3afa681..06074c449 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(); @@ -17,7 +17,7 @@ pub mod named_svgs { const DEFAULT_SVG_KEY: &str = "__RIRBIR_DEFAULT_SVG__"; static SVGS: LazyLock>> = LazyLock::new(|| { - let svg = include_crate_svg!("src/builtin_widgets/default_named.svg"); + let svg = include_crate_svg!("src/builtin_widgets/default_named.svg", true, false); let mut set = ahash::AHashMap::new(); set.insert(DEFAULT_SVG_KEY, svg); Mutex::new(set) @@ -51,8 +51,8 @@ mod tests { fn svgs_smoke() -> Painter { named_svgs::register( "test::add", - Svg::parse_from_bytes( - r#""#.as_bytes(), + Svg::parse_from_bytes(r#""#.as_bytes(), + true, false ).unwrap(), ); let mut painter = Painter::new(Rect::from_size(Size::new(128., 64.))); diff --git a/core/src/builtin_widgets/text_style.rs b/core/src/builtin_widgets/text_style.rs index ac88f360b..2875b5d36 100644 --- a/core/src/builtin_widgets/text_style.rs +++ b/core/src/builtin_widgets/text_style.rs @@ -33,7 +33,10 @@ impl<'c> ComposeChild<'c> for TextStyleWidget { impl WrapRender for TextStyleWidget { fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { - host.perform_layout(clamp, ctx) + let old = ctx.set_text_style(self.text_style.clone()); + let size = host.perform_layout(clamp, ctx); + ctx.set_text_style(old); + size } fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { diff --git a/core/src/builtin_widgets/theme/icon_theme.rs b/core/src/builtin_widgets/theme/icon_theme.rs index 1de01aba9..1326b5988 100644 --- a/core/src/builtin_widgets/theme/icon_theme.rs +++ b/core/src/builtin_widgets/theme/icon_theme.rs @@ -46,7 +46,7 @@ macro_rules! define_named_svg { macro_rules! fill_svgs { ($theme: expr, $($name: path: $path: literal),+) => { $( - let icon = Resource::new(include_crate_svg!($path)); + let icon = Resource::new(include_crate_svg!($path, true, false)); $theme.set_svg($name, icon); )+ }; @@ -70,7 +70,7 @@ impl Compose for NamedSvg { impl IconTheme { pub fn new(icon_size: IconSize) -> Self { - let svg = include_crate_svg!("src/builtin_widgets/default_named.svg"); + let svg = include_crate_svg!("src/builtin_widgets/default_named.svg", true, false); let miss_icon = Resource::new(svg); let mut icons = HashMap::<_, _, ahash::RandomState>::default(); icons.insert(MISS_ICON, miss_icon); diff --git a/core/src/context/layout_ctx.rs b/core/src/context/layout_ctx.rs index bd473dd1b..4c139aec3 100644 --- a/core/src/context/layout_ctx.rs +++ b/core/src/context/layout_ctx.rs @@ -1,4 +1,5 @@ use ribir_geom::{Point, Size}; +use ribir_painter::{PaintingStyle, TextStyle}; use super::{WidgetCtx, WidgetCtxImpl}; use crate::{ @@ -17,6 +18,8 @@ pub struct LayoutCtx<'a> { /// The widget tree of the window, not borrow it from `wnd` is because a /// `LayoutCtx` always in a mutable borrow. pub(crate) tree: &'a mut WidgetTree, + painting_style: PaintingStyle, + text_style: TextStyle, } impl<'a> WidgetCtxImpl for LayoutCtx<'a> { @@ -28,6 +31,20 @@ impl<'a> WidgetCtxImpl for LayoutCtx<'a> { } impl<'a> LayoutCtx<'a> { + pub(crate) fn new(id: WidgetId, tree: &'a mut WidgetTree) -> Self { + let painting_style = if let Some(style) = id.query_ancestors_ref::(tree) { + style.clone() + } else { + PaintingStyle::Fill + }; + let text_style = if let Some(style) = id.query_ancestors_ref::(tree) { + style.clone() + } else { + TextStyle::default() + }; + + Self { id, tree, painting_style, text_style } + } /// Perform layout of the child widget referenced, resetting the widget /// position to (0, 0) relative to the parent if its position is not set by /// its layout logic, and return the resulting size after layout. @@ -40,7 +57,12 @@ impl<'a> LayoutCtx<'a> { // Safety: the `tree` just use to get the widget of `id`, and `tree2` not drop // or modify it during perform layout. let tree2 = unsafe { &mut *(self.tree as *mut WidgetTree) }; - let mut ctx = LayoutCtx { id: child, tree: tree2 }; + let mut ctx = LayoutCtx { + id: child, + tree: tree2, + painting_style: self.painting_style.clone(), + text_style: self.text_style.clone(), + }; let size = child .assert_get(self.tree) .perform_layout(clamp, &mut ctx); @@ -129,4 +151,16 @@ impl<'a> LayoutCtx<'a> { assert_eq!(child.parent(self.tree), Some(self.id)); self.tree.store.force_layout(child).is_some() } + + pub fn set_painting_style(&mut self, style: PaintingStyle) -> PaintingStyle { + std::mem::replace(&mut self.painting_style, style) + } + + pub fn painting_style(&self) -> &PaintingStyle { &self.painting_style } + + pub fn set_text_style(&mut self, style: TextStyle) -> TextStyle { + std::mem::replace(&mut self.text_style, style) + } + + pub fn text_style(&self) -> &TextStyle { &self.text_style } } diff --git a/core/src/render_helper.rs b/core/src/render_helper.rs index 65b43643a..497317e9e 100644 --- a/core/src/render_helper.rs +++ b/core/src/render_helper.rs @@ -66,8 +66,13 @@ impl RenderProxy for Sc { } impl Render for Resource { - fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size { - let size = self.bounds().max().to_vector().to_size(); + fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { + let line_width = ctx.painting_style().line_width(); + let size = self + .bounds(line_width) + .max() + .to_vector() + .to_size(); clamp.clamp(size) } diff --git a/core/src/widget_tree.rs b/core/src/widget_tree.rs index 49102c55c..84a9adffd 100644 --- a/core/src/widget_tree.rs +++ b/core/src/widget_tree.rs @@ -84,7 +84,7 @@ impl WidgetTree { .map(|info| info.clamp) .unwrap_or_else(|| BoxClamp { min: Size::zero(), max: win_size }); - let mut ctx = LayoutCtx { id: wid, tree: self }; + let mut ctx = LayoutCtx::new(wid, self); ctx.perform_child_layout(wid, clamp); } } diff --git a/core/src/widget_tree/widget_id.rs b/core/src/widget_tree/widget_id.rs index afcb2b6da..d30d09bcd 100644 --- a/core/src/widget_tree/widget_id.rs +++ b/core/src/widget_tree/widget_id.rs @@ -310,6 +310,12 @@ impl WidgetId { .and_then(QueryHandle::into_ref) } + pub(crate) fn query_ancestors_ref(self, tree: &WidgetTree) -> Option> { + self + .ancestors(tree) + .find_map(|id| id.query_ref::(tree)) + } + /// return if this object contain type `T` pub(crate) fn contain_type(self, tree: &WidgetTree) -> bool { self diff --git a/gpu/src/gpu_backend.rs b/gpu/src/gpu_backend.rs index 5b90964bc..4c893e0e5 100644 --- a/gpu/src/gpu_backend.rs +++ b/gpu/src/gpu_backend.rs @@ -5,8 +5,8 @@ use ribir_geom::{ DeviceRect, DeviceSize, Point, Transform, rect_corners, transform_to_device_rect, }; use ribir_painter::{ - Color, PaintCommand, PaintPath, PaintPathAction, PainterBackend, PathCommand, PixelImage, Vertex, - VertexBuffers, image::ColorFormat, + Color, CommandBrush, PaintCommand, PaintPath, PaintPathAction, PainterBackend, PaintingStyle, + PathCommand, PixelImage, Vertex, VertexBuffers, image::ColorFormat, }; use crate::{ @@ -184,69 +184,77 @@ where } let matrix = transform.then(global_matrix); - let (rect, mask_head) = self.new_mask_layer(&viewport, &matrix, path); - - match &action { - PaintPathAction::Color(color) => { - let color = color.into_components(); - let color_attr = ColorAttr { color, mask_head }; - let buffer = &mut self.color_vertices_buffer; - add_rect_vertices(rect, output_tex_size, color_attr, buffer); - self.current_phase = CurrentPhase::Color; + let (rect, mask_head) = match action { + PaintPathAction::Clip => { + self.new_mask_layer(&viewport, &matrix, path, &PaintingStyle::Fill) } - PaintPathAction::Image { img, opacity } => { - let slice = self.tex_mgr.store_image(img, &mut self.gpu_impl); - let ts = matrix.inverse().unwrap(); - self.draw_img_slice(slice, &ts, mask_head, *opacity, output_tex_size, rect); - } - PaintPathAction::Radial(radial) => { - let prim: RadialGradientPrimitive = RadialGradientPrimitive { - transform: matrix.inverse().unwrap().to_array(), - stop_start: self.radial_gradient_stops.len() as u32, - stop_cnt: radial.stops.len() as u32, - start_center: radial.start_center.to_array(), - start_radius: radial.start_radius, - end_center: radial.end_center.to_array(), - end_radius: radial.end_radius, - mask_head, - spread: radial.spread_method as u32, - }; - let stops = radial - .stops - .iter() - .map(GradientStopPrimitive::new); - self.radial_gradient_stops.extend(stops); - let prim_idx = self.radial_gradient_prims.len() as u32; - self.radial_gradient_prims.push(prim); - let buffer = &mut self.radial_gradient_vertices_buffer; - - add_rect_vertices(rect, output_tex_size, RadialGradientPrimIndex(prim_idx), buffer); - self.current_phase = CurrentPhase::RadialGradient; - } - PaintPathAction::Linear(linear) => { - let stop = (self.linear_gradient_stops.len() << 16 | linear.stops.len()) as u32; - let mask_head_and_spread = mask_head << 16 | linear.spread_method as i32; - let prim: LinearGradientPrimitive = LinearGradientPrimitive { - transform: matrix.inverse().unwrap().to_array(), - stop, - start_position: linear.start.to_array(), - end_position: linear.end.to_array(), - mask_head_and_spread, - }; - let stops = linear - .stops - .iter() - .map(GradientStopPrimitive::new); - self.linear_gradient_stops.extend(stops); - let prim_idx = self.linear_gradient_prims.len() as u32; - self.linear_gradient_prims.push(prim); - let buffer = &mut self.linear_gradient_vertices_buffer; - add_rect_vertices(rect, output_tex_size, LinearGradientPrimIndex(prim_idx), buffer); - self.current_phase = CurrentPhase::LinearGradient; + PaintPathAction::Paint { painting_style, .. } => { + self.new_mask_layer(&viewport, &matrix, path, painting_style) } + }; + match action { PaintPathAction::Clip => self .clip_layer_stack .push(ClipLayer { viewport, mask_head }), + PaintPathAction::Paint { brush, .. } => match brush { + CommandBrush::Color(color) => { + let color = color.into_components(); + let color_attr = ColorAttr { color, mask_head }; + let buffer = &mut self.color_vertices_buffer; + add_rect_vertices(rect, output_tex_size, color_attr, buffer); + self.current_phase = CurrentPhase::Color; + } + CommandBrush::Image { img, opacity } => { + let slice = self.tex_mgr.store_image(img, &mut self.gpu_impl); + let ts = matrix.inverse().unwrap(); + self.draw_img_slice(slice, &ts, mask_head, *opacity, output_tex_size, rect); + } + CommandBrush::Radial(radial) => { + let prim: RadialGradientPrimitive = RadialGradientPrimitive { + transform: matrix.inverse().unwrap().to_array(), + stop_start: self.radial_gradient_stops.len() as u32, + stop_cnt: radial.stops.len() as u32, + start_center: radial.start_center.to_array(), + start_radius: radial.start_radius, + end_center: radial.end_center.to_array(), + end_radius: radial.end_radius, + mask_head, + spread: radial.spread_method as u32, + }; + let stops = radial + .stops + .iter() + .map(GradientStopPrimitive::new); + self.radial_gradient_stops.extend(stops); + let prim_idx = self.radial_gradient_prims.len() as u32; + self.radial_gradient_prims.push(prim); + let buffer = &mut self.radial_gradient_vertices_buffer; + + add_rect_vertices(rect, output_tex_size, RadialGradientPrimIndex(prim_idx), buffer); + self.current_phase = CurrentPhase::RadialGradient; + } + CommandBrush::Linear(linear) => { + let stop = (self.linear_gradient_stops.len() << 16 | linear.stops.len()) as u32; + let mask_head_and_spread = mask_head << 16 | linear.spread_method as i32; + let prim: LinearGradientPrimitive = LinearGradientPrimitive { + transform: matrix.inverse().unwrap().to_array(), + stop, + start_position: linear.start.to_array(), + end_position: linear.end.to_array(), + mask_head_and_spread, + }; + let stops = linear + .stops + .iter() + .map(GradientStopPrimitive::new); + self.linear_gradient_stops.extend(stops); + let prim_idx = self.linear_gradient_prims.len() as u32; + self.linear_gradient_prims.push(prim); + let buffer = &mut self.linear_gradient_vertices_buffer; + add_rect_vertices(rect, output_tex_size, LinearGradientPrimIndex(prim_idx), buffer); + self.current_phase = CurrentPhase::LinearGradient; + } + }, } } PaintCommand::PopClip => { @@ -423,20 +431,23 @@ where fn can_batch_path_command(&self, cmd: &PathCommand) -> bool { let limits = self.gpu_impl.limits(); let tex_used = self.tex_ids_map.len(); - match (self.current_phase, &cmd.action) { + + let PaintPathAction::Paint { brush, .. } = &cmd.action else { + return tex_used < limits.max_tex_load; + }; + + match (self.current_phase, brush) { (CurrentPhase::None, _) => true, - (_, PaintPathAction::Clip) | (CurrentPhase::Color, PaintPathAction::Color(_)) => { - tex_used < limits.max_tex_load - } - (CurrentPhase::Img, PaintPathAction::Image { .. }) => { + (CurrentPhase::Color, CommandBrush::Color(_)) => tex_used < limits.max_tex_load, + (CurrentPhase::Img, CommandBrush::Image { .. }) => { tex_used < limits.max_tex_load - 1 && self.img_prims.len() < limits.max_image_primitives } - (CurrentPhase::RadialGradient, PaintPathAction::Radial(_)) => { + (CurrentPhase::RadialGradient, CommandBrush::Radial(_)) => { tex_used < limits.max_tex_load && self.radial_gradient_prims.len() < limits.max_radial_gradient_primitives && self.radial_gradient_stops.len() < limits.max_gradient_stop_primitives } - (CurrentPhase::LinearGradient, PaintPathAction::Linear(_)) => { + (CurrentPhase::LinearGradient, CommandBrush::Linear(_)) => { tex_used < limits.max_tex_load && self.linear_gradient_prims.len() < limits.max_linear_gradient_primitives && self.linear_gradient_stops.len() < limits.max_gradient_stop_primitives @@ -460,12 +471,12 @@ where } fn new_mask_layer( - &mut self, view: &DeviceRect, matrix: &Transform, path: &PaintPath, + &mut self, view: &DeviceRect, matrix: &Transform, path: &PaintPath, style: &PaintingStyle, ) -> ([Point; 4], i32) { let (mask, mask_to_view) = self .tex_mgr - .store_alpha_path(path, matrix, view, &mut self.gpu_impl); + .store_alpha_path(path, style, matrix, view, &mut self.gpu_impl); let mut points = rect_corners(&mask.rect.to_f32().cast_unit()); for p in points.iter_mut() { @@ -748,8 +759,12 @@ mod tests { painter_backend_eq_image_test!(draw_svg_gradient, comparison = 0.0025); fn draw_svg_gradient() -> Painter { let mut painter = painter(Size::new(64., 64.)); - let svg = - Svg::parse_from_bytes(include_bytes!("../../tests/assets/fill_with_gradient.svg")).unwrap(); + let svg = Svg::parse_from_bytes( + include_bytes!("../../tests/assets/fill_with_gradient.svg"), + true, + false, + ) + .unwrap(); painter.draw_svg(&svg); painter @@ -783,16 +798,18 @@ mod tests { .map(|i| { let color = if i % 2 == 0 { Color::GREEN } else { Color::RED }; PaintCommand::Path(PathCommand { - paint_bounds: *circle.bounds(), + paint_bounds: circle.bounds(None), path: circle.clone().into(), transform: Transform::translation(i as f32 * 8., i as f32 * 8.), - action: PaintPathAction::Color(color), + action: PaintPathAction::Paint { + brush: CommandBrush::Color(color), + painting_style: PaintingStyle::Fill, + }, }) }) .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/gpu/src/gpu_backend/atlas.rs b/gpu/src/gpu_backend/atlas.rs index e6a50d1a0..ef9a931d0 100644 --- a/gpu/src/gpu_backend/atlas.rs +++ b/gpu/src/gpu_backend/atlas.rs @@ -1,7 +1,7 @@ -use std::{any::Any, hash::Hash}; +use std::hash::Hash; use guillotiere::{Allocation, AtlasAllocator}; -use ribir_algo::{FrameCache, Resource}; +use ribir_algo::FrameCache; use ribir_geom::{DeviceRect, DeviceSize}; use ribir_painter::image::ColorFormat; use slab::Slab; @@ -27,19 +27,20 @@ pub(crate) struct AtlasConfig { max_size: DeviceSize, } -pub(crate) struct Atlas { +pub(crate) struct Atlas { config: AtlasConfig, atlas_allocator: AtlasAllocator, texture: T, - cache: FrameCache, AtlasHandle>, + cache: FrameCache, /// Extra textures which store only single allocation. extras: Slab, /// All allocations in the current frame and not cached. islands: ahash::HashSet, } -impl Atlas +impl Atlas where + K: Hash + Eq, T::Host: GPUBackendImpl, { pub fn new(config: AtlasConfig, format: ColorFormat, gpu_impl: &mut T::Host) -> Self { @@ -55,7 +56,7 @@ where } } - pub fn get(&mut self, key: &Resource, scale: f32) -> Option<&AtlasHandle> { + pub fn get(&mut self, key: &K, scale: f32) -> Option<&AtlasHandle> { self .cache .get(key) @@ -65,7 +66,7 @@ where /// Cache a handle to the atlas. If the key already exists, the old handle /// will be replaced - pub fn cache(&mut self, key: Resource, scale: f32, dist: AtlasDist) -> AtlasHandle { + pub fn cache(&mut self, key: K, scale: f32, dist: AtlasDist) -> AtlasHandle { let handle = AtlasHandle { scale, dist }; if self.islands.contains(&dist) { @@ -83,7 +84,7 @@ where /// Return the handle of cached resource. If the resource is not cached, /// allocate it and call `init` to initialize the texture. pub fn get_or_cache( - &mut self, key: Resource, scale: f32, size: DeviceSize, gpu: &mut T::Host, + &mut self, key: K, scale: f32, size: DeviceSize, gpu: &mut T::Host, init: impl FnOnce(&DeviceRect, &mut T, &mut T::Host), ) -> AtlasHandle { if let Some(h) = self.get(&key, scale) { @@ -198,7 +199,7 @@ impl AtlasDist { } } - pub(super) fn tex_rect(&self, atlas: &Atlas) -> DeviceRect + pub(super) fn tex_rect(&self, atlas: &Atlas) -> DeviceRect where T: Texture, { @@ -217,7 +218,7 @@ impl AtlasHandle { } } - pub(super) fn tex_rect(&self, atlas: &Atlas) -> DeviceRect + pub(super) fn tex_rect(&self, atlas: &Atlas) -> DeviceRect where T: Texture, { @@ -243,7 +244,10 @@ impl Eq for AtlasDist {} #[cfg(feature = "wgpu")] #[cfg(test)] mod tests { + use std::any::Any; + use futures::executor::block_on; + use ribir_algo::Resource; use super::*; use crate::{WgpuImpl, WgpuTexture}; @@ -252,8 +256,11 @@ mod tests { fn resource_hit() { let mut gpu = block_on(WgpuImpl::headless()); let size = gpu.limits().texture_size; - let mut atlas = - Atlas::::new(AtlasConfig::new("", size), ColorFormat::Rgba8, &mut gpu); + let mut atlas = Atlas::, WgpuTexture>::new( + AtlasConfig::new("", size), + ColorFormat::Rgba8, + &mut gpu, + ); let resource = Resource::new(1); let h1 = atlas.get_or_cache(resource.clone().into_any(), 1., size, &mut gpu, |_, _, _| {}); let h2 = atlas.get_or_cache(resource.clone().into_any(), 0.8, size, &mut gpu, |_, _, _| {}); @@ -268,7 +275,7 @@ mod tests { #[test] fn atlas_grow_to_alloc() { let mut gpu_impl = block_on(WgpuImpl::headless()); - let mut atlas = Atlas::::new( + let mut atlas = Atlas::, WgpuTexture>::new( AtlasConfig::new("", DeviceSize::new(4096, 4096)), ColorFormat::Alpha8, &mut gpu_impl, @@ -286,8 +293,11 @@ mod tests { fn resource_clear() { let mut wgpu = block_on(WgpuImpl::headless()); let size = wgpu.limits().texture_size; - let mut atlas = - Atlas::::new(AtlasConfig::new("", size), ColorFormat::Rgba8, &mut wgpu); + let mut atlas = Atlas::, WgpuTexture>::new( + AtlasConfig::new("", size), + ColorFormat::Rgba8, + &mut wgpu, + ); let dist = atlas.allocate(DeviceSize::new(32, 32), &mut wgpu); atlas.cache(Resource::new(1).into_any(), 1., dist); atlas.allocate(size, &mut wgpu); @@ -302,7 +312,7 @@ mod tests { #[test] fn fix_scale_path_cache_miss() { let mut wgpu = block_on(WgpuImpl::headless()); - let mut atlas = Atlas::::new( + let mut atlas = Atlas::, WgpuTexture>::new( AtlasConfig::new("", DeviceSize::new(4096, 4096)), ColorFormat::Rgba8, &mut wgpu, @@ -333,7 +343,7 @@ mod tests { #[test] fn fix_atlas_expand_overlap() { let mut wgpu = block_on(WgpuImpl::headless()); - let mut atlas = Atlas::::new( + let mut atlas = Atlas::, WgpuTexture>::new( AtlasConfig::new("", DeviceSize::new(4096, 4096)), ColorFormat::Alpha8, &mut wgpu, diff --git a/gpu/src/gpu_backend/textures_mgr.rs b/gpu/src/gpu_backend/textures_mgr.rs index cb3a6666e..52770097d 100644 --- a/gpu/src/gpu_backend/textures_mgr.rs +++ b/gpu/src/gpu_backend/textures_mgr.rs @@ -4,7 +4,10 @@ use guillotiere::euclid::SideOffsets2D; use rayon::{prelude::ParallelIterator, slice::ParallelSlice}; use ribir_algo::Resource; use ribir_geom::{DeviceRect, DeviceSize, Size, Transform, transform_to_device_rect}; -use ribir_painter::{PaintPath, Path, PixelImage, Vertex, VertexBuffers, image::ColorFormat}; +use ribir_painter::{ + PaintPath, PaintingStyle, Path, PixelImage, StrokeOptions, Vertex, VertexBuffers, + image::ColorFormat, +}; use super::{ Texture, @@ -21,24 +24,31 @@ pub(super) enum TextureID { Bundle(usize), } +#[derive(PartialEq, Clone)] +enum PathKey { + Fill(Resource), + Stroke { resource: Resource, options: StrokeOptions }, +} + pub(super) struct TexturesMgr { - alpha_atlas: Atlas, - rgba_atlas: Atlas, + alpha_atlas: Atlas, + rgba_atlas: Atlas, T>, /// Similar to the `rgba_atlas`, this is used to allocate the target texture /// for drawing commands. /// /// We keep it separate from `rgba_atlas` because the backend may not permit a /// texture to be used both as a target and as a sampled resource in the same /// draw call. - target_atlas: Atlas, - fill_task: Vec, - fill_task_buffers: VertexBuffers<()>, + target_atlas: Atlas, T>, + tess_task: Vec, + tess_task_buffer: VertexBuffers<()>, need_clear_areas: Vec, } -struct FillTask { +struct TessTask { slice: TextureSlice, path: PaintPath, + style: PaintingStyle, // transform to construct vertex transform: Transform, clip_rect: Option, @@ -94,8 +104,8 @@ where ColorFormat::Rgba8, gpu_impl, ), - fill_task: <_>::default(), - fill_task_buffers: <_>::default(), + tess_task: <_>::default(), + tess_task_buffer: <_>::default(), need_clear_areas: vec![], } } @@ -103,29 +113,40 @@ where /// Store an alpha path in texture and return the texture and a transform that /// can transform the mask to viewport pub(super) fn store_alpha_path( - &mut self, path: &PaintPath, matrix: &Transform, viewport: &DeviceRect, gpu: &mut T::Host, + &mut self, path: &PaintPath, style: &PaintingStyle, matrix: &Transform, viewport: &DeviceRect, + gpu: &mut T::Host, ) -> (TextureSlice, Transform) { + let path_bounds = path.bounds(style.line_width()); match path { PaintPath::Share(p) => { - let cache_scale: f32 = self.cache_scale(&path.bounds().size, matrix); - let key = p.clone().into_any(); + let resource = p.clone().into_any(); + let cache_scale: f32 = self.cache_scale(&path_bounds.size, matrix); + let key = match style { + PaintingStyle::Fill => PathKey::Fill(resource), + PaintingStyle::Stroke(options) => PathKey::Stroke { resource, options: options.clone() }, + }; + let (slice, scale) = if let Some(h) = self.alpha_atlas.get(&key, cache_scale).copied() { - let mask_slice = self.alpha_atlas_dist_to_tex_silice(&h.dist); + let mask_slice = self.alpha_atlas_dist_to_tex_slice(&h.dist); (mask_slice, h.scale) } else { - let scale_bounds = p.bounds().scale(cache_scale, cache_scale); + let scale_bounds = path_bounds.scale(cache_scale, cache_scale); let (dist, slice) = self.alpha_allocate(scale_bounds.round_out().size.to_i32().cast_unit(), gpu); let _ = self.alpha_atlas.cache(key, cache_scale, dist); let offset = slice.rect.origin.to_f32().cast_unit() - scale_bounds.origin; let transform = Transform::scale(cache_scale, cache_scale).then_translate(offset); - self - .fill_task - .push(FillTask { slice, path: path.clone(), transform, clip_rect: None }); + self.tess_task.push(TessTask { + slice, + path: path.clone(), + transform, + clip_rect: None, + style: style.clone(), + }); (slice, cache_scale) }; - let path_origin = p.bounds().origin * scale; + let path_origin = path_bounds.origin * scale; let slice_origin = slice.rect.origin.to_vector().to_f32(); // back to slice origin let matrix = Transform::translation(-slice_origin.x, -slice_origin.y) @@ -139,7 +160,7 @@ where (slice.expand_for_paste(), matrix) } PaintPath::Own(_) => { - let paint_bounds = transform_to_device_rect(path.bounds(), matrix); + let paint_bounds = transform_to_device_rect(&path_bounds, matrix); let alloc_size = size_expand_blank(paint_bounds.size); let (visual_rect, clip_rect) = if self.alpha_atlas.is_good_size_to_alloc(alloc_size) { @@ -156,8 +177,9 @@ where .to_f32() .cast_unit(); let ts = matrix.then_translate(offset); - let task = FillTask { slice, transform: ts, path: path.clone(), clip_rect }; - self.fill_task.push(task); + let task = + TessTask { slice, transform: ts, path: path.clone(), style: style.clone(), clip_rect }; + self.tess_task.push(task); let offset = (visual_rect.origin - slice.rect.origin).to_f32(); (slice.expand_for_paste(), Transform::translation(offset.x, offset.y)) @@ -168,17 +190,24 @@ where pub(super) fn store_image( &mut self, img: &Resource, gpu: &mut T::Host, ) -> TextureSlice { - let atlas = match img.color_format() { - ColorFormat::Rgba8 => &mut self.rgba_atlas, - ColorFormat::Alpha8 => &mut self.alpha_atlas, - }; - - let h = - atlas.get_or_cache(img.clone().into_any(), 1., img.size(), gpu, |rect, texture, gpu| { - texture.write_data(rect, img.pixel_bytes(), gpu) - }); - - TextureSlice { tex_id: TextureID::Rgba(h.tex_id()), rect: h.tex_rect(atlas) } + match img.color_format() { + ColorFormat::Rgba8 => { + let atlas = &mut self.rgba_atlas; + let h = + atlas.get_or_cache(img.clone().into_any(), 1., img.size(), gpu, |rect, texture, gpu| { + texture.write_data(rect, img.pixel_bytes(), gpu) + }); + TextureSlice { tex_id: TextureID::Rgba(h.tex_id()), rect: h.tex_rect(atlas) } + } + ColorFormat::Alpha8 => { + let key = PathKey::Fill(img.clone().into_any()); + let atlas = &mut self.alpha_atlas; + let h = atlas.get_or_cache(key, 1., img.size(), gpu, |rect, texture, gpu| { + texture.write_data(rect, img.pixel_bytes(), gpu) + }); + TextureSlice { tex_id: TextureID::Rgba(h.tex_id()), rect: h.tex_rect(atlas) } + } + } } pub(super) fn store_commands( @@ -204,10 +233,10 @@ where // affect the current slice. let dist = self.alpha_atlas.allocate(size, gpu); - (dist, self.alpha_atlas_dist_to_tex_silice(&dist)) + (dist, self.alpha_atlas_dist_to_tex_slice(&dist)) } - fn alpha_atlas_dist_to_tex_silice(&self, dist: &AtlasDist) -> TextureSlice { + fn alpha_atlas_dist_to_tex_slice(&self, dist: &AtlasDist) -> TextureSlice { let blank_side = SideOffsets2D::new_all_same(ALPHA_BLANK_EDGE); let rect = dist.tex_rect(&self.alpha_atlas); @@ -229,17 +258,26 @@ where } } - fn fill_tess( - path: &Path, ts: &Transform, slice_size: &DeviceSize, buffer: &mut VertexBuffers<()>, + fn tessellate( + path: &Path, style: &PaintingStyle, ts: &Transform, slice_size: &DeviceSize, + buffer: &mut VertexBuffers<()>, ) -> Range { let start = buffer.indices.len() as u32; - let path_size = path.bounds().size; + let path_size = path.bounds(style.line_width()).size; let slice_size = slice_size.to_f32(); let scale = (slice_size.width / path_size.width).max(slice_size.height / path_size.height); - path.tessellate(TOLERANCE / scale, buffer, |pos| { + let tolerance = TOLERANCE / scale; + let vertex_ctor = |pos| { let pos = ts.transform_point(pos); Vertex::new([pos.x, pos.y], ()) - }); + }; + match style { + PaintingStyle::Fill => path.fill_tessellate(tolerance, buffer, vertex_ctor), + PaintingStyle::Stroke(options) => { + path.stroke_tessellate(tolerance, options.clone(), buffer, vertex_ctor) + } + } + start..buffer.indices.len() as u32 } @@ -247,7 +285,7 @@ where where T: Texture, { - if self.fill_task.is_empty() { + if self.tess_task.is_empty() { return; } @@ -257,7 +295,7 @@ where self.need_clear_areas.clear(); } - self.fill_task.sort_by(|a, b| { + self.tess_task.sort_by(|a, b| { let a_clip = a.clip_rect.is_some(); let b_clip = b.clip_rect.is_some(); if a_clip == b_clip { @@ -269,18 +307,19 @@ where } }); - let mut draw_indices = Vec::with_capacity(self.fill_task.len()); - if self.fill_task.len() < PAR_CHUNKS_SIZE { - for f in self.fill_task.iter() { - let FillTask { slice, path, clip_rect, transform: ts } = f; - let rg = Self::fill_tess(path, ts, &slice.rect.size, &mut self.fill_task_buffers); + let mut draw_indices = Vec::with_capacity(self.tess_task.len()); + if self.tess_task.len() < PAR_CHUNKS_SIZE { + for f in self.tess_task.iter() { + let TessTask { slice, path, clip_rect, transform, style } = f; + let rg = + Self::tessellate(path, style, transform, &slice.rect.size, &mut self.tess_task_buffer); draw_indices.push((slice.tex_id, rg, clip_rect)); } } else { - let mut tasks = Vec::with_capacity(self.fill_task.len()); - for f in self.fill_task.iter() { - let FillTask { slice, path, clip_rect, transform: ts } = f; - tasks.push((slice, ts, path, clip_rect)); + let mut tasks = Vec::with_capacity(self.tess_task.len()); + for f in self.tess_task.iter() { + let TessTask { slice, path, clip_rect, transform, style } = f; + tasks.push((slice, style, transform, path, clip_rect)); } let par_tess_res = tasks @@ -288,8 +327,8 @@ where .map(|tasks| { let mut buffer = VertexBuffers::default(); let mut indices = Vec::with_capacity(tasks.len()); - for (slice, ts, path, clip_rect) in tasks.iter() { - let rg = Self::fill_tess(path, ts, &slice.rect.size, &mut buffer); + for (slice, style, ts, path, clip_rect) in tasks.iter() { + let rg = Self::tessellate(path, style, ts, &slice.rect.size, &mut buffer); indices.push((slice.tex_id, rg, *clip_rect)); } (indices, buffer) @@ -299,17 +338,17 @@ where par_tess_res .into_iter() .for_each(|(indices, buffer)| { - let offset = self.fill_task_buffers.indices.len() as u32; + let offset = self.tess_task_buffer.indices.len() as u32; draw_indices.extend(indices.into_iter().map(|(id, mut rg, clip)| { rg.start += offset; rg.end += offset; (id, rg, clip) })); - extend_buffer(&mut self.fill_task_buffers, buffer); + extend_buffer(&mut self.tess_task_buffer, buffer); }) }; - gpu_impl.load_alpha_vertices(&self.fill_task_buffers); + gpu_impl.load_alpha_vertices(&self.tess_task_buffer); let mut idx = 0; loop { @@ -343,16 +382,16 @@ where rg.start..end.start } else { idx = draw_indices.len(); - rg.start..self.fill_task_buffers.indices.len() as u32 + rg.start..self.tess_task_buffer.indices.len() as u32 }; let texture = id_to_texture_mut!(self, *tex_id); gpu_impl.draw_alpha_triangles(&indices, texture); } - self.fill_task.clear(); - self.fill_task_buffers.vertices.clear(); - self.fill_task_buffers.indices.clear(); + self.tess_task.clear(); + self.tess_task_buffer.vertices.clear(); + self.tess_task_buffer.indices.clear(); } pub(crate) fn end_frame(&mut self) { @@ -400,6 +439,24 @@ impl TextureSlice { } } +impl Hash for PathKey { + fn hash(&self, state: &mut H) { + match self { + PathKey::Fill(path) => path.hash(state), + PathKey::Stroke { resource: path, options } => { + path.hash(state); + let StrokeOptions { width, miter_limit, line_cap, line_join } = options; + width.to_bits().hash(state); + miter_limit.to_bits().hash(state); + line_cap.hash(state); + line_join.hash(state); + } + } + } +} + +impl Eq for PathKey {} + #[cfg(feature = "wgpu")] #[cfg(test)] pub mod tests { @@ -477,10 +534,21 @@ pub mod tests { let p = PaintPath::Share(p.clone()); let viewport = rect(0, 0, 1024, 1024); - let (slice1, ts1) = mgr.store_alpha_path(&p, &Transform::scale(2., 2.), &viewport, &mut wgpu); + let (slice1, ts1) = mgr.store_alpha_path( + &p, + &PaintingStyle::Fill, + &Transform::scale(2., 2.), + &viewport, + &mut wgpu, + ); - let (slice2, ts2) = - mgr.store_alpha_path(&p, &Transform::translation(100., 100.), &viewport, &mut wgpu); + let (slice2, ts2) = mgr.store_alpha_path( + &p, + &PaintingStyle::Fill, + &Transform::translation(100., 100.), + &viewport, + &mut wgpu, + ); assert_eq!(slice1, slice2); assert_eq!(ts1, Transform::new(1., 0., 0., 1., -2., -2.)); diff --git a/macros/src/lib.rs b/macros/src/lib.rs index f8045892f..fd5b2bdbb 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -248,10 +248,11 @@ pub fn part_writer(input: TokenStream) -> TokenStream { /// This macro returns an expression of type `Svg`. #[proc_macro] pub fn include_crate_svg(input: TokenStream) -> TokenStream { - let file = parse_macro_input! { input as syn::LitStr }; + let IncludeSvgArgs { path, inherit_fill, inherit_stroke } = + parse_macro_input! { input as IncludeSvgArgs }; let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let path = std::path::Path::new(&dir).join(file.value()); - include_svg_from_path(path) + let path = std::path::Path::new(&dir).join(path); + include_svg_from_path(path, inherit_fill, inherit_stroke) } /// Includes an SVG file as an `Svg`. @@ -265,7 +266,8 @@ pub fn include_crate_svg(input: TokenStream) -> TokenStream { #[cfg(feature = "nightly")] #[proc_macro] pub fn include_svg(input: TokenStream) -> TokenStream { - let rf = parse_macro_input! { input as syn::LitStr }; + let IncludeSvgArgs { path, inherit_fill, inherit_stroke } = + parse_macro_input! { input as IncludeSvgArgs }; let mut span = proc_macro::Span::call_site(); while let Some(p) = span.parent() { @@ -273,14 +275,16 @@ pub fn include_svg(input: TokenStream) -> TokenStream { } let mut file = span.source_file().path(); file.pop(); - file.push(rf.value()); + file.push(path); - include_svg_from_path(file) + include_svg_from_path(file, inherit_fill, inherit_stroke) } -fn include_svg_from_path(path: std::path::PathBuf) -> TokenStream { - let encoded_bytes = - ribir_painter::Svg::open(path.as_path()).and_then(|reader| reader.serialize()); +fn include_svg_from_path( + path: std::path::PathBuf, inherit_fill: bool, inherit_stroke: bool, +) -> TokenStream { + let encoded_bytes = ribir_painter::Svg::open(path.as_path(), inherit_fill, inherit_stroke) + .and_then(|reader| reader.serialize()); match encoded_bytes { Ok(data) => quote! { Svg::deserialize(#data).unwrap() @@ -292,3 +296,21 @@ fn include_svg_from_path(path: std::path::PathBuf) -> TokenStream { } } } + +struct IncludeSvgArgs { + path: String, + inherit_fill: bool, + inherit_stroke: bool, +} + +impl syn::parse::Parse for IncludeSvgArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let path = input.parse::()?.value(); + input.parse::()?; + let inherit_fill = input.parse::()?.value; + input.parse::()?; + let inherit_stroke = input.parse::()?.value; + + Ok(IncludeSvgArgs { path, inherit_fill, inherit_stroke }) + } +} 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 44e826979..0a27f28a1 100644 --- a/painter/src/painter.rs +++ b/painter/src/painter.rs @@ -72,11 +72,21 @@ pub enum PaintPath { /// The action to apply to the path, such as fill color, image, gradient, etc. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaintPathAction { + Paint { + brush: CommandBrush, + /// The style to paint the path. + painting_style: PaintingStyle, + }, + + Clip, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CommandBrush { Color(Color), Image { img: Resource, opacity: f32 }, Radial(RadialGradient), Linear(LinearGradient), - Clip, } #[repr(u32)] @@ -111,6 +121,17 @@ pub struct PathCommand { pub action: PaintPathAction, } +/// Explain the method for rendering shapes and paths, including filling or +/// stroking them. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub enum PaintingStyle { + /// Fill the path. + #[default] + Fill, + /// Stroke path with line width. + Stroke(StrokeOptions), +} + /// Define the default method for the painter to render paths, including filling /// or stroking them. #[derive(Debug, Clone, Copy, Default)] @@ -366,8 +387,9 @@ impl Painter { pub fn clip(&mut self, path: PaintPath) -> &mut Self { invisible_return!(self); - if locatable_bounds(&path.bounds) { - if let Some(bounds) = self.intersection_paint_bounds(&path.bounds) { + let p_bounds = path.bounds(None); + if locatable_bounds(&p_bounds) { + if let Some(bounds) = self.intersection_paint_bounds(&p_bounds) { let s = self.current_state_mut(); s.bounds = s.transform.outer_transformed_rect(&bounds); let cmd = PathCommand::new(path, PaintPathAction::Clip, s.transform); @@ -381,63 +403,23 @@ impl Painter { /// Fill a path with fill brush. pub fn fill_path(&mut self, path: PaintPath) -> &mut Self { - invisible_return!(self); - - let brush = self.fill_brush().clone(); - if locatable_bounds(&path.bounds) - && self.intersect_paint_bounds(&path.bounds) - && brush.is_visible() - { - let mut action = match brush { - Brush::Color(color) => PaintPathAction::Color(color), - Brush::Image(img) => PaintPathAction::Image { img, opacity: 1. }, - Brush::RadialGradient(radial_gradient) => PaintPathAction::Radial(radial_gradient), - Brush::LinearGradient(linear_gradient) => PaintPathAction::Linear(linear_gradient), - }; - action.apply_alpha(self.alpha()); - let ts = *self.transform(); - let cmd = PathCommand::new(path, action, ts); - self.commands.push(PaintCommand::Path(cmd)); - } - - self + self.inner_draw_path(path, PathStyle::Fill) } /// Draw a path with the default style. pub fn draw_path(&mut self, path: PaintPath) -> &mut Self { - match &self.current_state().style { - PathStyle::Fill => self.fill_path(path), - PathStyle::Stroke => { - let path = path.deref().clone(); - self.stroke_path(path) - } - } + self.inner_draw_path(path, self.current_state().style) } /// Outlines the current path with the current brush and `StrokeOptions`. - /// - /// ## Note - /// - /// Unlike `fill_path`, `stroke_path` accepts a `Path` instead of a - /// `PaintPath`. Therefore, the path will not be cached across `stroke_path` - /// calls, as the actual path depends on the current `StrokeOptions` of the - /// painter. - /// - /// If you want to stroke a path using `Resource`, you should retain the - /// result of `Path::stroke` with `Resource` and pass it to `fill_path`. - pub fn stroke_path(&mut self, path: Path) -> &mut Self { - if let Some(stroke_path) = path.stroke(self.stroke_options(), Some(self.transform())) { - self.swap_brush(); - self.fill_path(stroke_path.into()); - self.swap_brush(); - } - self + pub fn stroke_path(&mut self, path: PaintPath) -> &mut Self { + self.inner_draw_path(path, PathStyle::Stroke) } /// Strokes (outlines) the current path with the current brush and line width. pub fn stroke(&mut self) -> &mut Self { let builder = std::mem::take(&mut self.path_builder); - self.stroke_path(builder.build()) + self.stroke_path(builder.build().into()) } /// Fill the current path with current brush. @@ -587,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 @@ -594,15 +577,17 @@ 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); - path.action.apply_alpha(alpha); + if let PaintPathAction::Paint { ref mut brush, .. } = path.action { + brush.apply_alpha(alpha); + } PaintCommand::Path(path) } PaintCommand::PopClip => PaintCommand::PopClip, @@ -616,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 @@ -678,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 @@ -726,9 +711,42 @@ impl Painter { self } - fn swap_brush(&mut self) { - let state = self.current_state_mut(); - std::mem::swap(&mut state.fill_brush, &mut state.stroke_brush); + fn inner_draw_path(&mut self, path: PaintPath, path_style: PathStyle) -> &mut Self { + invisible_return!(self); + let line_width = matches!(path_style, PathStyle::Stroke).then(|| self.line_width()); + let p_bounds = path.bounds(line_width); + if !locatable_bounds(&p_bounds) || !self.intersect_paint_bounds(&p_bounds) { + return self; + } + + let brush = match path_style { + PathStyle::Fill => self.fill_brush().clone(), + PathStyle::Stroke => self.stroke_brush().clone(), + }; + + if brush.is_visible() { + let mut brush = CommandBrush::from(brush); + let painting_style = match path_style { + PathStyle::Fill => PaintingStyle::Fill, + PathStyle::Stroke => PaintingStyle::Stroke(self.stroke_options().clone()), + }; + brush.apply_alpha(self.alpha()); + let ts = *self.transform(); + let action = PaintPathAction::Paint { brush, painting_style }; + let cmd = PathCommand::new(path, action, ts); + self.commands.push(PaintCommand::Path(cmd)); + } + + self + } +} + +impl PaintingStyle { + pub fn line_width(&self) -> Option { + match self { + PaintingStyle::Fill => None, + PaintingStyle::Stroke(stroke) => Some(stroke.width), + } } } @@ -784,6 +802,17 @@ impl Painter { } } +impl From for CommandBrush { + fn from(brush: Brush) -> Self { + match brush { + Brush::Color(color) => CommandBrush::Color(color), + Brush::Image(img) => CommandBrush::Image { img, opacity: 1. }, + Brush::RadialGradient(radial_gradient) => CommandBrush::Radial(radial_gradient), + Brush::LinearGradient(linear_gradient) => CommandBrush::Linear(linear_gradient), + } + } +} + impl Drop for PainterResult<'_> { fn drop(&mut self) { self.0.clear() } } @@ -843,7 +872,12 @@ impl From> for PaintPath { impl PathCommand { pub fn new(path: PaintPath, action: PaintPathAction, transform: Transform) -> Self { - let paint_bounds = transform.outer_transformed_rect(path.bounds()); + let line_width = if let PaintPathAction::Paint { painting_style, .. } = &action { + painting_style.line_width() + } else { + None + }; + let paint_bounds = transform.outer_transformed_rect(&path.bounds(line_width)); Self { path, transform, paint_bounds, action } } @@ -856,20 +890,19 @@ impl PathCommand { self.transform = self.transform.then(transform); self.paint_bounds = self .transform - .outer_transformed_rect(self.path.bounds()); + .outer_transformed_rect(&self.path.bounds(None)); } } -impl PaintPathAction { +impl CommandBrush { pub fn apply_alpha(&mut self, alpha: f32) -> &mut Self { match self { - PaintPathAction::Color(color) => *color = color.apply_alpha(alpha), - PaintPathAction::Image { opacity, .. } => *opacity *= alpha, - PaintPathAction::Radial(RadialGradient { stops, .. }) - | PaintPathAction::Linear(LinearGradient { stops, .. }) => stops + CommandBrush::Color(color) => *color = color.apply_alpha(alpha), + CommandBrush::Image { opacity, .. } => *opacity *= alpha, + CommandBrush::Radial(RadialGradient { stops, .. }) + | CommandBrush::Linear(LinearGradient { stops, .. }) => stops .iter_mut() .for_each(|s| s.color = s.color.apply_alpha(alpha)), - PaintPathAction::Clip => {} } self } @@ -952,7 +985,8 @@ mod test { fn filter_invalid_commands() { let mut painter = painter(); - let svg = Svg::parse_from_bytes(include_bytes!("../../tests/assets/test1.svg")).unwrap(); + let svg = + Svg::parse_from_bytes(include_bytes!("../../tests/assets/test1.svg"), true, false).unwrap(); painter .save() .set_transform(Transform::translation(f32::NAN, f32::INFINITY)) @@ -963,8 +997,12 @@ mod test { #[test] fn draw_svg_gradient() { let mut painter = Painter::new(Rect::from_size(Size::new(64., 64.))); - let svg = - Svg::parse_from_bytes(include_bytes!("../../tests/assets/fill_with_gradient.svg")).unwrap(); + let svg = Svg::parse_from_bytes( + include_bytes!("../../tests/assets/fill_with_gradient.svg"), + true, + false, + ) + .unwrap(); painter.draw_svg(&svg); } diff --git a/painter/src/path.rs b/painter/src/path.rs index 771a76504..240875d31 100644 --- a/painter/src/path.rs +++ b/painter/src/path.rs @@ -1,24 +1,25 @@ use std::ops::Range; use lyon_algorithms::{ + geom::euclid::SideOffsets2D, measure::{PathMeasurements, SampleType}, path::{Event, Path as LyonPath}, }; use ribir_geom::{Point, Rect, Transform}; use serde::{Deserialize, Serialize}; -use crate::path_builder::{PathBuilder, stroke_path}; +use crate::path_builder::PathBuilder; /// Path widget describe a shape, build the shape from [`Builder`]! #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Path { pub(crate) lyon_path: LyonPath, // the bounds of the path. - pub(crate) bounds: Rect, + bounds: Rect, } /// Stroke properties. -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct StrokeOptions { /// A stroke thickness. /// @@ -107,11 +108,20 @@ pub struct PathSampler { } impl Path { + pub(crate) fn new(lyon_path: LyonPath, bounds: Rect) -> Self { Self { lyon_path, bounds } } + #[inline] pub fn builder() -> PathBuilder { PathBuilder::default() } - #[inline] - pub fn bounds(&self) -> &Rect { &self.bounds } + pub fn bounds(&self, line_width: Option) -> Rect { + if let Some(line_width) = line_width { + self + .bounds + .outer_rect(SideOffsets2D::new_all_same(line_width / 2.)) + } else { + self.bounds + } + } /// create a rect path. pub fn rect(rect: &Rect) -> Self { @@ -135,14 +145,6 @@ impl Path { builder.build() } - /// Convert this path to a stroked path - /// - /// `ts` is the current transform of the path pre applied. Provide it have a - /// more precise convert. - pub fn stroke(&self, options: &StrokeOptions, ts: Option<&Transform>) -> Option { - stroke_path(&self.lyon_path, options, ts).map(Into::into) - } - /// Returns a transformed path in place. /// /// Some points may become NaN/inf therefore this method can fail. @@ -175,7 +177,7 @@ impl Path { } #[cfg(feature = "tessellation")] - pub fn tessellate( + pub fn fill_tessellate( &self, tolerance: f32, buffer: &mut VertexBuffers, vertex_ctor: impl Fn(Point) -> Vertex, ) { @@ -192,6 +194,46 @@ impl Path { ) .unwrap(); } + + #[cfg(feature = "tessellation")] + pub fn stroke_tessellate( + &self, tolerance: f32, options: StrokeOptions, buffer: &mut VertexBuffers, + vertex_ctor: impl Fn(Point) -> Vertex, + ) { + use lyon_tessellation::{ + BuffersBuilder, StrokeOptions as TessOptions, StrokeTessellator, StrokeVertex, + }; + + let mut stroke_tess = StrokeTessellator::default(); + let StrokeOptions { width, miter_limit, line_cap, line_join } = options; + let cap = match line_cap { + LineCap::Butt => lyon_tessellation::LineCap::Butt, + LineCap::Round => lyon_tessellation::LineCap::Round, + LineCap::Square => lyon_tessellation::LineCap::Square, + }; + let join = match line_join { + LineJoin::Miter => lyon_tessellation::LineJoin::Miter, + LineJoin::Round => lyon_tessellation::LineJoin::Round, + LineJoin::Bevel => lyon_tessellation::LineJoin::Bevel, + LineJoin::MiterClip => lyon_tessellation::LineJoin::MiterClip, + }; + let options = TessOptions::tolerance(tolerance) + .with_start_cap(cap) + .with_end_cap(cap) + .with_line_join(join) + .with_miter_limit(miter_limit) + .with_line_width(width); + + stroke_tess + .tessellate_path( + &self.lyon_path, + &options, + &mut BuffersBuilder::new(buffer, move |v: StrokeVertex| { + vertex_ctor(v.position().cast_unit()) + }), + ) + .unwrap(); + } } impl PathSampler { diff --git a/painter/src/path_builder.rs b/painter/src/path_builder.rs index 92a1d474d..da58afcb8 100644 --- a/painter/src/path_builder.rs +++ b/painter/src/path_builder.rs @@ -1,13 +1,12 @@ use lyon_algorithms::path::{ - Event, Path as LyonPath, Winding, + Winding, builder::BorderRadii, geom::{Arc, LineSegment}, path::Builder as LyonBuilder, }; -use ribir_geom::{Angle, Point, Rect, Transform, Vector}; -use usvg::tiny_skia_path; +use ribir_geom::{Angle, Point, Rect, Vector}; -use crate::{LineCap, LineJoin, Path, Radius, StrokeOptions}; +use crate::{Path, Radius}; #[derive(Default)] pub struct PathBuilder { @@ -154,14 +153,6 @@ impl PathBuilder { self } - /// Return a path that strokes (outlines) the current path with the stroke - /// options. - #[inline] - pub fn stroke(self, options: &StrokeOptions, ts: Option<&Transform>) -> Option { - let path = self.lyon_builder.build(); - stroke_path(&path, options, ts).map(Into::into) - } - /// Construct a path from the current state of the builder. #[inline] pub fn build(self) -> Path { @@ -175,80 +166,6 @@ impl PathBuilder { /// Caller must ensure that the bounds are correct. pub fn build_with_bounds(self, bounds: Rect) -> Path { let path = self.lyon_builder.build(); - Path { lyon_path: path, bounds } - } -} - -pub(crate) fn stroke_path( - path: &LyonPath, options: &StrokeOptions, ts: Option<&Transform>, -) -> Option { - let mut builder = tiny_skia_path::PathBuilder::default(); - let resolution = ts.map_or(1., |t| { - let t = into_tiny_transform(*t); - tiny_skia_path::PathStroker::compute_resolution_scale(&t) - }); - - path.iter().for_each(|e| match e { - Event::Begin { at } => builder.move_to(at.x, at.y), - Event::Line { to, .. } => builder.line_to(to.x, to.y), - Event::Quadratic { ctrl, to, .. } => builder.quad_to(ctrl.x, ctrl.y, to.x, to.y), - Event::Cubic { ctrl1, ctrl2, to, .. } => { - builder.cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y) - } - Event::End { close, .. } => { - if close { - builder.close() - } - } - }); - - let path = builder - .finish() - .unwrap() - .stroke(&options.clone().into(), resolution)?; - - let mut builder = LyonPath::svg_builder(); - path.segments().for_each(|seg| match seg { - tiny_skia_path::PathSegment::MoveTo(at) => { - builder.move_to((at.x, at.y).into()); - } - tiny_skia_path::PathSegment::LineTo(to) => { - builder.line_to((to.x, to.y).into()); - } - tiny_skia_path::PathSegment::QuadTo(c, t) => { - builder.quadratic_bezier_to((c.x, c.y).into(), (t.x, t.y).into()); - } - tiny_skia_path::PathSegment::CubicTo(c1, c2, to) => { - builder.cubic_bezier_to((c1.x, c1.y).into(), (c2.x, c2.y).into(), (to.x, to.y).into()); - } - tiny_skia_path::PathSegment::Close => builder.close(), - }); - Some(builder.build()) -} - -fn into_tiny_transform(ts: Transform) -> tiny_skia_path::Transform { - let Transform { m11, m12, m21, m22, m31, m32, .. } = ts; - tiny_skia_path::Transform { sx: m11, kx: m21, ky: m12, sy: m22, tx: m31, ty: m32 } -} - -impl From for tiny_skia_path::Stroke { - fn from(value: StrokeOptions) -> Self { - let StrokeOptions { width, miter_limit, line_cap, line_join } = value; - tiny_skia_path::Stroke { - width, - miter_limit, - line_cap: match line_cap { - LineCap::Butt => tiny_skia_path::LineCap::Butt, - LineCap::Round => tiny_skia_path::LineCap::Round, - LineCap::Square => tiny_skia_path::LineCap::Square, - }, - line_join: match line_join { - LineJoin::Miter => tiny_skia_path::LineJoin::Miter, - LineJoin::Round => tiny_skia_path::LineJoin::Round, - LineJoin::Bevel => tiny_skia_path::LineJoin::Bevel, - LineJoin::MiterClip => tiny_skia_path::LineJoin::MiterClip, - }, - dash: None, - } + Path::new(path, bounds) } } diff --git a/painter/src/svg.rs b/painter/src/svg.rs index b7348dd3a..e5c86f5ca 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,49 @@ 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>, + + inherited_fill: bool, + inherited_stroke: bool, + #[serde(skip)] + last: RefCell>, +} + +#[derive(Clone)] +struct StaticSvg { + inherited_fill: Brush, + inherited_stroke: Brush, + commands: Resource>, } -// todo: we need to support currentColor to change svg color. // todo: share fontdb impl Svg { - pub fn parse_from_bytes(svg_data: &[u8]) -> Result> { - let opt = Options { ..<_>::default() }; + // FIXME: This is a temporary workaround. Utilize the magic color for the SVG, + // and replace it with the actual color when rendering. + const DYNAMIC_COLOR: Color = Color::from_u32(0x191B1901); + const DYNAMIC_COLOR_STR: &'static str = "#191B1901"; + + /// Parse SVG from bytes. + /// + /// - **inherit_fill**: Indicates whether this SVG will inherit the fill color + /// from the environment. + /// - **inherit_stroke**: Indicates whether this SVG will inherit the stroke + /// color from the environment. + pub fn parse_from_bytes( + svg_data: &[u8], inherit_fill: bool, inherit_stroke: bool, + ) -> Result> { + let magic = Self::DYNAMIC_COLOR_STR; + let style_sheet = match (inherit_fill, inherit_stroke) { + (true, true) => Some(format!("svg {{ fill: {magic}; stroke: {magic} }}")), + (true, false) => Some(format!("svg {{ fill: {magic} }}")), + (false, true) => Some(format!("svg {{ stroke: {magic} }}")), + _ => None, + }; + + let opt = Options { style_sheet, ..<_>::default() }; let tree = Tree::from_data(svg_data, &opt).unwrap(); let size = tree.size(); @@ -34,20 +67,61 @@ 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), + inherited_fill: used_fill_fallback, + inherited_stroke: used_stroke_fallback, + last: RefCell::new(None), }) } - pub fn open>(path: P) -> Result> { + /// Parse SVG from a file. + /// + /// - **inherit_fill**: Indicates whether this SVG will inherit the fill color + /// from the environment. + /// - **inherit_stroke**: Indicates whether this SVG will inherit the stroke + /// color from the environment. + pub fn open>( + path: P, fill_inject: bool, stroke_inject: bool, + ) -> Result> { let mut file = std::fs::File::open(path)?; let mut bytes = vec![]; file.read_to_end(&mut bytes)?; - Self::parse_from_bytes(&bytes) + Self::parse_from_bytes(&bytes, fill_inject, stroke_inject) + } + + pub fn size(&self) -> Size { self.size } + + pub fn commands( + &self, fill_brush: &Brush, stroke_brush: &Brush, + ) -> Resource> { + if !self.inherited_fill && !self.inherited_stroke { + self.commands.clone() + } else { + let mut last = self.last.borrow_mut(); + if let Some(last) = last + .as_ref() + .filter(|last| &last.inherited_fill == fill_brush && &last.inherited_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 { + inherited_fill: fill_brush.clone(), + inherited_stroke: stroke_brush.clone(), + commands: commands.clone(), + }); + commands + } + } } + pub fn command_size(&self) -> usize { self.commands.len() } + pub fn serialize(&self) -> Result> { // use json replace bincode, because https://github.com/Ogeon/palette/issues/130 Ok(serde_json::to_string(self)?) @@ -101,15 +175,9 @@ fn paint_group(g: &usvg::Group, painter: &mut crate::Painter) { let (brush, transform) = brush_from_usvg_paint(stroke.paint(), stroke.opacity()); painter .set_stroke_brush(brush.clone()) - .apply_transform(&transform); - - let path = path - .transform(&transform.inverse().unwrap()) - .stroke(&options, Some(painter.transform())); - - if let Some(p) = path { - painter.fill_path(Resource::new(p).into()); - } + .set_strokes(options) + .apply_transform(&transform) + .stroke_path(path.into()); }; } usvg::Node::Image(_) => { @@ -187,6 +255,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()); @@ -236,3 +361,15 @@ impl From for LineJoin { } } } + +impl Clone for Svg { + fn clone(&self) -> Self { + Svg { + size: self.size, + commands: self.commands.clone(), + inherited_fill: self.inherited_fill, + inherited_stroke: self.inherited_stroke, + last: RefCell::new(self.last.borrow().clone()), + } + } +} diff --git a/painter/src/text/svg_glyph_cache.rs b/painter/src/text/svg_glyph_cache.rs index 6f82f52de..87176d392 100644 --- a/painter/src/text/svg_glyph_cache.rs +++ b/painter/src/text/svg_glyph_cache.rs @@ -107,7 +107,7 @@ impl SvgDocument { std::str::from_utf8(&writer.into_inner()) .ok() - .and_then(|str| Svg::parse_from_bytes(str.as_bytes()).ok()) + .and_then(|str| Svg::parse_from_bytes(str.as_bytes(), true, false).ok()) } fn parse(data: &[u8]) -> Option> { diff --git a/test_cases/include_svg/fix_draw_svg_not_apply_alpha_wgpu.png b/test_cases/include_svg/fix_draw_svg_not_apply_alpha_wgpu.png index 65dedc1b8..a06bff75f 100644 Binary files a/test_cases/include_svg/fix_draw_svg_not_apply_alpha_wgpu.png and b/test_cases/include_svg/fix_draw_svg_not_apply_alpha_wgpu.png differ diff --git a/test_cases/ribir_core/builtin_widgets/svg/tests/svgs_smoke_wgpu.png b/test_cases/ribir_core/builtin_widgets/svg/tests/svgs_smoke_wgpu.png index 461336091..776a07acf 100644 Binary files a/test_cases/ribir_core/builtin_widgets/svg/tests/svgs_smoke_wgpu.png and b/test_cases/ribir_core/builtin_widgets/svg/tests/svgs_smoke_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png index 40643243c..71575bb83 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/checked_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/checked_with_material_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/checked_with_material_by_wgpu.png index 40643243c..71575bb83 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/checked_with_material_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/checked_with_material_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png index 927a1ebac..754b83f87 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_material_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_material_by_wgpu.png index 927a1ebac..754b83f87 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_material_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/indeterminate_with_material_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png index a1ec87942..e6d7008d8 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_material_by_wgpu.png b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_material_by_wgpu.png index a1ec87942..e6d7008d8 100644 Binary files a/test_cases/ribir_widgets/checkbox/tests/unchecked_with_material_by_wgpu.png and b/test_cases/ribir_widgets/checkbox/tests/unchecked_with_material_by_wgpu.png differ 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..5d14c3486 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..fd06b4e24 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/test_cases/ribir_widgets/path/tests/stroke_circle40_with_default_by_wgpu.png b/test_cases/ribir_widgets/path/tests/stroke_circle40_with_default_by_wgpu.png index 398eb025d..b5037c9ed 100644 Binary files a/test_cases/ribir_widgets/path/tests/stroke_circle40_with_default_by_wgpu.png and b/test_cases/ribir_widgets/path/tests/stroke_circle40_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/path/tests/stroke_circle40_with_material_by_wgpu.png b/test_cases/ribir_widgets/path/tests/stroke_circle40_with_material_by_wgpu.png index 398eb025d..b5037c9ed 100644 Binary files a/test_cases/ribir_widgets/path/tests/stroke_circle40_with_material_by_wgpu.png and b/test_cases/ribir_widgets/path/tests/stroke_circle40_with_material_by_wgpu.png differ diff --git a/tests/include_svg_test.rs b/tests/include_svg_test.rs index cfcb31c43..b63d44d3d 100644 --- a/tests/include_svg_test.rs +++ b/tests/include_svg_test.rs @@ -3,13 +3,13 @@ use ribir_dev_helper::*; #[test] fn include_svg() { - let svg: Svg = include_crate_svg!("./assets/test1.svg"); - assert_eq!(svg.commands.len(), 2); + let svg: Svg = include_crate_svg!("./assets/test1.svg", true, false); + assert_eq!(svg.command_size(), 2); } fn fix_draw_svg_not_apply_alpha() -> Painter { let mut painter = Painter::new(Rect::from_size(Size::new(64., 64.))); - let svg: Svg = include_crate_svg!("./assets/test1.svg"); + let svg: Svg = include_crate_svg!("./assets/test1.svg", true, false); painter.apply_alpha(0.5).draw_svg(&svg); painter } diff --git a/widgets/src/icon.rs b/widgets/src/icon.rs index 0dcccc13f..626ee8a4e 100644 --- a/widgets/src/icon.rs +++ b/widgets/src/icon.rs @@ -20,7 +20,7 @@ use crate::text::*; /// theme /// .font_files /// .push("the font file path".to_string()); -/// theme.default_icon_font = FontFace { +/// theme.icon_font = FontFace { /// families: Box::new([FontFamily::Name("Your icon font family name".into())]), /// // The rest of the face configuration depends on your font file /// ..<_>::default() @@ -54,7 +54,7 @@ use crate::text::*; /// class_impl: style_class! { /// clamp: BoxClamp::fixed_size(Size::new(64., 64.)), /// text_style: TextStyle { -/// font_face: Theme::of(BuildCtx::get()).default_icon_font.clone(), +/// font_face: Theme::of(BuildCtx::get()).icon_font.clone(), /// line_height: 64., /// font_size: 64., /// ..<_>::default() @@ -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. diff --git a/widgets/src/path.rs b/widgets/src/path.rs index f2919877e..6137b121f 100644 --- a/widgets/src/path.rs +++ b/widgets/src/path.rs @@ -80,7 +80,7 @@ mod tests { .foreground(Color::BLACK) }) .with_wnd_size(WND_SIZE) - .with_comparison(0.000025), - LayoutCase::default().with_size(SIZE_40) + .with_comparison(0.00003), + LayoutCase::default().with_size(Size::splat(40.5)) ); } diff --git a/widgets/src/text.rs b/widgets/src/text.rs index 5462cd2cb..69353a350 100644 --- a/widgets/src/text.rs +++ b/widgets/src/text.rs @@ -15,12 +15,12 @@ pub struct Text { impl Render for Text { fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { - let style = Provider::of::(&ctx).unwrap(); + let style = ctx.text_style(); let info = AppCtx::typography_store() .borrow_mut() .typography( self.text.substr(..), - &style, + style, clamp.max, self.text_align, PlaceLineDirection::TopToBottom,