1use alloc::{vec, vec::Vec};
4use nami::{Computed, Signal, SignalExt, collection::Collection};
5use waterui_core::{
6 AnyView, IntoSignalF32, View, env::with, id::Identifiable, layout::LayoutInvalidationCallback,
7 view::TupleViews, views::ForEach,
8};
9
10use crate::{
11 HorizontalAlignment, Layout, LazyContainer, PlacedSubview, Point, ProposalSize, Rect, Size,
12 StretchAxis, SubView, SubviewPlacement,
13 container::FixedContainer,
14 stack::{
15 Axis,
16 distribute::{
17 ChildMeasurement, LineAnchor, container_line, cross_envelope, measure_stack,
18 place_cross_extent, stack_spacing,
19 },
20 stack_stretch_axis,
21 },
22};
23
24#[derive(Debug, Clone)]
26pub struct VStackLayout {
27 pub alignment: HorizontalAlignment,
29 pub spacing: Computed<f32>,
31}
32
33impl Default for VStackLayout {
34 fn default() -> Self {
35 Self {
36 alignment: HorizontalAlignment::default(),
37 spacing: Computed::constant(10.0),
38 }
39 }
40}
41
42fn vstack_intrinsic_cross_metrics(
53 measurements: &[ChildMeasurement],
54 alignment: HorizontalAlignment,
55) -> (f32, f32) {
56 cross_envelope(measurements.iter().map(|measurement| {
57 (
58 measurement.size().width,
59 measurement.horizontal_guide(alignment),
60 )
61 }))
62}
63
64impl Layout for VStackLayout {
65 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
66 if children.is_empty() {
67 return Size::zero();
68 }
69
70 let spacing = self.spacing.get();
71 let measurements = measure_stack(Axis::Vertical, proposal, spacing, children);
72 let final_height = measurements.iter().map(|m| m.size().height).sum::<f32>()
73 + stack_spacing(spacing, children.len());
74
75 let (leading, trailing) = vstack_intrinsic_cross_metrics(&measurements, self.alignment);
76 let width = if measurements.iter().any(|m| m.size().width.is_infinite()) {
77 f32::INFINITY
78 } else {
79 leading + trailing
80 };
81 Size::new(width, final_height)
82 }
83
84 fn place(
85 &self,
86 bounds: Rect,
87 _proposal: ProposalSize,
88 children: &[&dyn SubView],
89 ) -> Vec<SubviewPlacement> {
90 if children.is_empty() {
91 return vec![];
92 }
93
94 let spacing = self.spacing.get();
95 let placement = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
103 let measurements = measure_stack(Axis::Vertical, placement, spacing, children);
104
105 let (above, below) = vstack_intrinsic_cross_metrics(&measurements, self.alignment);
106 let guide_line = bounds.x()
107 + container_line(
108 bounds.width(),
109 LineAnchor::horizontal(self.alignment),
110 above,
111 below,
112 );
113
114 let mut placements = Vec::with_capacity(children.len());
116 let mut current_y = bounds.y();
117
118 for (i, measurement) in measurements.iter().enumerate() {
119 if i > 0 {
120 current_y += spacing;
121 }
122
123 let child_width = place_cross_extent(
124 measurement.size().width,
125 bounds.width(),
126 measurement.stretches_cross_axis(),
127 );
128
129 let child_height = measurement.size().height;
133
134 let mut adjusted_dimensions = measurement.dimensions.clone();
135 adjusted_dimensions.size = Size::new(child_width, child_height);
136
137 let x = if measurement.stretches_cross_axis() {
141 bounds.x()
142 } else {
143 guide_line - adjusted_dimensions.horizontal(self.alignment)
144 };
145
146 placements.push(SubviewPlacement::new(
147 Rect::new(
148 Point::new(x, current_y),
149 Size::new(child_width, child_height),
150 ),
151 measurement.proposal,
152 ));
153
154 current_y += child_height;
155 }
156
157 placements
158 }
159
160 fn explicit_horizontal(
161 &self,
162 alignment: HorizontalAlignment,
163 _bounds: Rect,
164 children: &[PlacedSubview<'_>],
165 ) -> Option<f32> {
166 if alignment == self.alignment {
167 return children
168 .iter()
169 .filter_map(|child| child.explicit_horizontal(alignment))
170 .min_by(f32::total_cmp);
171 }
172
173 None
174 }
175
176 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
183 stack_stretch_axis(Axis::Vertical, children)
184 }
185
186 fn watch_invalidation(
187 &self,
188 invalidate: LayoutInvalidationCallback,
189 ) -> Vec<nami::watcher::BoxWatcherGuard> {
190 vec![self.spacing.watch(move |_| invalidate())]
191 }
192}
193
194#[derive(Debug, Clone)]
234pub struct VStack<C> {
235 layout: VStackLayout,
236 contents: C,
237}
238
239impl<C: TupleViews> VStack<(C,)> {
240 pub fn new(alignment: HorizontalAlignment, spacing: f32, contents: C) -> Self {
243 Self {
244 layout: VStackLayout {
245 alignment,
246 spacing: Computed::constant(spacing),
247 },
248 contents: (contents,),
249 }
250 }
251}
252
253crate::stack::impl_stack_for_each!(VStack, VStackLayout);
254
255impl<C> VStack<C> {
256 #[must_use]
264 pub fn alignment(mut self, alignment: impl Into<HorizontalAlignment>) -> Self {
265 self.layout.alignment = alignment.into();
266 self
267 }
268
269 crate::alignment::horizontal_alignment_methods!();
270
271 #[must_use]
276 pub fn spacing(mut self, spacing: impl IntoSignalF32 + 'static) -> Self {
277 self.layout.spacing = spacing.into_signal_f32().computed();
278 self
279 }
280}
281
282impl<V> FromIterator<V> for VStack<(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(HorizontalAlignment::default(), 10.0, contents)
289 }
290}
291
292pub fn vstack<C: TupleViews>(contents: C) -> VStack<(C,)> {
294 VStack::new(HorizontalAlignment::Center, 10.0, contents)
295}
296
297impl<C, F, V> View for VStack<ForEach<C, F, V>>
298where
299 C: Collection + Clone,
300 C::Item: Identifiable,
301 F: 'static + Fn(C::Item) -> V,
302 V: View,
303{
304 fn body(self, _env: &waterui_core::Environment) -> impl View {
305 with(
307 LazyContainer::new(self.layout, self.contents),
308 Axis::Vertical,
309 )
310 }
311
312 fn stretch_axis(&self) -> StretchAxis {
316 self.layout.stretch_axis(&[])
317 }
318}
319
320impl<C: TupleViews + 'static> View for VStack<(C,)> {
321 fn body(self, _env: &waterui_core::Environment) -> impl View {
322 with(
324 FixedContainer::new(self.layout, self.contents.0),
325 Axis::Vertical,
326 )
327 }
328
329 fn stretch_axis(&self) -> StretchAxis {
333 self.layout.stretch_axis(&self.contents.0.stretch_axes())
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::ViewDimensions;
341 use crate::tests::CompressibleHeightView;
342
343 struct MockSubView {
344 size: Size,
345 stretch_axis: StretchAxis,
346 }
347
348 impl SubView for MockSubView {
349 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
350 ViewDimensions::new(self.size)
351 }
352 fn stretch_axis(&self) -> StretchAxis {
353 self.stretch_axis
354 }
355 fn priority(&self) -> i32 {
356 0
357 }
358 }
359
360 #[test]
361 fn test_vstack_size_two_children() {
362 let layout = VStackLayout {
363 alignment: HorizontalAlignment::Center,
364 spacing: Computed::constant(10.0),
365 };
366
367 let mut child1 = MockSubView {
368 size: Size::new(100.0, 30.0),
369 stretch_axis: StretchAxis::None,
370 };
371 let mut child2 = MockSubView {
372 size: Size::new(80.0, 40.0),
373 stretch_axis: StretchAxis::None,
374 };
375
376 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
377
378 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
379
380 assert!((size.width - 100.0).abs() < f32::EPSILON); assert!((size.height - 80.0).abs() < f32::EPSILON); }
383
384 #[test]
389 fn a_column_counts_a_growing_child_in_its_intrinsic_height() {
390 let layout = VStackLayout {
391 alignment: HorizontalAlignment::Leading,
392 spacing: Computed::constant(0.0),
393 };
394
395 let mut header = MockSubView {
396 size: Size::new(100.0, 30.0),
397 stretch_axis: StretchAxis::None,
398 };
399 let mut list = MockSubView {
401 size: Size::new(100.0, 200.0),
402 stretch_axis: StretchAxis::Both,
403 };
404 let children: Vec<&dyn SubView> = vec![&mut header, &mut list];
405
406 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
407
408 assert!(
409 (size.height - 230.0).abs() < f32::EPSILON,
410 "the column dropped its growing child from its own height: got {}, want 230",
411 size.height
412 );
413 }
414
415 #[test]
418 fn a_spacer_adds_nothing_to_the_intrinsic_height() {
419 let layout = VStackLayout {
420 alignment: HorizontalAlignment::Leading,
421 spacing: Computed::constant(0.0),
422 };
423
424 let mut row = MockSubView {
425 size: Size::new(100.0, 30.0),
426 stretch_axis: StretchAxis::None,
427 };
428 let mut spacer = MockSubView {
429 size: Size::zero(),
430 stretch_axis: StretchAxis::Both,
431 };
432 let children: Vec<&dyn SubView> = vec![&mut row, &mut spacer];
433
434 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
435
436 assert!(
437 (size.height - 30.0).abs() < f32::EPSILON,
438 "a spacer changed the column's intrinsic height: got {}, want 30",
439 size.height
440 );
441 }
442
443 #[test]
444 fn test_vstack_with_spacer() {
445 let layout = VStackLayout {
446 alignment: HorizontalAlignment::Center,
447 spacing: Computed::constant(0.0),
448 };
449
450 let mut child1 = MockSubView {
451 size: Size::new(100.0, 30.0),
452 stretch_axis: StretchAxis::None,
453 };
454 let mut spacer = MockSubView {
455 size: Size::zero(),
456 stretch_axis: StretchAxis::Both, };
458 let mut child2 = MockSubView {
459 size: Size::new(100.0, 30.0),
460 stretch_axis: StretchAxis::None,
461 };
462
463 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
464
465 let size = layout.size_that_fits(ProposalSize::new(None, Some(200.0)), &children);
467
468 assert!((size.height - 200.0).abs() < f32::EPSILON);
469
470 let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
472
473 let mut child1 = MockSubView {
475 size: Size::new(100.0, 30.0),
476 stretch_axis: StretchAxis::None,
477 };
478 let mut spacer = MockSubView {
479 size: Size::zero(),
480 stretch_axis: StretchAxis::Both,
481 };
482 let mut child2 = MockSubView {
483 size: Size::new(100.0, 30.0),
484 stretch_axis: StretchAxis::None,
485 };
486 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
487
488 let placements = layout.place(bounds, ProposalSize::new(None, Some(200.0)), &children);
489
490 assert!((placements[0].frame.height() - 30.0).abs() < f32::EPSILON);
491 assert!((placements[1].frame.height() - 140.0).abs() < f32::EPSILON); assert!((placements[2].frame.height() - 30.0).abs() < f32::EPSILON);
493 assert!((placements[2].frame.y() - 170.0).abs() < f32::EPSILON); }
495
496 #[test]
501 fn a_cross_filling_child_still_sets_the_column_width() {
502 let layout = VStackLayout {
504 alignment: HorizontalAlignment::Center,
505 spacing: Computed::constant(10.0),
506 };
507
508 let mut label = MockSubView {
509 size: Size::new(50.0, 20.0),
510 stretch_axis: StretchAxis::None,
511 };
512 let mut text_field = MockSubView {
513 size: Size::new(100.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
516 let mut button = MockSubView {
517 size: Size::new(80.0, 44.0),
518 stretch_axis: StretchAxis::None,
519 };
520
521 let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
522
523 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
524
525 assert!(
527 (size.width - 100.0).abs() < f32::EPSILON,
528 "the text field's own width has to reach the column, got {}",
529 size.width
530 );
531 assert!((size.height - 124.0).abs() < f32::EPSILON);
534 }
535
536 #[test]
540 fn a_column_reports_its_widest_child_however_it_is_asked() {
541 let layout = VStackLayout {
542 alignment: HorizontalAlignment::Center,
543 spacing: Computed::constant(10.0),
544 };
545
546 let mut label = MockSubView {
547 size: Size::new(50.0, 20.0),
548 stretch_axis: StretchAxis::None,
549 };
550 let mut toggle = MockSubView {
551 size: Size::new(200.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
554 let mut button = MockSubView {
555 size: Size::new(80.0, 44.0),
556 stretch_axis: StretchAxis::None,
557 };
558
559 let children: Vec<&dyn SubView> = vec![&mut label, &mut toggle, &mut button];
560
561 let min_size = layout.size_that_fits(ProposalSize::ZERO, &children);
563 assert!(
564 (min_size.width - 200.0).abs() < f32::EPSILON,
565 "a window cannot shrink below the toggle, got {}",
566 min_size.width
567 );
568
569 let children2: Vec<&dyn SubView> = vec![&mut label, &mut toggle, &mut button];
570 let intrinsic_size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children2);
571 assert!(
572 (intrinsic_size.width - 200.0).abs() < f32::EPSILON,
573 "and it wants the same width when nothing constrains it, got {}",
574 intrinsic_size.width
575 );
576 }
577
578 #[test]
579 fn compressible_children_shrink_to_fit_the_column_bounded_proposal() {
580 let layout = VStackLayout {
584 alignment: HorizontalAlignment::Center,
585 spacing: Computed::constant(0.0),
586 };
587
588 let mut rows: Vec<CompressibleHeightView> = (0..3)
589 .map(|_| CompressibleHeightView {
590 ideal: Size::new(50.0, 60.0),
591 floor: 0.0,
592 })
593 .collect();
594 let children: Vec<&dyn SubView> = rows.iter_mut().map(|row| row as &dyn SubView).collect();
595
596 let bounds = Rect::new(Point::zero(), Size::new(50.0, 120.0));
597 let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
598 let placements = layout.place(bounds, proposal, &children);
599
600 for placement in &placements {
601 assert!(
602 (placement.frame.height() - 40.0).abs() < 0.01,
603 "expected each row to give up 20pt, got {}",
604 placement.frame.height()
605 );
606 }
607 }
608
609 #[test]
610 fn a_row_never_shrinks_below_the_height_it_reports_bounded_proposal() {
611 let layout = VStackLayout {
614 alignment: HorizontalAlignment::Center,
615 spacing: Computed::constant(0.0),
616 };
617
618 let mut top = CompressibleHeightView {
619 ideal: Size::new(50.0, 60.0),
620 floor: 0.0,
621 };
622 let mut middle = CompressibleHeightView {
623 ideal: Size::new(50.0, 60.0),
624 floor: 50.0,
625 };
626 let mut bottom = CompressibleHeightView {
627 ideal: Size::new(50.0, 60.0),
628 floor: 0.0,
629 };
630 let children: Vec<&dyn SubView> = vec![&mut top, &mut middle, &mut bottom];
631
632 let bounds = Rect::new(Point::zero(), Size::new(50.0, 90.0));
633 let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
634 let placements = layout.place(bounds, proposal, &children);
635
636 assert!(
637 placements[1].frame.height() >= 50.0 - 0.01,
638 "the middle row reported a 50pt floor, got {}",
639 placements[1].frame.height()
640 );
641 }
642}