1use 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
12struct ChildMeasurement {
14 size: Size,
15}
16
17#[derive(Debug, Clone, Default)]
25pub struct ZStackLayout {
26 pub alignment: Alignment,
28}
29
30impl Layout for ZStackLayout {
31 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 let measurements: Vec<ChildMeasurement> = children
43 .iter()
44 .map(|child| ChildMeasurement {
45 size: child.size_that_fits(proposal),
46 })
47 .collect();
48
49 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 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 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 let mut rects = Vec::with_capacity(children.len());
93
94 for measurement in &measurements {
95 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 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#[derive(Debug, Clone)]
180pub struct ZStack<C> {
181 layout: ZStackLayout,
182 contents: C,
183}
184
185impl<C> ZStack<C> {
186 #[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 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 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
238pub 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 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 assert!((rects[0].x() - 30.0).abs() < f32::EPSILON); assert!((rects[0].y() - 40.0).abs() < f32::EPSILON); assert!((rects[1].x() - 20.0).abs() < f32::EPSILON); assert!((rects[1].y() - 30.0).abs() < f32::EPSILON); }
338}