waterui_layout/
frame.rs

1//! Placeholder for fixed-size frame layouts.
2//!
3//! A future iteration will add a public `Frame` view capable of overriding a
4//! child's incoming proposal. The struct below documents the intent so that
5//! renderers and component authors have a reference point.
6
7use alloc::{vec, vec::Vec};
8use waterui_core::{AnyView, View};
9
10use crate::{
11    Layout, Point, ProposalSize, Rect, Size, SubView,
12    container::FixedContainer,
13    stack::{Alignment, HorizontalAlignment, VerticalAlignment},
14};
15
16/// Planned layout that clamps a single child's proposal.
17#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
18pub struct FrameLayout {
19    min_width: Option<f32>,
20    ideal_width: Option<f32>,
21    max_width: Option<f32>,
22    min_height: Option<f32>,
23    ideal_height: Option<f32>,
24    max_height: Option<f32>,
25    alignment: Alignment,
26}
27
28impl Layout for FrameLayout {
29    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
30        // A Frame proposes a modified size to its single child.
31        // It uses its own ideal dimensions if they exist, otherwise parent's proposal.
32        // This is then clamped by the frame's min/max constraints.
33
34        let proposed_width = self.ideal_width.or(proposal.width);
35        let proposed_height = self.ideal_height.or(proposal.height);
36
37        let child_proposal = ProposalSize {
38            width: proposed_width.map(|w| {
39                w.max(self.min_width.unwrap_or(f32::NEG_INFINITY))
40                    .min(self.max_width.unwrap_or(f32::INFINITY))
41            }),
42            height: proposed_height.map(|h| {
43                h.max(self.min_height.unwrap_or(f32::NEG_INFINITY))
44                    .min(self.max_height.unwrap_or(f32::INFINITY))
45            }),
46        };
47
48        // Measure the child with our constrained proposal
49        let child_size = children
50            .first()
51            .map_or(Size::zero(), |c| c.size_that_fits(child_proposal));
52
53        // 1. Determine the frame's ideal width based on its own properties and its child.
54        let mut target_width = self.ideal_width.unwrap_or(child_size.width);
55        target_width = target_width
56            .max(self.min_width.unwrap_or(f32::NEG_INFINITY))
57            .min(self.max_width.unwrap_or(f32::INFINITY));
58
59        // 2. Determine the frame's ideal height.
60        let mut target_height = self.ideal_height.unwrap_or(child_size.height);
61        target_height = target_height
62            .max(self.min_height.unwrap_or(f32::NEG_INFINITY))
63            .min(self.max_height.unwrap_or(f32::INFINITY));
64
65        // 3. The final size is the target size, but it must also respect the parent's proposal.
66        // If the parent proposed a fixed size, we must take it.
67        Size::new(
68            proposal.width.unwrap_or(target_width),
69            proposal.height.unwrap_or(target_height),
70        )
71    }
72
73    fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
74        if children.is_empty() {
75            return vec![];
76        }
77
78        // Create constrained proposal for child
79        let proposed_width = self.ideal_width.unwrap_or_else(|| bounds.width());
80        let proposed_height = self.ideal_height.unwrap_or_else(|| bounds.height());
81
82        let child_proposal = ProposalSize {
83            width: Some(
84                proposed_width
85                    .max(self.min_width.unwrap_or(0.0))
86                    .min(self.max_width.unwrap_or(f32::INFINITY))
87                    .min(bounds.width()),
88            ),
89            height: Some(
90                proposed_height
91                    .max(self.min_height.unwrap_or(0.0))
92                    .min(self.max_height.unwrap_or(f32::INFINITY))
93                    .min(bounds.height()),
94            ),
95        };
96
97        let child_size = children
98            .first()
99            .map_or(Size::zero(), |c| c.size_that_fits(child_proposal));
100
101        // Handle infinite dimensions (axis-expanding views)
102        let child_width = if child_size.width.is_infinite() {
103            bounds.width()
104        } else {
105            child_size.width
106        };
107
108        let child_height = if child_size.height.is_infinite() {
109            bounds.height()
110        } else {
111            child_size.height
112        };
113
114        let final_child_size = Size::new(child_width, child_height);
115
116        // Calculate the child's origin point (top-left) based on alignment.
117        let child_x = match self.alignment.horizontal() {
118            HorizontalAlignment::Leading => bounds.x(),
119            HorizontalAlignment::Center => {
120                bounds.x() + (bounds.width() - final_child_size.width) / 2.0
121            }
122            HorizontalAlignment::Trailing => bounds.max_x() - final_child_size.width,
123        };
124
125        let child_y = match self.alignment.vertical() {
126            VerticalAlignment::Top => bounds.y(),
127            VerticalAlignment::Center => {
128                bounds.y() + (bounds.height() - final_child_size.height) / 2.0
129            }
130            VerticalAlignment::Bottom => bounds.max_y() - final_child_size.height,
131        };
132
133        vec![Rect::new(Point::new(child_x, child_y), final_child_size)]
134    }
135}
136
137/// A view that provides a frame with optional size constraints and alignment for its child.
138///
139/// The Frame view allows you to specify minimum, ideal, and maximum dimensions
140/// for width and height, and controls how the child is aligned within the frame.
141#[derive(Debug)]
142pub struct Frame {
143    layout: FrameLayout,
144    content: AnyView,
145}
146
147impl Frame {
148    /// Creates a new Frame with the specified content and alignment.
149    ///
150    /// # Arguments
151    /// * `content` - The child view to be contained within the frame
152    /// * `alignment` - How the child should be aligned within the frame
153    #[must_use]
154    pub fn new(content: impl View) -> Self {
155        Self {
156            layout: FrameLayout::default(),
157            content: AnyView::new(content),
158        }
159    }
160
161    /// Sets the alignment of the child within the frame.
162    ///
163    /// # Arguments
164    /// * `alignment` - The alignment to apply to the child view
165    #[must_use]
166    pub const fn alignment(mut self, alignment: Alignment) -> Self {
167        self.layout.alignment = alignment;
168        self
169    }
170
171    /// Sets the ideal width of the frame.
172    #[must_use]
173    pub const fn width(mut self, width: f32) -> Self {
174        self.layout.ideal_width = Some(width);
175        self
176    }
177
178    /// Sets the ideal height of the frame.
179    #[must_use]
180    pub const fn height(mut self, height: f32) -> Self {
181        self.layout.ideal_height = Some(height);
182        self
183    }
184
185    /// Sets the minimum width of the frame.
186    #[must_use]
187    pub const fn min_width(mut self, width: f32) -> Self {
188        self.layout.min_width = Some(width);
189        self
190    }
191
192    /// Sets the maximum width of the frame.
193    #[must_use]
194    pub const fn max_width(mut self, width: f32) -> Self {
195        self.layout.max_width = Some(width);
196        self
197    }
198
199    /// Sets the minimum height of the frame.
200    #[must_use]
201    pub const fn min_height(mut self, height: f32) -> Self {
202        self.layout.min_height = Some(height);
203        self
204    }
205
206    /// Sets the maximum height of the frame.
207    #[must_use]
208    pub const fn max_height(mut self, height: f32) -> Self {
209        self.layout.max_height = Some(height);
210        self
211    }
212}
213
214impl View for Frame {
215    fn body(self, _env: &waterui_core::Environment) -> impl View {
216        // The Frame view's body is just a Container with our custom layout and the child content.
217        FixedContainer::new(self.layout, vec![self.content])
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::StretchAxis;
225
226    struct MockSubView {
227        size: Size,
228    }
229
230    impl SubView for MockSubView {
231        fn size_that_fits(&self, _proposal: ProposalSize) -> Size {
232            self.size
233        }
234        fn stretch_axis(&self) -> StretchAxis {
235            StretchAxis::None
236        }
237        fn priority(&self) -> i32 {
238            0
239        }
240    }
241
242    #[test]
243    fn test_frame_with_ideal_size() {
244        let layout = FrameLayout {
245            ideal_width: Some(100.0),
246            ideal_height: Some(50.0),
247            ..Default::default()
248        };
249
250        let mut child = MockSubView {
251            size: Size::new(30.0, 20.0),
252        };
253        let children: Vec<&dyn SubView> = vec![&mut child];
254
255        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
256
257        // Frame uses ideal dimensions
258        assert!((size.width - 100.0).abs() < f32::EPSILON);
259        assert!((size.height - 50.0).abs() < f32::EPSILON);
260    }
261
262    #[test]
263    fn test_frame_alignment() {
264        let layout = FrameLayout {
265            alignment: Alignment::BottomTrailing,
266            ..Default::default()
267        };
268
269        let mut child = MockSubView {
270            size: Size::new(30.0, 20.0),
271        };
272        let children: Vec<&dyn SubView> = vec![&mut child];
273
274        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
275        let rects = layout.place(bounds, &children);
276
277        // Child should be at bottom-trailing corner
278        assert!((rects[0].x() - 70.0).abs() < f32::EPSILON); // 100 - 30
279        assert!((rects[0].y() - 80.0).abs() < f32::EPSILON); // 100 - 20
280    }
281}