use alloc::{format, vec, vec::Vec};
use nami::Computed;
use crate::stack::{HStackLayout, HorizontalAlignment, VStackLayout, VerticalAlignment};
use crate::{Layout, Point, ProposalSize, Rect, Size, StretchAxis, SubView, ViewDimensions};
struct FixedSizeView {
size: Size,
}
impl SubView for FixedSizeView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(self.size)
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
pub struct CompressibleView {
pub(crate) ideal: Size,
pub(crate) floor: f32,
}
impl SubView for CompressibleView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let width = proposal.width.map_or(self.ideal.width, |proposed| {
proposed.clamp(self.floor, self.ideal.width)
});
ViewDimensions::new(Size::new(width, self.ideal.height))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
struct FlexibleTextView {
intrinsic_size: Size,
line_height: f32,
}
impl FlexibleTextView {
fn new(text_width: f32, line_height: f32) -> Self {
Self {
intrinsic_size: Size::new(text_width, line_height),
line_height,
}
}
}
impl SubView for FlexibleTextView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(match proposal.width {
Some(max_width) if max_width < self.intrinsic_size.width => {
let lines = (self.intrinsic_size.width / max_width).ceil();
Size::new(max_width, lines * self.line_height)
}
_ => {
self.intrinsic_size
}
})
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
struct SpacerView;
impl SubView for SpacerView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(Size::zero())
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Both
}
fn priority(&self) -> i32 {
0
}
}
struct HorizontalExpandingView {
height: f32,
}
impl SubView for HorizontalExpandingView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let width = proposal.width.unwrap_or(f32::INFINITY);
ViewDimensions::new(Size::new(width, self.height))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Horizontal
}
fn priority(&self) -> i32 {
0
}
}
struct VerticalExpandingView {
width: f32,
}
struct BaselineTextView {
size: Size,
first_baseline: f32,
last_baseline: f32,
}
impl SubView for BaselineTextView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(self.size)
.with_vertical(VerticalAlignment::FirstBaseline, self.first_baseline)
.with_vertical(VerticalAlignment::LastBaseline, self.last_baseline)
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
struct GuidedBoxView {
size: Size,
guide: f32,
}
impl SubView for GuidedBoxView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(self.size).with_horizontal(HorizontalAlignment::Leading, self.guide)
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::None
}
fn priority(&self) -> i32 {
0
}
}
impl SubView for VerticalExpandingView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let height = proposal.height.unwrap_or(f32::INFINITY);
ViewDimensions::new(Size::new(self.width, height))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Vertical
}
fn priority(&self) -> i32 {
0
}
}
fn assert_rect_within_bounds(rect: &Rect, bounds: &Rect, msg: &str) {
assert!(
rect.x() >= bounds.x() - 0.001,
"{}: rect.x ({}) < bounds.x ({})",
msg,
rect.x(),
bounds.x()
);
assert!(
rect.y() >= bounds.y() - 0.001,
"{}: rect.y ({}) < bounds.y ({})",
msg,
rect.y(),
bounds.y()
);
assert!(
rect.x() + rect.width() <= bounds.x() + bounds.width() + 0.001,
"{}: rect.right ({}) > bounds.right ({})",
msg,
rect.x() + rect.width(),
bounds.x() + bounds.width()
);
assert!(
rect.y() + rect.height() <= bounds.y() + bounds.height() + 0.001,
"{}: rect.bottom ({}) > bounds.bottom ({})",
msg,
rect.y() + rect.height(),
bounds.y() + bounds.height()
);
}
fn assert_no_overlap(rects: &[Rect], axis: &str) {
for i in 0..rects.len() {
for j in (i + 1)..rects.len() {
let r1 = &rects[i];
let r2 = &rects[j];
let overlap = match axis {
"horizontal" => {
let r1_right = r1.x() + r1.width();
let r2_left = r2.x();
r1_right > r2_left + 0.001
}
"vertical" => {
let r1_bottom = r1.y() + r1.height();
let r2_top = r2.y();
r1_bottom > r2_top + 0.001
}
_ => false,
};
assert!(!overlap, "Children {i} and {j} overlap on {axis} axis");
}
}
}
#[test]
fn test_hstack_children_exceed_bounds() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut text1 = CompressibleView {
ideal: Size::new(60.0, 20.0),
floor: 0.0,
};
let mut spacer = SpacerView;
let mut text2 = CompressibleView {
ideal: Size::new(60.0, 20.0),
floor: 0.0,
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 40.0));
let children: Vec<&dyn SubView> = vec![&mut text1, &mut spacer, &mut text2];
let rects = layout.place(bounds, &children);
for (i, rect) in rects.iter().enumerate() {
assert_rect_within_bounds(rect, &bounds, &format!("child {i}"));
}
assert!(
rects[1].width() <= 0.001,
"Spacer should be zero when children exceed bounds, got {}",
rects[1].width()
);
}
#[test]
fn test_hstack_single_child_exceeds_bounds() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut wide_child = FixedSizeView {
size: Size::new(200.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 50.0));
let children: Vec<&dyn SubView> = vec![&mut wide_child];
let rects = layout.place(bounds, &children);
assert_eq!(rects.len(), 1);
assert!(
(rects[0].width() - 200.0).abs() < 0.001,
"the child reports 200pt at every proposal, so it must keep 200pt, got {}",
rects[0].width()
);
}
#[test]
fn test_hstack_multiple_children_total_exceeds_bounds() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut child2 = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut child3 = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 40.0));
let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3];
let rects = layout.place(bounds, &children);
for (index, rect) in rects.iter().enumerate() {
assert!(
(rect.width() - 50.0).abs() < 0.001,
"child {index} reports 50pt at every proposal, so it must keep 50pt, got {}",
rect.width()
);
}
let overflow = rects.last().expect("three children were placed").max_x() - bounds.max_x();
assert!(
(overflow - 70.0).abs() < 0.001,
"170pt of rigid content in 100pt must overflow by 70pt, got {overflow}"
);
assert_no_overlap(&rects, "horizontal");
}
#[test]
fn test_hstack_with_flexible_text() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut long_text = FlexibleTextView::new(200.0, 20.0);
let bounds = Rect::new(Point::zero(), Size::new(150.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut long_text];
let rects = layout.place(bounds, &children);
let total_children_width: f32 = rects.iter().map(waterui_core::layout::Rect::width).sum();
let expected_max = bounds.width() - 10.0;
assert!(
total_children_width <= expected_max + 0.001,
"Total children width {total_children_width} exceeds available space {expected_max}"
);
for (i, rect) in rects.iter().enumerate() {
assert_rect_within_bounds(rect, &bounds, &format!("child {i}"));
}
}
#[test]
fn hstack_intrinsic_round_trip_does_not_spuriously_wrap_text() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(6.0),
};
let mut title = FlexibleTextView::new(111.068_21, 20.0);
let mut icon = FixedSizeView {
size: Size::new(10.973_763, 20.0),
};
let children: Vec<&dyn SubView> = vec![&mut title, &mut icon];
let intrinsic = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
let rects = layout.place(
Rect::new(Point::zero(), Size::new(intrinsic.width, 40.0)),
&children,
);
assert_eq!(
rects[0].height(),
20.0,
"an intrinsic-size HStack must not wrap text due only to f32 round-trip noise"
);
}
#[test]
fn test_hstack_empty() {
let layout = HStackLayout::default();
let children: Vec<&dyn SubView> = vec![];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.width, 0.0);
assert_eq!(size.height, 0.0);
let bounds = Rect::new(Point::zero(), Size::new(100.0, 100.0));
let rects = layout.place(bounds, &children);
assert!(rects.is_empty());
}
#[test]
fn test_hstack_single_child() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let children: Vec<&dyn SubView> = vec![&mut child];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.width, 50.0);
assert_eq!(size.height, 30.0);
}
#[test]
fn test_hstack_multiple_spacers() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = FixedSizeView {
size: Size::new(20.0, 30.0),
};
let mut spacer1 = SpacerView;
let mut child2 = FixedSizeView {
size: Size::new(20.0, 30.0),
};
let mut spacer2 = SpacerView;
let mut child3 = FixedSizeView {
size: Size::new(20.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::new(200.0, 50.0));
let children: Vec<&dyn SubView> = vec![
&mut child1,
&mut spacer1,
&mut child2,
&mut spacer2,
&mut child3,
];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].width(), 20.0);
assert!(
(rects[1].width() - 70.0).abs() < 0.001,
"Spacer 1 width: {}",
rects[1].width()
);
assert_eq!(rects[2].width(), 20.0);
assert!(
(rects[3].width() - 70.0).abs() < 0.001,
"Spacer 2 width: {}",
rects[3].width()
);
assert_eq!(rects[4].width(), 20.0);
}
#[test]
fn test_hstack_alignment_top() {
let layout = HStackLayout {
alignment: VerticalAlignment::Top,
spacing: Computed::constant(10.0),
};
let mut short = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut tall = FixedSizeView {
size: Size::new(30.0, 50.0),
};
let bounds = Rect::new(Point::new(10.0, 10.0), Size::new(100.0, 60.0));
let children: Vec<&dyn SubView> = vec![&mut short, &mut tall];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].y(), 10.0);
assert_eq!(rects[1].y(), 10.0);
}
#[test]
fn test_hstack_alignment_bottom() {
let layout = HStackLayout {
alignment: VerticalAlignment::Bottom,
spacing: Computed::constant(10.0),
};
let mut short = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut tall = FixedSizeView {
size: Size::new(30.0, 50.0),
};
let bounds = Rect::new(Point::new(10.0, 10.0), Size::new(100.0, 60.0));
let children: Vec<&dyn SubView> = vec![&mut short, &mut tall];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].y() + rects[0].height(), 70.0); assert_eq!(rects[1].y() + rects[1].height(), 70.0);
}
#[test]
fn test_hstack_alignment_center() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut short = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut tall = FixedSizeView {
size: Size::new(30.0, 50.0),
};
let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 60.0));
let children: Vec<&dyn SubView> = vec![&mut short, &mut tall];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].y(), 20.0);
assert_eq!(rects[1].y(), 5.0);
}
#[test]
fn test_vstack_children_exceed_bounds() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = FixedSizeView {
size: Size::new(50.0, 50.0),
};
let mut child2 = FixedSizeView {
size: Size::new(50.0, 50.0),
};
let mut child3 = FixedSizeView {
size: Size::new(50.0, 50.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2, &mut child3];
let rects = layout.place(bounds, &children);
assert_no_overlap(&rects, "vertical");
}
#[test]
fn test_vstack_child_width_exceeds_bounds() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut wide_child = FixedSizeView {
size: Size::new(200.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 50.0));
let children: Vec<&dyn SubView> = vec![&mut wide_child];
let rects = layout.place(bounds, &children);
assert_eq!(rects.len(), 1);
assert!(
rects[0].width() <= bounds.width() + 0.001,
"Child width {} exceeds bounds width {}",
rects[0].width(),
bounds.width()
);
assert_rect_within_bounds(&rects[0], &bounds, "wide child");
}
#[test]
fn test_vstack_text_wrapping() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(10.0),
};
let mut long_text = FlexibleTextView::new(200.0, 20.0);
let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
let children: Vec<&dyn SubView> = vec![&mut long_text];
let rects = layout.place(bounds, &children);
assert!(
rects[0].width() <= bounds.width() + 0.001,
"Text width {} exceeds bounds width {}",
rects[0].width(),
bounds.width()
);
assert!(
rects[0].height() >= 40.0 - 0.001,
"Text should wrap to multiple lines, height: {}",
rects[0].height()
);
}
#[test]
fn test_vstack_with_axis_expanding_child() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut text_field = HorizontalExpandingView { height: 30.0 };
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].width(), 50.0);
assert!(
rects[1].width() <= bounds.width() + 0.001,
"TextField width {} should not exceed bounds {}",
rects[1].width(),
bounds.width()
);
}
#[test]
fn test_vstack_empty() {
let layout = VStackLayout::default();
let children: Vec<&dyn SubView> = vec![];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.width, 0.0);
assert_eq!(size.height, 0.0);
}
#[test]
fn test_vstack_alignment_leading() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(10.0),
};
let mut narrow = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut wide = FixedSizeView {
size: Size::new(80.0, 20.0),
};
let bounds = Rect::new(Point::new(10.0, 10.0), Size::new(100.0, 60.0));
let children: Vec<&dyn SubView> = vec![&mut narrow, &mut wide];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].x(), 10.0);
assert_eq!(rects[1].x(), 10.0);
}
#[test]
fn test_vstack_alignment_trailing() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Trailing,
spacing: Computed::constant(10.0),
};
let mut narrow = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut wide = FixedSizeView {
size: Size::new(80.0, 20.0),
};
let bounds = Rect::new(Point::new(10.0, 10.0), Size::new(100.0, 60.0));
let children: Vec<&dyn SubView> = vec![&mut narrow, &mut wide];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].x() + rects[0].width(), 110.0); assert_eq!(rects[1].x() + rects[1].width(), 110.0);
}
#[test]
fn test_vstack_alignment_center() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut narrow = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut wide = FixedSizeView {
size: Size::new(80.0, 20.0),
};
let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 60.0));
let children: Vec<&dyn SubView> = vec![&mut narrow, &mut wide];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].x(), 35.0);
assert_eq!(rects[1].x(), 10.0);
}
#[test]
fn test_vstack_multiple_spacers() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut spacer1 = SpacerView;
let mut child2 = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut spacer2 = SpacerView;
let mut child3 = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
let children: Vec<&dyn SubView> = vec![
&mut child1,
&mut spacer1,
&mut child2,
&mut spacer2,
&mut child3,
];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].height(), 20.0);
assert!(
(rects[1].height() - 70.0).abs() < 0.001,
"Spacer 1 height: {}",
rects[1].height()
);
assert_eq!(rects[2].height(), 20.0);
assert!(
(rects[3].height() - 70.0).abs() < 0.001,
"Spacer 2 height: {}",
rects[3].height()
);
assert_eq!(rects[4].height(), 20.0);
}
#[test]
fn test_hstack_in_vstack_respects_width() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = CompressibleView {
ideal: Size::new(50.0, 20.0),
floor: 0.0,
};
let mut long_text = CompressibleView {
ideal: Size::new(500.0, 20.0),
floor: 0.0,
};
let bounds = Rect::new(Point::zero(), Size::new(200.0, 30.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut long_text];
let rects = layout.place(bounds, &children);
for (i, rect) in rects.iter().enumerate() {
assert_rect_within_bounds(rect, &bounds, &format!("child {i}"));
}
let last_rect = rects.last().unwrap();
assert!(
last_rect.x() + last_rect.width() <= bounds.width() + 0.001,
"Content overflows bounds: {} > {}",
last_rect.x() + last_rect.width(),
bounds.width()
);
}
#[test]
fn test_hstack_size_respects_proposal() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = FixedSizeView {
size: Size::new(100.0, 30.0),
};
let mut child2 = FixedSizeView {
size: Size::new(100.0, 30.0),
};
let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
let size = layout.size_that_fits(ProposalSize::new(Some(150.0), None), &children);
assert_eq!(size.width, 150.0);
}
#[test]
fn test_vstack_size_respects_proposal() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = FixedSizeView {
size: Size::new(100.0, 50.0),
};
let mut child2 = FixedSizeView {
size: Size::new(80.0, 50.0),
};
let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
let size = layout.size_that_fits(ProposalSize::new(Some(90.0), None), &children);
assert_eq!(size.width, 90.0); assert_eq!(size.height, 110.0); }
#[test]
fn test_zero_bounds() {
let hstack = HStackLayout::default();
let vstack = VStackLayout::default();
let mut child = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::zero());
let children: Vec<&dyn SubView> = vec![&mut child];
let rects = hstack.place(bounds, &children);
assert!(rects[0].width() <= 0.001 || rects[0].width() <= 50.0);
let mut child = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let children: Vec<&dyn SubView> = vec![&mut child];
let rects = vstack.place(bounds, &children);
assert!(rects[0].height() <= 0.001 || rects[0].height() <= 30.0);
}
#[test]
fn test_negative_remaining_space_with_spacer() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = FixedSizeView {
size: Size::new(60.0, 20.0),
};
let mut spacer = SpacerView;
let mut child2 = FixedSizeView {
size: Size::new(60.0, 20.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 30.0));
let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
let rects = layout.place(bounds, &children);
assert!(
rects[1].width() >= 0.0,
"Spacer has negative width: {}",
rects[1].width()
);
assert!(
rects[1].height() >= 0.0,
"Spacer has negative height: {}",
rects[1].height()
);
}
#[test]
fn test_bounds_with_offset() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut child1 = FixedSizeView {
size: Size::new(30.0, 20.0),
};
let mut child2 = FixedSizeView {
size: Size::new(40.0, 25.0),
};
let bounds = Rect::new(Point::new(50.0, 100.0), Size::new(200.0, 50.0));
let children: Vec<&dyn SubView> = vec![&mut child1, &mut child2];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].x(), 50.0);
for (i, rect) in rects.iter().enumerate() {
assert_rect_within_bounds(rect, &bounds, &format!("child {i} with offset"));
}
}
#[test]
fn test_vstack_horizontal_expanding_child_height() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut text_field = HorizontalExpandingView { height: 40.0 }; let mut button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
let size = layout.size_that_fits(ProposalSize::new(None, Some(300.0)), &children);
assert_eq!(size.height, 124.0);
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut text_field = HorizontalExpandingView { height: 40.0 };
let mut button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
let bounds = Rect::new(Point::zero(), Size::new(200.0, 300.0));
let rects = layout.place(bounds, &children);
assert_eq!(
rects[1].height(),
40.0,
"TextField height should be intrinsic, not stretched"
);
}
#[test]
fn test_vstack_with_both_spacer_and_horizontal_expanding() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut spacer = SpacerView; let mut text_field = HorizontalExpandingView { height: 40.0 };
let children: Vec<&dyn SubView> = vec![&mut label, &mut spacer, &mut text_field];
let bounds = Rect::new(Point::zero(), Size::new(200.0, 200.0));
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].height(), 20.0);
assert_eq!(rects[2].height(), 40.0);
assert!(
(rects[1].height() - 120.0).abs() < 0.001,
"Spacer height: {}",
rects[1].height()
);
}
#[test]
fn test_hstack_vertical_expanding_child_width() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut vertical_component = VerticalExpandingView { width: 60.0 }; let mut button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut vertical_component, &mut button];
let size = layout.size_that_fits(ProposalSize::new(Some(400.0), None), &children);
assert_eq!(size.width, 230.0);
}
#[test]
fn test_vstack_intrinsic_width_excludes_horizontal_stretch() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(80.0, 20.0),
};
let mut text_field = HorizontalExpandingView { height: 40.0 }; let mut button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut text_field, &mut button];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.width, 100.0);
}
#[test]
fn test_hstack_intrinsic_height_excludes_vertical_stretch() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut vertical_component = VerticalExpandingView { width: 60.0 }; let mut button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![&mut label, &mut vertical_component, &mut button];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.height, 44.0);
}
#[test]
fn test_vstack_multiple_vertical_stretch_children() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let mut spacer1 = SpacerView; let mut child2 = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let mut spacer2 = SpacerView; let mut child3 = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let children: Vec<&dyn SubView> = vec![
&mut child1,
&mut spacer1,
&mut child2,
&mut spacer2,
&mut child3,
];
let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].height(), 30.0);
assert!(
(rects[1].height() - 55.0).abs() < 0.001,
"Spacer 1 height: {}",
rects[1].height()
);
assert_eq!(rects[2].height(), 30.0);
assert!(
(rects[3].height() - 55.0).abs() < 0.001,
"Spacer 2 height: {}",
rects[3].height()
);
assert_eq!(rects[4].height(), 30.0);
}
#[test]
fn test_hstack_multiple_horizontal_stretch_children() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = FixedSizeView {
size: Size::new(30.0, 50.0),
};
let mut spacer1 = SpacerView; let mut child2 = FixedSizeView {
size: Size::new(30.0, 50.0),
};
let mut spacer2 = SpacerView; let mut child3 = FixedSizeView {
size: Size::new(30.0, 50.0),
};
let children: Vec<&dyn SubView> = vec![
&mut child1,
&mut spacer1,
&mut child2,
&mut spacer2,
&mut child3,
];
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].width(), 30.0);
assert!(
(rects[1].width() - 55.0).abs() < 0.001,
"Spacer 1 width: {}",
rects[1].width()
);
assert_eq!(rects[2].width(), 30.0);
assert!(
(rects[3].width() - 55.0).abs() < 0.001,
"Spacer 2 width: {}",
rects[3].width()
);
assert_eq!(rects[4].width(), 30.0);
}
#[test]
fn test_vstack_form_layout() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(8.0),
};
let mut name_label = FixedSizeView {
size: Size::new(60.0, 20.0),
};
let mut name_field = HorizontalExpandingView { height: 34.0 };
let mut email_label = FixedSizeView {
size: Size::new(60.0, 20.0),
};
let mut email_field = HorizontalExpandingView { height: 34.0 };
let mut submit_button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![
&mut name_label,
&mut name_field,
&mut email_label,
&mut email_field,
&mut submit_button,
];
let bounds = Rect::new(Point::zero(), Size::new(300.0, 500.0));
let rects = layout.place(bounds, &children);
assert_eq!(rects[1].height(), 34.0, "Name field should be 34pt");
assert_eq!(rects[3].height(), 34.0, "Email field should be 34pt");
assert_eq!(rects[1].width(), 300.0, "Name field should expand to width");
assert_eq!(
rects[3].width(),
300.0,
"Email field should expand to width"
);
assert_no_overlap(&rects, "vertical");
}
#[test]
fn test_hstack_first_baseline_uses_text_baselines() {
let layout = HStackLayout {
alignment: VerticalAlignment::FirstBaseline,
spacing: Computed::constant(10.0),
};
let left = BaselineTextView {
size: Size::new(80.0, 20.0),
first_baseline: 14.0,
last_baseline: 14.0,
};
let right = BaselineTextView {
size: Size::new(40.0, 40.0),
first_baseline: 28.0,
last_baseline: 28.0,
};
let children: Vec<&dyn SubView> = vec![&left, &right];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.height, 40.0);
let rects = layout.place(Rect::from_size(size), &children);
let left_baseline = rects[0].y() + left.first_baseline;
let right_baseline = rects[1].y() + right.first_baseline;
assert!((left_baseline - right_baseline).abs() < f32::EPSILON);
}
#[test]
fn test_vstack_explicit_horizontal_alignment_uses_guides() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(8.0),
};
let top = GuidedBoxView {
size: Size::new(120.0, 20.0),
guide: 30.0,
};
let bottom = GuidedBoxView {
size: Size::new(80.0, 20.0),
guide: 10.0,
};
let children: Vec<&dyn SubView> = vec![&top, &bottom];
let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);
assert_eq!(size.width, 120.0);
let rects = layout.place(Rect::from_size(size), &children);
let top_guide = rects[0].x() + top.guide;
let bottom_guide = rects[1].x() + bottom.guide;
assert!((top_guide - bottom_guide).abs() < f32::EPSILON);
}
#[test]
fn test_vstack_form_with_spacer() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(8.0),
};
let mut name_label = FixedSizeView {
size: Size::new(60.0, 20.0),
};
let mut name_field = HorizontalExpandingView { height: 34.0 };
let mut spacer = SpacerView;
let mut submit_button = FixedSizeView {
size: Size::new(100.0, 44.0),
};
let children: Vec<&dyn SubView> = vec![
&mut name_label,
&mut name_field,
&mut spacer,
&mut submit_button,
];
let bounds = Rect::new(Point::zero(), Size::new(300.0, 300.0));
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].height(), 20.0);
assert_eq!(rects[1].height(), 34.0, "TextField keeps intrinsic height");
assert_eq!(
rects[3].y() + rects[3].height(),
300.0,
"Button should be at bottom"
);
}
struct MainAxisSpacerView;
impl SubView for MainAxisSpacerView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(Size::zero())
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::MainAxis
}
fn priority(&self) -> i32 {
0
}
}
struct CrossAxisDividerView {
thickness: f32,
}
impl SubView for CrossAxisDividerView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(Size::new(self.thickness, self.thickness))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::CrossAxis
}
fn priority(&self) -> i32 {
0
}
}
#[test]
fn test_vstack_main_axis_spacer() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = FixedSizeView {
size: Size::new(100.0, 30.0),
};
let mut spacer = MainAxisSpacerView;
let mut child2 = FixedSizeView {
size: Size::new(100.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::new(100.0, 200.0));
let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].height(), 30.0);
assert_eq!(
rects[1].height(),
140.0,
"MainAxis spacer should expand vertically in VStack"
);
assert_eq!(rects[2].height(), 30.0);
}
#[test]
fn test_hstack_main_axis_spacer() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut child1 = FixedSizeView {
size: Size::new(30.0, 100.0),
};
let mut spacer = MainAxisSpacerView;
let mut child2 = FixedSizeView {
size: Size::new(30.0, 100.0),
};
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut child1, &mut spacer, &mut child2];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].width(), 30.0);
assert_eq!(
rects[1].width(),
140.0,
"MainAxis spacer should expand horizontally in HStack"
);
assert_eq!(rects[2].width(), 30.0);
}
#[test]
fn test_vstack_cross_axis_divider() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut divider = CrossAxisDividerView { thickness: 1.0 };
let mut button = FixedSizeView {
size: Size::new(80.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut divider, &mut button];
let rects = layout.place(bounds, &children);
assert_eq!(
rects[1].width(),
200.0,
"CrossAxis divider should expand to full width in VStack"
);
assert_eq!(
rects[1].height(),
1.0,
"CrossAxis divider should keep intrinsic height in VStack"
);
}
#[test]
fn test_hstack_cross_axis_divider() {
let layout = HStackLayout {
alignment: VerticalAlignment::Top,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut divider = CrossAxisDividerView { thickness: 1.0 };
let mut button = FixedSizeView {
size: Size::new(80.0, 30.0),
};
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut divider, &mut button];
let rects = layout.place(bounds, &children);
assert_eq!(
rects[1].height(),
100.0,
"CrossAxis divider should expand to full height in HStack"
);
assert_eq!(
rects[1].width(),
1.0,
"CrossAxis divider should keep intrinsic width in HStack"
);
}
struct HorizontalStretchView {
min_width: f32,
height: f32,
}
impl SubView for HorizontalStretchView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let width = proposal
.width
.map_or(self.min_width, |w| w.max(self.min_width));
ViewDimensions::new(Size::new(width, self.height))
}
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Horizontal
}
fn priority(&self) -> i32 {
0
}
}
#[test]
fn test_vstack_horizontal_stretch_textfield() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut textfield = HorizontalStretchView {
min_width: 100.0,
height: 34.0,
};
let mut button = FixedSizeView {
size: Size::new(80.0, 40.0),
};
let bounds = Rect::new(Point::zero(), Size::new(300.0, 200.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut textfield, &mut button];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].width(), 50.0, "Label should keep intrinsic width");
assert_eq!(
rects[1].width(),
300.0,
"Horizontal stretch TextField should expand to full width in VStack"
);
assert_eq!(rects[2].width(), 80.0, "Button should keep intrinsic width");
}
#[test]
fn test_hstack_horizontal_stretch_textfield() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(10.0),
};
let mut label = FixedSizeView {
size: Size::new(50.0, 20.0),
};
let mut textfield = HorizontalStretchView {
min_width: 100.0,
height: 34.0,
};
let mut button = FixedSizeView {
size: Size::new(80.0, 40.0),
};
let bounds = Rect::new(Point::zero(), Size::new(300.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut label, &mut textfield, &mut button];
let rects = layout.place(bounds, &children);
assert_eq!(rects[0].width(), 50.0, "Label should keep intrinsic width");
assert_eq!(
rects[1].width(),
150.0,
"Horizontal stretch TextField should expand in HStack"
);
assert_eq!(rects[2].width(), 80.0, "Button should keep intrinsic width");
}
#[test]
fn test_hstack_preserves_horizontal_stretch_min_width_when_fixed_content_overflows() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(8.0),
};
let mut leading = FixedSizeView {
size: Size::new(120.0, 36.0),
};
let mut slider = HorizontalStretchView {
min_width: 72.0,
height: 34.0,
};
let mut trailing = FixedSizeView {
size: Size::new(120.0, 36.0),
};
let bounds = Rect::new(Point::zero(), Size::new(180.0, 48.0));
let children: Vec<&dyn SubView> = vec![&mut leading, &mut slider, &mut trailing];
let rects = layout.place(bounds, &children);
assert_eq!(
rects[1].width(),
72.0,
"Horizontal stretch controls such as Slider must keep their intrinsic minimum width"
);
}
struct NestedStackView {
size: Size,
stretch_axis: StretchAxis,
}
impl SubView for NestedStackView {
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_stack_cross_axis_stretch_is_symmetric() {
let vstack = VStackLayout {
alignment: HorizontalAlignment::Leading,
spacing: Computed::constant(0.0),
};
let hstack = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
assert_eq!(
vstack.stretch_axis(&[]),
StretchAxis::None,
"VStack is content-sized, like SwiftUI's"
);
assert_eq!(
hstack.stretch_axis(&[]),
StretchAxis::None,
"HStack is content-sized, mirroring VStack"
);
}
#[test]
fn test_hstack_keeps_nested_stack_content_sized_and_centered() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let mut nested = NestedStackView {
size: Size::new(40.0, 20.0),
stretch_axis: layout.stretch_axis(&[]),
};
let mut sibling = FixedSizeView {
size: Size::new(40.0, 20.0),
};
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let children: Vec<&dyn SubView> = vec![&mut nested, &mut sibling];
let rects = layout.place(bounds, &children);
assert_eq!(
rects[0].height(),
20.0,
"a nested HStack keeps its intrinsic height instead of filling the row"
);
assert_eq!(
rects[0].width(),
40.0,
"A nested HStack keeps its intrinsic main-axis width"
);
assert_eq!(
rects[0].y(),
40.0,
"an undersized nested stack is centered in the 100pt row"
);
assert_eq!(rects[1].height(), 20.0);
}
struct RealNestedStackView<L: Layout> {
layout: L,
children: Vec<SpacerView>,
}
impl<L: Layout> SubView for RealNestedStackView<L> {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let refs: Vec<&dyn SubView> = self.children.iter().map(|c| c as &dyn SubView).collect();
ViewDimensions::new(self.layout.size_that_fits(proposal, &refs))
}
fn stretch_axis(&self) -> StretchAxis {
let axes: Vec<StretchAxis> = self.children.iter().map(SubView::stretch_axis).collect();
self.layout.stretch_axis(&axes)
}
fn priority(&self) -> i32 {
0
}
}
fn greedy_row() -> RealNestedStackView<HStackLayout> {
RealNestedStackView {
layout: HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
},
children: vec![SpacerView, SpacerView],
}
}
#[test]
fn a_row_of_fillers_asks_for_space_on_their_behalf() {
assert_eq!(greedy_row().stretch_axis(), StretchAxis::Both);
}
#[test]
fn a_row_of_labels_stays_content_sized() {
let row: RealNestedStackView<HStackLayout> = RealNestedStackView {
layout: HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
},
children: Vec::new(),
};
assert_eq!(row.stretch_axis(), StretchAxis::None);
}
#[test]
fn rows_of_fillers_share_the_column_they_sit_in() {
let column = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let top = greedy_row();
let bottom = greedy_row();
let children: Vec<&dyn SubView> = vec![&top, &bottom];
let bounds = Rect::new(Point::zero(), Size::new(200.0, 100.0));
let rects = column.place(bounds, &children);
assert_eq!(rects.len(), 2);
for rect in &rects {
assert!(
(rect.height() - 50.0).abs() < f32::EPSILON,
"each row takes half the column, got {}",
rect.height()
);
assert!(
(rect.width() - 200.0).abs() < f32::EPSILON,
"and the full width, got {}",
rect.width()
);
}
}
#[test]
fn one_element_tuple_is_a_single_child_payload() {
use waterui_core::{View, view::TupleViews};
use crate::spacer::spacer;
use crate::stack::{hstack, vstack, zstack};
fn accepts_view(_view: impl View) {}
assert_eq!((spacer(),).into_views().len(), 1);
accepts_view(vstack((spacer(),)));
accepts_view(hstack((spacer(),)));
accepts_view(zstack((spacer(),)));
}