waterui_layout/modifiers/
padding.rs1use alloc::{vec, vec::Vec};
4use nami::{Computed, Signal, signal::IntoComputed, watcher::BoxWatcherGuard};
5use waterui_core::{AnyView, View, layout::LayoutInvalidationCallback};
6
7use crate::{
8 HorizontalAlignment, Layout, PlacedSubview, Point, ProposalSize, Rect, Size, StretchAxis,
9 SubView, SubviewPlacement, VerticalAlignment, container::FixedContainer,
10};
11
12#[derive(Debug, Clone)]
17pub struct PaddingLayout {
18 edges: Computed<EdgeInsets>,
19}
20
21impl Layout for PaddingLayout {
22 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
25 children.first().copied().unwrap_or_default()
26 }
27
28 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
29 let edges = self.edges.get();
30 let horizontal_padding = edges.leading + edges.trailing;
32 let vertical_padding = edges.top + edges.bottom;
33
34 let child_proposal = ProposalSize {
36 width: proposal.width.map(|w| (w - horizontal_padding).max(0.0)),
37 height: proposal.height.map(|h| (h - vertical_padding).max(0.0)),
38 };
39
40 let child_size = children
42 .first()
43 .map_or(Size::zero(), |c| c.measure(child_proposal).size);
44
45 let child_width = if child_size.width.is_infinite() {
51 (proposal.width.unwrap_or(0.0) - horizontal_padding).max(0.0)
52 } else {
53 child_size.width
54 };
55
56 let child_height = if child_size.height.is_infinite() {
57 (proposal.height.unwrap_or(0.0) - vertical_padding).max(0.0)
58 } else {
59 child_size.height
60 };
61
62 Size::new(
64 child_width + horizontal_padding,
65 child_height + vertical_padding,
66 )
67 }
68
69 fn place(
70 &self,
71 bounds: Rect,
72 _proposal: ProposalSize,
73 children: &[&dyn SubView],
74 ) -> Vec<SubviewPlacement> {
75 if children.is_empty() {
76 return vec![];
77 }
78
79 let edges = self.edges.get();
80 let child_origin = Point::new(bounds.x() + edges.leading, bounds.y() + edges.top);
82
83 let horizontal_padding = edges.leading + edges.trailing;
84 let vertical_padding = edges.top + edges.bottom;
85
86 let child_size = Size::new(
87 (bounds.width() - horizontal_padding).max(0.0),
88 (bounds.height() - vertical_padding).max(0.0),
89 );
90
91 let child_proposal = ProposalSize::new(Some(child_size.width), Some(child_size.height));
95
96 vec![SubviewPlacement::new(
97 Rect::new(child_origin, child_size),
98 child_proposal,
99 )]
100 }
101
102 fn explicit_horizontal(
103 &self,
104 alignment: HorizontalAlignment,
105 _bounds: Rect,
106 children: &[PlacedSubview<'_>],
107 ) -> Option<f32> {
108 children
109 .first()
110 .and_then(|child| child.explicit_horizontal(alignment))
111 }
112
113 fn explicit_vertical(
114 &self,
115 alignment: VerticalAlignment,
116 _bounds: Rect,
117 children: &[PlacedSubview<'_>],
118 ) -> Option<f32> {
119 children
120 .first()
121 .and_then(|child| child.explicit_vertical(alignment))
122 }
123
124 fn watch_invalidation(&self, invalidate: LayoutInvalidationCallback) -> Vec<BoxWatcherGuard> {
125 vec![self.edges.watch(move |_| invalidate())]
126 }
127}
128
129#[derive(Debug, Clone, PartialEq)]
131pub struct EdgeInsets {
132 top: f32,
133 bottom: f32,
134 leading: f32,
135 trailing: f32,
136}
137
138nami::impl_constant!(EdgeInsets);
139
140macro_rules! from_scalar {
146 (lossless: $($ty:ty),+ $(,)?) => {
147 $(
148 impl From<$ty> for EdgeInsets {
149 fn from(value: $ty) -> Self {
150 Self::all(f32::from(value))
151 }
152 }
153 )+
154 };
155 (lossy: $($ty:ty),+ $(,)?) => {
156 $(
157 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
158 impl From<$ty> for EdgeInsets {
159 fn from(value: $ty) -> Self {
160 Self::all(value as f32)
161 }
162 }
163 )+
164 };
165}
166
167from_scalar!(lossless: f32, i8, i16, u8, u16);
168from_scalar!(lossy: f64, i32, u32);
169
170macro_rules! from_pair {
174 ($($ty:ty),+ $(,)?) => {
175 $(
176 #[allow(clippy::cast_possible_truncation)]
177 impl From<($ty, $ty)> for EdgeInsets {
178 fn from((vertical, horizontal): ($ty, $ty)) -> Self {
179 Self::symmetric(vertical as f32, horizontal as f32)
180 }
181 }
182 )+
183 };
184}
185
186from_pair!(f32, f64);
187
188macro_rules! from_quad {
191 ($($ty:ty),+ $(,)?) => {
192 $(
193 #[allow(clippy::cast_possible_truncation)]
194 impl From<[$ty; 4]> for EdgeInsets {
195 fn from([top, bottom, leading, trailing]: [$ty; 4]) -> Self {
196 Self::new(top as f32, bottom as f32, leading as f32, trailing as f32)
197 }
198 }
199 )+
200 };
201}
202
203from_quad!(f32, f64);
204
205impl core::ops::Add for EdgeInsets {
206 type Output = Self;
207
208 fn add(self, rhs: Self) -> Self {
210 Self {
211 top: self.top + rhs.top,
212 bottom: self.bottom + rhs.bottom,
213 leading: self.leading + rhs.leading,
214 trailing: self.trailing + rhs.trailing,
215 }
216 }
217}
218
219impl Default for EdgeInsets {
220 fn default() -> Self {
221 Self::all(0.0)
222 }
223}
224
225impl EdgeInsets {
226 #[must_use]
228 pub const fn new(top: f32, bottom: f32, leading: f32, trailing: f32) -> Self {
229 Self {
230 top,
231 bottom,
232 leading,
233 trailing,
234 }
235 }
236
237 #[must_use]
239 pub const fn all(value: f32) -> Self {
240 Self {
241 top: value,
242 bottom: value,
243 leading: value,
244 trailing: value,
245 }
246 }
247
248 #[must_use]
250 pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
251 Self {
252 top: vertical,
253 bottom: vertical,
254 leading: horizontal,
255 trailing: horizontal,
256 }
257 }
258
259 #[must_use]
261 pub const fn top(&self) -> f32 {
262 self.top
263 }
264
265 #[must_use]
267 pub const fn bottom(&self) -> f32 {
268 self.bottom
269 }
270
271 #[must_use]
273 pub const fn leading(&self) -> f32 {
274 self.leading
275 }
276
277 #[must_use]
279 pub const fn trailing(&self) -> f32 {
280 self.trailing
281 }
282}
283
284#[derive(Debug)]
286pub struct Padding {
287 layout: PaddingLayout,
288 content: AnyView,
289}
290
291impl Padding {
292 pub fn new(edges: impl IntoComputed<EdgeInsets>, content: impl View + 'static) -> Self {
294 Self {
295 layout: PaddingLayout {
296 edges: edges.into_computed(),
297 },
298 content: AnyView::new(content),
299 }
300 }
301
302 pub fn into_inner(self) -> (Computed<EdgeInsets>, AnyView) {
304 (self.layout.edges, self.content)
305 }
306}
307
308impl View for Padding {
309 fn body(self, _env: &waterui_core::Environment) -> impl View {
310 FixedContainer::new(self.layout, vec![self.content])
311 }
312
313 fn stretch_axis(&self) -> StretchAxis {
316 self.layout.stretch_axis(&[self.content.stretch_axis()])
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::StretchAxis;
324 use crate::ViewDimensions;
325 use crate::measure_layout;
326 use alloc::rc::Rc;
327 use core::cell::Cell;
328 use nami::binding;
329
330 struct MockSubView {
331 size: Size,
332 }
333
334 impl SubView for MockSubView {
335 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
336 ViewDimensions::new(self.size)
337 }
338 fn stretch_axis(&self) -> StretchAxis {
339 StretchAxis::None
340 }
341 fn priority(&self) -> i32 {
342 0
343 }
344 }
345
346 struct GuidedSubview {
347 size: Size,
348 horizontal_guide: f32,
349 vertical_guide: f32,
350 }
351
352 impl SubView for GuidedSubview {
353 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
354 ViewDimensions::new(self.size)
355 .with_horizontal(HorizontalAlignment::Leading, self.horizontal_guide)
356 .with_vertical(VerticalAlignment::Top, self.vertical_guide)
357 }
358
359 fn stretch_axis(&self) -> StretchAxis {
360 StretchAxis::None
361 }
362
363 fn priority(&self) -> i32 {
364 0
365 }
366 }
367
368 #[test]
369 fn test_padding_size() {
370 let layout = PaddingLayout {
371 edges: EdgeInsets::all(10.0).into_computed(),
372 };
373
374 let mut child = MockSubView {
375 size: Size::new(50.0, 30.0),
376 };
377 let children: Vec<&dyn SubView> = vec![&mut child];
378
379 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
380
381 assert!((size.width - 70.0).abs() < f32::EPSILON); assert!((size.height - 50.0).abs() < f32::EPSILON); }
385
386 #[test]
387 fn test_padding_placement_bounded_proposal() {
388 let layout = PaddingLayout {
389 edges: EdgeInsets::new(10.0, 20.0, 15.0, 25.0).into_computed(),
390 };
391
392 let mut child = MockSubView {
393 size: Size::new(50.0, 30.0),
394 };
395 let children: Vec<&dyn SubView> = vec![&mut child];
396
397 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
398 let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
399 let placements = layout.place(bounds, proposal, &children);
400
401 assert!((placements[0].frame.x() - 15.0).abs() < f32::EPSILON);
403 assert!((placements[0].frame.y() - 10.0).abs() < f32::EPSILON);
404
405 assert!((placements[0].frame.width() - 60.0).abs() < f32::EPSILON); assert!((placements[0].frame.height() - 70.0).abs() < f32::EPSILON); }
409
410 #[test]
411 fn reactive_insets_invalidate_and_change_measurement() {
412 let inset = binding(EdgeInsets::all(10.0));
413 let layout = PaddingLayout {
414 edges: inset.clone().into_computed(),
415 };
416
417 let invalidations = Rc::new(Cell::new(0));
418 let counted = Rc::clone(&invalidations);
419 let _guards = layout.watch_invalidation(Rc::new(move || {
420 counted.set(counted.get() + 1);
421 }));
422
423 let child = MockSubView {
424 size: Size::new(50.0, 30.0),
425 };
426 let children: Vec<&dyn SubView> = vec![&child];
427 assert_eq!(
428 layout
429 .size_that_fits(ProposalSize::UNSPECIFIED, &children)
430 .width,
431 70.0
432 );
433
434 inset.set(EdgeInsets::all(20.0));
435
436 assert_eq!(invalidations.get(), 1);
437 assert_eq!(
438 layout
439 .size_that_fits(ProposalSize::UNSPECIFIED, &children)
440 .width,
441 90.0
442 );
443 }
444
445 #[test]
446 fn test_padding_offsets_explicit_guides() {
447 let layout = PaddingLayout {
448 edges: EdgeInsets::new(10.0, 0.0, 15.0, 0.0).into_computed(),
449 };
450 let child = GuidedSubview {
451 size: Size::new(50.0, 30.0),
452 horizontal_guide: 8.0,
453 vertical_guide: 6.0,
454 };
455 let children: Vec<&dyn SubView> = vec![&child];
456
457 let dimensions = measure_layout(&layout, ProposalSize::UNSPECIFIED, &children);
458
459 assert_eq!(
460 dimensions.explicit_horizontal(HorizontalAlignment::Leading),
461 Some(23.0)
462 );
463 assert_eq!(
464 dimensions.explicit_vertical(VerticalAlignment::Top),
465 Some(16.0)
466 );
467 }
468}