Skip to main content

waterui_layout/
stack.rs

1//! Stack-based layout primitives.
2//!
3//! The submodules implement horizontal, vertical, and overlay stacks. These
4//! views arrange child content according to alignments and spacing and are the
5//! backbone of most declarative layouts in `WaterUI`.
6//!
7//! ![Stack](https://raw.githubusercontent.com/water-rs/waterui/dev/docs/illustrations/stack.svg)
8
9/// Emits `for_each` on a stack wrapper around `ForEach<C, F, V>`.
10///
11/// Reused by HStack/VStack/ZStack so the three families share a single
12/// implementation without exposing a trait users would have to import.
13/// The macro is private to this crate (no `#[macro_export]`) and is
14/// invoked at the call site in each stack module.
15macro_rules! impl_stack_for_each {
16    ($Stack:ident, $Layout:ident) => {
17        impl<C, F, V> $Stack<ForEach<C, F, V>>
18        where
19            C: Collection,
20            C::Item: Identifiable,
21            F: 'static + Fn(C::Item) -> V,
22            V: View,
23        {
24            /// Creates the stack by iterating over a collection and generating
25            /// views.
26            pub fn for_each(collection: C, generator: F) -> Self {
27                Self {
28                    layout: $Layout::default(),
29                    contents: ForEach::new(collection, generator),
30                }
31            }
32        }
33    };
34}
35
36pub(crate) use impl_stack_for_each;
37
38mod distribute;
39mod vstack;
40pub use vstack::*;
41mod hstack;
42pub use hstack::*;
43mod zstack;
44pub use zstack::*;
45
46pub use waterui_core::layout::{Alignment, HorizontalAlignment, VerticalAlignment};
47
48use crate::Layout;
49use nami::Computed;
50use waterui_core::layout::StretchAxis;
51
52/// The stretch axis a stack of `children` claims, along `main`.
53///
54/// A stack claims nothing of its own: a row of labels stays content-sized, as
55/// `SwiftUI`'s does. What it cannot do is stay silent about a child that wants
56/// to fill. A stack's parent hands it the size it reports, and only then does
57/// the stack distribute that size among its children — so a row that holds a
58/// `Color`, a `Spacer` or a filling frame and reports "content-sized" is handed
59/// its own intrinsic size, and the greedy child fills a box of nothing. The
60/// space a child asks for has to be asked for again by every container between
61/// it and whoever owns the space.
62///
63/// `MainAxis` and `CrossAxis` resolve against `main`, the same way each stack's
64/// own child-measurement helpers resolve them.
65pub(crate) fn stack_stretch_axis(main: Axis, children: &[StretchAxis]) -> StretchAxis {
66    let mut fills_main = false;
67    let mut fills_cross = false;
68    for child in children {
69        match child {
70            StretchAxis::None => {}
71            StretchAxis::Both => {
72                fills_main = true;
73                fills_cross = true;
74            }
75            StretchAxis::MainAxis => fills_main = true,
76            StretchAxis::CrossAxis => fills_cross = true,
77            StretchAxis::Horizontal => {
78                if main.is_horizontal() {
79                    fills_main = true;
80                } else {
81                    fills_cross = true;
82                }
83            }
84            StretchAxis::Vertical => {
85                if main.is_vertical() {
86                    fills_main = true;
87                } else {
88                    fills_cross = true;
89                }
90            }
91        }
92    }
93    let (horizontal, vertical) = if main.is_horizontal() {
94        (fills_main, fills_cross)
95    } else {
96        (fills_cross, fills_main)
97    };
98    match (horizontal, vertical) {
99        (true, true) => StretchAxis::Both,
100        (true, false) => StretchAxis::Horizontal,
101        (false, true) => StretchAxis::Vertical,
102        (false, false) => StretchAxis::None,
103    }
104}
105
106/// Defines the axis of a stack.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum Axis {
110    /// Horizontal axis is the x-axis (`HStack`)
111    Horizontal,
112    /// Vertical axis is the y-axis (`VStack`)
113    Vertical,
114}
115
116impl Axis {
117    /// Returns true if this axis is horizontal.
118    #[must_use]
119    pub const fn is_horizontal(&self) -> bool {
120        matches!(self, Self::Horizontal)
121    }
122
123    /// Returns true if this axis is vertical.
124    #[must_use]
125    pub const fn is_vertical(&self) -> bool {
126        matches!(self, Self::Vertical)
127    }
128}
129
130/// The axis a [`LazyContainer`](crate::container::LazyContainer) can be
131/// virtualized along, together with the spacing and cross-axis alignment the
132/// stack was configured with.
133///
134/// Only the two stacks scroll in one direction with a well-defined item order,
135/// which is what viewport virtualization needs; every other layout — a grid, a
136/// `ZStack`, an absolute overlay — lays out its whole membership and returns
137/// `None` here.
138#[derive(Debug, Clone)]
139pub enum LazyStackAxis {
140    /// Items run top to bottom, aligned horizontally.
141    Vertical {
142        /// Gap between consecutive items.
143        spacing: Computed<f32>,
144        /// How items line up across the axis.
145        alignment: HorizontalAlignment,
146    },
147    /// Items run leading to trailing, aligned vertically.
148    Horizontal {
149        /// Gap between consecutive items.
150        spacing: Computed<f32>,
151        /// How items line up across the axis.
152        alignment: VerticalAlignment,
153    },
154}
155
156/// Resolves a layout to the axis a lazy container can virtualize along.
157///
158/// Every renderer that virtualizes a lazy container asks this, so the answer is
159/// the same everywhere. Deriving it per backend is how GTK ended up reading
160/// `Layout::stretch_axis` — a different question entirely, and one whose answer
161/// changed when the stacks became content-sized, silently laying every lazy
162/// horizontal stack out vertically.
163#[must_use]
164pub fn lazy_stack_axis(layout: &dyn Layout) -> Option<LazyStackAxis> {
165    let layout = layout as &dyn core::any::Any;
166    if let Some(vstack) = layout.downcast_ref::<VStackLayout>() {
167        return Some(LazyStackAxis::Vertical {
168            spacing: vstack.spacing.clone(),
169            alignment: vstack.alignment,
170        });
171    }
172    if let Some(hstack) = layout.downcast_ref::<HStackLayout>() {
173        return Some(LazyStackAxis::Horizontal {
174            spacing: hstack.spacing.clone(),
175            alignment: hstack.alignment,
176        });
177    }
178    None
179}