use teksilo_canvas::{Size, SizeProposal};
use teksilo_core::widget::{LayoutContext, Widget};
use teksilo_core::widget_tree::WidgetTree;
use teksilo_i18n::lit;
use teksilo_tokens::{Alignment, HAlignment};
use crate::primitives::{
Center, Expand, FixedSize, HStack, MinSize, Padding, Spacer, TextWidget, VStack, ZStack,
};
#[derive(Debug)]
struct FixedLeaf(f32, f32);
impl Widget for FixedLeaf {
fn layout_response(
&self,
_proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
Size::new(self.0, self.1).into()
}
}
#[test]
fn vstack_with_mixed_sizes_and_center_alignment() {
let mut tree = WidgetTree::new();
let a = tree.add(FixedLeaf(80.0, 30.0));
let b = tree.add(FixedLeaf(120.0, 40.0));
let c = tree.add(FixedLeaf(60.0, 20.0));
let _stack = tree.add(
VStack::new()
.alignment(HAlignment::Center)
.spacing(5.0)
.add_child(a)
.add_child(b)
.add_child(c),
);
tree.layout(SizeProposal::exact(200.0, 300.0));
assert!((tree.bounds(a).x - 60.0).abs() < 0.01);
assert!((tree.bounds(a).y - 0.0).abs() < 0.01);
assert!((tree.bounds(b).x - 40.0).abs() < 0.01);
assert!((tree.bounds(b).y - 35.0).abs() < 0.01);
assert!((tree.bounds(c).x - 70.0).abs() < 0.01);
assert!((tree.bounds(c).y - 80.0).abs() < 0.01);
}
#[test]
fn hstack_toolbar_pattern_with_spacer() {
let mut tree = WidgetTree::new();
let back = tree.add(FixedLeaf(60.0, 30.0));
let spacer = tree.add(Spacer::new());
let edit = tree.add(FixedLeaf(50.0, 30.0));
let save = tree.add(FixedLeaf(50.0, 30.0));
let _toolbar = tree.add(
HStack::new()
.spacing(8.0)
.add_child(back)
.add_child(spacer)
.add_child(edit)
.add_child(save),
);
tree.layout(SizeProposal::exact(400.0, 40.0));
assert!((tree.bounds(back).x - 0.0).abs() < 0.01);
assert!((tree.bounds(edit).x - 292.0).abs() < 0.01);
assert!((tree.bounds(save).x - 350.0).abs() < 0.01);
}
#[test]
fn zstack_per_child_alignment_overrides() {
let mut tree = WidgetTree::new();
let bg = tree.add(FixedLeaf(200.0, 100.0));
let title = tree.add(FixedLeaf(80.0, 20.0));
let button = tree.add(FixedLeaf(40.0, 30.0));
let _stack = tree.add(
ZStack::new()
.alignment(Alignment::CENTER)
.add_child(bg)
.add_child(title)
.add_child(button),
);
tree.set_alignment(button, Alignment::BOTTOM_TRAILING);
tree.layout(SizeProposal::exact(300.0, 200.0));
let tb = tree.bounds(title);
assert!((tb.x - 110.0).abs() < 0.01); assert!((tb.y - 90.0).abs() < 0.01);
let bb = tree.bounds(button);
assert!((bb.x - 260.0).abs() < 0.01); assert!((bb.y - 170.0).abs() < 0.01); }
#[test]
fn nested_hstack_in_vstack() {
let mut tree = WidgetTree::new();
let a = tree.add(FixedLeaf(60.0, 25.0));
let b = tree.add(FixedLeaf(40.0, 25.0));
let row1 = tree.add(HStack::new().spacing(5.0).add_child(a).add_child(b));
let c = tree.add(FixedLeaf(80.0, 30.0));
let _col = tree.add(
VStack::new()
.alignment(HAlignment::Center)
.spacing(10.0)
.add_child(row1)
.add_child(c),
);
tree.layout(SizeProposal::exact(200.0, 200.0));
assert!((tree.bounds(row1).x - 47.5).abs() < 0.01);
assert!((tree.bounds(c).x - 60.0).abs() < 0.01);
assert!((tree.bounds(c).y - 35.0).abs() < 0.01);
}
#[test]
fn min_size_wrapping_small_widget() {
let mut tree = WidgetTree::new();
let small = tree.add(FixedLeaf(20.0, 10.0));
let min = tree.add(MinSize::new(48.0, 48.0).child_id(small));
let _stack = tree.add(HStack::new().add_child(min));
tree.layout(SizeProposal::exact(200.0, 60.0));
let mb = tree.bounds(min);
assert!((mb.width - 48.0).abs() < 0.01);
assert!((mb.height - 48.0).abs() < 0.01);
}
#[test]
fn expand_horizontal_in_hstack() {
let mut tree = WidgetTree::new();
let fixed = tree.add(FixedLeaf(60.0, 30.0));
let inner = tree.add(FixedLeaf(40.0, 20.0));
let expanded = tree.add(Expand::horizontal().child_id(inner));
let _stack = tree.add(HStack::new().add_child(fixed).add_child(expanded));
tree.layout(SizeProposal::exact(300.0, 50.0));
assert!((tree.bounds(fixed).width - 60.0).abs() < 0.01);
let eb = tree.bounds(expanded);
assert!(
(eb.width - 240.0).abs() < 0.01,
"Expand should claim leftover slack, got width={}",
eb.width
);
}
#[test]
fn center_widget() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(40.0, 20.0));
let _center = tree.add(Center::new().child_id(child));
tree.layout(SizeProposal::exact(200.0, 100.0));
let cb = tree.bounds(child);
assert!((cb.x - 80.0).abs() < 0.01);
assert!((cb.y - 40.0).abs() < 0.01);
}
#[test]
fn fixed_size_in_hstack_resists_stretching() {
let mut tree = WidgetTree::new();
let a = tree.add(FixedLeaf(40.0, 20.0));
let fixed = tree.add(FixedSize::new().child_id(a));
let b = tree.add(FixedLeaf(60.0, 30.0));
let _stack = tree.add(HStack::new().spacing(5.0).add_child(fixed).add_child(b));
tree.layout(SizeProposal::exact(300.0, 50.0));
assert!((tree.bounds(fixed).width - 40.0).abs() < 0.01);
assert!((tree.bounds(fixed).height - 20.0).abs() < 0.01);
assert!((tree.bounds(b).x - 45.0).abs() < 0.01);
}
#[test]
fn demo_layout_no_overlap_between_sections() {
let mut tree = WidgetTree::new();
let title = tree.add(TextWidget::new(lit!("Title text here"))); let toolbar_spacer = tree.add(Spacer::new());
let btn = tree.add(FixedLeaf(140.0, 36.0)); let toolbar = tree.add(
HStack::new()
.add_child(title)
.add_child(toolbar_spacer)
.add_child(btn),
);
let typo_heading = tree.add(TextWidget::new(lit!("Typography"))); let typo_body1 = tree.add(TextWidget::new(lit!("Body line one"))); let typo_body2 = tree.add(TextWidget::new(lit!("Body line two"))); let typography = tree.add(
VStack::new()
.spacing(6.0)
.add_child(typo_heading)
.add_child(typo_body1)
.add_child(typo_body2),
);
let section_heading = tree.add(TextWidget::new(lit!("Layout Primitives"))); let box_a = tree.add(FixedLeaf(40.0, 32.0));
let box_b = tree.add(FixedLeaf(40.0, 32.0));
let box_c = tree.add(FixedLeaf(40.0, 32.0));
let color_row = tree.add(
HStack::new()
.spacing(8.0)
.add_child(box_a)
.add_child(box_b)
.add_child(box_c),
);
let caption1 = tree.add(TextWidget::new(lit!("Three colored boxes"))); let leading = tree.add(TextWidget::new(lit!("Leading"))); let inner_spacer = tree.add(Spacer::new());
let trailing = tree.add(TextWidget::new(lit!("Trailing"))); let spacer_row = tree.add(
HStack::new()
.add_child(leading)
.add_child(inner_spacer)
.add_child(trailing),
);
let caption2 = tree.add(TextWidget::new(lit!("Spacer pushing items to edges")));
let showcase = tree.add(
VStack::new()
.spacing(6.0)
.add_child(section_heading)
.add_child(color_row)
.add_child(caption1)
.add_child(spacer_row)
.add_child(caption2),
);
let outer = tree.add(
VStack::new()
.spacing(20.0)
.add_child(toolbar)
.add_child(typography)
.add_child(showcase),
);
let _root = tree.add(Padding::uniform(24.0).child_id(outer));
tree.layout(SizeProposal::exact(600.0, 500.0));
let toolbar_b = tree.bounds(toolbar);
let typography_b = tree.bounds(typography);
let showcase_b = tree.bounds(showcase);
eprintln!(
"toolbar: y={:.1}, h={:.1}, bottom={:.1}",
toolbar_b.y,
toolbar_b.height,
toolbar_b.y + toolbar_b.height
);
eprintln!(
"typography: y={:.1}, h={:.1}, bottom={:.1}",
typography_b.y,
typography_b.height,
typography_b.y + typography_b.height
);
eprintln!(
"showcase: y={:.1}, h={:.1}, bottom={:.1}",
showcase_b.y,
showcase_b.height,
showcase_b.y + showcase_b.height
);
assert!(
(toolbar_b.y - 24.0).abs() < 0.01,
"toolbar.y = {}",
toolbar_b.y
);
let expected_typo_y = toolbar_b.y + toolbar_b.height + 20.0;
assert!(
(typography_b.y - expected_typo_y).abs() < 0.01,
"typography.y = {} (expected {})",
typography_b.y,
expected_typo_y,
);
let expected_showcase_y = typography_b.y + typography_b.height + 20.0;
assert!(
(showcase_b.y - expected_showcase_y).abs() < 0.01,
"showcase.y = {} (expected {})",
showcase_b.y,
expected_showcase_y,
);
let title_b = tree.bounds(title);
let caption2_b = tree.bounds(caption2);
eprintln!("title: bounds={:?}", title_b);
eprintln!("caption2: bounds={:?}", caption2_b);
assert!(
caption2_b.y > title_b.y + title_b.height,
"caption2 (y={}) should be well below title (bottom={})",
caption2_b.y,
title_b.y + title_b.height,
);
let spacer_row_b = tree.bounds(spacer_row);
let leading_b = tree.bounds(leading);
let trailing_b = tree.bounds(trailing);
eprintln!("spacer_row: bounds={:?}", spacer_row_b);
eprintln!("leading: bounds={:?}", leading_b);
eprintln!("trailing: bounds={:?}", trailing_b);
assert!(
(spacer_row_b.width - showcase_b.width).abs() < 0.01,
"spacer_row width={} should match showcase width={}",
spacer_row_b.width,
showcase_b.width,
);
let expected_trailing_x = spacer_row_b.x + spacer_row_b.width - trailing_b.width;
assert!(
(trailing_b.x - expected_trailing_x).abs() < 0.01,
"trailing.x={} should be at right edge (expected {})",
trailing_b.x,
expected_trailing_x,
);
assert!(
(leading_b.x - spacer_row_b.x).abs() < 0.01,
"leading.x={} should be at left edge (expected {})",
leading_b.x,
spacer_row_b.x,
);
let all_texts = [
("title", tree.bounds(title)),
("typo_heading", tree.bounds(typo_heading)),
("typo_body1", tree.bounds(typo_body1)),
("typo_body2", tree.bounds(typo_body2)),
("section_heading", tree.bounds(section_heading)),
("caption1", tree.bounds(caption1)),
("caption2", tree.bounds(caption2)),
];
for i in 0..all_texts.len() {
for j in (i + 1)..all_texts.len() {
let (name_a, a) = all_texts[i];
let (name_b, b) = all_texts[j];
let overlap = a.y < b.y + b.height && b.y < a.y + a.height;
assert!(
!overlap,
"{} (y={:.1}..{:.1}) overlaps {} (y={:.1}..{:.1})",
name_a,
a.y,
a.y + a.height,
name_b,
b.y,
b.y + b.height,
);
}
}
}