mod contract;
use alloc::{format, vec, vec::Vec};
use nami::Computed;
use crate::spacer::Spacer;
use crate::stack::{HStackLayout, HorizontalAlignment, VStackLayout, VerticalAlignment};
use crate::{
Layout, Point, ProposalSize, Rect, Size, StretchAxis, SubView, SubviewPlacement, 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
}
}
pub struct CompressibleHeightView {
pub(crate) ideal: Size,
pub(crate) 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
}
}
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 {
Spacer::DEFAULT_LAYOUT_PRIORITY
}
}
struct MinLengthSpacerView {
min_length: f32,
axis: StretchAxis,
}
impl SubView for MinLengthSpacerView {
fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
ViewDimensions::new(match self.axis {
StretchAxis::Horizontal => Size::new(self.min_length, 0.0),
StretchAxis::Vertical => Size::new(0.0, self.min_length),
_ => unreachable!("MinLengthSpacerView models one physical stack axis"),
})
}
fn stretch_axis(&self) -> StretchAxis {
self.axis
}
fn priority(&self) -> i32 {
Spacer::DEFAULT_LAYOUT_PRIORITY
}
}
struct ContentFillView {
intrinsic: f32,
axis: StretchAxis,
priority: i32,
}
impl SubView for ContentFillView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let fill =
|proposed: Option<f32>| proposed.map_or(self.intrinsic, |p| p.max(self.intrinsic));
let size = match self.axis {
StretchAxis::Horizontal => Size::new(fill(proposal.width), self.intrinsic),
StretchAxis::Vertical => Size::new(self.intrinsic, fill(proposal.height)),
_ => unreachable!("ContentFillView models one physical fill axis"),
};
ViewDimensions::new(size)
}
fn stretch_axis(&self) -> StretchAxis {
self.axis
}
fn priority(&self) -> i32 {
self.priority
}
}
struct HorizontalExpandingView {
height: f32,
}
impl SubView for HorizontalExpandingView {
fn measure(&self, proposal: ProposalSize) -> ViewDimensions {
let width = proposal.width.unwrap_or(0.0);
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(0.0);
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(placements: &[SubviewPlacement], axis: &str) {
for i in 0..placements.len() {
for j in (i + 1)..placements.len() {
let r1 = &placements[i].frame;
let r2 = &placements[j].frame;
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");
}
}
}
fn bounded_proposal(bounds: Rect) -> ProposalSize {
ProposalSize::new(Some(bounds.width()), Some(bounds.height()))
}
#[test]
fn test_hstack_children_exceed_bounds_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
for (i, placement) in placements.iter().enumerate() {
assert_rect_within_bounds(&placement.frame, &bounds, &format!("child {i}"));
}
assert!(
placements[1].frame.width() <= 0.001,
"Spacer should be zero when children exceed bounds, got {}",
placements[1].frame.width()
);
}
#[test]
fn test_hstack_single_child_exceeds_bounds_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements.len(), 1);
assert!(
(placements[0].frame.width() - 200.0).abs() < 0.001,
"the child reports 200pt at every proposal, so it must keep 200pt, got {}",
placements[0].frame.width()
);
}
#[test]
fn test_hstack_multiple_children_total_exceeds_bounds_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
for (index, placement) in placements.iter().enumerate() {
assert!(
(placement.frame.width() - 50.0).abs() < 0.001,
"child {index} reports 50pt at every proposal, so it must keep 50pt, got {}",
placement.frame.width()
);
}
let overflow = placements
.last()
.expect("three children were placed")
.frame
.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(&placements, "horizontal");
}
#[test]
fn test_hstack_with_flexible_text_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
let total_children_width: f32 = placements.iter().map(|p| p.frame.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, placement) in placements.iter().enumerate() {
assert_rect_within_bounds(&placement.frame, &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 placements = layout.place(
Rect::new(Point::zero(), Size::new(intrinsic.width, 40.0)),
ProposalSize::UNSPECIFIED,
&children,
);
assert_eq!(
placements[0].frame.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 placements = layout.place(bounds, ProposalSize::UNSPECIFIED, &children);
assert!(placements.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_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.width(), 20.0);
assert!(
(placements[1].frame.width() - 70.0).abs() < 0.001,
"Spacer 1 width: {}",
placements[1].frame.width()
);
assert_eq!(placements[2].frame.width(), 20.0);
assert!(
(placements[3].frame.width() - 70.0).abs() < 0.001,
"Spacer 2 width: {}",
placements[3].frame.width()
);
assert_eq!(placements[4].frame.width(), 20.0);
}
#[test]
fn test_hstack_alignment_top_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.y(), 10.0);
assert_eq!(placements[1].frame.y(), 10.0);
}
#[test]
fn test_hstack_alignment_bottom_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.y() + placements[0].frame.height(), 70.0); assert_eq!(placements[1].frame.y() + placements[1].frame.height(), 70.0);
}
#[test]
fn test_hstack_alignment_center_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.y(), 20.0);
assert_eq!(placements[1].frame.y(), 5.0);
}
#[test]
fn test_vstack_children_exceed_bounds_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_no_overlap(&placements, "vertical");
}
#[test]
fn test_vstack_child_width_exceeds_bounds_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements.len(), 1);
assert_eq!(
placements[0].frame,
Rect::new(Point::new(-50.0, 0.0), Size::new(200.0, 30.0))
);
}
#[test]
fn test_vstack_text_wrapping_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
placements[0].frame.width() <= bounds.width() + 0.001,
"Text width {} exceeds bounds width {}",
placements[0].frame.width(),
bounds.width()
);
assert!(
placements[0].frame.height() >= 40.0 - 0.001,
"Text should wrap to multiple lines, height: {}",
placements[0].frame.height()
);
}
#[test]
fn test_vstack_with_axis_expanding_child_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.width(), 50.0);
assert!(
placements[1].frame.width() <= bounds.width() + 0.001,
"TextField width {} should not exceed bounds {}",
placements[1].frame.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_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.x(), 10.0);
assert_eq!(placements[1].frame.x(), 10.0);
}
#[test]
fn test_vstack_alignment_trailing_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.x() + placements[0].frame.width(), 110.0); assert_eq!(placements[1].frame.x() + placements[1].frame.width(), 110.0);
}
#[test]
fn test_vstack_alignment_center_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.x(), 35.0);
assert_eq!(placements[1].frame.x(), 10.0);
}
#[test]
fn test_vstack_multiple_spacers_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.height(), 20.0);
assert!(
(placements[1].frame.height() - 70.0).abs() < 0.001,
"Spacer 1 height: {}",
placements[1].frame.height()
);
assert_eq!(placements[2].frame.height(), 20.0);
assert!(
(placements[3].frame.height() - 70.0).abs() < 0.001,
"Spacer 2 height: {}",
placements[3].frame.height()
);
assert_eq!(placements[4].frame.height(), 20.0);
}
#[test]
fn test_hstack_in_vstack_respects_width_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
for (i, placement) in placements.iter().enumerate() {
assert_rect_within_bounds(&placement.frame, &bounds, &format!("child {i}"));
}
let last_placement = placements.last().unwrap();
assert!(
last_placement.frame.x() + last_placement.frame.width() <= bounds.width() + 0.001,
"Content overflows bounds: {} > {}",
last_placement.frame.x() + last_placement.frame.width(),
bounds.width()
);
}
#[test]
fn test_hstack_reports_rigid_minimum_under_smaller_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, Size::new(210.0, 30.0));
}
#[test]
fn test_vstack_reports_rigid_cross_axis_minimum() {
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, 100.0); assert_eq!(size.height, 110.0); }
#[test]
fn test_zero_bounds_bounded_proposal() {
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 placements = hstack.place(bounds, bounded_proposal(bounds), &children);
assert!(placements[0].frame.width() <= 0.001 || placements[0].frame.width() <= 50.0);
let mut child = FixedSizeView {
size: Size::new(50.0, 30.0),
};
let children: Vec<&dyn SubView> = vec![&mut child];
let placements = vstack.place(bounds, bounded_proposal(bounds), &children);
assert!(placements[0].frame.height() <= 0.001 || placements[0].frame.height() <= 30.0);
}
#[test]
fn test_negative_remaining_space_with_spacer_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
placements[1].frame.width() >= 0.0,
"Spacer has negative width: {}",
placements[1].frame.width()
);
assert!(
placements[1].frame.height() >= 0.0,
"Spacer has negative height: {}",
placements[1].frame.height()
);
}
#[test]
fn test_bounds_with_offset_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.x(), 50.0);
for (i, placement) in placements.iter().enumerate() {
assert_rect_within_bounds(&placement.frame, &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 proposal = ProposalSize::new(None, Some(300.0));
let size = layout.size_that_fits(proposal, &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 placements = layout.place(bounds, proposal, &children);
assert_eq!(
placements[1].frame.height(),
40.0,
"TextField height should be intrinsic, not stretched"
);
}
#[test]
fn test_vstack_with_both_spacer_and_horizontal_expanding_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.height(), 20.0);
assert_eq!(placements[2].frame.height(), 40.0);
assert!(
(placements[1].frame.height() - 120.0).abs() < 0.001,
"Spacer height: {}",
placements[1].frame.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_includes_zero_ideal_fill() {
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_includes_zero_ideal_fill() {
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_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.height(), 30.0);
assert!(
(placements[1].frame.height() - 55.0).abs() < 0.001,
"Spacer 1 height: {}",
placements[1].frame.height()
);
assert_eq!(placements[2].frame.height(), 30.0);
assert!(
(placements[3].frame.height() - 55.0).abs() < 0.001,
"Spacer 2 height: {}",
placements[3].frame.height()
);
assert_eq!(placements[4].frame.height(), 30.0);
}
#[test]
fn test_hstack_multiple_horizontal_stretch_children_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.width(), 30.0);
assert!(
(placements[1].frame.width() - 55.0).abs() < 0.001,
"Spacer 1 width: {}",
placements[1].frame.width()
);
assert_eq!(placements[2].frame.width(), 30.0);
assert!(
(placements[3].frame.width() - 55.0).abs() < 0.001,
"Spacer 2 width: {}",
placements[3].frame.width()
);
assert_eq!(placements[4].frame.width(), 30.0);
}
#[test]
fn test_vstack_form_layout_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[1].frame.height(),
34.0,
"Name field should be 34pt"
);
assert_eq!(
placements[3].frame.height(),
34.0,
"Email field should be 34pt"
);
assert_eq!(
placements[1].frame.width(),
300.0,
"Name field should expand to width"
);
assert_eq!(
placements[3].frame.width(),
300.0,
"Email field should expand to width"
);
assert_no_overlap(&placements, "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 placements = layout.place(Rect::from_size(size), ProposalSize::UNSPECIFIED, &children);
let left_baseline = placements[0].frame.y() + left.first_baseline;
let right_baseline = placements[1].frame.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 placements = layout.place(Rect::from_size(size), ProposalSize::UNSPECIFIED, &children);
let top_guide = placements[0].frame.x() + top.guide;
let bottom_guide = placements[1].frame.x() + bottom.guide;
assert!((top_guide - bottom_guide).abs() < f32::EPSILON);
}
#[test]
fn test_vstack_form_with_spacer_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.height(), 20.0);
assert_eq!(
placements[1].frame.height(),
34.0,
"TextField keeps intrinsic height"
);
assert_eq!(
placements[3].frame.y() + placements[3].frame.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 {
Spacer::DEFAULT_LAYOUT_PRIORITY
}
}
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_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.height(), 30.0);
assert_eq!(
placements[1].frame.height(),
140.0,
"MainAxis spacer should expand vertically in VStack"
);
assert_eq!(placements[2].frame.height(), 30.0);
}
#[test]
fn test_hstack_main_axis_spacer_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements[0].frame.width(), 30.0);
assert_eq!(
placements[1].frame.width(),
140.0,
"MainAxis spacer should expand horizontally in HStack"
);
assert_eq!(placements[2].frame.width(), 30.0);
}
#[test]
fn test_vstack_cross_axis_divider_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[1].frame.width(),
200.0,
"CrossAxis divider should expand to full width in VStack"
);
assert_eq!(
placements[1].frame.height(),
1.0,
"CrossAxis divider should keep intrinsic height in VStack"
);
}
#[test]
fn test_hstack_cross_axis_divider_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[1].frame.height(),
100.0,
"CrossAxis divider should expand to full height in HStack"
);
assert_eq!(
placements[1].frame.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_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[0].frame.width(),
50.0,
"Label should keep intrinsic width"
);
assert_eq!(
placements[1].frame.width(),
300.0,
"Horizontal stretch TextField should expand to full width in VStack"
);
assert_eq!(
placements[2].frame.width(),
80.0,
"Button should keep intrinsic width"
);
}
#[test]
fn test_hstack_horizontal_stretch_textfield_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[0].frame.width(),
50.0,
"Label should keep intrinsic width"
);
assert_eq!(
placements[1].frame.width(),
150.0,
"Horizontal stretch TextField should expand in HStack"
);
assert_eq!(
placements[2].frame.width(),
80.0,
"Button should keep intrinsic width"
);
}
#[test]
fn test_hstack_preserves_horizontal_stretch_min_width_when_fixed_content_overflows_bounded_proposal()
{
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[1].frame.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_bounded_proposal() {
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 placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(
placements[0].frame.height(),
20.0,
"a nested HStack keeps its intrinsic height instead of filling the row"
);
assert_eq!(
placements[0].frame.width(),
40.0,
"A nested HStack keeps its intrinsic main-axis width"
);
assert_eq!(
placements[0].frame.y(),
40.0,
"an undersized nested stack is centered in the 100pt row"
);
assert_eq!(placements[1].frame.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_bounded_proposal() {
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 placements = column.place(bounds, bounded_proposal(bounds), &children);
assert_eq!(placements.len(), 2);
for placement in &placements {
assert!(
(placement.frame.height() - 50.0).abs() < f32::EPSILON,
"each row takes half the column, got {}",
placement.frame.height()
);
assert!(
(placement.frame.width() - 200.0).abs() < f32::EPSILON,
"and the full width, got {}",
placement.frame.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(),)));
}
#[test]
fn a_spacer_keeps_its_minimum_length_when_the_column_is_short_bounded_proposal() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let top = CompressibleHeightView {
ideal: Size::new(80.0, 30.0),
floor: 0.0,
};
let spacer = MinLengthSpacerView {
min_length: 40.0,
axis: StretchAxis::Vertical,
};
let bottom = CompressibleHeightView {
ideal: Size::new(80.0, 30.0),
floor: 0.0,
};
let children: Vec<&dyn SubView> = vec![&top, &spacer, &bottom];
let bounds = Rect::new(Point::zero(), Size::new(80.0, 50.0));
let placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
(placements[1].frame.height() - 40.0).abs() < 0.01,
"the spacer keeps its 40pt minimum, got {}",
placements[1].frame.height()
);
assert!(
(placements[0].frame.height() - 5.0).abs() < 0.01
&& (placements[2].frame.height() - 5.0).abs() < 0.01,
"the rows split the remaining 10pt, got {} and {}",
placements[0].frame.height(),
placements[2].frame.height()
);
assert!(
(placements[2].frame.y() - 45.0).abs() < 0.01,
"the bottom row sits below the spacer, got y {}",
placements[2].frame.y()
);
for (i, placement) in placements.iter().enumerate() {
assert_rect_within_bounds(&placement.frame, &bounds, &format!("child {i}"));
}
assert_no_overlap(&placements, "vertical");
}
#[test]
fn a_spacer_keeps_its_minimum_length_when_the_row_is_short_bounded_proposal() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let leading = CompressibleView {
ideal: Size::new(30.0, 20.0),
floor: 0.0,
};
let spacer = MinLengthSpacerView {
min_length: 40.0,
axis: StretchAxis::Horizontal,
};
let trailing = CompressibleView {
ideal: Size::new(30.0, 20.0),
floor: 0.0,
};
let children: Vec<&dyn SubView> = vec![&leading, &spacer, &trailing];
let bounds = Rect::new(Point::zero(), Size::new(50.0, 20.0));
let placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
(placements[1].frame.width() - 40.0).abs() < 0.01,
"the spacer keeps its 40pt minimum, got {}",
placements[1].frame.width()
);
assert!(
(placements[0].frame.width() - 5.0).abs() < 0.01
&& (placements[2].frame.width() - 5.0).abs() < 0.01,
"the labels split the remaining 10pt, got {} and {}",
placements[0].frame.width(),
placements[2].frame.width()
);
assert!(
(placements[2].frame.x() - 45.0).abs() < 0.01,
"the trailing label sits after the spacer, got x {}",
placements[2].frame.x()
);
for (i, placement) in placements.iter().enumerate() {
assert_rect_within_bounds(&placement.frame, &bounds, &format!("child {i}"));
}
assert_no_overlap(&placements, "horizontal");
}
#[test]
fn a_zero_minimum_spacer_still_collapses_before_the_column_content_bounded_proposal() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let top = CompressibleHeightView {
ideal: Size::new(80.0, 30.0),
floor: 10.0,
};
let spacer = MinLengthSpacerView {
min_length: 0.0,
axis: StretchAxis::Vertical,
};
let bottom = CompressibleHeightView {
ideal: Size::new(80.0, 30.0),
floor: 10.0,
};
let children: Vec<&dyn SubView> = vec![&top, &spacer, &bottom];
let bounds = Rect::new(Point::zero(), Size::new(80.0, 50.0));
let placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
placements[1].frame.height().abs() < 0.01,
"a zero-minimum spacer collapses first, got {}",
placements[1].frame.height()
);
assert!(
(placements[0].frame.height() - 25.0).abs() < 0.01
&& (placements[2].frame.height() - 25.0).abs() < 0.01,
"the rows share the whole column once the spacer is gone, got {} and {}",
placements[0].frame.height(),
placements[2].frame.height()
);
assert_no_overlap(&placements, "vertical");
}
#[test]
fn a_zero_minimum_spacer_still_collapses_before_the_row_content_bounded_proposal() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let leading = CompressibleView {
ideal: Size::new(30.0, 20.0),
floor: 10.0,
};
let spacer = MinLengthSpacerView {
min_length: 0.0,
axis: StretchAxis::Horizontal,
};
let trailing = CompressibleView {
ideal: Size::new(30.0, 20.0),
floor: 10.0,
};
let children: Vec<&dyn SubView> = vec![&leading, &spacer, &trailing];
let bounds = Rect::new(Point::zero(), Size::new(50.0, 20.0));
let placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
placements[1].frame.width().abs() < 0.01,
"a zero-minimum spacer collapses first, got {}",
placements[1].frame.width()
);
assert!(
(placements[0].frame.width() - 25.0).abs() < 0.01
&& (placements[2].frame.width() - 25.0).abs() < 0.01,
"the labels share the whole row once the spacer is gone, got {} and {}",
placements[0].frame.width(),
placements[2].frame.width()
);
assert_no_overlap(&placements, "horizontal");
}
#[test]
fn a_stretcher_above_the_deficit_share_keeps_its_floor_in_a_column_bounded_proposal() {
let layout = VStackLayout {
alignment: HorizontalAlignment::Center,
spacing: Computed::constant(0.0),
};
let list = ContentFillView {
intrinsic: 70.0,
axis: StretchAxis::Vertical,
priority: 0,
};
let row = CompressibleHeightView {
ideal: Size::new(70.0, 50.0),
floor: 0.0,
};
let footer = ContentFillView {
intrinsic: 0.0,
axis: StretchAxis::Vertical,
priority: -1,
};
let children: Vec<&dyn SubView> = vec![&list, &row, &footer];
let bounds = Rect::new(Point::zero(), Size::new(70.0, 100.0));
let placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
(placements[0].frame.height() - 70.0).abs() < 0.01,
"the list keeps the 70pt it measured, got {}",
placements[0].frame.height()
);
assert!(
(placements[1].frame.height() - 30.0).abs() < 0.01,
"the row at the same priority absorbs the deficit, got {}",
placements[1].frame.height()
);
assert!(
placements[2].frame.height().abs() < 0.01,
"the lower-priority filler gives way first, got {}",
placements[2].frame.height()
);
assert_no_overlap(&placements, "vertical");
}
#[test]
fn a_stretcher_above_the_deficit_share_keeps_its_floor_in_a_row_bounded_proposal() {
let layout = HStackLayout {
alignment: VerticalAlignment::Center,
spacing: Computed::constant(0.0),
};
let fill = ContentFillView {
intrinsic: 70.0,
axis: StretchAxis::Horizontal,
priority: 0,
};
let label = CompressibleView {
ideal: Size::new(50.0, 20.0),
floor: 0.0,
};
let trailing = ContentFillView {
intrinsic: 0.0,
axis: StretchAxis::Horizontal,
priority: -1,
};
let children: Vec<&dyn SubView> = vec![&fill, &label, &trailing];
let bounds = Rect::new(Point::zero(), Size::new(100.0, 70.0));
let placements = layout.place(bounds, bounded_proposal(bounds), &children);
assert!(
(placements[0].frame.width() - 70.0).abs() < 0.01,
"the fill keeps the 70pt it measured, got {}",
placements[0].frame.width()
);
assert!(
(placements[1].frame.width() - 30.0).abs() < 0.01,
"the label at the same priority absorbs the deficit, got {}",
placements[1].frame.width()
);
assert!(
placements[2].frame.width().abs() < 0.01,
"the lower-priority filler gives way first, got {}",
placements[2].frame.width()
);
assert_no_overlap(&placements, "horizontal");
}