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, ViewDimensions,
container::FixedContainer,
stack::{
Axis,
distribute::{Extent, compress_to_fit, exceeds},
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),
}
}
}
struct ChildMeasurement {
dimensions: ViewDimensions,
stretch_axis: StretchAxis,
}
impl ChildMeasurement {
const fn size(&self) -> Size {
self.dimensions.size
}
fn horizontal_guide(&self, alignment: HorizontalAlignment) -> f32 {
self.dimensions.horizontal(alignment)
}
const fn stretches_main_axis(&self) -> bool {
matches!(
self.stretch_axis,
StretchAxis::Vertical | StretchAxis::Both | StretchAxis::MainAxis
)
}
const fn stretches_cross_axis(&self) -> bool {
matches!(
self.stretch_axis,
StretchAxis::Horizontal | StretchAxis::Both | StretchAxis::CrossAxis
)
}
}
fn vstack_intrinsic_cross_metrics(
measurements: &[ChildMeasurement],
alignment: HorizontalAlignment,
) -> (f32, f32) {
let mut max_leading = 0.0_f32;
let mut max_trailing = 0.0_f32;
for measurement in measurements
.iter()
.filter(|measurement| measurement.size().width.is_finite())
{
let size = measurement.size();
let guide = measurement
.horizontal_guide(alignment)
.clamp(0.0, size.width);
max_leading = max_leading.max(guide);
max_trailing = max_trailing.max((size.width - guide).max(0.0));
}
(max_leading, max_trailing)
}
fn compress_children(
measurements: &mut [ChildMeasurement],
children: &[&dyn SubView],
compress_indices: &[usize],
available: f32,
width_proposal: Option<f32>,
) {
if compress_indices.is_empty() {
return;
}
let ideal_total: f32 = compress_indices
.iter()
.map(|&index| measurements[index].size().height)
.sum();
if !exceeds(ideal_total, available, compress_indices.len()) {
return;
}
let extents: Vec<Extent> = compress_indices
.iter()
.map(|&index| Extent {
ideal: measurements[index].size().height,
min: children[index]
.measure(ProposalSize::new(width_proposal, Some(0.0)))
.size
.height,
priority: children[index].priority(),
})
.collect();
for (&index, resolved) in compress_indices
.iter()
.zip(compress_to_fit(&extents, available))
{
if resolved >= measurements[index].size().height {
continue;
}
let constrained = ProposalSize::new(width_proposal, Some(resolved));
measurements[index].dimensions = children[index].measure(constrained);
measurements[index].dimensions.size.height =
measurements[index].size().height.min(resolved);
}
}
fn usize_to_f32(value: usize) -> f32 {
use num_traits::ToPrimitive;
value
.to_f32()
.expect("VStackLayout: child count must be representable as f32")
}
impl Layout for VStackLayout {
fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
if children.is_empty() {
return Size::zero();
}
let child_proposal = ProposalSize::new(proposal.width, None);
let measurements: Vec<ChildMeasurement> = children
.iter()
.map(|child| ChildMeasurement {
dimensions: child.measure(child_proposal),
stretch_axis: child.stretch_axis(),
})
.collect();
let has_main_axis_stretch = measurements
.iter()
.any(ChildMeasurement::stretches_main_axis);
let content_height: f32 = measurements.iter().map(|m| m.size().height).sum();
let spacing = self.spacing.get();
let total_spacing = if children.len() > 1 {
usize_to_f32(children.len() - 1) * spacing
} else {
0.0
};
let intrinsic_height = content_height + total_spacing;
let final_height = if has_main_axis_stretch {
proposal.height.unwrap_or(intrinsic_height)
} else {
intrinsic_height
};
let is_min_size_query = proposal.width == Some(0.0);
let (max_leading, max_trailing) =
vstack_intrinsic_cross_metrics(&measurements, self.alignment);
let max_width = max_leading + max_trailing;
let final_width = if is_min_size_query {
max_width
} else {
proposal.width.unwrap_or(max_width)
};
Size::new(final_width, final_height)
}
fn place(&self, bounds: Rect, children: &[&dyn SubView]) -> Vec<Rect> {
if children.is_empty() {
return vec![];
}
let child_proposal = ProposalSize::new(Some(bounds.width()), None);
let mut measurements: Vec<ChildMeasurement> = children
.iter()
.map(|child| ChildMeasurement {
dimensions: child.measure(child_proposal),
stretch_axis: child.stretch_axis(),
})
.collect();
let spacing = self.spacing.get();
let total_spacing = if children.len() > 1 {
usize_to_f32(children.len() - 1) * spacing
} else {
0.0
};
let compress_indices: Vec<usize> = measurements
.iter()
.enumerate()
.filter(|(_, m)| !m.stretches_main_axis())
.map(|(index, _)| index)
.collect();
compress_children(
&mut measurements,
children,
&compress_indices,
(bounds.height() - total_spacing).max(0.0),
Some(bounds.width()),
);
let main_axis_stretch_count = measurements
.iter()
.filter(|m| m.stretches_main_axis())
.count();
let non_stretch_height: f32 = measurements
.iter()
.filter(|m| !m.stretches_main_axis())
.map(|m| m.size().height)
.sum();
let remaining_height = bounds.height() - non_stretch_height - total_spacing;
let stretch_height = if main_axis_stretch_count > 0 {
(remaining_height / usize_to_f32(main_axis_stretch_count)).max(0.0)
} else {
0.0
};
let has_explicit_alignment_guides = measurements.iter().any(|measurement| {
measurement
.dimensions
.explicit_horizontal(self.alignment)
.is_some()
});
let guide_line = has_explicit_alignment_guides.then(|| {
let (intrinsic_leading, _intrinsic_trailing) =
vstack_intrinsic_cross_metrics(&measurements, self.alignment);
bounds.x() + intrinsic_leading
});
let mut rects = 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 = if measurement.stretches_cross_axis() {
bounds.width()
} else if measurement.size().width.is_infinite() {
bounds.width()
} else {
measurement.size().width.min(bounds.width())
};
let child_height = if measurement.stretches_main_axis() {
stretch_height
} else {
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 if let Some(guide_line) = guide_line {
let guide = adjusted_dimensions
.horizontal(self.alignment)
.clamp(0.0, child_width);
guide_line - guide
} else if self.alignment == HorizontalAlignment::Leading {
bounds.x()
} else if self.alignment == HorizontalAlignment::Trailing {
bounds.x() + bounds.width() - child_width
} else {
let guide = adjusted_dimensions
.horizontal(self.alignment)
.clamp(0.0, child_width);
bounds.x() + bounds.width() * 0.5 - guide
};
rects.push(Rect::new(
Point::new(x, current_y),
Size::new(child_width, child_height),
));
current_y += child_height;
}
rects
}
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 const fn alignment(mut self, alignment: HorizontalAlignment) -> Self {
self.layout.alignment = alignment;
self
}
#[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,
)
}
}
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,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
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 rects = layout.place(bounds, &children);
assert!((rects[0].height() - 30.0).abs() < f32::EPSILON);
assert!((rects[1].height() - 140.0).abs() < f32::EPSILON); assert!((rects[2].height() - 30.0).abs() < f32::EPSILON);
assert!((rects[2].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() {
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 rects = layout.place(bounds, &children);
for rect in &rects {
assert!(
(rect.height() - 40.0).abs() < 0.01,
"expected each row to give up 20pt, got {}",
rect.height()
);
}
}
#[test]
fn a_row_never_shrinks_below_the_height_it_reports() {
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 rects = layout.place(bounds, &children);
assert!(
rects[1].height() >= 50.0 - 0.01,
"the middle row reported a 50pt floor, got {}",
rects[1].height()
);
}
struct CompressibleHeightView {
ideal: Size,
floor: f32,
}
impl SubView for CompressibleHeightView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let height = proposal.height.map_or(self.ideal.height, |proposed| {
proposed.clamp(self.floor, self.ideal.height)
});
ViewDimensions::new(Size::new(self.ideal.width, height))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
}