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, PlacedSubview, Point, ProposalSize, Rect, Size, StretchAxis, SubView,
9 ViewDimensions,
10 container::FixedContainer,
11 stack::{Alignment, HorizontalAlignment, VerticalAlignment},
12};
13
14struct 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#[derive(Debug, Clone, Default)]
69pub struct ZStackLayout {
70 pub alignment: Alignment,
72}
73
74impl Layout for ZStackLayout {
75 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 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 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 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 let mut rects = Vec::with_capacity(children.len());
152
153 for measurement in &measurements {
154 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#[derive(Debug, Clone)]
252pub struct ZStack<C> {
253 layout: ZStackLayout,
254 contents: C,
255}
256
257impl<C> ZStack<C> {
258 #[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 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
292pub 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 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 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); }
392}