use alloc::{vec, vec::Vec};
use nami::{Computed, Signal, SignalExt, collection::Collection};
use waterui_core::{
AnyView, IntoSignalF32, View, env::with, id::Identifiable, layout::LayoutInvalidationCallback,
view::TupleViews, views::ForEach,
};
use crate::{
HorizontalAlignment, Layout, LazyContainer, PlacedSubview, Point, ProposalSize, Rect, Size,
StretchAxis, SubView, SubviewPlacement,
container::FixedContainer,
stack::{
Axis,
distribute::{
ChildMeasurement, LineAnchor, container_line, cross_envelope, measure_stack,
place_cross_extent, stack_spacing,
},
stack_stretch_axis,
},
};
#[derive(Debug, Clone)]
pub struct VStackLayout {
pub alignment: HorizontalAlignment,
pub spacing: Computed<f32>,
}
impl Default for VStackLayout {
fn default() -> Self {
Self {
alignment: HorizontalAlignment::default(),
spacing: Computed::constant(10.0),
}
}
}
fn vstack_intrinsic_cross_metrics(
measurements: &[ChildMeasurement],
alignment: HorizontalAlignment,
) -> (f32, f32) {
cross_envelope(measurements.iter().map(|measurement| {
(
measurement.size().width,
measurement.horizontal_guide(alignment),
)
}))
}
impl Layout for VStackLayout {
fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
if children.is_empty() {
return Size::zero();
}
let spacing = self.spacing.get();
let measurements = measure_stack(Axis::Vertical, proposal, spacing, children);
let final_height = measurements.iter().map(|m| m.size().height).sum::<f32>()
+ stack_spacing(spacing, children.len());
let (leading, trailing) = vstack_intrinsic_cross_metrics(&measurements, self.alignment);
let width = if measurements.iter().any(|m| m.size().width.is_infinite()) {
f32::INFINITY
} else {
leading + trailing
};
Size::new(width, final_height)
}
fn place(
&self,
bounds: Rect,
_proposal: ProposalSize,
children: &[&dyn SubView],
) -> Vec<SubviewPlacement> {
if children.is_empty() {
return vec![];
}
let spacing = self.spacing.get();
let placement = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
let measurements = measure_stack(Axis::Vertical, placement, spacing, children);
let (above, below) = vstack_intrinsic_cross_metrics(&measurements, self.alignment);
let guide_line = bounds.x()
+ container_line(
bounds.width(),
LineAnchor::horizontal(self.alignment),
above,
below,
);
let mut placements = Vec::with_capacity(children.len());
let mut current_y = bounds.y();
for (i, measurement) in measurements.iter().enumerate() {
if i > 0 {
current_y += spacing;
}
let child_width = place_cross_extent(
measurement.size().width,
bounds.width(),
measurement.stretches_cross_axis(),
);
let child_height = measurement.size().height;
let mut adjusted_dimensions = measurement.dimensions.clone();
adjusted_dimensions.size = Size::new(child_width, child_height);
let x = if measurement.stretches_cross_axis() {
bounds.x()
} else {
guide_line - adjusted_dimensions.horizontal(self.alignment)
};
placements.push(SubviewPlacement::new(
Rect::new(
Point::new(x, current_y),
Size::new(child_width, child_height),
),
measurement.proposal,
));
current_y += child_height;
}
placements
}
fn explicit_horizontal(
&self,
alignment: HorizontalAlignment,
_bounds: Rect,
children: &[PlacedSubview<'_>],
) -> Option<f32> {
if alignment == self.alignment {
return children
.iter()
.filter_map(|child| child.explicit_horizontal(alignment))
.min_by(f32::total_cmp);
}
None
}
fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
stack_stretch_axis(Axis::Vertical, children)
}
fn watch_invalidation(
&self,
invalidate: LayoutInvalidationCallback,
) -> Vec<nami::watcher::BoxWatcherGuard> {
vec![self.spacing.watch(move |_| invalidate())]
}
}
#[derive(Debug, Clone)]
pub struct VStack<C> {
layout: VStackLayout,
contents: C,
}
impl<C: TupleViews> VStack<(C,)> {
pub fn new(alignment: HorizontalAlignment, spacing: f32, contents: C) -> Self {
Self {
layout: VStackLayout {
alignment,
spacing: Computed::constant(spacing),
},
contents: (contents,),
}
}
}
crate::stack::impl_stack_for_each!(VStack, VStackLayout);
impl<C> VStack<C> {
#[must_use]
pub fn alignment(mut self, alignment: impl Into<HorizontalAlignment>) -> Self {
self.layout.alignment = alignment.into();
self
}
crate::alignment::horizontal_alignment_methods!();
#[must_use]
pub fn spacing(mut self, spacing: impl IntoSignalF32 + 'static) -> Self {
self.layout.spacing = spacing.into_signal_f32().computed();
self
}
}
impl<V> FromIterator<V> for VStack<(Vec<AnyView>,)>
where
V: View,
{
fn from_iter<T: IntoIterator<Item = V>>(iter: T) -> Self {
let contents = iter.into_iter().map(AnyView::new).collect::<Vec<_>>();
Self::new(HorizontalAlignment::default(), 10.0, contents)
}
}
pub fn vstack<C: TupleViews>(contents: C) -> VStack<(C,)> {
VStack::new(HorizontalAlignment::Center, 10.0, contents)
}
impl<C, F, V> View for VStack<ForEach<C, F, V>>
where
C: Collection + Clone,
C::Item: Identifiable,
F: 'static + Fn(C::Item) -> V,
V: View,
{
fn body(self, _env: &waterui_core::Environment) -> impl View {
with(
LazyContainer::new(self.layout, self.contents),
Axis::Vertical,
)
}
fn stretch_axis(&self) -> StretchAxis {
self.layout.stretch_axis(&[])
}
}
impl<C: TupleViews + 'static> View for VStack<(C,)> {
fn body(self, _env: &waterui_core::Environment) -> impl View {
with(
FixedContainer::new(self.layout, self.contents.0),
Axis::Vertical,
)
}
fn stretch_axis(&self) -> StretchAxis {
self.layout.stretch_axis(&self.contents.0.stretch_axes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ViewDimensions;
use crate::tests::CompressibleHeightView;
struct MockSubView {
size: Size,
stretch_axis: StretchAxis,
}
impl SubView for MockSubView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(self.size)
}
fn stretch_axis(&self) -> StretchAxis {
self.stretch_axis
}
fn priority(&self) -> i32 {
0
}
}
#[test]
fn test_vstack_size_two_children() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let mut child2 = MockSubView {
size: Size::new(80.0, 40.0),
stretch_axis: StretchAxis::None,
};
let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert!((size.width - 100.0).abs() < f32::EPSILON); assert!((size.height - 80.0).abs() < f32::EPSILON); }
#[test]
fn a_column_counts_a_growing_child_in_its_intrinsic_height() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(0.0),
};
let mut header = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let mut list = MockSubView {
size: Size::new(100.0, 200.0),
stretch_axis: StretchAxis::Both,
};
let children: Vec<&dyn SubView> = vec![&mut header, &mut list];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert!(
(size.height - 230.0).abs() < f32::EPSILON,
"the column dropped its growing child from its own height: got {}, want 230",
size.height
);
}
#[test]
fn a_spacer_adds_nothing_to_the_intrinsic_height() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(0.0),
};
let mut row = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let mut spacer = MockSubView {
size: Size::zero(),
stretch_axis: StretchAxis::Both,
};
let children: Vec<&dyn SubView> = vec![&mut row, &mut spacer];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert!(
(size.height - 30.0).abs() < f32::EPSILON,
"a spacer changed the column's intrinsic height: got {}, want 30",
size.height
);
}
#[test]
fn test_vstack_with_spacer() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let mut spacer = MockSubView {
size: Size::zero(),
stretch_axis: StretchAxis::Both, };
let mut child2 = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
let size = layout.size_that_fits(ProposalSize::new(None, Some(200.0)), &children);
assert!((size.height - 200.0).abs() < f32::EPSILON);
let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
let mut child1 = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let mut spacer = MockSubView {
size: Size::zero(),
stretch_axis: StretchAxis::Both,
};
let mut child2 = MockSubView {
size: Size::new(100.0, 30.0),
stretch_axis: StretchAxis::None,
};
let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
let placements = layout.place(bounds, ProposalSize::new(None, Some(200.0)), &children);
assert!((placements[0].frame.height() - 30.0).abs() < f32::EPSILON);
assert!((placements[1].frame.height() - 140.0).abs() < f32::EPSILON); assert!((placements[2].frame.height() - 30.0).abs() < f32::EPSILON);
assert!((placements[2].frame.y() - 170.0).abs() < f32::EPSILON); }
#[test]
fn a_cross_filling_child_still_sets_the_column_width() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = MockSubView {
size: Size::new(50.0, 20.0),
stretch_axis: StretchAxis::None,
};
let mut text_field = MockSubView {
size: Size::new(100.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
let mut button = MockSubView {
size: Size::new(80.0, 44.0),
stretch_axis: StretchAxis::None,
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert!(
(size.width - 100.0).abs() < f32::EPSILON,
"the text field's own width has to reach the column, got {}",
size.width
);
assert!((size.height - 124.0).abs() < f32::EPSILON);
}
#[test]
fn a_column_reports_its_widest_child_however_it_is_asked() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = MockSubView {
size: Size::new(50.0, 20.0),
stretch_axis: StretchAxis::None,
};
let mut toggle = MockSubView {
size: Size::new(200.0, 40.0), stretch_axis: StretchAxis::Horizontal, };
let mut button = MockSubView {
size: Size::new(80.0, 44.0),
stretch_axis: StretchAxis::None,
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut toggle, &mut button];
let min_size = layout.size_that_fits(ProposalSize::ZERO, &children);
assert!(
(min_size.width - 200.0).abs() < f32::EPSILON,
"a window cannot shrink below the toggle, got {}",
min_size.width
);
let children2: Vec<&dyn SubView> = vec![&mut label, &mut toggle, &mut button];
let intrinsic_size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children2);
assert!(
(intrinsic_size.width - 200.0).abs() < f32::EPSILON,
"and it wants the same width when nothing constrains it, got {}",
intrinsic_size.width
);
}
#[test]
fn compressible_children_shrink_to_fit_the_column_bounded_proposal() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut rows: Vec<CompressibleHeightView> = (0..3)
.map(|_| CompressibleHeightView {
ideal: Size::new(50.0, 60.0),
floor: 0.0,
})
.collect();
let children: Vec<&dyn SubView> = rows.iter_mut().map(|row| row as &dyn SubView).collect();
let bounds = Rect::new(Point::zero(), Size::new(50.0, 120.0));
let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
let placements = layout.place(bounds, proposal, &children);
for placement in &placements {
assert!(
(placement.frame.height() - 40.0).abs() < 0.01,
"expected each row to give up 20pt, got {}",
placement.frame.height()
);
}
}
#[test]
fn a_row_never_shrinks_below_the_height_it_reports_bounded_proposal() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut top = CompressibleHeightView {
ideal: Size::new(50.0, 60.0),
floor: 0.0,
};
let mut middle = CompressibleHeightView {
ideal: Size::new(50.0, 60.0),
floor: 50.0,
};
let mut bottom = CompressibleHeightView {
ideal: Size::new(50.0, 60.0),
floor: 0.0,
};
let children: Vec<&dyn SubView> = vec![&mut top, &mut middle, &mut bottom];
let bounds = Rect::new(Point::zero(), Size::new(50.0, 90.0));
let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
let placements = layout.place(bounds, proposal, &children);
assert!(
placements[1].frame.height() >= 50.0 - 0.01,
"the middle row reported a 50pt floor, got {}",
placements[1].frame.height()
);
}
}