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//! 
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}