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, ViewDimensions,
13 container::FixedContainer,
14 stack::{
15 Axis,
16 distribute::{Extent, compress_to_fit, exceeds},
17 stack_stretch_axis,
18 },
19};
20
21#[derive(Debug, Clone)]
23pub struct VStackLayout {
24 pub alignment: HorizontalAlignment,
26 pub spacing: Computed<f32>,
28}
29
30impl Default for VStackLayout {
31 fn default() -> Self {
32 Self {
33 alignment: HorizontalAlignment::default(),
34 spacing: Computed::constant(10.0),
35 }
36 }
37}
38
39struct ChildMeasurement {
41 dimensions: ViewDimensions,
42 stretch_axis: StretchAxis,
43}
44
45impl ChildMeasurement {
46 const fn size(&self) -> Size {
47 self.dimensions.size
48 }
49
50 fn horizontal_guide(&self, alignment: HorizontalAlignment) -> f32 {
51 self.dimensions.horizontal(alignment)
52 }
53
54 const fn stretches_main_axis(&self) -> bool {
59 matches!(
60 self.stretch_axis,
61 StretchAxis::Vertical | StretchAxis::Both | StretchAxis::MainAxis
62 )
63 }
64
65 const fn stretches_cross_axis(&self) -> bool {
69 matches!(
70 self.stretch_axis,
71 StretchAxis::Horizontal | StretchAxis::Both | StretchAxis::CrossAxis
72 )
73 }
74}
75
76fn vstack_intrinsic_cross_metrics(
86 measurements: &[ChildMeasurement],
87 alignment: HorizontalAlignment,
88) -> (f32, f32) {
89 let mut max_leading = 0.0_f32;
90 let mut max_trailing = 0.0_f32;
91
92 for measurement in measurements
93 .iter()
94 .filter(|measurement| measurement.size().width.is_finite())
95 {
96 let size = measurement.size();
97 let guide = measurement
98 .horizontal_guide(alignment)
99 .clamp(0.0, size.width);
100 max_leading = max_leading.max(guide);
101 max_trailing = max_trailing.max((size.width - guide).max(0.0));
102 }
103
104 (max_leading, max_trailing)
105}
106
107fn compress_children(
115 measurements: &mut [ChildMeasurement],
116 children: &[&dyn SubView],
117 compress_indices: &[usize],
118 available: f32,
119 width_proposal: Option<f32>,
120) {
121 if compress_indices.is_empty() {
122 return;
123 }
124
125 let ideal_total: f32 = compress_indices
127 .iter()
128 .map(|&index| measurements[index].size().height)
129 .sum();
130 if !exceeds(ideal_total, available, compress_indices.len()) {
131 return;
132 }
133
134 let extents: Vec<Extent> = compress_indices
135 .iter()
136 .map(|&index| Extent {
137 ideal: measurements[index].size().height,
138 min: children[index]
139 .measure(ProposalSize::new(width_proposal, Some(0.0)))
140 .size
141 .height,
142 priority: children[index].priority(),
143 })
144 .collect();
145
146 for (&index, resolved) in compress_indices
147 .iter()
148 .zip(compress_to_fit(&extents, available))
149 {
150 if resolved >= measurements[index].size().height {
151 continue;
152 }
153 let constrained = ProposalSize::new(width_proposal, Some(resolved));
154 measurements[index].dimensions = children[index].measure(constrained);
155 measurements[index].dimensions.size.height =
156 measurements[index].size().height.min(resolved);
157 }
158}
159
160fn usize_to_f32(value: usize) -> f32 {
161 use num_traits::ToPrimitive;
162 value
163 .to_f32()
164 .expect("VStackLayout: child count must be representable as f32")
165}
166
167impl Layout for VStackLayout {
168 fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
169 if children.is_empty() {
170 return Size::zero();
171 }
172
173 let child_proposal = ProposalSize::new(proposal.width, None);
175
176 let measurements: Vec<ChildMeasurement> = children
177 .iter()
178 .map(|child| ChildMeasurement {
179 dimensions: child.measure(child_proposal),
180 stretch_axis: child.stretch_axis(),
181 })
182 .collect();
183
184 let has_main_axis_stretch = measurements
186 .iter()
187 .any(ChildMeasurement::stretches_main_axis);
188
189 let content_height: f32 = measurements.iter().map(|m| m.size().height).sum();
196
197 let spacing = self.spacing.get();
198 let total_spacing = if children.len() > 1 {
199 usize_to_f32(children.len() - 1) * spacing
200 } else {
201 0.0
202 };
203
204 let intrinsic_height = content_height + total_spacing;
205 let final_height = if has_main_axis_stretch {
208 proposal.height.unwrap_or(intrinsic_height)
209 } else {
210 intrinsic_height
211 };
212
213 let is_min_size_query = proposal.width == Some(0.0);
216 let (max_leading, max_trailing) =
217 vstack_intrinsic_cross_metrics(&measurements, self.alignment);
218 let max_width = max_leading + max_trailing;
219
220 let final_width = if is_min_size_query {
223 max_width
224 } else {
225 proposal.width.unwrap_or(max_width)
226 };
227
228 Size::new(final_width, final_height)
229 }
230
231 fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
232 if children.is_empty() {
233 return vec![];
234 }
235
236 let child_proposal = ProposalSize::new(Some(bounds.width()), None);
238
239 let mut measurements: Vec<ChildMeasurement> = children
240 .iter()
241 .map(|child| ChildMeasurement {
242 dimensions: child.measure(child_proposal),
243 stretch_axis: child.stretch_axis(),
244 })
245 .collect();
246
247 let spacing = self.spacing.get();
248 let total_spacing = if children.len() > 1 {
249 usize_to_f32(children.len() - 1) * spacing
250 } else {
251 0.0
252 };
253
254 let compress_indices: Vec<usize> = measurements
257 .iter()
258 .enumerate()
259 .filter(|(_, m)| !m.stretches_main_axis())
260 .map(|(index, _)| index)
261 .collect();
262 compress_children(
263 &mut measurements,
264 children,
265 &compress_indices,
266 (bounds.height() - total_spacing).max(0.0),
267 Some(bounds.width()),
268 );
269
270 let main_axis_stretch_count = measurements
272 .iter()
273 .filter(|m| m.stretches_main_axis())
274 .count();
275 let non_stretch_height: f32 = measurements
276 .iter()
277 .filter(|m| !m.stretches_main_axis())
278 .map(|m| m.size().height)
279 .sum();
280
281 let remaining_height = bounds.height() - non_stretch_height - total_spacing;
282 let stretch_height = if main_axis_stretch_count > 0 {
283 (remaining_height / usize_to_f32(main_axis_stretch_count)).max(0.0)
284 } else {
285 0.0
286 };
287
288 let has_explicit_alignment_guides = measurements.iter().any(|measurement| {
289 measurement
290 .dimensions
291 .explicit_horizontal(self.alignment)
292 .is_some()
293 });
294 let guide_line = has_explicit_alignment_guides.then(|| {
295 let (intrinsic_leading, _intrinsic_trailing) =
296 vstack_intrinsic_cross_metrics(&measurements, self.alignment);
297 bounds.x() + intrinsic_leading
298 });
299
300 let mut rects = Vec::with_capacity(children.len());
302 let mut current_y = bounds.y();
303
304 for (i, measurement) in measurements.iter().enumerate() {
305 if i > 0 {
306 current_y += spacing;
307 }
308
309 let child_width = if measurement.stretches_cross_axis() {
311 bounds.width()
313 } else if measurement.size().width.is_infinite() {
314 bounds.width()
315 } else {
316 measurement.size().width.min(bounds.width())
318 };
319
320 let child_height = if measurement.stretches_main_axis() {
321 stretch_height
322 } else {
323 measurement.size().height
324 };
325
326 let mut adjusted_dimensions = measurement.dimensions.clone();
327 adjusted_dimensions.size = Size::new(child_width, child_height);
328
329 let x = if measurement.stretches_cross_axis() {
330 bounds.x()
331 } else if let Some(guide_line) = guide_line {
332 let guide = adjusted_dimensions
333 .horizontal(self.alignment)
334 .clamp(0.0, child_width);
335 guide_line - guide
336 } else if self.alignment == HorizontalAlignment::Leading {
337 bounds.x()
338 } else if self.alignment == HorizontalAlignment::Trailing {
339 bounds.x() + bounds.width() - child_width
340 } else {
341 let guide = adjusted_dimensions
342 .horizontal(self.alignment)
343 .clamp(0.0, child_width);
344 bounds.x() + bounds.width() * 0.5 - guide
345 };
346
347 rects.push(Rect::new(
348 Point::new(x, current_y),
349 Size::new(child_width, child_height),
350 ));
351
352 current_y += child_height;
353 }
354
355 rects
356 }
357
358 fn explicit_horizontal(
359 &self,
360 alignment: HorizontalAlignment,
361 _bounds: Rect,
362 children: &[PlacedSubview<'_>],
363 ) -> Option<f32> {
364 if alignment == self.alignment {
365 return children
366 .iter()
367 .filter_map(|child| child.explicit_horizontal(alignment))
368 .min_by(f32::total_cmp);
369 }
370
371 None
372 }
373
374 fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
381 stack_stretch_axis(Axis::Vertical, children)
382 }
383
384 fn watch_invalidation(
385 &self,
386 invalidate: LayoutInvalidationCallback,
387 ) -> Vec<nami::watcher::BoxWatcherGuard> {
388 vec![self.spacing.watch(move |_| invalidate())]
389 }
390}
391
392#[derive(Debug, Clone)]
432pub struct VStack<C> {
433 layout: VStackLayout,
434 contents: C,
435}
436
437impl<C: TupleViews> VStack<(C,)> {
438 pub fn new(alignment: HorizontalAlignment, spacing: f32, contents: C) -> Self {
441 Self {
442 layout: VStackLayout {
443 alignment,
444 spacing: Computed::constant(spacing),
445 },
446 contents: (contents,),
447 }
448 }
449}
450
451crate::stack::impl_stack_for_each!(VStack, VStackLayout);
452
453impl<C> VStack<C> {
454 #[must_use]
456 pub const fn alignment(mut self, alignment: HorizontalAlignment) -> Self {
457 self.layout.alignment = alignment;
458 self
459 }
460
461 #[must_use]
466 pub fn spacing(mut self, spacing: impl IntoSignalF32 + 'static) -> Self {
467 self.layout.spacing = spacing.into_signal_f32().computed();
468 self
469 }
470}
471
472impl<V> FromIterator<V> for VStack<(Vec<AnyView>,)>
473where
474 V: View,
475{
476 fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
477 let contents = iter.into_iter().map(AnyView::new).collect::<Vec<_>>();
478 Self::new(HorizontalAlignment::default(), 10.0, contents)
479 }
480}
481
482pub fn vstack<C: TupleViews>(contents: C) -> VStack<(C,)> {
484 VStack::new(HorizontalAlignment::Center, 10.0, contents)
485}
486
487impl<C, F, V> View for VStack<ForEach<C, F, V>>
488where
489 C: Collection + Clone,
490 C::Item: Identifiable,
491 F: 'static + Fn(C::Item) -> V,
492 V: View,
493{
494 fn body(self, _env: &waterui_core::Environment) -> impl View {
495 with(
497 LazyContainer::new(self.layout, self.contents),
498 Axis::Vertical,
499 )
500 }
501}
502
503impl<C: TupleViews + 'static> View for VStack<(C,)> {
504 fn body(self, _env: &waterui_core::Environment) -> impl View {
505 with(
507 FixedContainer::new(self.layout, self.contents.0),
508 Axis::Vertical,
509 )
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 struct MockSubView {
518 size: Size,
519 stretch_axis: StretchAxis,
520 }
521
522 impl SubView for MockSubView {
523 fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
524 ViewDimensions::new(self.size)
525 }
526 fn stretch_axis(&self) -> StretchAxis {
527 self.stretch_axis
528 }
529 fn priority(&self) -> i32 {
530 0
531 }
532 }
533
534 #[test]
535 fn test_vstack_size_two_children() {
536 let layout = VStackLayout {
537 alignment: HorizontalAlignment::Center,
538 spacing: Computed::constant(10.0),
539 };
540
541 let mut child1 = MockSubView {
542 size: Size::new(100.0, 30.0),
543 stretch_axis: StretchAxis::None,
544 };
545 let mut child2 = MockSubView {
546 size: Size::new(80.0, 40.0),
547 stretch_axis: StretchAxis::None,
548 };
549
550 let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
551
552 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
553
554 assert!((size.width - 100.0).abs() < f32::EPSILON); assert!((size.height - 80.0).abs() < f32::EPSILON); }
557
558 #[test]
563 fn a_column_counts_a_growing_child_in_its_intrinsic_height() {
564 let layout = VStackLayout {
565 alignment: HorizontalAlignment::Leading,
566 spacing: Computed::constant(0.0),
567 };
568
569 let mut header = MockSubView {
570 size: Size::new(100.0, 30.0),
571 stretch_axis: StretchAxis::None,
572 };
573 let mut list = MockSubView {
575 size: Size::new(100.0, 200.0),
576 stretch_axis: StretchAxis::Both,
577 };
578 let children: Vec<&dyn SubView> = vec![&mut header, &mut list];
579
580 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
581
582 assert!(
583 (size.height - 230.0).abs() < f32::EPSILON,
584 "the column dropped its growing child from its own height: got {}, want 230",
585 size.height
586 );
587 }
588
589 #[test]
592 fn a_spacer_adds_nothing_to_the_intrinsic_height() {
593 let layout = VStackLayout {
594 alignment: HorizontalAlignment::Leading,
595 spacing: Computed::constant(0.0),
596 };
597
598 let mut row = MockSubView {
599 size: Size::new(100.0, 30.0),
600 stretch_axis: StretchAxis::None,
601 };
602 let mut spacer = MockSubView {
603 size: Size::zero(),
604 stretch_axis: StretchAxis::Both,
605 };
606 let children: Vec<&dyn SubView> = vec![&mut row, &mut spacer];
607
608 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
609
610 assert!(
611 (size.height - 30.0).abs() < f32::EPSILON,
612 "a spacer changed the column's intrinsic height: got {}, want 30",
613 size.height
614 );
615 }
616
617 #[test]
618 fn test_vstack_with_spacer() {
619 let layout = VStackLayout {
620 alignment: HorizontalAlignment::Center,
621 spacing: Computed::constant(0.0),
622 };
623
624 let mut child1 = MockSubView {
625 size: Size::new(100.0, 30.0),
626 stretch_axis: StretchAxis::None,
627 };
628 let mut spacer = MockSubView {
629 size: Size::zero(),
630 stretch_axis: StretchAxis::Both, };
632 let mut child2 = MockSubView {
633 size: Size::new(100.0, 30.0),
634 stretch_axis: StretchAxis::None,
635 };
636
637 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
638
639 let size = layout.size_that_fits(ProposalSize::new(None, Some(200.0)), &children);
641
642 assert!((size.height - 200.0).abs() < f32::EPSILON);
643
644 let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
646
647 let mut child1 = MockSubView {
649 size: Size::new(100.0, 30.0),
650 stretch_axis: StretchAxis::None,
651 };
652 let mut spacer = MockSubView {
653 size: Size::zero(),
654 stretch_axis: StretchAxis::Both,
655 };
656 let mut child2 = MockSubView {
657 size: Size::new(100.0, 30.0),
658 stretch_axis: StretchAxis::None,
659 };
660 let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
661
662 let rects = layout.place(bounds, &children);
663
664 assert!((rects[0].height() - 30.0).abs() < f32::EPSILON);
665 assert!((rects[1].height() - 140.0).abs() < f32::EPSILON); assert!((rects[2].height() - 30.0).abs() < f32::EPSILON);
667 assert!((rects[2].y() - 170.0).abs() < f32::EPSILON); }
669
670 #[test]
675 fn a_cross_filling_child_still_sets_the_column_width() {
676 let layout = VStackLayout {
678 alignment: HorizontalAlignment::Center,
679 spacing: Computed::constant(10.0),
680 };
681
682 let mut label = MockSubView {
683 size: Size::new(50.0, 20.0),
684 stretch_axis: StretchAxis::None,
685 };
686 let mut text_field = MockSubView {
687 size: Size::new(100.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
690 let mut button = MockSubView {
691 size: Size::new(80.0, 44.0),
692 stretch_axis: StretchAxis::None,
693 };
694
695 let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
696
697 let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
698
699 assert!(
701 (size.width - 100.0).abs() < f32::EPSILON,
702 "the text field's own width has to reach the column, got {}",
703 size.width
704 );
705 assert!((size.height - 124.0).abs() < f32::EPSILON);
708 }
709
710 #[test]
714 fn a_column_reports_its_widest_child_however_it_is_asked() {
715 let layout = VStackLayout {
716 alignment: HorizontalAlignment::Center,
717 spacing: Computed::constant(10.0),
718 };
719
720 let mut label = MockSubView {
721 size: Size::new(50.0, 20.0),
722 stretch_axis: StretchAxis::None,
723 };
724 let mut toggle = MockSubView {
725 size: Size::new(200.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
728 let mut button = MockSubView {
729 size: Size::new(80.0, 44.0),
730 stretch_axis: StretchAxis::None,
731 };
732
733 let children: Vec<&dyn SubView> = vec![&mut label, &mut toggle, &mut button];
734
735 let min_size = layout.size_that_fits(ProposalSize::ZERO, &children);
737 assert!(
738 (min_size.width - 200.0).abs() < f32::EPSILON,
739 "a window cannot shrink below the toggle, got {}",
740 min_size.width
741 );
742
743 let children2: Vec<&dyn SubView> = vec![&mut label, &mut toggle, &mut button];
744 let intrinsic_size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children2);
745 assert!(
746 (intrinsic_size.width - 200.0).abs() < f32::EPSILON,
747 "and it wants the same width when nothing constrains it, got {}",
748 intrinsic_size.width
749 );
750 }
751
752 #[test]
753 fn compressible_children_shrink_to_fit_the_column() {
754 let layout = VStackLayout {
758 alignment: HorizontalAlignment::Center,
759 spacing: Computed::constant(0.0),
760 };
761
762 let mut rows: Vec<CompressibleHeightView> = (0..3)
763 .map(|_| CompressibleHeightView {
764 ideal: Size::new(50.0, 60.0),
765 floor: 0.0,
766 })
767 .collect();
768 let children: Vec<&dyn SubView> = rows.iter_mut().map(|row| row as &dyn SubView).collect();
769
770 let bounds = Rect::new(Point::zero(), Size::new(50.0, 120.0));
771 let rects = layout.place(bounds, &children);
772
773 for rect in &rects {
774 assert!(
775 (rect.height() - 40.0).abs() < 0.01,
776 "expected each row to give up 20pt, got {}",
777 rect.height()
778 );
779 }
780 }
781
782 #[test]
783 fn a_row_never_shrinks_below_the_height_it_reports() {
784 let layout = VStackLayout {
787 alignment: HorizontalAlignment::Center,
788 spacing: Computed::constant(0.0),
789 };
790
791 let mut top = CompressibleHeightView {
792 ideal: Size::new(50.0, 60.0),
793 floor: 0.0,
794 };
795 let mut middle = CompressibleHeightView {
796 ideal: Size::new(50.0, 60.0),
797 floor: 50.0,
798 };
799 let mut bottom = CompressibleHeightView {
800 ideal: Size::new(50.0, 60.0),
801 floor: 0.0,
802 };
803 let children: Vec<&dyn SubView> = vec![&mut top, &mut middle, &mut bottom];
804
805 let bounds = Rect::new(Point::zero(), Size::new(50.0, 90.0));
806 let rects = layout.place(bounds, &children);
807
808 assert!(
809 rects[1].height() >= 50.0 - 0.01,
810 "the middle row reported a 50pt floor, got {}",
811 rects[1].height()
812 );
813 }
814
815 struct CompressibleHeightView {
817 ideal: Size,
818 floor: f32,
819 }
820
821 impl SubView for CompressibleHeightView {
822 fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
823 let height = proposal.height.map_or(self.ideal.height, |proposed| {
824 proposed.clamp(self.floor, self.ideal.height)
825 });
826 ViewDimensions::new(Size::new(self.ideal.width, height))
827 }
828 fn stretch_axis(&self) -> StretchAxis {
829 StretchAxis::None
830 }
831 fn priority(&self) -> i32 {
832 0
833 }
834 }
835}