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, SubView,
9 VerticalAlignment, container::FixedContainer,
10};
11
12#[derive(Debug, Clone)]
18pub struct PaddingLayout {
19 edges: Computed<EdgeInsets>,
20}
21
22impl Layout for PaddingLayout {
23 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
24 let edges = self.edges.get();
25 let horizontal_padding = edges.leading + edges.trailing;
27 let vertical_padding = edges.top + edges.bottom;
28
29 let child_proposal = ProposalSize {
31 width: proposal.width.map(|w| (w - horizontal_padding).max(0.0)),
32 height: proposal.height.map(|h| (h - vertical_padding).max(0.0)),
33 };
34
35 let child_size = children
37 .first()
38 .map_or(Size::zero(), |c| c.measure(child_proposal).size);
39
40 let child_width = if child_size.width.is_infinite() {
46 (proposal.width.unwrap_or(0.0) - horizontal_padding).max(0.0)
47 } else {
48 child_size.width
49 };
50
51 let child_height = if child_size.height.is_infinite() {
52 (proposal.height.unwrap_or(0.0) - vertical_padding).max(0.0)
53 } else {
54 child_size.height
55 };
56
57 Size::new(
59 child_width + horizontal_padding,
60 child_height + vertical_padding,
61 )
62 }
63
64 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
65 if children.is_empty() {
66 return vec![];
67 }
68
69 let edges = self.edges.get();
70 let child_origin = Point::new(bounds.x() + edges.leading, bounds.y() + edges.top);
72
73 let horizontal_padding = edges.leading + edges.trailing;
74 let vertical_padding = edges.top + edges.bottom;
75
76 let child_size = Size::new(
77 (bounds.width() - horizontal_padding).max(0.0),
78 (bounds.height() - vertical_padding).max(0.0),
79 );
80
81 vec![Rect::new(child_origin, child_size)]
82 }
83
84 fn explicit_horizontal(
85 &self,
86 alignment: HorizontalAlignment,
87 _bounds: Rect,
88 children: &[PlacedSubview<'_>],
89 ) -> Option<f32> {
90 children
91 .first()
92 .and_then(|child| child.explicit_horizontal(alignment))
93 }
94
95 fn explicit_vertical(
96 &self,
97 alignment: VerticalAlignment,
98 _bounds: Rect,
99 children: &[PlacedSubview<'_>],
100 ) -> Option<f32> {
101 children
102 .first()
103 .and_then(|child| child.explicit_vertical(alignment))
104 }
105
106 fn watch_invalidation(&self, invalidate: LayoutInvalidationCallback) -> Vec<BoxWatcherGuard> {
107 vec![self.edges.watch(move |_| invalidate())]
108 }
109}
110
111#[derive(Debug, Clone, PartialEq)]
113pub struct EdgeInsets {
114 top: f32,
115 bottom: f32,
116 leading: f32,
117 trailing: f32,
118}
119
120nami::impl_constant!(EdgeInsets);
121
122#[allow(clippy::cast_possible_truncation)]
123impl<T: Into<f64>> From<T> for EdgeInsets {
124 fn from(value: T) -> Self {
125 let v = value.into() as f32;
126 Self::all(v)
127 }
128}
129
130impl core::ops::Add for EdgeInsets {
131 type Output = Self;
132
133 fn add(self, rhs: Self) -> Self {
135 Self {
136 top: self.top + rhs.top,
137 bottom: self.bottom + rhs.bottom,
138 leading: self.leading + rhs.leading,
139 trailing: self.trailing + rhs.trailing,
140 }
141 }
142}
143
144impl Default for EdgeInsets {
145 fn default() -> Self {
146 Self::all(0.0)
147 }
148}
149
150impl EdgeInsets {
151 #[must_use]
153 pub const fn new(top: f32, bottom: f32, leading: f32, trailing: f32) -> Self {
154 Self {
155 top,
156 bottom,
157 leading,
158 trailing,
159 }
160 }
161
162 #[must_use]
164 pub const fn all(value: f32) -> Self {
165 Self {
166 top: value,
167 bottom: value,
168 leading: value,
169 trailing: value,
170 }
171 }
172
173 #[must_use]
175 pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
176 Self {
177 top: vertical,
178 bottom: vertical,
179 leading: horizontal,
180 trailing: horizontal,
181 }
182 }
183
184 #[must_use]
186 pub const fn top(&self) -> f32 {
187 self.top
188 }
189
190 #[must_use]
192 pub const fn bottom(&self) -> f32 {
193 self.bottom
194 }
195
196 #[must_use]
198 pub const fn leading(&self) -> f32 {
199 self.leading
200 }
201
202 #[must_use]
204 pub const fn trailing(&self) -> f32 {
205 self.trailing
206 }
207}
208
209#[derive(Debug)]
211pub struct Padding {
212 layout: PaddingLayout,
213 content: AnyView,
214}
215
216impl Padding {
217 pub fn new(edges: impl IntoComputed<EdgeInsets>, content: impl View + 'static) -> Self {
219 Self {
220 layout: PaddingLayout {
221 edges: edges.into_computed(),
222 },
223 content: AnyView::new(content),
224 }
225 }
226
227 pub fn into_inner(self) -> (Computed<EdgeInsets>, AnyView) {
229 (self.layout.edges, self.content)
230 }
231}
232
233impl View for Padding {
234 fn body(self, _env: &waterui_core::Environment) -> impl View {
235 FixedContainer::new(self.layout, vec![self.content])
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::StretchAxis;
243 use crate::ViewDimensions;
244 use crate::measure_layout;
245 use alloc::rc::Rc;
246 use core::cell::Cell;
247 use nami::binding;
248
249 struct MockSubView {
250 size: Size,
251 }
252
253 impl SubView for MockSubView {
254 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
255 ViewDimensions::new(self.size)
256 }
257 fn stretch_axis(&self) -> StretchAxis {
258 StretchAxis::None
259 }
260 fn priority(&self) -> i32 {
261 0
262 }
263 }
264
265 struct GuidedSubview {
266 size: Size,
267 horizontal_guide: f32,
268 vertical_guide: f32,
269 }
270
271 impl SubView for GuidedSubview {
272 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
273 ViewDimensions::new(self.size)
274 .with_horizontal(HorizontalAlignment::Leading, self.horizontal_guide)
275 .with_vertical(VerticalAlignment::Top, self.vertical_guide)
276 }
277
278 fn stretch_axis(&self) -> StretchAxis {
279 StretchAxis::None
280 }
281
282 fn priority(&self) -> i32 {
283 0
284 }
285 }
286
287 #[test]
288 fn test_padding_size() {
289 let layout = PaddingLayout {
290 edges: EdgeInsets::all(10.0).into_computed(),
291 };
292
293 let mut child = MockSubView {
294 size: Size::new(50.0, 30.0),
295 };
296 let children: Vec<&dyn SubView> = vec![&mut child];
297
298 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
299
300 assert!((size.width - 70.0).abs() < f32::EPSILON); assert!((size.height - 50.0).abs() < f32::EPSILON); }
304
305 #[test]
306 fn test_padding_placement() {
307 let layout = PaddingLayout {
308 edges: EdgeInsets::new(10.0, 20.0, 15.0, 25.0).into_computed(),
309 };
310
311 let mut child = MockSubView {
312 size: Size::new(50.0, 30.0),
313 };
314 let children: Vec<&dyn SubView> = vec![&mut child];
315
316 let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
317 let rects = layout.place(bounds, &children);
318
319 assert!((rects[0].x() - 15.0).abs() < f32::EPSILON);
321 assert!((rects[0].y() - 10.0).abs() < f32::EPSILON);
322
323 assert!((rects[0].width() - 60.0).abs() < f32::EPSILON); assert!((rects[0].height() - 70.0).abs() < f32::EPSILON); }
327
328 #[test]
329 fn reactive_insets_invalidate_and_change_measurement() {
330 let inset = binding(EdgeInsets::all(10.0));
331 let layout = PaddingLayout {
332 edges: inset.clone().into_computed(),
333 };
334
335 let invalidations = Rc::new(Cell::new(0));
336 let counted = Rc::clone(&invalidations);
337 let _guards = layout.watch_invalidation(Rc::new(move || {
338 counted.set(counted.get() + 1);
339 }));
340
341 let child = MockSubView {
342 size: Size::new(50.0, 30.0),
343 };
344 let children: Vec<&dyn SubView> = vec![&child];
345 assert_eq!(
346 layout
347 .size_that_fits(ProposalSize::UNSPECIFIED, &children)
348 .width,
349 70.0
350 );
351
352 inset.set(EdgeInsets::all(20.0));
353
354 assert_eq!(invalidations.get(), 1);
355 assert_eq!(
356 layout
357 .size_that_fits(ProposalSize::UNSPECIFIED, &children)
358 .width,
359 90.0
360 );
361 }
362
363 #[test]
364 fn test_padding_offsets_explicit_guides() {
365 let layout = PaddingLayout {
366 edges: EdgeInsets::new(10.0, 0.0, 15.0, 0.0).into_computed(),
367 };
368 let child = GuidedSubview {
369 size: Size::new(50.0, 30.0),
370 horizontal_guide: 8.0,
371 vertical_guide: 6.0,
372 };
373 let children: Vec<&dyn SubView> = vec![&child];
374
375 let dimensions = measure_layout(&layout, ProposalSize::UNSPECIFIED, &children);
376
377 assert_eq!(
378 dimensions.explicit_horizontal(HorizontalAlignment::Leading),
379 Some(23.0)
380 );
381 assert_eq!(
382 dimensions.explicit_vertical(VerticalAlignment::Top),
383 Some(16.0)
384 );
385 }
386}