Skip to content

Commit

Permalink
Add padding attribute to sized_box view (#736)
Browse files Browse the repository at this point in the history
## 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.
<img width="912" alt="Screenshot 2024-11-10 at 16 22 52"
src="https://github.com/user-attachments/assets/ce5fd4e6-412b-46c1-9387-6886ef97e653">

## 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<State, Action>: WidgetView<State, Action> {
  fn sized_box(self) -> SizedBox<Self, State, Action> {
    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.)
```
  • Loading branch information
viktorstrate authored Nov 12, 2024
1 parent 41207ef commit 9ee0280
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 10 deletions.
2 changes: 1 addition & 1 deletion masonry/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
175 changes: 173 additions & 2 deletions masonry/src/widget/sized_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -44,6 +63,84 @@ pub struct SizedBox {
background: Option<Brush>,
border: Option<BorderStyle>,
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<f64> 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 ---
Expand All @@ -57,6 +154,7 @@ impl SizedBox {
background: None,
border: None,
corner_radius: RoundedRectRadii::from_single_radius(0.0),
padding: Padding::ZERO,
}
}

Expand All @@ -69,6 +167,7 @@ impl SizedBox {
background: None,
border: None,
corner_radius: RoundedRectRadii::from_single_radius(0.0),
padding: Padding::ZERO,
}
}

Expand All @@ -81,6 +180,7 @@ impl SizedBox {
background: None,
border: None,
corner_radius: RoundedRectRadii::from_single_radius(0.0),
padding: Padding::ZERO,
}
}

Expand All @@ -97,6 +197,7 @@ impl SizedBox {
background: None,
border: None,
corner_radius: RoundedRectRadii::from_single_radius(0.0),
padding: Padding::ZERO,
}
}

Expand Down Expand Up @@ -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<Padding>) -> Self {
self.padding = padding.into();
self
}

// TODO - child()
}

Expand Down Expand Up @@ -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<Padding>) {
this.widget.padding = padding.into();
this.ctx.request_layout();
}

// TODO - Doc
pub fn child_mut<'t>(
this: &'t mut WidgetMut<'_, Self>,
Expand Down Expand Up @@ -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) => {
Expand All @@ -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))),
};
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/sized_box.rs
expression: harness.root_widget()
---
SizedBox(
Label<hello>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: masonry/src/widget/sized_box.rs
expression: harness.root_widget()
---
SizedBox(
Label<hello>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: masonry/src/widget/sized_box.rs
expression: harness.root_widget()
---
SizedBox(
SizedBox(
Label<hello>,
),
)
13 changes: 8 additions & 5 deletions xilem/examples/http_cats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -47,13 +47,15 @@ enum ImageState {

impl HttpCats {
fn view(&mut self) -> impl WidgetView<HttpCats> {
let left_column = portal(flex((
let left_column = sized_box(portal(flex((
prose("Status"),
self.statuses
.iter_mut()
.map(Status::list_view)
.collect::<Vec<_>>(),
)));
))))
.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)
Expand Down Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit 9ee0280

Please sign in to comment.