From 9ee02806c80adea4133db280a722b542da284ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Strate=20Kl=C3=B8vedal?= Date: Tue, 12 Nov 2024 14:14:40 +0100 Subject: [PATCH] Add padding attribute to `sized_box` view (#736) ## Changes ### Masonry - Added new padding attribute to sized_box Masonry widget. - 3 unit tests demonstrating how padding is used. ### Xilem - Added `Padding` struct with convenience methods for working with paddings. - Updated `http_cats` example to use padding when more convenient and fixed hack. ## Examples ```rs sized_box(label("hello world")).padding(10.) // Equal padding on all edges sized_box(label("hello world")).padding(Padding::top(10.)) // Padding only on top edge sized_box(label("hello world")).padding((10., 20., 30., 40.)) // Different padding for each edge ``` ## HTTP Cats Added padding on the left in the `http_cats` example, to make it more balanced with the right side. Screenshot 2024-11-10 at 16 22 52 ## Discussion ### Rename `sized_box` to `frame`? In swiftUI the view modifier [`.frame()`](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:)) is used to change the size of a view. I think the name `frame` better describes the purpose of `sized_box`, since it also does backgrounds, borders (and now padding). So I wanted to suggest that `sized_box` be renamed to `frame`. ### Add `SizedBoxExt` for better ergonomics? Similar to [`FlexExt`](https://github.com/linebender/xilem/blob/62588565692584e16197fb6d19cd3b41c104b675/xilem/src/view/flex.rs#L340C11-L340C18) and [`GridExt`](https://github.com/linebender/xilem/blob/62588565692584e16197fb6d19cd3b41c104b675/xilem/src/view/grid.rs#L248), I was thinking that a new `SizedBoxExt` could be introduced to easily wrap any view in a `sized_box`, something like the following. ```rust pub trait SizedBoxExt: WidgetView { fn sized_box(self) -> SizedBox { sized_box(self) } } ``` This would allow for chaining modifiers like this: ```rust label("Hello world") .text_size(20.) .sized_box() // After this padding, background, width, height can be added .padding(20.) ``` Or even somthing more advanced like this: ```rust label("Hello world") .sized_box() .padding(20.) .background(Color::Teal) .border(Color::Blue, 4.) .sized_box() // By wrapping in another sized box we add another border rather than overwriting the previous one .border(Color::Magenta, 4.) ``` --- masonry/src/widget/mod.rs | 2 +- ..._label_box_with_background_and_padding.png | 3 + ...x__tests__label_box_with_outer_padding.png | 3 + ...zed_box__tests__label_box_with_padding.png | 3 + masonry/src/widget/sized_box.rs | 175 +++++++++++++++++- ...ed_box__tests__label_box_with_padding.snap | 7 + ...label_box_with_padding_and_background.snap | 7 + ...tests__label_box_with_padding_outside.snap | 9 + xilem/examples/http_cats.rs | 13 +- xilem/src/view/sized_box.rs | 17 +- 10 files changed, 229 insertions(+), 10 deletions(-) create mode 100644 masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_background_and_padding.png create mode 100644 masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_outer_padding.png create mode 100644 masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_padding.png create mode 100644 masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding.snap create mode 100644 masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_and_background.snap create mode 100644 masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_outside.snap diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index ec41bf3c6..e547e0a22 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -44,7 +44,7 @@ pub use progress_bar::ProgressBar; pub use prose::Prose; pub use root_widget::RootWidget; pub use scroll_bar::ScrollBar; -pub use sized_box::SizedBox; +pub use sized_box::{Padding, SizedBox}; pub use spinner::Spinner; pub use split::Split; pub use textbox::Textbox; diff --git a/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_background_and_padding.png b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_background_and_padding.png new file mode 100644 index 000000000..07a51d7fa --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_background_and_padding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c48f94e963359d2828503301a009a70ed979548a72ee4bd8ffd69bede970eee5 +size 4962 diff --git a/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_outer_padding.png b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_outer_padding.png new file mode 100644 index 000000000..e7bc61590 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_outer_padding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:065c6243f27710e0ccf57eb352fb1b835430cde82b143aa5108e9550e5905513 +size 5020 diff --git a/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_padding.png b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_padding.png new file mode 100644 index 000000000..ce1c956aa --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__sized_box__tests__label_box_with_padding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a455d1ba7eb1806b0d228d2b67aac89a657f77c02feca4f8b41c216e7718d447 +size 5422 diff --git a/masonry/src/widget/sized_box.rs b/masonry/src/widget/sized_box.rs index a2d3c1de6..338345646 100644 --- a/masonry/src/widget/sized_box.rs +++ b/masonry/src/widget/sized_box.rs @@ -25,8 +25,27 @@ struct BorderStyle { color: Color, } +/// Padding specifies the spacing between the edges of the box and the child view. +/// +/// A Padding can also be constructed using [`from(value: f64)`][Self::from] +/// as well as from a `(f64, f64)` tuple, or `(f64, f64, f64, f64)` tuple, following the CSS padding conventions. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Padding { + /// The amount of padding in logical pixels for the top edge. + pub top: f64, + /// The amount of padding in logical pixels for the trailing edge. + /// + /// For LTR contexts this is the right edge, for RTL it is the left edge. + pub trailing: f64, + /// The amount of padding in logical pixels for the bottom edge. + pub bottom: f64, + /// The amount of padding in logical pixels for the leading edge. + /// + /// For LTR contexts this is the left edge, for RTL it is the right edge. + pub leading: f64, +} + // TODO - Have Widget type as generic argument -// TODO - Add Padding /// A widget with predefined size. /// @@ -44,6 +63,84 @@ pub struct SizedBox { background: Option, border: Option, corner_radius: RoundedRectRadii, + padding: Padding, +} + +// --- MARK: IMPL PADDING --- + +impl Padding { + /// Constructs a new `Padding` by specifying the amount of padding for each edge. + pub const fn new(top: f64, trailing: f64, bottom: f64, leading: f64) -> Self { + Self { + top, + trailing, + bottom, + leading, + } + } + + /// A padding of zero for all edges. + pub const ZERO: Padding = Padding::all(0.); + + /// Constructs a new `Padding` with equal amount of padding for all edges. + pub const fn all(padding: f64) -> Self { + Self::new(padding, padding, padding, padding) + } + + /// Constructs a new `Padding` with the same amount of padding for the horizontal edges, + /// and zero padding for the vertical edges. + pub const fn horizontal(padding: f64) -> Self { + Self::new(0., padding, 0., padding) + } + + /// Constructs a new `Padding` with the same amount of padding for the vertical edges, + /// and zero padding for the horizontal edges. + pub const fn vertical(padding: f64) -> Self { + Self::new(padding, 0., padding, 0.) + } + + /// Constructs a new `Padding` with padding only at the top edge and zero padding for all other edges. + pub const fn top(padding: f64) -> Self { + Self::new(padding, 0., 0., 0.) + } + + /// Constructs a new `Padding` with padding only at the trailing edge and zero padding for all other edges. + pub const fn trailing(padding: f64) -> Self { + Self::new(0., padding, 0., 0.) + } + + /// Constructs a new `Padding` with padding only at the bottom edge and zero padding for all other edges. + pub const fn bottom(padding: f64) -> Self { + Self::new(0., 0., padding, 0.) + } + + /// Constructs a new `Padding` with padding only at the leading edge and zero padding for all other edges. + pub const fn leading(padding: f64) -> Self { + Self::new(0., 0., 0., padding) + } +} + +impl From for Padding { + /// Converts the value to a `Padding` object with that amount of padding on all edges. + fn from(value: f64) -> Self { + Self::all(value) + } +} + +impl From<(f64, f64, f64, f64)> for Padding { + /// Converts the tuple to a `Padding` object, + /// following CSS padding order for 4 values (top, trailing, bottom, leading). + fn from(value: (f64, f64, f64, f64)) -> Self { + Self::new(value.0, value.1, value.2, value.3) + } +} + +impl From<(f64, f64)> for Padding { + /// Converts the tuple to a `Padding` object, + /// following CSS padding order for 2 values (vertical, horizontal) + fn from(value: (f64, f64)) -> Self { + Self::new(value.0, value.1, value.0, value.1) + } } // --- MARK: BUILDERS --- @@ -57,6 +154,7 @@ impl SizedBox { background: None, border: None, corner_radius: RoundedRectRadii::from_single_radius(0.0), + padding: Padding::ZERO, } } @@ -69,6 +167,7 @@ impl SizedBox { background: None, border: None, corner_radius: RoundedRectRadii::from_single_radius(0.0), + padding: Padding::ZERO, } } @@ -81,6 +180,7 @@ impl SizedBox { background: None, border: None, corner_radius: RoundedRectRadii::from_single_radius(0.0), + padding: Padding::ZERO, } } @@ -97,6 +197,7 @@ impl SizedBox { background: None, border: None, corner_radius: RoundedRectRadii::from_single_radius(0.0), + padding: Padding::ZERO, } } @@ -180,6 +281,12 @@ impl SizedBox { self } + /// Builder style method for specifying the padding added by the box. + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self + } + // TODO - child() } @@ -266,6 +373,17 @@ impl SizedBox { this.ctx.request_paint_only(); } + /// Clears padding. + pub fn clear_padding(this: &mut WidgetMut<'_, Self>) { + Self::set_padding(this, Padding::ZERO); + } + + /// Set the padding around this widget. + pub fn set_padding(this: &mut WidgetMut<'_, Self>, padding: impl Into) { + this.widget.padding = padding.into(); + this.ctx.request_layout(); + } + // TODO - Doc pub fn child_mut<'t>( this: &'t mut WidgetMut<'_, Self>, @@ -333,6 +451,14 @@ impl Widget for SizedBox { let child_bc = child_bc.shrink((2.0 * border_width, 2.0 * border_width)); let origin = Point::new(border_width, border_width); + // Shrink constraints by padding inset + let padding_size = Size::new( + self.padding.leading + self.padding.trailing, + self.padding.top + self.padding.bottom, + ); + let child_bc = child_bc.shrink(padding_size); + let origin = origin + (self.padding.leading, self.padding.top); + let mut size; match self.child.as_mut() { Some(child) => { @@ -341,7 +467,7 @@ impl Widget for SizedBox { size = Size::new( size.width + 2.0 * border_width, size.height + 2.0 * border_width, - ); + ) + padding_size; } None => size = bc.constrain((self.width.unwrap_or(0.0), self.height.unwrap_or(0.0))), }; @@ -476,6 +602,19 @@ mod tests { assert_render_snapshot!(harness, "label_box_with_size"); } + #[test] + fn label_box_with_padding() { + let widget = SizedBox::new(Label::new("hello")) + .border(Color::BLUE, 5.0) + .rounded(5.0) + .padding((60., 40.)); + + let mut harness = TestHarness::create(widget); + + assert_debug_snapshot!(harness.root_widget()); + assert_render_snapshot!(harness, "label_box_with_padding"); + } + #[test] fn label_box_with_solid_background() { let widget = SizedBox::new(Label::new("hello")) @@ -512,5 +651,37 @@ mod tests { assert_render_snapshot!(harness, "empty_box_with_gradient_background"); } + #[test] + fn label_box_with_padding_and_background() { + let widget = SizedBox::new(Label::new("hello")) + .width(40.0) + .height(40.0) + .background(Color::PLUM) + .border(Color::LIGHT_SKY_BLUE, 5.) + .padding(100.); + + let mut harness = TestHarness::create(widget); + + assert_debug_snapshot!(harness.root_widget()); + assert_render_snapshot!(harness, "label_box_with_background_and_padding"); + } + + #[test] + fn label_box_with_padding_outside() { + let widget = SizedBox::new( + SizedBox::new(Label::new("hello")) + .width(40.0) + .height(40.0) + .background(Color::PLUM) + .border(Color::LIGHT_SKY_BLUE, 5.), + ) + .padding(100.); + + let mut harness = TestHarness::create(widget); + + assert_debug_snapshot!(harness.root_widget()); + assert_render_snapshot!(harness, "label_box_with_outer_padding"); + } + // TODO - add screenshot tests for different brush types } diff --git a/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding.snap b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding.snap new file mode 100644 index 000000000..83e3661a6 --- /dev/null +++ b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding.snap @@ -0,0 +1,7 @@ +--- +source: masonry/src/widget/sized_box.rs +expression: harness.root_widget() +--- +SizedBox( + Label, +) diff --git a/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_and_background.snap b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_and_background.snap new file mode 100644 index 000000000..83e3661a6 --- /dev/null +++ b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_and_background.snap @@ -0,0 +1,7 @@ +--- +source: masonry/src/widget/sized_box.rs +expression: harness.root_widget() +--- +SizedBox( + Label, +) diff --git a/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_outside.snap b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_outside.snap new file mode 100644 index 000000000..b55891144 --- /dev/null +++ b/masonry/src/widget/snapshots/masonry__widget__sized_box__tests__label_box_with_padding_outside.snap @@ -0,0 +1,9 @@ +--- +source: masonry/src/widget/sized_box.rs +expression: harness.root_widget() +--- +SizedBox( + SizedBox( + Label, + ), +) diff --git a/xilem/examples/http_cats.rs b/xilem/examples/http_cats.rs index a6d869527..fa7de8868 100644 --- a/xilem/examples/http_cats.rs +++ b/xilem/examples/http_cats.rs @@ -18,7 +18,7 @@ use xilem::core::fork; use xilem::core::one_of::OneOf3; use xilem::view::{ button, flex, image, inline_prose, portal, prose, sized_box, spinner, worker, Axis, FlexExt, - FlexSpacer, + FlexSpacer, Padding, }; use xilem::{Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem}; @@ -47,13 +47,15 @@ enum ImageState { impl HttpCats { fn view(&mut self) -> impl WidgetView { - let left_column = portal(flex(( + let left_column = sized_box(portal(flex(( prose("Status"), self.statuses .iter_mut() .map(Status::list_view) .collect::>(), - ))); + )))) + .padding(Padding::leading(5.)); + let (info_area, worker_value) = if let Some(selected_code) = self.selected_code { if let Some(selected_status) = self.statuses.iter_mut().find(|it| it.code == selected_code) @@ -197,8 +199,9 @@ impl Status { FlexSpacer::Fixed(10.), image, // TODO: Overlay on top of the image? - // HACK: Trailing spaces workaround scrollbar covering content - prose("Copyright ©️ https://http.cat ").alignment(TextAlignment::End), + // HACK: Trailing padding workaround scrollbar covering content + sized_box(prose("Copyright ©️ https://http.cat").alignment(TextAlignment::End)) + .padding(Padding::trailing(15.)), )) .main_axis_alignment(xilem::view::MainAxisAlignment::Start) } diff --git a/xilem/src/view/sized_box.rs b/xilem/src/view/sized_box.rs index d872d7cda..78c98f1ae 100644 --- a/xilem/src/view/sized_box.rs +++ b/xilem/src/view/sized_box.rs @@ -4,6 +4,7 @@ use std::marker::PhantomData; use masonry::widget; +pub use masonry::widget::Padding; use vello::kurbo::RoundedRectRadii; use vello::peniko::{Brush, Color}; @@ -26,6 +27,7 @@ where background: None, border: None, corner_radius: RoundedRectRadii::from_single_radius(0.0), + padding: Padding::ZERO, phantom: PhantomData, } } @@ -38,6 +40,7 @@ pub struct SizedBox { background: Option, border: Option, corner_radius: RoundedRectRadii, + padding: Padding, phantom: PhantomData (State, Action)>, } @@ -104,11 +107,17 @@ impl SizedBox { self } - /// Builder style method for rounding off corners of this container by setting a corner radius + /// 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 } + + /// Builder style method for adding a padding around the widget. + pub fn padding(mut self, padding: impl Into) -> Self { + self.padding = padding.into(); + self + } } impl ViewMarker for SizedBox {} @@ -126,7 +135,8 @@ where let mut widget = widget::SizedBox::new_pod(child.inner.boxed()) .raw_width(self.width) .raw_height(self.height) - .rounded(self.corner_radius); + .rounded(self.corner_radius) + .padding(self.padding); if let Some(background) = &self.background { widget = widget.background(background.clone()); } @@ -174,6 +184,9 @@ where if self.corner_radius != prev.corner_radius { widget::SizedBox::set_rounded(&mut element, self.corner_radius); } + if self.padding != prev.padding { + widget::SizedBox::set_padding(&mut element, self.padding); + } { let mut child = widget::SizedBox::child_mut(&mut element) .expect("We only create SizedBox with a child");