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    SubviewPlacement, ViewDimensions,
10    container::FixedContainer,
11    stack::{
12        Alignment, HorizontalAlignment, VerticalAlignment,
13        distribute::{LineAnchor, container_line, cross_envelope},
14    },
15};
16
17/// Cached measurement for a child during layout
18struct ChildMeasurement {
19    dimensions: ViewDimensions,
20}
21
22impl ChildMeasurement {
23    const fn size(&self) -> Size {
24        self.dimensions.size
25    }
26}
27
28/// The horizontal envelope of the layers lined up on `alignment`.
29fn zstack_horizontal_metrics(
30    measurements: &[ChildMeasurement],
31    alignment: HorizontalAlignment,
32) -> (f32, f32) {
33    cross_envelope(measurements.iter().map(|measurement| {
34        (
35            measurement.size().width,
36            measurement.dimensions.horizontal(alignment),
37        )
38    }))
39}
40
41/// The vertical envelope of the layers lined up on `alignment`.
42fn zstack_vertical_metrics(
43    measurements: &[ChildMeasurement],
44    alignment: VerticalAlignment,
45) -> (f32, f32) {
46    cross_envelope(measurements.iter().map(|measurement| {
47        (
48            measurement.size().height,
49            measurement.dimensions.vertical(alignment),
50        )
51    }))
52}
53
54/// Stacks an arbitrary number of children with a shared alignment.
55///
56/// `ZStackLayout` positions every child within the same bounds, overlaying them
57/// according to the specified alignment. Each child is sized independently,
58/// and the container's final width/height are the maxima of the children's
59/// reported sizes. If you instead need the base child to dictate the container
60/// size while layering secondary content, see [`crate::overlay::OverlayLayout`].
61#[derive(Debug, Clone, Default)]
62pub struct ZStackLayout {
63    /// The alignment used to position children within the `ZStack`
64    pub alignment: Alignment,
65}
66
67impl Layout for ZStackLayout {
68    /// A `ZStack` is content-sized only while every child is — a child that
69    /// stretches (a `Color`, a `GpuSurface`) makes the stack stretch on the
70    /// same axes, since `place` hands such children the full bounds.
71    ///
72    /// Axis-relative answers have no direction to resolve against here:
73    /// `MainAxis` children (`Spacer`) want whatever space is offered, which in
74    /// a zstack is both axes; `CrossAxis` children (`Divider`) resolve to
75    /// their default orientation — a horizontal rule — and fill horizontally.
76    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
77        let mut fills_h = false;
78        let mut fills_v = false;
79        for child in children {
80            match child {
81                StretchAxis::None => {}
82                StretchAxis::Both | StretchAxis::MainAxis => {
83                    fills_h = true;
84                    fills_v = true;
85                }
86                StretchAxis::Horizontal | StretchAxis::CrossAxis => fills_h = true,
87                StretchAxis::Vertical => fills_v = true,
88            }
89        }
90        match (fills_h, fills_v) {
91            (true, true) => StretchAxis::Both,
92            (true, false) => StretchAxis::Horizontal,
93            (false, true) => StretchAxis::Vertical,
94            (false, false) => StretchAxis::None,
95        }
96    }
97
98    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
99        if children.is_empty() {
100            return Size::zero();
101        }
102
103        // Measure each child with the parent's proposal
104        let measurements: Vec<ChildMeasurement> = children
105            .iter()
106            .map(|child| ChildMeasurement {
107                dimensions: child.measure(proposal),
108            })
109            .collect();
110
111        // The stack is the envelope of its layers, like every container: a
112        // layer wider than the offer widens the stack rather than being
113        // shrunk, and a layer that answers an unbounded extent makes the
114        // stack answer it too so the fill pass above resolves it.
115        let (max_leading, max_trailing) =
116            zstack_horizontal_metrics(&measurements, self.alignment.horizontal());
117        let (max_above, max_below) =
118            zstack_vertical_metrics(&measurements, self.alignment.vertical());
119        let width = if measurements.iter().any(|m| m.size().width.is_infinite()) {
120            f32::INFINITY
121        } else {
122            max_leading + max_trailing
123        };
124        let height = if measurements.iter().any(|m| m.size().height.is_infinite()) {
125            f32::INFINITY
126        } else {
127            max_above + max_below
128        };
129        Size::new(width, height)
130    }
131
132    fn place(
133        &self,
134        bounds: Rect,
135        _proposal: ProposalSize,
136        children: &[&dyn SubView],
137    ) -> Vec<SubviewPlacement> {
138        if children.is_empty() {
139            return vec![];
140        }
141
142        // Placement is a fresh negotiation against the bounds the stack was
143        // placed in: every layer is proposed the resolved extent, so what it
144        // lays out under is the space it actually has.
145        let placement = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
146        let measurements: Vec<ChildMeasurement> = children
147            .iter()
148            .map(|child| ChildMeasurement {
149                dimensions: child.measure(placement),
150            })
151            .collect();
152
153        let horizontal = self.alignment.horizontal();
154        let vertical = self.alignment.vertical();
155        let (max_leading, max_trailing) = zstack_horizontal_metrics(&measurements, horizontal);
156        let (max_above, max_below) = zstack_vertical_metrics(&measurements, vertical);
157        let line_x = bounds.x()
158            + container_line(
159                bounds.width(),
160                LineAnchor::horizontal(horizontal),
161                max_leading,
162                max_trailing,
163            );
164        let line_y = bounds.y()
165            + container_line(
166                bounds.height(),
167                LineAnchor::vertical(vertical),
168                max_above,
169                max_below,
170            );
171
172        let mut placements = Vec::with_capacity(children.len());
173
174        for measurement in &measurements {
175            // A layer that answers an unbounded extent fills the bounds; every
176            // other layer keeps its answer, overflowing the bounds when it is
177            // larger, and sits with its guide on the stack's line.
178            let child_width = if measurement.dimensions.size.width.is_infinite() {
179                bounds.width()
180            } else {
181                measurement.dimensions.size.width
182            };
183            let child_height = if measurement.dimensions.size.height.is_infinite() {
184                bounds.height()
185            } else {
186                measurement.dimensions.size.height
187            };
188
189            let child_size = Size::new(child_width, child_height);
190            let mut adjusted_dimensions = measurement.dimensions.clone();
191            adjusted_dimensions.size = child_size;
192            let x = line_x - adjusted_dimensions.horizontal(horizontal);
193            let y = line_y - adjusted_dimensions.vertical(vertical);
194
195            placements.push(SubviewPlacement::new(
196                Rect::new(Point::new(x, y), child_size),
197                placement,
198            ));
199        }
200
201        placements
202    }
203
204    fn explicit_horizontal(
205        &self,
206        alignment: HorizontalAlignment,
207        _bounds: Rect,
208        children: &[PlacedSubview<'_>],
209    ) -> Option<f32> {
210        if alignment == self.alignment.horizontal() {
211            return children
212                .iter()
213                .filter_map(|child| child.explicit_horizontal(alignment))
214                .min_by(f32::total_cmp);
215        }
216        None
217    }
218
219    fn explicit_vertical(
220        &self,
221        alignment: VerticalAlignment,
222        _bounds: Rect,
223        children: &[PlacedSubview<'_>],
224    ) -> Option<f32> {
225        if alignment == VerticalAlignment::LastBaseline {
226            return children
227                .iter()
228                .filter_map(|child| child.explicit_vertical(alignment))
229                .max_by(f32::total_cmp);
230        }
231        if alignment == VerticalAlignment::FirstBaseline || alignment == self.alignment.vertical() {
232            return children
233                .iter()
234                .filter_map(|child| child.explicit_vertical(alignment))
235                .min_by(f32::total_cmp);
236        }
237        None
238    }
239}
240
241/// A view that overlays its children, aligning them in front of each other.
242///
243/// Use a `ZStack` when you want to layer views on top of each other. The stack
244/// sizes itself to fit its largest child.
245///
246/// ```rust
247/// # use waterui::prelude::*;
248/// # fn banner() -> impl View {
249/// zstack((
250///     Color::blue(),
251///     text("Overlay Text"),
252/// ))
253/// # }
254/// ```
255///
256/// You can control how children align within the stack:
257///
258/// ```rust
259/// # use waterui::prelude::*;
260/// # fn aligned(background_view: impl View, content_view: impl View) -> impl View {
261/// ZStack::new(Alignment::TopLeading, (
262///     background_view,
263///     content_view,
264/// ))
265/// # }
266/// ```
267///
268/// **Note:** If you only need a decorative background without affecting layout size,
269/// use `.background()` instead.
270#[derive(Debug, Clone)]
271pub struct ZStack<C> {
272    layout: ZStackLayout,
273    contents: C,
274}
275
276impl<C> ZStack<C> {
277    /// Sets the alignment for the `ZStack`. An [`Alignment`] or one of the
278    /// tokens (`Leading`, `TopTrailing`, `Center`, …).
279    #[must_use]
280    pub fn alignment(mut self, alignment: impl Into<Alignment>) -> Self {
281        self.layout.alignment = alignment.into();
282        self
283    }
284
285    crate::alignment::two_dimensional_alignment_methods!();
286}
287
288crate::stack::impl_stack_for_each!(ZStack, ZStackLayout);
289
290impl<C: TupleViews> ZStack<(C,)> {
291    /// Creates a new `ZStack` with the specified alignment and contents.
292    ///
293    /// # Arguments
294    /// * `alignment` - The alignment to use for positioning children within the stack
295    /// * `contents` - A collection of views to be stacked
296    pub const fn new(alignment: Alignment, contents: C) -> Self {
297        Self {
298            layout: ZStackLayout { alignment },
299            contents: (contents,),
300        }
301    }
302}
303
304impl<V> FromIterator<V> for ZStack<(Vec<AnyView>,)>
305where
306    V: View,
307{
308    fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
309        let contents = iter.into_iter().map(AnyView::new).collect::<Vec<_>>();
310        Self::new(Alignment::default(), contents)
311    }
312}
313
314/// Creates a new `ZStack` with center alignment and the specified contents.
315///
316/// This is a convenience function that creates a `ZStack` with `Alignment::Center`.
317pub const fn zstack<C: TupleViews>(contents: C) -> ZStack<(C,)> {
318    ZStack::new(Alignment::Center, contents)
319}
320
321impl<C> View for ZStack<(C,)>
322where
323    C: TupleViews + 'static,
324{
325    fn body(self, _env: &waterui_core::Environment) -> impl View {
326        FixedContainer::new(self.layout, self.contents.0)
327    }
328
329    /// Resolves to `FixedContainer` over the same layout and children;
330    /// reports what that container would — matching `FixedContainer`'s
331    /// `View::stretch_axis`.
332    fn stretch_axis(&self) -> StretchAxis {
333        self.layout.stretch_axis(&self.contents.0.stretch_axes())
334    }
335}
336
337impl<C, F, V> View for ZStack<ForEach<C, F, V>>
338where
339    C: Collection + Clone,
340    C::Item: Identifiable,
341    F: 'static + Fn(C::Item) -> V,
342    V: View,
343{
344    fn body(self, _env: &waterui_core::Environment) -> impl View {
345        LazyContainer::new(self.layout, self.contents)
346    }
347
348    /// Resolves to `LazyContainer`, which cannot enumerate children without
349    /// materializing them and answers its layout's axis over an empty child
350    /// set — matching `LazyContainer::stretch_axis`.
351    fn stretch_axis(&self) -> StretchAxis {
352        self.layout.stretch_axis(&[])
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::StretchAxis;
360
361    struct MockSubView {
362        size: Size,
363    }
364
365    impl SubView for MockSubView {
366        fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
367            ViewDimensions::new(self.size)
368        }
369        fn stretch_axis(&self) -> StretchAxis {
370            StretchAxis::None
371        }
372        fn priority(&self) -> i32 {
373            0
374        }
375    }
376
377    #[test]
378    fn test_zstack_size_multiple_children() {
379        let layout = ZStackLayout {
380            alignment: Alignment::Center,
381        };
382
383        let mut child1 = MockSubView {
384            size: Size::new(50.0, 30.0),
385        };
386        let mut child2 = MockSubView {
387            size: Size::new(80.0, 40.0),
388        };
389        let mut child3 = MockSubView {
390            size: Size::new(60.0, 60.0),
391        };
392
393        let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3];
394
395        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
396
397        // ZStack takes the max width and max height
398        assert!((size.width - 80.0).abs() < f32::EPSILON);
399        assert!((size.height - 60.0).abs() < f32::EPSILON);
400    }
401
402    #[test]
403    fn test_zstack_placement_center_bounded_proposal() {
404        let layout = ZStackLayout {
405            alignment: Alignment::Center,
406        };
407
408        let mut child1 = MockSubView {
409            size: Size::new(40.0, 20.0),
410        };
411        let mut child2 = MockSubView {
412            size: Size::new(60.0, 40.0),
413        };
414
415        let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
416
417        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
418        let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
419        let placements = layout.place(bounds, proposal, &children);
420
421        // Child 1: centered in 100x100
422        assert!((placements[0].frame.x() - 30.0).abs() < f32::EPSILON); // (100 - 40) / 2
423        assert!((placements[0].frame.y() - 40.0).abs() < f32::EPSILON); // (100 - 20) / 2
424
425        // Child 2: centered in 100x100
426        assert!((placements[1].frame.x() - 20.0).abs() < f32::EPSILON); // (100 - 60) / 2
427        assert!((placements[1].frame.y() - 30.0).abs() < f32::EPSILON); // (100 - 40) / 2
428    }
429}