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>
This commit is contained in:
Muhammad Ragib Hasin 2024-08-27 16:57:16 +06:00 committed by GitHub
parent 04a8e8ec74
commit 92fc669493
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 145 additions and 66 deletions

View File

@ -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;

View File

@ -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

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d089700d96178cbb0b101cfd9c637bb7e41ee2911fa4d15fd66a2ca0067d9c56
size 81747

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8c3813349a80712477a0fe65e931c046c804bb8698da4ecdebd15bd3e59f473
size 5421

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea70d4d9e961064f6988c225eda221719fee5f3fafdce1b41db38a2e3d72bead
size 4862

View File

@ -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<dyn FnMut(&mut PaintCtx)>),
}
/// Something that can be used as the border for a widget.
struct BorderStyle {
width: f64,
@ -51,7 +41,7 @@ pub struct SizedBox {
child: Option<WidgetPod<Box<dyn Widget>>>,
width: Option<f64>,
height: Option<f64>,
background: Option<BackgroundBrush>,
background: Option<Brush>,
border: Option<BorderStyle>,
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<BackgroundBrush>) -> 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<Brush>) -> 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<BackgroundBrush>) {
/// 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<Brush>) {
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<Color> for BackgroundBrush {
fn from(src: Color) -> BackgroundBrush {
BackgroundBrush::Color(src)
}
}
impl From<Gradient> for BackgroundBrush {
fn from(src: Gradient) -> BackgroundBrush {
BackgroundBrush::Gradient(src)
}
}
impl<Painter: FnMut(&mut PaintCtx) + 'static> From<Painter> 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

View File

@ -0,0 +1,5 @@
---
source: masonry/src/widget/sized_box.rs
expression: harness.root_widget()
---
SizedBox

View File

@ -0,0 +1,7 @@
---
source: masonry/src/widget/sized_box.rs
expression: harness.root_widget()
---
SizedBox(
Label<hello>,
)

View File

@ -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<V, State, Action = ()> {
inner: V,
width: Option<f64>,
height: Option<f64>,
background: Option<Brush>,
border: Option<BorderStyle>,
corner_radius: RoundedRectRadii,
phantom: PhantomData<fn() -> (State, Action)>,
}
@ -77,6 +85,32 @@ impl<V, State, Action> SizedBox<V, State, Action> {
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<Brush>) -> 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<Color>, width: impl Into<f64>) -> 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<RoundedRectRadii>) -> Self {
self.corner_radius = radius.into();
self
}
}
impl<V, State, Action> ViewMarker for SizedBox<V, State, Action> {}
@ -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,
}