1use alloc::{vec, vec::Vec};
4use nami::collection::Collection;
5use waterui_core::{AnyView, View, env::with, id::Identifiable, view::TupleViews, views::ForEach};
6
7use crate::{
8 Layout, LazyContainer, Point, ProposalSize, Rect, Size, StretchAxis, SubView,
9 container::FixedContainer,
10 stack::{Axis, HorizontalAlignment},
11};
12
13#[derive(Debug, Default, Clone)]
15pub struct VStackLayout {
16 pub alignment: HorizontalAlignment,
18 pub spacing: f32,
20}
21
22struct ChildMeasurement {
24 size: Size,
25 stretch_axis: StretchAxis,
26}
27
28impl ChildMeasurement {
29 const fn stretches_main_axis(&self) -> bool {
34 matches!(
35 self.stretch_axis,
36 StretchAxis::Vertical | StretchAxis::Both | StretchAxis::MainAxis
37 )
38 }
39
40 const fn stretches_cross_axis(&self) -> bool {
44 matches!(
45 self.stretch_axis,
46 StretchAxis::Horizontal | StretchAxis::Both | StretchAxis::CrossAxis
47 )
48 }
49}
50
51#[allow(clippy::cast_precision_loss)]
52impl Layout for VStackLayout {
53 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
54 if children.is_empty() {
55 return Size::zero();
56 }
57
58 let child_proposal = ProposalSize::new(proposal.width, None);
60
61 let measurements: Vec<ChildMeasurement> = children
62 .iter()
63 .map(|child| ChildMeasurement {
64 size: child.size_that_fits(child_proposal),
65 stretch_axis: child.stretch_axis(),
66 })
67 .collect();
68
69 let has_main_axis_stretch = measurements
71 .iter()
72 .any(ChildMeasurement::stretches_main_axis);
73
74 let non_stretch_height: f32 = measurements
77 .iter()
78 .filter(|m| !m.stretches_main_axis())
79 .map(|m| m.size.height)
80 .sum();
81
82 let total_spacing = if children.len() > 1 {
83 (children.len() - 1) as f32 * self.spacing
84 } else {
85 0.0
86 };
87
88 let intrinsic_height = non_stretch_height + total_spacing;
89 let final_height = if has_main_axis_stretch {
90 proposal.height.unwrap_or(intrinsic_height)
91 } else {
92 intrinsic_height
93 };
94
95 let max_width = measurements
98 .iter()
99 .filter(|m| !m.stretches_cross_axis())
100 .map(|m| m.size.width)
101 .max_by(f32::total_cmp)
102 .unwrap_or(0.0);
103
104 let final_width = proposal.width.unwrap_or(max_width);
107
108 Size::new(final_width, final_height)
109 }
110
111 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
112 if children.is_empty() {
113 return vec![];
114 }
115
116 let child_proposal = ProposalSize::new(Some(bounds.width()), None);
118
119 let measurements: Vec<ChildMeasurement> = children
120 .iter()
121 .map(|child| ChildMeasurement {
122 size: child.size_that_fits(child_proposal),
123 stretch_axis: child.stretch_axis(),
124 })
125 .collect();
126
127 let main_axis_stretch_count = measurements
129 .iter()
130 .filter(|m| m.stretches_main_axis())
131 .count();
132 let non_stretch_height: f32 = measurements
133 .iter()
134 .filter(|m| !m.stretches_main_axis())
135 .map(|m| m.size.height)
136 .sum();
137
138 let total_spacing = if children.len() > 1 {
139 (children.len() - 1) as f32 * self.spacing
140 } else {
141 0.0
142 };
143
144 let remaining_height = bounds.height() - non_stretch_height - total_spacing;
145 let stretch_height = if main_axis_stretch_count > 0 {
146 (remaining_height / main_axis_stretch_count as f32).max(0.0)
147 } else {
148 0.0
149 };
150
151 let mut rects = Vec::with_capacity(children.len());
153 let mut current_y = bounds.y();
154
155 for (i, measurement) in measurements.iter().enumerate() {
156 if i > 0 {
157 current_y += self.spacing;
158 }
159
160 let child_width = if measurement.stretches_cross_axis() {
162 bounds.width()
164 } else if measurement.size.width.is_infinite() {
165 bounds.width()
166 } else {
167 measurement.size.width.min(bounds.width())
169 };
170
171 let child_height = if measurement.stretches_main_axis() {
172 stretch_height
173 } else {
174 measurement.size.height
175 };
176
177 let x = match self.alignment {
178 HorizontalAlignment::Leading => bounds.x(),
179 HorizontalAlignment::Center => bounds.x() + (bounds.width() - child_width) / 2.0,
180 HorizontalAlignment::Trailing => bounds.x() + bounds.width() - child_width,
181 };
182
183 rects.push(Rect::new(
184 Point::new(x, current_y),
185 Size::new(child_width, child_height),
186 ));
187
188 current_y += child_height;
189 }
190
191 rects
192 }
193
194 fn stretch_axis(&self) -> StretchAxis {
197 StretchAxis::Horizontal
198 }
199}
200
201#[derive(Debug, Clone)]
232pub struct VStack<C> {
233 layout: VStackLayout,
234 contents: C,
235}
236
237impl<C: TupleViews> VStack<(C,)> {
238 pub const fn new(alignment: HorizontalAlignment, spacing: f32, contents: C) -> Self {
241 Self {
242 layout: VStackLayout { alignment, spacing },
243 contents: (contents,),
244 }
245 }
246}
247
248impl<C, F, V> VStack<ForEach<C, F, V>>
249where
250 C: Collection,
251 C::Item: Identifiable,
252 F: 'static + Fn(C::Item) -> V,
253 V: View,
254{
255 pub fn for_each(collection: C, generator: F) -> Self {
257 Self {
258 layout: VStackLayout::default(),
259 contents: ForEach::new(collection, generator),
260 }
261 }
262}
263
264impl<C> VStack<C> {
265 #[must_use]
267 pub const fn alignment(mut self, alignment: HorizontalAlignment) -> Self {
268 self.layout.alignment = alignment;
269 self
270 }
271
272 #[must_use]
274 pub const fn spacing(mut self, spacing: f32) -> Self {
275 self.layout.spacing = spacing;
276 self
277 }
278}
279
280impl<V> FromIterator<V> for VStack<(Vec<AnyView>,)>
281where
282 V: View,
283{
284 fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
285 let contents = iter.into_iter().map(AnyView::new).collect::<Vec<_>>();
286 Self::new(HorizontalAlignment::default(), 10.0, contents)
287 }
288}
289
290pub const fn vstack<C: TupleViews>(contents: C) -> VStack<(C,)> {
292 VStack::new(HorizontalAlignment::Center, 10.0, contents)
293}
294
295impl<C, F, V> View for VStack<ForEach<C, F, V>>
296where
297 C: Collection,
298 C::Item: Identifiable,
299 F: 'static + Fn(C::Item) -> V,
300 V: View,
301{
302 fn body(self, _env: &waterui_core::Environment) -> impl View {
303 with(
305 LazyContainer::new(self.layout, self.contents),
306 Axis::Vertical,
307 )
308 }
309}
310
311impl<C: TupleViews + 'static> View for VStack<(C,)> {
312 fn body(self, _env: &waterui_core::Environment) -> impl View {
313 with(
315 FixedContainer::new(self.layout, self.contents.0),
316 Axis::Vertical,
317 )
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 struct MockSubView {
326 size: Size,
327 stretch_axis: StretchAxis,
328 }
329
330 impl SubView for MockSubView {
331 fn size_that_fits(&self, _proposal: ProposalSize) -> Size {
332 self.size
333 }
334 fn stretch_axis(&self) -> StretchAxis {
335 self.stretch_axis
336 }
337 fn priority(&self) -> i32 {
338 0
339 }
340 }
341
342 #[test]
343 fn test_vstack_size_two_children() {
344 let layout = VStackLayout {
345 alignment: HorizontalAlignment::Center,
346 spacing: 10.0,
347 };
348
349 let mut child1 = MockSubView {
350 size: Size::new(100.0, 30.0),
351 stretch_axis: StretchAxis::None,
352 };
353 let mut child2 = MockSubView {
354 size: Size::new(80.0, 40.0),
355 stretch_axis: StretchAxis::None,
356 };
357
358 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
359
360 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
361
362 assert!((size.width - 100.0).abs() < f32::EPSILON); assert!((size.height - 80.0).abs() < f32::EPSILON); }
365
366 #[test]
367 fn test_vstack_with_spacer() {
368 let layout = VStackLayout {
369 alignment: HorizontalAlignment::Center,
370 spacing: 0.0,
371 };
372
373 let mut child1 = MockSubView {
374 size: Size::new(100.0, 30.0),
375 stretch_axis: StretchAxis::None,
376 };
377 let mut spacer = MockSubView {
378 size: Size::zero(),
379 stretch_axis: StretchAxis::Both, };
381 let mut child2 = MockSubView {
382 size: Size::new(100.0, 30.0),
383 stretch_axis: StretchAxis::None,
384 };
385
386 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
387
388 let size = layout.size_that_fits(ProposalSize::new(None, Some(200.0)), &children);
390
391 assert!((size.height - 200.0).abs() < f32::EPSILON);
392
393 let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
395
396 let mut child1 = MockSubView {
398 size: Size::new(100.0, 30.0),
399 stretch_axis: StretchAxis::None,
400 };
401 let mut spacer = MockSubView {
402 size: Size::zero(),
403 stretch_axis: StretchAxis::Both,
404 };
405 let mut child2 = MockSubView {
406 size: Size::new(100.0, 30.0),
407 stretch_axis: StretchAxis::None,
408 };
409 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
410
411 let rects = layout.place(bounds, &children);
412
413 assert!((rects[0].height() - 30.0).abs() < f32::EPSILON);
414 assert!((rects[1].height() - 140.0).abs() < f32::EPSILON); assert!((rects[2].height() - 30.0).abs() < f32::EPSILON);
416 assert!((rects[2].y() - 170.0).abs() < f32::EPSILON); }
418
419 #[test]
420 fn test_vstack_with_horizontal_stretch() {
421 let layout = VStackLayout {
423 alignment: HorizontalAlignment::Center,
424 spacing: 10.0,
425 };
426
427 let mut label = MockSubView {
428 size: Size::new(50.0, 20.0),
429 stretch_axis: StretchAxis::None,
430 };
431 let mut text_field = MockSubView {
432 size: Size::new(100.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
435 let mut button = MockSubView {
436 size: Size::new(80.0, 44.0),
437 stretch_axis: StretchAxis::None,
438 };
439
440 let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
441
442 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
443
444 assert!((size.width - 80.0).abs() < f32::EPSILON);
447 assert!((size.height - 124.0).abs() < f32::EPSILON);
450 }
451}