waterui_layout/
padding.rs

1//! Padding layouts that inset a child by fixed edge distances.
2
3use alloc::{vec, vec::Vec};
4use waterui_core::{AnyView, View};
5
6use crate::{Layout, Point, ProposalSize, Rect, Size, SubView, container::FixedContainer};
7
8/// Layout that insets its single child by the configured edge values.
9#[derive(Debug, Clone)]
10pub struct PaddingLayout {
11    edges: EdgeInsets,
12}
13
14impl Layout for PaddingLayout {
15    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
16        // The horizontal and vertical space consumed by padding.
17        let horizontal_padding = self.edges.leading + self.edges.trailing;
18        let vertical_padding = self.edges.top + self.edges.bottom;
19
20        // Reduce the proposed size for the child by the padding amount.
21        let child_proposal = ProposalSize {
22            width: proposal.width.map(|w| (w - horizontal_padding).max(0.0)),
23            height: proposal.height.map(|h| (h - vertical_padding).max(0.0)),
24        };
25
26        // Measure the child
27        let child_size = children
28            .first()
29            .map_or(Size::zero(), |c| c.size_that_fits(child_proposal));
30
31        // Handle infinite dimensions
32        let child_width = if child_size.width.is_infinite() {
33            proposal.width.unwrap_or(0.0) - horizontal_padding
34        } else {
35            child_size.width
36        };
37
38        let child_height = if child_size.height.is_infinite() {
39            proposal.height.unwrap_or(0.0) - vertical_padding
40        } else {
41            child_size.height
42        };
43
44        // The final size is the child's size plus the padding.
45        Size::new(
46            child_width + horizontal_padding,
47            child_height + vertical_padding,
48        )
49    }
50
51    fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
52        if children.is_empty() {
53            return vec![];
54        }
55
56        // Create the child's frame by insetting the parent's bound by the padding amount.
57        let child_origin = Point::new(bounds.x() + self.edges.leading, bounds.y() + self.edges.top);
58
59        let horizontal_padding = self.edges.leading + self.edges.trailing;
60        let vertical_padding = self.edges.top + self.edges.bottom;
61
62        let child_size = Size::new(
63            (bounds.width() - horizontal_padding).max(0.0),
64            (bounds.height() - vertical_padding).max(0.0),
65        );
66
67        vec![Rect::new(child_origin, child_size)]
68    }
69}
70
71/// Insets applied to the four edges of a rectangle.
72#[derive(Debug, Clone, PartialEq)]
73pub struct EdgeInsets {
74    top: f32,
75    bottom: f32,
76    leading: f32,
77    trailing: f32,
78}
79
80#[allow(clippy::cast_possible_truncation)]
81impl<T: Into<f64>> From<T> for EdgeInsets {
82    fn from(value: T) -> Self {
83        let v = value.into() as f32;
84        Self::all(v)
85    }
86}
87
88impl Default for EdgeInsets {
89    fn default() -> Self {
90        Self::all(0.0)
91    }
92}
93
94impl EdgeInsets {
95    /// Creates an [`EdgeInsets`] value with explicit edges.
96    #[must_use]
97    pub const fn new(top: f32, bottom: f32, leading: f32, trailing: f32) -> Self {
98        Self {
99            top,
100            bottom,
101            leading,
102            trailing,
103        }
104    }
105
106    /// Returns equal insets on every edge.
107    #[must_use]
108    pub const fn all(value: f32) -> Self {
109        Self {
110            top: value,
111            bottom: value,
112            leading: value,
113            trailing: value,
114        }
115    }
116
117    /// Returns symmetric vertical and horizontal insets.
118    #[must_use]
119    pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
120        Self {
121            top: vertical,
122            bottom: vertical,
123            leading: horizontal,
124            trailing: horizontal,
125        }
126    }
127}
128
129/// View wrapper that applies [`PaddingLayout`] to a single child.
130#[derive(Debug)]
131pub struct Padding {
132    layout: PaddingLayout,
133    content: AnyView,
134}
135
136impl Padding {
137    /// Wraps a view with custom `edges`.
138    pub fn new(edges: EdgeInsets, content: impl View + 'static) -> Self {
139        Self {
140            layout: PaddingLayout { edges },
141            content: AnyView::new(content),
142        }
143    }
144}
145
146impl View for Padding {
147    fn body(self, _env: &waterui_core::Environment) -> impl View {
148        FixedContainer::new(self.layout, vec![self.content])
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::StretchAxis;
156
157    struct MockSubView {
158        size: Size,
159    }
160
161    impl SubView for MockSubView {
162        fn size_that_fits(&self, _proposal: ProposalSize) -> Size {
163            self.size
164        }
165        fn stretch_axis(&self) -> StretchAxis {
166            StretchAxis::None
167        }
168        fn priority(&self) -> i32 {
169            0
170        }
171    }
172
173    #[test]
174    fn test_padding_size() {
175        let layout = PaddingLayout {
176            edges: EdgeInsets::all(10.0),
177        };
178
179        let mut child = MockSubView {
180            size: Size::new(50.0, 30.0),
181        };
182        let children: Vec<&dyn SubView> = vec![&mut child];
183
184        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
185
186        // Size = child size + padding on all sides
187        assert!((size.width - 70.0).abs() < f32::EPSILON); // 50 + 10 + 10
188        assert!((size.height - 50.0).abs() < f32::EPSILON); // 30 + 10 + 10
189    }
190
191    #[test]
192    fn test_padding_placement() {
193        let layout = PaddingLayout {
194            edges: EdgeInsets::new(10.0, 20.0, 15.0, 25.0),
195        };
196
197        let mut child = MockSubView {
198            size: Size::new(50.0, 30.0),
199        };
200        let children: Vec<&dyn SubView> = vec![&mut child];
201
202        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
203        let rects = layout.place(bounds, &children);
204
205        // Child origin is offset by leading and top
206        assert!((rects[0].x() - 15.0).abs() < f32::EPSILON);
207        assert!((rects[0].y() - 10.0).abs() < f32::EPSILON);
208
209        // Child size is bounds minus padding
210        assert!((rects[0].width() - 60.0).abs() < f32::EPSILON); // 100 - 15 - 25
211        assert!((rects[0].height() - 70.0).abs() < f32::EPSILON); // 100 - 10 - 20
212    }
213}