Skip to main content

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