Skip to content

Commit 9ee0280

Browse files
authored
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. <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.) ```
1 parent 41207ef commit 9ee0280

10 files changed

+229
-10
lines changed

masonry/src/widget/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub use progress_bar::ProgressBar;
4444
pub use prose::Prose;
4545
pub use root_widget::RootWidget;
4646
pub use scroll_bar::ScrollBar;
47-
pub use sized_box::SizedBox;
47+
pub use sized_box::{Padding, SizedBox};
4848
pub use spinner::Spinner;
4949
pub use split::Split;
5050
pub use textbox::Textbox;
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

masonry/src/widget/sized_box.rs

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,27 @@ struct BorderStyle {
2525
color: Color,
2626
}
2727

28+
/// Padding specifies the spacing between the edges of the box and the child view.
29+
///
30+
/// A Padding can also be constructed using [`from(value: f64)`][Self::from]
31+
/// as well as from a `(f64, f64)` tuple, or `(f64, f64, f64, f64)` tuple, following the CSS padding conventions.
32+
#[derive(Debug, Clone, Copy, PartialEq)]
33+
pub struct Padding {
34+
/// The amount of padding in logical pixels for the top edge.
35+
pub top: f64,
36+
/// The amount of padding in logical pixels for the trailing edge.
37+
///
38+
/// For LTR contexts this is the right edge, for RTL it is the left edge.
39+
pub trailing: f64,
40+
/// The amount of padding in logical pixels for the bottom edge.
41+
pub bottom: f64,
42+
/// The amount of padding in logical pixels for the leading edge.
43+
///
44+
/// For LTR contexts this is the left edge, for RTL it is the right edge.
45+
pub leading: f64,
46+
}
47+
2848
// TODO - Have Widget type as generic argument
29-
// TODO - Add Padding
3049

3150
/// A widget with predefined size.
3251
///
@@ -44,6 +63,84 @@ pub struct SizedBox {
4463
background: Option<Brush>,
4564
border: Option<BorderStyle>,
4665
corner_radius: RoundedRectRadii,
66+
padding: Padding,
67+
}
68+
69+
// --- MARK: IMPL PADDING ---
70+
71+
impl Padding {
72+
/// Constructs a new `Padding` by specifying the amount of padding for each edge.
73+
pub const fn new(top: f64, trailing: f64, bottom: f64, leading: f64) -> Self {
74+
Self {
75+
top,
76+
trailing,
77+
bottom,
78+
leading,
79+
}
80+
}
81+
82+
/// A padding of zero for all edges.
83+
pub const ZERO: Padding = Padding::all(0.);
84+
85+
/// Constructs a new `Padding` with equal amount of padding for all edges.
86+
pub const fn all(padding: f64) -> Self {
87+
Self::new(padding, padding, padding, padding)
88+
}
89+
90+
/// Constructs a new `Padding` with the same amount of padding for the horizontal edges,
91+
/// and zero padding for the vertical edges.
92+
pub const fn horizontal(padding: f64) -> Self {
93+
Self::new(0., padding, 0., padding)
94+
}
95+
96+
/// Constructs a new `Padding` with the same amount of padding for the vertical edges,
97+
/// and zero padding for the horizontal edges.
98+
pub const fn vertical(padding: f64) -> Self {
99+
Self::new(padding, 0., padding, 0.)
100+
}
101+
102+
/// Constructs a new `Padding` with padding only at the top edge and zero padding for all other edges.
103+
pub const fn top(padding: f64) -> Self {
104+
Self::new(padding, 0., 0., 0.)
105+
}
106+
107+
/// Constructs a new `Padding` with padding only at the trailing edge and zero padding for all other edges.
108+
pub const fn trailing(padding: f64) -> Self {
109+
Self::new(0., padding, 0., 0.)
110+
}
111+
112+
/// Constructs a new `Padding` with padding only at the bottom edge and zero padding for all other edges.
113+
pub const fn bottom(padding: f64) -> Self {
114+
Self::new(0., 0., padding, 0.)
115+
}
116+
117+
/// Constructs a new `Padding` with padding only at the leading edge and zero padding for all other edges.
118+
pub const fn leading(padding: f64) -> Self {
119+
Self::new(0., 0., 0., padding)
120+
}
121+
}
122+
123+
impl From<f64> for Padding {
124+
/// Converts the value to a `Padding` object with that amount of padding on all edges.
125+
fn from(value: f64) -> Self {
126+
Self::all(value)
127+
}
128+
}
129+
130+
impl From<(f64, f64, f64, f64)> for Padding {
131+
/// Converts the tuple to a `Padding` object,
132+
/// following CSS padding order for 4 values (top, trailing, bottom, leading).
133+
fn from(value: (f64, f64, f64, f64)) -> Self {
134+
Self::new(value.0, value.1, value.2, value.3)
135+
}
136+
}
137+
138+
impl From<(f64, f64)> for Padding {
139+
/// Converts the tuple to a `Padding` object,
140+
/// following CSS padding order for 2 values (vertical, horizontal)
141+
fn from(value: (f64, f64)) -> Self {
142+
Self::new(value.0, value.1, value.0, value.1)
143+
}
47144
}
48145

49146
// --- MARK: BUILDERS ---
@@ -57,6 +154,7 @@ impl SizedBox {
57154
background: None,
58155
border: None,
59156
corner_radius: RoundedRectRadii::from_single_radius(0.0),
157+
padding: Padding::ZERO,
60158
}
61159
}
62160

@@ -69,6 +167,7 @@ impl SizedBox {
69167
background: None,
70168
border: None,
71169
corner_radius: RoundedRectRadii::from_single_radius(0.0),
170+
padding: Padding::ZERO,
72171
}
73172
}
74173

@@ -81,6 +180,7 @@ impl SizedBox {
81180
background: None,
82181
border: None,
83182
corner_radius: RoundedRectRadii::from_single_radius(0.0),
183+
padding: Padding::ZERO,
84184
}
85185
}
86186

@@ -97,6 +197,7 @@ impl SizedBox {
97197
background: None,
98198
border: None,
99199
corner_radius: RoundedRectRadii::from_single_radius(0.0),
200+
padding: Padding::ZERO,
100201
}
101202
}
102203

@@ -180,6 +281,12 @@ impl SizedBox {
180281
self
181282
}
182283

284+
/// Builder style method for specifying the padding added by the box.
285+
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
286+
self.padding = padding.into();
287+
self
288+
}
289+
183290
// TODO - child()
184291
}
185292

@@ -266,6 +373,17 @@ impl SizedBox {
266373
this.ctx.request_paint_only();
267374
}
268375

376+
/// Clears padding.
377+
pub fn clear_padding(this: &mut WidgetMut<'_, Self>) {
378+
Self::set_padding(this, Padding::ZERO);
379+
}
380+
381+
/// Set the padding around this widget.
382+
pub fn set_padding(this: &mut WidgetMut<'_, Self>, padding: impl Into<Padding>) {
383+
this.widget.padding = padding.into();
384+
this.ctx.request_layout();
385+
}
386+
269387
// TODO - Doc
270388
pub fn child_mut<'t>(
271389
this: &'t mut WidgetMut<'_, Self>,
@@ -333,6 +451,14 @@ impl Widget for SizedBox {
333451
let child_bc = child_bc.shrink((2.0 * border_width, 2.0 * border_width));
334452
let origin = Point::new(border_width, border_width);
335453

454+
// Shrink constraints by padding inset
455+
let padding_size = Size::new(
456+
self.padding.leading + self.padding.trailing,
457+
self.padding.top + self.padding.bottom,
458+
);
459+
let child_bc = child_bc.shrink(padding_size);
460+
let origin = origin + (self.padding.leading, self.padding.top);
461+
336462
let mut size;
337463
match self.child.as_mut() {
338464
Some(child) => {
@@ -341,7 +467,7 @@ impl Widget for SizedBox {
341467
size = Size::new(
342468
size.width + 2.0 * border_width,
343469
size.height + 2.0 * border_width,
344-
);
470+
) + padding_size;
345471
}
346472
None => size = bc.constrain((self.width.unwrap_or(0.0), self.height.unwrap_or(0.0))),
347473
};
@@ -476,6 +602,19 @@ mod tests {
476602
assert_render_snapshot!(harness, "label_box_with_size");
477603
}
478604

605+
#[test]
606+
fn label_box_with_padding() {
607+
let widget = SizedBox::new(Label::new("hello"))
608+
.border(Color::BLUE, 5.0)
609+
.rounded(5.0)
610+
.padding((60., 40.));
611+
612+
let mut harness = TestHarness::create(widget);
613+
614+
assert_debug_snapshot!(harness.root_widget());
615+
assert_render_snapshot!(harness, "label_box_with_padding");
616+
}
617+
479618
#[test]
480619
fn label_box_with_solid_background() {
481620
let widget = SizedBox::new(Label::new("hello"))
@@ -512,5 +651,37 @@ mod tests {
512651
assert_render_snapshot!(harness, "empty_box_with_gradient_background");
513652
}
514653

654+
#[test]
655+
fn label_box_with_padding_and_background() {
656+
let widget = SizedBox::new(Label::new("hello"))
657+
.width(40.0)
658+
.height(40.0)
659+
.background(Color::PLUM)
660+
.border(Color::LIGHT_SKY_BLUE, 5.)
661+
.padding(100.);
662+
663+
let mut harness = TestHarness::create(widget);
664+
665+
assert_debug_snapshot!(harness.root_widget());
666+
assert_render_snapshot!(harness, "label_box_with_background_and_padding");
667+
}
668+
669+
#[test]
670+
fn label_box_with_padding_outside() {
671+
let widget = SizedBox::new(
672+
SizedBox::new(Label::new("hello"))
673+
.width(40.0)
674+
.height(40.0)
675+
.background(Color::PLUM)
676+
.border(Color::LIGHT_SKY_BLUE, 5.),
677+
)
678+
.padding(100.);
679+
680+
let mut harness = TestHarness::create(widget);
681+
682+
assert_debug_snapshot!(harness.root_widget());
683+
assert_render_snapshot!(harness, "label_box_with_outer_padding");
684+
}
685+
515686
// TODO - add screenshot tests for different brush types
516687
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: masonry/src/widget/sized_box.rs
3+
expression: harness.root_widget()
4+
---
5+
SizedBox(
6+
Label<hello>,
7+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: masonry/src/widget/sized_box.rs
3+
expression: harness.root_widget()
4+
---
5+
SizedBox(
6+
Label<hello>,
7+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
source: masonry/src/widget/sized_box.rs
3+
expression: harness.root_widget()
4+
---
5+
SizedBox(
6+
SizedBox(
7+
Label<hello>,
8+
),
9+
)

xilem/examples/http_cats.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use xilem::core::fork;
1818
use xilem::core::one_of::OneOf3;
1919
use xilem::view::{
2020
button, flex, image, inline_prose, portal, prose, sized_box, spinner, worker, Axis, FlexExt,
21-
FlexSpacer,
21+
FlexSpacer, Padding,
2222
};
2323
use xilem::{Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem};
2424

@@ -47,13 +47,15 @@ enum ImageState {
4747

4848
impl HttpCats {
4949
fn view(&mut self) -> impl WidgetView<HttpCats> {
50-
let left_column = portal(flex((
50+
let left_column = sized_box(portal(flex((
5151
prose("Status"),
5252
self.statuses
5353
.iter_mut()
5454
.map(Status::list_view)
5555
.collect::<Vec<_>>(),
56-
)));
56+
))))
57+
.padding(Padding::leading(5.));
58+
5759
let (info_area, worker_value) = if let Some(selected_code) = self.selected_code {
5860
if let Some(selected_status) =
5961
self.statuses.iter_mut().find(|it| it.code == selected_code)
@@ -197,8 +199,9 @@ impl Status {
197199
FlexSpacer::Fixed(10.),
198200
image,
199201
// TODO: Overlay on top of the image?
200-
// HACK: Trailing spaces workaround scrollbar covering content
201-
prose("Copyright ©️ https://http.cat ").alignment(TextAlignment::End),
202+
// HACK: Trailing padding workaround scrollbar covering content
203+
sized_box(prose("Copyright ©️ https://http.cat").alignment(TextAlignment::End))
204+
.padding(Padding::trailing(15.)),
202205
))
203206
.main_axis_alignment(xilem::view::MainAxisAlignment::Start)
204207
}

0 commit comments

Comments
 (0)