waterui_layout/stack/
zstack.rs

1//! Overlay stack layout for multiple layers.
2
3use alloc::{vec, vec::Vec};
4use nami::collection::Collection;
5use waterui_core::{AnyView, View, id::Identifiable, view::TupleViews, views::ForEach};
6
7use crate::{
8    Layout, LazyContainer, Point, ProposalSize, Rect, Size, StretchAxis, SubView,
9    container::FixedContainer, stack::Alignment,
10};
11
12/// Cached measurement for a child during layout
13struct ChildMeasurement {
14    size: Size,
15}
16
17/// Stacks an arbitrary number of children with a shared alignment.
18///
19/// `ZStackLayout` positions every child within the same bounds, overlaying them
20/// according to the specified alignment. Each child is sized independently,
21/// and the container's final width/height are the maxima of the children's
22/// reported sizes. If you instead need the base child to dictate the container
23/// size while layering secondary content, see [`crate::overlay::OverlayLayout`].
24#[derive(Debug, Clone, Default)]
25pub struct ZStackLayout {
26    /// The alignment used to position children within the `ZStack`
27    pub alignment: Alignment,
28}
29
30impl Layout for ZStackLayout {
31    /// `ZStack` stretches in both directions to fill available space.
32    fn stretch_axis(&self) -> StretchAxis {
33        StretchAxis::Both
34    }
35
36    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
37        if children.is_empty() {
38            return Size::zero();
39        }
40
41        // Measure each child with the parent's proposal
42        let measurements: Vec<ChildMeasurement> = children
43            .iter()
44            .map(|child| ChildMeasurement {
45                size: child.size_that_fits(proposal),
46            })
47            .collect();
48
49        // ZStack's size is determined by the largest child
50        let max_width = measurements
51            .iter()
52            .map(|m| m.size.width)
53            .filter(|w| w.is_finite())
54            .max_by(f32::total_cmp)
55            .unwrap_or(0.0);
56
57        let max_height = measurements
58            .iter()
59            .map(|m| m.size.height)
60            .filter(|h| h.is_finite())
61            .max_by(f32::total_cmp)
62            .unwrap_or(0.0);
63
64        // Respect parent constraints - don't exceed them
65        let final_width = proposal
66            .width
67            .map_or(max_width, |parent_width| max_width.min(parent_width));
68
69        let final_height = proposal
70            .height
71            .map_or(max_height, |parent_height| max_height.min(parent_height));
72
73        Size::new(final_width, final_height)
74    }
75
76    fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
77        if children.is_empty() {
78            return vec![];
79        }
80
81        // Re-measure children with the bounds as proposal
82        let child_proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
83
84        let measurements: Vec<ChildMeasurement> = children
85            .iter()
86            .map(|child| ChildMeasurement {
87                size: child.size_that_fits(child_proposal),
88            })
89            .collect();
90
91        // Place each child according to alignment
92        let mut rects = Vec::with_capacity(children.len());
93
94        for measurement in &measurements {
95            // Handle infinite dimensions (axis-expanding views)
96            let child_width = if measurement.size.width.is_infinite() {
97                bounds.width()
98            } else {
99                measurement.size.width.min(bounds.width())
100            };
101
102            let child_height = if measurement.size.height.is_infinite() {
103                bounds.height()
104            } else {
105                measurement.size.height.min(bounds.height())
106            };
107
108            let child_size = Size::new(child_width, child_height);
109            let (x, y) = self.calculate_position(&bounds, child_size);
110
111            rects.push(Rect::new(Point::new(x, y), child_size));
112        }
113
114        rects
115    }
116}
117
118impl ZStackLayout {
119    /// Calculate the position of a child within the `ZStack` bounds based on alignment
120    fn calculate_position(&self, bound: &Rect, child_size: Size) -> (f32, f32) {
121        let available_width = bound.width();
122        let available_height = bound.height();
123
124        match self.alignment {
125            Alignment::TopLeading => (bound.x(), bound.y()),
126            Alignment::Top => (
127                bound.x() + (available_width - child_size.width) / 2.0,
128                bound.y(),
129            ),
130            Alignment::TopTrailing => (bound.max_x() - child_size.width, bound.y()),
131            Alignment::Leading => (
132                bound.x(),
133                bound.y() + (available_height - child_size.height) / 2.0,
134            ),
135            Alignment::Center => (
136                bound.x() + (available_width - child_size.width) / 2.0,
137                bound.y() + (available_height - child_size.height) / 2.0,
138            ),
139            Alignment::Trailing => (
140                bound.max_x() - child_size.width,
141                bound.y() + (available_height - child_size.height) / 2.0,
142            ),
143            Alignment::BottomLeading => (bound.x(), bound.max_y() - child_size.height),
144            Alignment::Bottom => (
145                bound.x() + (available_width - child_size.width) / 2.0,
146                bound.max_y() - child_size.height,
147            ),
148            Alignment::BottomTrailing => (
149                bound.max_x() - child_size.width,
150                bound.max_y() - child_size.height,
151            ),
152        }
153    }
154}
155
156/// A view that overlays its children, aligning them in front of each other.
157///
158/// Use a `ZStack` when you want to layer views on top of each other. The stack
159/// sizes itself to fit its largest child.
160///
161/// ```ignore
162/// zstack((
163///     Color::blue(),
164///     text("Overlay Text"),
165/// ))
166/// ```
167///
168/// You can control how children align within the stack:
169///
170/// ```ignore
171/// ZStack::new(Alignment::TopLeading, (
172///     background_view,
173///     content_view,
174/// ))
175/// ```
176///
177/// **Note:** If you only need a decorative background without affecting layout size,
178/// use `.background()` instead.
179#[derive(Debug, Clone)]
180pub struct ZStack<C> {
181    layout: ZStackLayout,
182    contents: C,
183}
184
185impl<C> ZStack<C> {
186    /// Sets the alignment for the `ZStack`.
187    #[must_use]
188    pub const fn alignment(mut self, alignment: Alignment) -> Self {
189        self.layout.alignment = alignment;
190        self
191    }
192}
193
194impl<C, F, V> ZStack<ForEach<C, F, V>>
195where
196    C: Collection,
197    C::Item: Identifiable,
198    F: 'static + Fn(C::Item) -> V,
199    V: View,
200{
201    /// Creates a new `ZStack` with views generated from a collection using `ForEach`.
202    ///
203    /// # Arguments
204    /// * `collection` - The collection of items to iterate over
205    /// * `generator` - A function that generates a view for each item in the collection
206    pub fn for_each(collection: C, generator: F) -> Self {
207        Self {
208            layout: ZStackLayout::default(),
209            contents: ForEach::new(collection, generator),
210        }
211    }
212}
213
214impl<C: TupleViews> ZStack<(C,)> {
215    /// Creates a new `ZStack` with the specified alignment and contents.
216    ///
217    /// # Arguments
218    /// * `alignment` - The alignment to use for positioning children within the stack
219    /// * `contents` - A collection of views to be stacked
220    pub const fn new(alignment: Alignment, contents: C) -> Self {
221        Self {
222            layout: ZStackLayout { alignment },
223            contents: (contents,),
224        }
225    }
226}
227
228impl<V> FromIterator<V> for ZStack<(Vec<AnyView>,)>
229where
230    V: View,
231{
232    fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
233        let contents = iter.into_iter().map(AnyView::new).collect::<Vec<_>>();
234        Self::new(Alignment::default(), contents)
235    }
236}
237
238/// Creates a new `ZStack` with center alignment and the specified contents.
239///
240/// This is a convenience function that creates a `ZStack` with `Alignment::Center`.
241pub const fn zstack<C: TupleViews>(contents: C) -> ZStack<(C,)> {
242    ZStack::new(Alignment::Center, contents)
243}
244
245impl<C> View for ZStack<(C,)>
246where
247    C: TupleViews + 'static,
248{
249    fn body(self, _env: &waterui_core::Environment) -> impl View {
250        FixedContainer::new(self.layout, self.contents.0)
251    }
252}
253
254impl<C, F, V> View for ZStack<ForEach<C, F, V>>
255where
256    C: Collection,
257    C::Item: Identifiable,
258    F: 'static + Fn(C::Item) -> V,
259    V: View,
260{
261    fn body(self, _env: &waterui_core::Environment) -> impl View {
262        LazyContainer::new(self.layout, self.contents)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::StretchAxis;
270
271    struct MockSubView {
272        size: Size,
273    }
274
275    impl SubView for MockSubView {
276        fn size_that_fits(&self, _proposal: ProposalSize) -> Size {
277            self.size
278        }
279        fn stretch_axis(&self) -> StretchAxis {
280            StretchAxis::None
281        }
282        fn priority(&self) -> i32 {
283            0
284        }
285    }
286
287    #[test]
288    fn test_zstack_size_multiple_children() {
289        let layout = ZStackLayout {
290            alignment: Alignment::Center,
291        };
292
293        let mut child1 = MockSubView {
294            size: Size::new(50.0, 30.0),
295        };
296        let mut child2 = MockSubView {
297            size: Size::new(80.0, 40.0),
298        };
299        let mut child3 = MockSubView {
300            size: Size::new(60.0, 60.0),
301        };
302
303        let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3];
304
305        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
306
307        // ZStack takes the max width and max height
308        assert!((size.width - 80.0).abs() < f32::EPSILON);
309        assert!((size.height - 60.0).abs() < f32::EPSILON);
310    }
311
312    #[test]
313    fn test_zstack_placement_center() {
314        let layout = ZStackLayout {
315            alignment: Alignment::Center,
316        };
317
318        let mut child1 = MockSubView {
319            size: Size::new(40.0, 20.0),
320        };
321        let mut child2 = MockSubView {
322            size: Size::new(60.0, 40.0),
323        };
324
325        let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
326
327        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
328        let rects = layout.place(bounds, &children);
329
330        // Child 1: centered in 100x100
331        assert!((rects[0].x() - 30.0).abs() < f32::EPSILON); // (100 - 40) / 2
332        assert!((rects[0].y() - 40.0).abs() < f32::EPSILON); // (100 - 20) / 2
333
334        // Child 2: centered in 100x100
335        assert!((rects[1].x() - 20.0).abs() < f32::EPSILON); // (100 - 60) / 2
336        assert!((rects[1].y() - 30.0).abs() < f32::EPSILON); // (100 - 40) / 2
337    }
338}