From 92fc669493e03c05b8e609463e983ce7768247ba Mon Sep 17 00:00:00 2001 From: Muhammad Ragib Hasin Date: Tue, 27 Aug 2024 16:57:16 +0600 Subject: [PATCH] Enable BackgroundBrush in SizedBox view (#541) The `SizedBox` widget in Masonry had the option to set a background brush that was not exposed in its corresponding view in Xilem. The `SizedBox` view now exposes the option. Incidentally, `BackgroundBrush::PainterFn` variant had the wrong type, as such it was not usable. A `scene` parameter is added and the closure now requires to be `Send + Sync`. --------- Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> --- masonry/src/lib.rs | 2 +- masonry/src/widget/mod.rs | 2 - ...ts__empty_box_with_gradient_background.png | 3 + ..._sized_box__tests__label_box_with_size.png | 3 + ...tests__label_box_with_solid_background.png | 3 + masonry/src/widget/sized_box.rs | 119 +++++++++--------- ...s__empty_box_with_gradient_background.snap | 5 + ...ests__label_box_with_solid_background.snap | 7 ++ xilem/src/view/sized_box.rs | 67 +++++++++- 9 files changed, 145 insertions(+), 66 deletions(-) create mode 100644 masonry/src/widget/screenshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.png create mode 100644 masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_size.png create mode 100644 masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_solid_background.png create mode 100644 masonry/src/widget/snapshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.snap create mode 100644 masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_solid_background.snap diff --git a/masonry/src/lib.rs b/masonry/src/lib.rs index 454fba6d..74eec6b5 100644 --- a/masonry/src/lib.rs +++ b/masonry/src/lib.rs @@ -142,6 +142,6 @@ pub use parley::layout::Alignment as TextAlignment; pub use util::{AsAny, Handled}; pub use vello::peniko::{Color, Gradient}; pub use widget::widget::{AllowRawMut, Widget, WidgetId}; -pub use widget::{BackgroundBrush, WidgetPod, WidgetState}; +pub use widget::{WidgetPod, WidgetState}; pub use text_helpers::ArcStr; diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index efacb1e1..fef13563 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -52,8 +52,6 @@ pub use widget_state::WidgetState; pub(crate) use widget_arena::WidgetArena; -pub use sized_box::BackgroundBrush; - use crate::{Affine, Size}; // These are based on https://api.flutter.dev/flutter/painting/BoxFit-class.html diff --git a/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.png b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.png new file mode 100644 index 00000000..da13754a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d089700d96178cbb0b101cfd9c637bb7e41ee2911fa4d15fd66a2ca0067d9c56 +size 81747 diff --git a/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_size.png b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_size.png new file mode 100644 index 00000000..e80c769a --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_size.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8c3813349a80712477a0fe65e931c046c804bb8698da4ecdebd15bd3e59f473 +size 5421 diff --git a/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_solid_background.png b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_solid_background.png new file mode 100644 index 00000000..b92cbfce --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_solid_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea70d4d9e961064f6988c225eda221719fee5f3fafdce1b41db38a2e3d72bead +size 4862 diff --git a/masonry/src/widget/sized_box.rs b/masonry/src/widget/sized_box.rs index 419f5d00..0a47f390 100644 --- a/masonry/src/widget/sized_box.rs +++ b/masonry/src/widget/sized_box.rs @@ -7,10 +7,10 @@ use accesskit::Role; use smallvec::{smallvec, SmallVec}; use tracing::{trace, trace_span, warn, Span}; use vello::kurbo::{Affine, RoundedRectRadii}; -use vello::peniko::{BlendMode, Color, Fill, Gradient}; +use vello::peniko::{Brush, Color, Fill}; use vello::Scene; -use crate::paint_scene_helpers::{fill_color, stroke}; +use crate::paint_scene_helpers::stroke; use crate::widget::{WidgetMut, WidgetPod}; use crate::{ AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, @@ -19,16 +19,6 @@ use crate::{ // FIXME - Improve all doc in this module ASAP. -/// Something that can be used as the background for a widget. -#[non_exhaustive] -#[allow(missing_docs)] -#[allow(clippy::type_complexity)] -pub enum BackgroundBrush { - Color(Color), - Gradient(Gradient), - PainterFn(Box), -} - /// Something that can be used as the border for a widget. struct BorderStyle { width: f64, @@ -51,7 +41,7 @@ pub struct SizedBox { child: Option>>, width: Option, height: Option, - background: Option, + background: Option, border: Option, corner_radius: RoundedRectRadii, } @@ -154,9 +144,11 @@ impl SizedBox { /// Builder-style method for setting the background for this widget. /// - /// This can be passed anything which can be represented by a [`BackgroundBrush`]; - /// notably, it can be any [`Color`], any gradient, or a fully custom painter `FnMut`. - pub fn background(mut self, brush: impl Into) -> Self { + /// This can be passed anything which can be represented by a [`Brush`]; + /// notably, it can be any [`Color`], any gradient, or an [`Image`]. + /// + /// [`Image`]: vello::peniko::Image + pub fn background(mut self, brush: impl Into) -> Self { self.background = Some(brush.into()); self } @@ -234,9 +226,11 @@ impl WidgetMut<'_, SizedBox> { /// Set the background for this widget. /// - /// This can be passed anything which can be represented by a [`BackgroundBrush`]; - /// notably, it can be any [`Color`], any gradient, or a fully custom painter `FnMut`. - pub fn set_background(&mut self, brush: impl Into) { + /// This can be passed anything which can be represented by a [`Brush`]; + /// notably, it can be any [`Color`], any gradient, or an [`Image`]. + /// + /// [`Image`]: vello::peniko::Image + pub fn set_background(&mut self, brush: impl Into) { self.widget.background = Some(brush.into()); self.ctx.request_paint(); } @@ -370,9 +364,13 @@ impl Widget for SizedBox { let panel = ctx.size().to_rounded_rect(corner_radius); trace_span!("paint background").in_scope(|| { - scene.push_layer(BlendMode::default(), 1., Affine::IDENTITY, &panel); - background.paint(ctx, scene); - scene.pop_layer(); + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + &*background, + Some(Affine::IDENTITY), + &panel, + ); }); } @@ -414,50 +412,13 @@ impl Widget for SizedBox { } } -// --- BackgroundBrush --- - -impl BackgroundBrush { - /// Draw this brush into a provided [`PaintCtx`]. - pub fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { - let bounds = ctx.size().to_rect(); - match self { - Self::Color(color) => fill_color(scene, &bounds, *color), - Self::Gradient(grad) => scene.fill( - Fill::NonZero, - Affine::IDENTITY, - &*grad, - Some(Affine::IDENTITY), - &bounds, - ), - Self::PainterFn(painter) => painter(ctx), - } - } -} - -impl From for BackgroundBrush { - fn from(src: Color) -> BackgroundBrush { - BackgroundBrush::Color(src) - } -} - -impl From for BackgroundBrush { - fn from(src: Gradient) -> BackgroundBrush { - BackgroundBrush::Gradient(src) - } -} - -impl From for BackgroundBrush { - fn from(src: Painter) -> BackgroundBrush { - BackgroundBrush::PainterFn(Box::new(src)) - } -} - // --- Tests --- // --- MARK: TESTS --- #[cfg(test)] mod tests { use insta::assert_debug_snapshot; + use vello::peniko::Gradient; use super::*; use crate::assert_render_snapshot; @@ -518,7 +479,43 @@ mod tests { let mut harness = TestHarness::create(widget); assert_debug_snapshot!(harness.root_widget()); - assert_render_snapshot!(harness, "label_box_no_size"); + assert_render_snapshot!(harness, "label_box_with_size"); + } + + #[test] + fn label_box_with_solid_background() { + let widget = SizedBox::new(Label::new("hello")) + .width(40.0) + .height(40.0) + .background(Color::PLUM); + + let mut harness = TestHarness::create(widget); + + assert_debug_snapshot!(harness.root_widget()); + assert_render_snapshot!(harness, "label_box_with_solid_background"); + } + + #[test] + fn empty_box_with_gradient_background() { + let widget = SizedBox::empty() + .width(40.) + .height(40.) + .rounded(20.) + .border(Color::LIGHT_SKY_BLUE, 5.) + .background( + Gradient::new_sweep((30., 30.), 0., std::f32::consts::TAU).with_stops([ + (0., Color::WHITE), + (0.25, Color::BLACK), + (0.5, Color::RED), + (0.75, Color::GREEN), + (1., Color::WHITE), + ]), + ); + + let mut harness = TestHarness::create(widget); + + assert_debug_snapshot!(harness.root_widget()); + assert_render_snapshot!(harness, "empty_box_with_gradient_background"); } // TODO - add screenshot tests for different brush types diff --git a/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.snap b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.snap new file mode 100644 index 00000000..d5e97348 --- /dev/null +++ b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__empty_box_with_gradient_background.snap @@ -0,0 +1,5 @@ +--- +source: masonry/src/widget/sized_box.rs +expression: harness.root_widget() +--- +SizedBox diff --git a/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_solid_background.snap b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_solid_background.snap new file mode 100644 index 00000000..83e3661a --- /dev/null +++ b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_solid_background.snap @@ -0,0 +1,7 @@ +--- +source: masonry/src/widget/sized_box.rs +expression: harness.root_widget() +--- +SizedBox( + Label, +) diff --git a/xilem/src/view/sized_box.rs b/xilem/src/view/sized_box.rs index 9d25fff2..6575723f 100644 --- a/xilem/src/view/sized_box.rs +++ b/xilem/src/view/sized_box.rs @@ -4,6 +4,8 @@ use std::marker::PhantomData; use masonry::widget; +use vello::kurbo::RoundedRectRadii; +use vello::peniko::{Brush, Color}; use xilem_core::ViewMarker; use crate::{ @@ -24,6 +26,9 @@ where inner, height: None, width: None, + background: None, + border: None, + corner_radius: RoundedRectRadii::from_single_radius(0.0), phantom: PhantomData, } } @@ -32,6 +37,9 @@ pub struct SizedBox { inner: V, width: Option, height: Option, + background: Option, + border: Option, + corner_radius: RoundedRectRadii, phantom: PhantomData (State, Action)>, } @@ -77,6 +85,32 @@ impl SizedBox { self.height = Some(f64::INFINITY); self } + + /// Builder-style method for setting the background for this widget. + /// + /// This can be passed anything which can be represented by a [`Brush`]; + /// notably, it can be any [`Color`], any gradient, or an [`Image`]. + /// + /// [`Image`]: vello::peniko::Image + pub fn background(mut self, brush: impl Into) -> Self { + self.background = Some(brush.into()); + self + } + + /// Builder-style method for painting a border around the widget with a color and width. + pub fn border(mut self, color: impl Into, width: impl Into) -> Self { + self.border = Some(BorderStyle { + color: color.into(), + width: width.into(), + }); + self + } + + /// Builder style method for rounding off corners of this container by setting a corner radius + pub fn rounded(mut self, radius: impl Into) -> Self { + self.corner_radius = radius.into(); + self + } } impl ViewMarker for SizedBox {} @@ -91,9 +125,16 @@ where fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { let (child, child_state) = self.inner.build(ctx); - let widget = widget::SizedBox::new_pod(child.inner.boxed()) + let mut widget = widget::SizedBox::new_pod(child.inner.boxed()) .raw_width(self.width) - .raw_height(self.height); + .raw_height(self.height) + .rounded(self.corner_radius); + if let Some(background) = &self.background { + widget = widget.background(background.clone()); + } + if let Some(border) = &self.border { + widget = widget.border(border.color, border.width); + } (Pod::new(widget), child_state) } @@ -116,6 +157,21 @@ where None => element.unset_height(), } } + if self.background != prev.background { + match &self.background { + Some(background) => element.set_background(background.clone()), + None => element.clear_background(), + } + } + if self.border != prev.border { + match &self.border { + Some(border) => element.set_border(border.color, border.width), + None => element.clear_border(), + } + } + if self.corner_radius != prev.corner_radius { + element.set_rounded(self.corner_radius); + } { let mut child = element .child_mut() @@ -148,3 +204,10 @@ where self.inner.message(view_state, id_path, message, app_state) } } + +/// Something that can be used as the border for a widget. +#[derive(PartialEq)] +struct BorderStyle { + width: f64, + color: Color, +}