use super::{IntrinsicSizeMode, LayoutEngine};
use crate::Interactions;
use crate::{Box as RBox, Column, Row, Text, TextStyle, ViewExt, row_scope};
use repose_core::*;
use std::collections::HashMap;
#[test]
fn test_render_z_index_paints_last() {
let red = Color::from_rgb(255, 0, 0);
let blue = Color::from_rgb(0, 0, 255);
let red_box = RBox(Modifier::new().size(Dp(100.0), Dp(100.0)).background(red));
let blue_box = RBox(
Modifier::new()
.size(Dp(100.0), Dp(100.0))
.background(blue)
.render_z_index(100.0),
);
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child((red_box, blue_box));
let mut engine = LayoutEngine::new();
let (scene, _hits, _sems) = engine.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rects: Vec<_> = scene
.nodes
.iter()
.filter_map(|n| {
if let SceneNode::Rect { brush, .. } = n {
Some(*brush)
} else {
None
}
})
.collect();
assert!(
rects.len() >= 2,
"Expected at least 2 rect nodes, got {}",
rects.len()
);
let last_rect_brush = rects.last().unwrap();
assert!(
matches!(last_rect_brush, Brush::Solid(c) if *c == blue),
"Expected blue box to be painted last, but got {:?}",
last_rect_brush
);
let second_to_last = rects.get(rects.len() - 2);
assert!(second_to_last.is_some(), "Expected at least 2 rect nodes");
let second_brush = second_to_last.unwrap();
assert!(
matches!(second_brush, Brush::Solid(c) if *c == red),
"Expected red box to be painted before blue, but got {:?}",
second_brush
);
}
#[test]
fn test_render_z_index_order_by_value() {
let red = Color::from_rgb(255, 0, 0);
let green = Color::from_rgb(0, 255, 0);
let blue = Color::from_rgb(0, 0, 255);
let box1 = RBox(
Modifier::new()
.size(Dp(50.0), Dp(50.0))
.background(red)
.render_z_index(10.0),
);
let box2 = RBox(
Modifier::new()
.size(Dp(50.0), Dp(50.0))
.background(green)
.render_z_index(20.0),
);
let box3 = RBox(
Modifier::new()
.size(Dp(50.0), Dp(50.0))
.background(blue)
.render_z_index(5.0),
);
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child((box1, box2, box3));
let mut engine = LayoutEngine::new();
let (scene, _hits, _sems) = engine.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rects: Vec<_> = scene
.nodes
.iter()
.filter_map(|n| {
if let SceneNode::Rect { brush, .. } = n {
Some(*brush)
} else {
None
}
})
.collect();
assert!(rects.len() >= 3, "Expected at least 3 rects");
let len = rects.len();
assert!(
matches!(&rects[len - 3], Brush::Solid(c) if *c == blue),
"Expected BLUE (z=5) third from last"
);
assert!(
matches!(&rects[len - 2], Brush::Solid(c) if *c == red),
"Expected RED (z=10) second from last"
);
assert!(
matches!(&rects[len - 1], Brush::Solid(c) if *c == green),
"Expected GREEN (z=20) last"
);
}
#[test]
fn test_render_z_index_with_nested_children() {
let red = Color::from_rgb(255, 0, 0);
let green = Color::from_rgb(0, 255, 0);
let blue = Color::from_rgb(0, 0, 255);
let red_box = RBox(Modifier::new().size(Dp(50.0), Dp(50.0)).background(red));
let green_box = RBox(Modifier::new().size(Dp(50.0), Dp(50.0)).background(green));
let content = Column(Modifier::new()).child((red_box, green_box));
let overlay = RBox(
Modifier::new()
.size(Dp(30.0), Dp(30.0))
.background(blue)
.render_z_index(1000.0),
);
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child((content, overlay));
let mut engine = LayoutEngine::new();
let (scene, _hits, _sems) = engine.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rects: Vec<_> = scene
.nodes
.iter()
.filter_map(|n| {
if let SceneNode::Rect { brush, .. } = n {
Some(*brush)
} else {
None
}
})
.collect();
assert!(
rects.len() >= 3,
"Expected at least 3 rects, got {}",
rects.len()
);
let len = rects.len();
assert!(
matches!(&rects[len - 1], Brush::Solid(c) if *c == blue),
"Expected BLUE (z=1000) to be painted last, but got {:?}",
rects[len - 1]
);
let blue_pos = rects
.iter()
.position(|b| matches!(b, Brush::Solid(c) if *c == blue))
.unwrap();
let red_pos = rects
.iter()
.position(|b| matches!(b, Brush::Solid(c) if *c == red))
.unwrap();
let green_pos = rects
.iter()
.position(|b| matches!(b, Brush::Solid(c) if *c == green))
.unwrap();
assert!(red_pos < blue_pos, "Red should be painted before blue");
assert!(green_pos < blue_pos, "Green should be painted before blue");
}
#[test]
fn test_render_z_index_paints_over_scrollbars() {
let content_color = Color::from_rgb(100, 100, 100);
let overlay_color = Color::from_rgb(0, 0, 255);
let tall_content = RBox(
Modifier::new()
.size(Dp(180.0), Dp(500.0))
.background(content_color),
);
let scroll = RBox(Modifier::new().size(Dp(200.0), Dp(200.0)).vertical_scroll(
ScrollAxisBinding {
show_scrollbar: true,
..Default::default()
},
))
.child(tall_content);
let overlay = RBox(
Modifier::new()
.size(Dp(50.0), Dp(50.0))
.background(overlay_color)
.render_z_index(1000.0),
);
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child((scroll, overlay));
let mut engine = LayoutEngine::new();
let (scene, _hits, _sems) = engine.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rects: Vec<_> = scene
.nodes
.iter()
.filter_map(|n| {
if let SceneNode::Rect { brush, .. } = n {
Some(*brush)
} else {
None
}
})
.collect();
let overlay_pos = rects
.iter()
.position(|b| matches!(b, Brush::Solid(c) if *c == overlay_color));
assert!(
overlay_pos.is_some(),
"Overlay should be present in scene, rects: {:?}",
rects
);
let overlay_pos = overlay_pos.unwrap();
assert_eq!(
overlay_pos,
rects.len() - 1,
"Overlay should be the last rect, but it's at position {} of {}. Rects: {:?}",
overlay_pos,
rects.len(),
rects
);
}
#[test]
fn test_render_z_index_with_overlay_host() {
use crate::overlay::OverlayHandle;
let content_color = Color::from_rgb(100, 100, 100);
let overlay_color = Color::from_rgb(0, 0, 255);
let tall_content = RBox(
Modifier::new()
.size(Dp(180.0), Dp(500.0))
.background(content_color),
);
let scroll = RBox(Modifier::new().size(Dp(200.0), Dp(200.0)).vertical_scroll(
ScrollAxisBinding {
show_scrollbar: true,
..Default::default()
},
))
.child(tall_content);
let overlay_handle = OverlayHandle::new();
let overlay_host = overlay_handle.host(Modifier::new().fill_max_size(), scroll);
let hint_box = RBox(
Modifier::new()
.size(Dp(50.0), Dp(50.0))
.background(overlay_color)
.render_z_index(1000.0),
);
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child((overlay_host, hint_box));
let mut engine = LayoutEngine::new();
let (scene, _hits, _sems) = engine.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rects: Vec<_> = scene
.nodes
.iter()
.filter_map(|n| {
if let SceneNode::Rect { brush, .. } = n {
Some(*brush)
} else {
None
}
})
.collect();
let overlay_pos = rects
.iter()
.position(|b| matches!(b, Brush::Solid(c) if *c == overlay_color));
assert!(
overlay_pos.is_some(),
"Hint box should be present in scene, rects: {:?}",
rects
);
let overlay_pos = overlay_pos.unwrap();
assert_eq!(
overlay_pos,
rects.len() - 1,
"Hint box should be the last rect, but it's at position {} of {}. Rects: {:?}",
overlay_pos,
rects.len(),
rects
);
}
#[test]
fn test_subcompose_layout_runs_closure_and_lays_out() {
use crate::subcompose::SubcomposeLayout;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let red = Color::from_rgb(255, 0, 0);
let call_count = Arc::new(AtomicUsize::new(0));
let count_clone = call_count.clone();
let sub = SubcomposeLayout(Modifier::new().size(Dp(200.0), Dp(100.0)), move |scope| {
count_clone.fetch_add(1, Ordering::SeqCst);
assert!(
scope.max_width.0 > 0.0,
"scope.max_width should be positive"
);
RBox(Modifier::new().size(Dp(100.0), Dp(50.0)).background(red))
});
let root = Column(Modifier::new()).child(sub);
let mut engine = LayoutEngine::new();
let (scene, _hits, _sems) = engine.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
call_count.load(Ordering::SeqCst) >= 1,
"SubcomposeLayout closure should have been invoked"
);
let has_red_rect = scene
.nodes
.iter()
.any(|n| matches!(n, SceneNode::Rect { brush: Brush::Solid(c), .. } if *c == red));
assert!(
has_red_rect,
"Subcomposed child (red box) should produce a Rect scene node"
);
}
#[test]
fn test_subcompose_layout_caches_closure_across_frames() {
use crate::subcompose::SubcomposeLayout;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let call_count = Arc::new(AtomicUsize::new(0));
let count_clone = call_count.clone();
let sub = SubcomposeLayout(Modifier::new().size(Dp(200.0), Dp(100.0)), move |_scope| {
count_clone.fetch_add(1, Ordering::SeqCst);
RBox(Modifier::new().size(Dp(50.0), Dp(50.0)))
});
let root = Column(Modifier::new()).child(sub);
let mut engine = LayoutEngine::new();
let _ = engine.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
let after_first = call_count.load(Ordering::SeqCst);
assert_eq!(after_first, 1, "closure should run once on first frame");
for _ in 0..10 {
let _ = engine.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
}
assert_eq!(
call_count.load(Ordering::SeqCst),
1,
"closure should NOT re-run when content and scope are stable"
);
}
#[test]
fn test_subcompose_layout_ancestor_modifier_narrows_scope_through_engine() {
use crate::subcompose::SubcomposeLayout;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
let captured_max_w = Arc::new(AtomicU32::new(0));
let cap = captured_max_w.clone();
let sub = SubcomposeLayout(Modifier::new(), move |scope| {
cap.store(scope.max_width.to_bits(), Ordering::SeqCst);
RBox(Modifier::new().size(Dp(100.0), Dp(50.0)))
});
let root =
Column(Modifier::new()).child(crate::Box(Modifier::new().width(Dp(320.0))).child(sub));
let mut engine = LayoutEngine::new();
let _ = engine.layout_frame(
&root,
(800, 600),
&HashMap::new(),
&Interactions::default(),
None,
);
let observed = f32::from_bits(captured_max_w.load(Ordering::SeqCst));
assert_eq!(observed, 320.0, "ancestor width should propagate to scope");
}
fn make_engine() -> LayoutEngine {
LayoutEngine::new()
}
#[test]
fn test_vertical_scroll_content_can_exceed_viewport() {
use crate::scroll::{ScrollArea, ScrollState};
use std::rc::Rc;
let state = ScrollState::new();
let root = ScrollArea(
Modifier::new().height(Dp(100.0)).width(Dp(200.0)),
Rc::new(state),
Column(Modifier::new())
.child(RBox(
Modifier::new().height(Dp(80.0)).background(Color::WHITE),
))
.child(RBox(
Modifier::new().height(Dp(80.0)).background(Color::BLACK),
)),
);
let mut eng = LayoutEngine::new();
let (_scene, hits, _) = eng.layout_frame(
&root,
(200, 100),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
hits.iter().any(|h| h.on_scroll.is_some()),
"scroll container should emit a scroll hit region when content overflows"
);
}
#[test]
fn test_nested_scroll_nav_like() {
use crate::scroll::{ScrollArea, ScrollState};
use std::rc::Rc;
let white = Color::WHITE;
let state = ScrollState::new();
let inner = ScrollArea(
Modifier::new().height(Dp(100.0)).fill_max_width(),
Rc::new(state),
Column(Modifier::new()).child(RBox(Modifier::new().height(Dp(300.0)).background(white))),
);
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child(inner);
let mut eng = LayoutEngine::new();
let (scene, hits, _) = eng.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
hits.iter().any(|h| h.on_scroll.is_some()),
"inner ScrollArea should emit a scroll hit region"
);
assert!(
scene.nodes.iter().any(|n| matches!(
n,
SceneNode::Rect {
brush: Brush::Solid(c),
rect,
..
} if *c == white && (rect.h - 300.0).abs() < 1.0
)),
"tall page should be laid out at its full height inside the scroller"
);
}
#[test]
fn test_intrinsic_size_scroll_content_height() {
use crate::scroll::{ScrollArea, ScrollState};
use std::rc::Rc;
let state = ScrollState::new();
let v = ScrollArea(
Modifier::new().width(Dp(200.0)),
Rc::new(state),
Column(Modifier::new())
.child(RBox(Modifier::new().height(Dp(80.0))))
.child(RBox(Modifier::new().height(Dp(80.0)))),
);
let mut eng = make_engine();
let (w, h) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
assert!(
h >= 160.0 - 1.0,
"scroll max-content height should reach child sum, got {}",
h
);
assert!(w > 0.0, "width should be positive, got {}", w);
}
#[test]
fn test_intrinsic_size_text_max_content() {
let mut eng = make_engine();
let v = Column(Modifier::new()).child(Text("Hello"));
let (w, h) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
assert!(
w > 0.0 && h > 0.0,
"text must have positive size, got ({}, {})",
w,
h
);
}
#[test]
fn test_intrinsic_size_min_content_shrinks() {
let mut eng = make_engine();
let v = Column(Modifier::new()).child(Text("Hello"));
let (min_w, _) = eng.intrinsic_size(&v, IntrinsicSizeMode::MinContent);
let (max_w, _) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
assert!(
min_w <= max_w,
"min-content width should be <= max-content width (min={}, max={})",
min_w,
max_w
);
}
#[test]
fn test_intrinsic_size_column_uses_max_child_width() {
let mut eng = make_engine();
let v = Column(Modifier::new())
.child(Text("Hi"))
.child(Text("Hello world"));
let (max_w, _) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
let single_w = eng
.intrinsic_size(
&Column(Modifier::new()).child(Text("Hello world")),
IntrinsicSizeMode::MaxContent,
)
.0;
assert!(
(max_w - single_w).abs() < 1.0,
"column max-content width should match widest child (col={}, single={})",
max_w,
single_w
);
}
#[cfg(test)]
mod layer_tests {
use super::*;
use crate::{Column, Text, ViewExt};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
fn collect_nodes(view: &View, _font_px: &dyn Fn(f32) -> f32) -> Vec<SceneNode> {
let mut engine = LayoutEngine::new();
let state: HashMap<u64, Rc<RefCell<crate::TextFieldState>>> = HashMap::new();
let interactions = crate::Interactions::default();
let (scene, _, _) = engine.layout_frame(view, (400, 400), &state, &interactions, None);
scene.nodes
}
#[test]
fn test_graphics_layer_emits_begin_end() {
let view = Column(Modifier::new().graphics_layer(0.5)).child(Text("hello"));
let nodes = collect_nodes(&view, &|d| d);
let begin_count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::BeginLayer { .. }))
.count();
let end_count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::EndLayer { .. }))
.count();
assert_eq!(
begin_count, 1,
"expected exactly one BeginLayer, got {}",
begin_count
);
assert_eq!(
end_count, 1,
"expected exactly one EndLayer, got {}",
end_count
);
}
#[test]
fn test_no_graphics_layer_means_no_begin_end() {
let view = Column(Modifier::new()).child(Text("hello"));
let nodes = collect_nodes(&view, &|d| d);
let begin_count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::BeginLayer { .. }))
.count();
let end_count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::EndLayer { .. }))
.count();
assert_eq!(begin_count, 0);
assert_eq!(end_count, 0);
}
#[test]
fn test_nested_graphics_layers_emit_nested_pairs() {
let view = Column(Modifier::new().graphics_layer(0.9))
.child(Column(Modifier::new().graphics_layer(0.5)).child(Text("nested")));
let nodes = collect_nodes(&view, &|d| d);
let begin_count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::BeginLayer { .. }))
.count();
let end_count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::EndLayer { .. }))
.count();
assert_eq!(
begin_count, 2,
"expected two BeginLayer nodes for nested layers"
);
assert_eq!(
end_count, 2,
"expected two EndLayer nodes for nested layers"
);
}
#[test]
fn test_begin_end_are_balanced() {
let view = Column(Modifier::new().graphics_layer(0.7))
.child(Column(Modifier::new()).child(Text("inner")));
let nodes = collect_nodes(&view, &|d| d);
let mut depth: i32 = 0;
for n in &nodes {
match n {
SceneNode::BeginLayer { .. } => depth += 1,
SceneNode::EndLayer { .. } => depth -= 1,
_ => {}
}
}
assert_eq!(depth, 0, "Begin/EndLayer must be balanced");
}
#[test]
fn test_graphics_layer_passes_alpha_through() {
let view = Column(Modifier::new().graphics_layer(0.42)).child(Text("x"));
let nodes = collect_nodes(&view, &|d| d);
let begin = nodes.iter().find_map(|n| match n {
SceneNode::BeginLayer { alpha, .. } => Some(*alpha),
_ => None,
});
assert_eq!(
begin,
Some(0.42),
"graphics_layer alpha should pass through"
);
}
#[test]
fn test_transformed_graphics_layer_uses_plain_origin_shift() {
let view = crate::Box(
Modifier::new()
.size(Dp(40.0), Dp(40.0))
.absolute()
.offset(Some(Dp(40.0)), Some(Dp(30.0)), None, None)
.graphics_layer(0.5)
.scale(0.8),
)
.child(Text("x"));
let nodes = collect_nodes(&view, &|d| d);
let begin_index = nodes
.iter()
.position(|node| matches!(node, SceneNode::BeginLayer { .. }))
.expect("graphics layer");
let layer_rect = match &nodes[begin_index] {
SceneNode::BeginLayer { rect, .. } => *rect,
_ => unreachable!(),
};
let transform = match &nodes[begin_index + 1] {
SceneNode::PushTransform { transform } => *transform,
node => panic!("expected transform after BeginLayer, got {node:?}"),
};
assert_eq!(transform.translate_x, -layer_rect.x);
assert_eq!(transform.translate_y, -layer_rect.y);
assert!(nodes.iter().any(|node| matches!(
node,
SceneNode::Text { text, .. } if text.as_ref() == "x"
)));
}
#[test]
fn test_transformed_graphics_layer_preserves_hit_rect() {
let view = crate::Box(
Modifier::new()
.size(Dp(200.0), Dp(160.0))
.translate(100.0, 50.0)
.scale(0.8)
.transform_origin(0.0, 0.0),
)
.child(crate::Box(
Modifier::new()
.size(Dp(100.0), Dp(80.0))
.graphics_layer(1.0)
.input_blocker(),
));
let mut engine = LayoutEngine::new();
let state: HashMap<u64, Rc<RefCell<crate::TextFieldState>>> = HashMap::new();
let (_, hits, _) = engine.layout_frame(
&view,
(800, 600),
&state,
&crate::Interactions::default(),
None,
);
let hit = hits
.iter()
.find(|hit| hit.rect.w > 0.0 && hit.rect.h > 0.0)
.expect("graphics layer hit");
assert!((hit.rect.x - 100.0).abs() < 0.01);
assert!((hit.rect.y - 50.0).abs() < 0.01);
assert!((hit.rect.w - 80.0).abs() < 0.01);
assert!((hit.rect.h - 64.0).abs() < 0.01);
}
#[test]
fn test_graphics_layer_alpha_is_clamped() {
let m = Modifier::new().graphics_layer(2.0);
assert_eq!(
m.graphics_layer,
Some(1.0),
"alpha above 1.0 should clamp to 1.0"
);
let m = Modifier::new().graphics_layer(-0.5);
assert_eq!(
m.graphics_layer,
Some(0.0),
"negative alpha should clamp to 0.0"
);
}
}
#[test]
fn test_annotated_text_large_font_not_clipped() {
use repose_core::SpanStyle;
let big = SpanStyle::default().font_size(Sp(24.0));
let anno = build_annotated_string(|b| {
b.push("Normal ");
b.push_with_style("Big", big.clone());
b.push(" normal");
});
let v = crate::AnnotatedText(anno).size(Sp(14.0));
let mut eng = make_engine();
let (w_anno, h_anno) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
let (w_base, h_base) = eng.intrinsic_size(
&crate::Text("Normal Big normal").size(Sp(14.0)),
IntrinsicSizeMode::MaxContent,
);
assert!(
w_anno > w_base,
"annotated big span should be wider: anno={} base={}",
w_anno,
w_base
);
assert!(
h_anno >= 24.0 - 1.0,
"height must fit 24dp big span, got {}",
h_anno
);
assert!(
h_anno > h_base,
"height with big span should exceed base 14dp line"
);
}
#[test]
fn test_annotated_stroke_expands_bounds() {
use repose_core::{DrawStyle, SpanStyle};
let s = SpanStyle::default()
.draw_style(DrawStyle::stroke(0.1))
.font_size(Sp(20.0));
let anno = build_annotated_string(|b| {
b.push_with_style("Stroked", s);
});
let v = crate::AnnotatedText(anno).size(Sp(16.0));
let mut eng = make_engine();
let (w, h) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
let (w0, h0) = eng.intrinsic_size(
&crate::Text("Stroked").size(Sp(16.0)),
IntrinsicSizeMode::MaxContent,
);
assert!(w > w0, "stroke should expand width");
assert!(h > h0, "stroke should expand height");
}
#[test]
fn test_annotated_superscript_expands_height() {
use repose_core::{BaselineShift, SpanStyle};
let sp = SpanStyle::default()
.font_size(Sp(14.0))
.baseline_shift(BaselineShift::Superscript);
let anno = build_annotated_string(|b| {
b.push("a");
b.push_with_style("super", sp);
b.push("b");
});
let v = crate::AnnotatedText(anno).size(Sp(16.0));
let mut eng = make_engine();
let (_, h) = eng.intrinsic_size(&v, IntrinsicSizeMode::MaxContent);
assert!(
h > 16.0,
"superscript should increase measured height, got {}",
h
);
}
#[cfg(test)]
mod shadow_tests {
use super::*;
use crate::{Column, Text, ViewExt};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
fn collect_nodes(view: &View) -> Vec<SceneNode> {
let mut engine = LayoutEngine::new();
let state: HashMap<u64, Rc<RefCell<crate::TextFieldState>>> = HashMap::new();
let interactions = crate::Interactions::default();
let (scene, _, _) = engine.layout_frame(view, (400, 400), &state, &interactions, None);
scene.nodes
}
#[test]
fn test_shadow_alone_does_not_emit_composite_shadow() {
let view = Column(Modifier::new().shadow(Dp(8.0), Dp(4.0))).child(Text("x"));
let nodes = collect_nodes(&view);
let count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::CompositeShadow { .. }))
.count();
assert_eq!(
count, 0,
"shadow without layer must not emit CompositeShadow"
);
}
#[test]
fn test_layer_with_shadow_emits_composite_shadow() {
let view =
Column(Modifier::new().graphics_layer(1.0).shadow(Dp(8.0), Dp(4.0))).child(Text("x"));
let nodes = collect_nodes(&view);
let count = nodes
.iter()
.filter(|n| matches!(n, SceneNode::CompositeShadow { .. }))
.count();
assert_eq!(count, 1, "expected one CompositeShadow");
}
#[test]
fn test_shadow_appears_after_end_layer() {
let view =
Column(Modifier::new().graphics_layer(1.0).shadow(Dp(8.0), Dp(4.0))).child(Text("x"));
let nodes = collect_nodes(&view);
let end_idx = nodes
.iter()
.position(|n| matches!(n, SceneNode::EndLayer { .. }))
.expect("EndLayer should be present");
let shadow_idx = nodes
.iter()
.position(|n| matches!(n, SceneNode::CompositeShadow { .. }))
.expect("CompositeShadow should be present");
assert!(
shadow_idx > end_idx,
"CompositeShadow (idx {}) should come after EndLayer (idx {})",
shadow_idx,
end_idx
);
}
#[test]
fn test_shadow_passes_through_blur_and_offset() {
let view = Column(
Modifier::new()
.graphics_layer(1.0)
.shadow(Dp(10.0), Dp(6.0)),
)
.child(Text("x"));
let nodes = collect_nodes(&view);
let shadow = nodes
.iter()
.find_map(|n| match n {
SceneNode::CompositeShadow {
blur_px, offset_px, ..
} => Some((*blur_px, *offset_px)),
_ => None,
})
.expect("CompositeShadow present");
let (blur, offset) = shadow;
assert!(blur.0 > 0.0, "blur should be > 0, got {}", blur);
assert!(offset.1.0 > 0.0, "offset_y should be > 0, got {}", offset.1);
}
#[test]
fn test_elevation_helper_sets_shadow() {
let m4 = Modifier::new().elevation(Dp(4.0));
assert!(m4.shadow.is_some(), "elevation(4) should set shadow");
let m0 = Modifier::new().elevation(Dp(0.0));
assert!(m0.shadow.is_none(), "elevation(0) should not set shadow");
}
#[test]
fn test_zstack_unkeyed_text_reuse_no_stale() {
use crate::{Column, ZStack};
let bigname_at = |label: &str, gx: f32, gy: f32| {
Column(
Modifier::new()
.fill_max_size()
.padding_values(PaddingValues {
left: Dp(gx - 60.0),
top: Dp(gy - 11.0),
right: Dp::ZERO,
bottom: Dp::ZERO,
}),
)
.child(
Column(
Modifier::new()
.width(Dp(120.0))
.height(Dp(22.0))
.justify_content(JustifyContent::CENTER)
.align_items(AlignItems::CENTER),
)
.child(
Text(label.to_string())
.size(Sp(10.0))
.font_family("Silkscreen"),
),
)
};
let normal_at = |label: String, gx: f32, gy: f32| {
Column(
Modifier::new()
.fill_max_size()
.padding_values(PaddingValues {
left: Dp(gx),
top: Dp(gy),
right: Dp::ZERO,
bottom: Dp::ZERO,
}),
)
.child(
Column(Modifier::new().width(Dp(200.0)).height(Dp(16.0)))
.child(Text(label).size(Sp(7.0)).font_family("Silkscreen")),
)
};
let hitbox_at = |x: f32, y: f32| {
Column(
Modifier::new()
.fill_max_size()
.padding_values(PaddingValues {
left: Dp(x),
top: Dp(y),
right: Dp::ZERO,
bottom: Dp::ZERO,
}),
)
.child(Column(
Modifier::new()
.width(Dp(24.0))
.height(Dp(16.0))
.clickable()
.on_click(|| {}),
))
};
let make_page = |page: u8| {
let mut layers: Vec<View> = Vec::new();
layers.push(Column(
Modifier::new()
.fill_max_size()
.background(Color::from_rgba(0, 0, 0, 230)),
));
if page == 0 {
for i in 0..4 {
let gy = 56.0 + i as f32 * 20.0;
layers.push(normal_at(format!("VOL {}", i), 80.0, gy));
layers.push(normal_at(format!("{}%", 80 + i * 5), 200.0, gy));
layers.push(hitbox_at(40.0, gy - 6.0));
layers.push(hitbox_at(240.0, gy - 6.0));
layers.push(normal_at("<".into(), 44.0, gy));
layers.push(normal_at(">".into(), 252.0, gy));
}
layers.push(bigname_at("BACK", 160.0, 200.0));
} else {
for i in 0..6 {
let gy = 48.0 + i as f32 * 18.0;
layers.push(normal_at(format!("V Row {}", i), 80.0, gy));
layers.push(hitbox_at(40.0, gy - 6.0));
layers.push(hitbox_at(240.0, gy - 6.0));
}
for (j, label) in ["VIEW CREDITS", "PROFILE", "COLOR"].iter().enumerate() {
let gy = 140.0 + j as f32 * 20.0;
layers.push(bigname_at(label, 160.0, gy));
}
layers.push(bigname_at("BACK", 160.0, 200.0));
}
ZStack(Modifier::new().fill_max_size()).child(layers)
};
let mut engine = LayoutEngine::new();
let root0 = Column(Modifier::new().fill_max_size()).child(make_page(0));
let _ = engine.layout_frame(
&root0,
(800, 600),
&HashMap::new(),
&Interactions::default(),
None,
);
let root1 = Column(Modifier::new().fill_max_size()).child(make_page(1));
let _ = engine.layout_frame(
&root1,
(800, 600),
&HashMap::new(),
&Interactions::default(),
None,
);
let _ = engine.layout_frame(
&root0,
(800, 600),
&HashMap::new(),
&Interactions::default(),
None,
);
let (scene, _, _) = engine.layout_frame(
&root1,
(800, 600),
&HashMap::new(),
&Interactions::default(),
None,
);
let mut backs: Vec<Rect> = Vec::new();
for node in &scene.nodes {
if let SceneNode::Text { rect, text, .. } = node
&& text.as_ref() == "BACK"
{
backs.push(*rect);
}
}
assert_eq!(backs.len(), 1, "expected 1 BACK, got {:?}", backs);
let r = backs[0];
assert!(r.x > 100.0 && r.x < 200.0, "BACK x off: {}", r.x);
assert!(
r.y > 150.0 && r.y < 250.0,
"BACK y off: {} rect {:?}",
r.y,
r
);
let mut Credits: Vec<Rect> = Vec::new();
for node in &scene.nodes {
if let SceneNode::Text { rect, text, .. } = node
&& text.as_ref() == "VIEW CREDITS"
{
Credits.push(*rect);
}
}
assert_eq!(Credits.len(), 1);
let rc = Credits[0];
assert!(
rc.y > 100.0 && rc.y < 200.0,
"VIEW CREDITS y off: {} {:?}",
rc.y,
rc
);
}
}
#[test]
fn test_row_baseline_alignment() {
let root = Row(Modifier::new().size(Dp(400.0), Dp(200.0))).child(row_scope(|s| {
vec![
s.align_by_baseline(Text("Ag").size(Sp(32.0)).single_line()),
s.align_by_baseline(Text("Ag").size(Sp(16.0)).single_line()),
]
}));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let mut baselines = Vec::new();
for node in &scene.nodes {
if let SceneNode::Text { rect, size, .. } = node {
let (ascent, _) =
repose_text::primary_font_vertical_metrics(Some("sans-serif"), 400, size.0);
baselines.push(rect.y + ascent);
}
}
assert_eq!(baselines.len(), 2, "expected two text runs: {baselines:?}");
assert!(
(baselines[0] - baselines[1]).abs() < 2.0,
"first baselines should coincide: {baselines:?}"
);
}
#[test]
fn test_row_baseline_alignment_grows_row() {
let root = Column(Modifier::new()).child((
Row(Modifier::new()).child(row_scope(|s| {
vec![
s.align_by_baseline(Text("Ag").size(Sp(64.0)).single_line()),
s.align_by_baseline(Text("Ag\nAg").size(Sp(32.0))),
]
})),
RBox(
Modifier::new()
.size(Dp(400.0), Dp(10.0))
.background(Color::WHITE),
),
));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
let mut big = Vec::new();
let mut small = Vec::new();
for node in &scene.nodes {
if let SceneNode::Text { rect, size, .. } = node {
let (ascent, _) =
repose_text::primary_font_vertical_metrics(Some("sans-serif"), 400, size.0);
let run = (rect.y + ascent, rect.y, rect.y + rect.h, rect.h);
if size.0 >= 64.0 {
big.push(run);
} else {
small.push(run);
}
}
}
assert_eq!(big.len(), 1, "expected the 64sp run");
assert_eq!(small.len(), 2, "expected two 32sp runs, got {small:?}");
let first_small_baseline = small.iter().map(|r| r.0).fold(f32::INFINITY, f32::min);
assert!(
(big[0].0 - first_small_baseline).abs() < 2.5,
"first baselines should coincide: {big:?} {small:?}"
);
let bottom = big
.iter()
.chain(small.iter())
.map(|r| r.2)
.fold(f32::NEG_INFINITY, f32::max);
let small_top = small.iter().map(|r| r.1).fold(f32::INFINITY, f32::min);
let small_bottom = small.iter().map(|r| r.2).fold(f32::NEG_INFINITY, f32::max);
let tallest = big[0].3.max(small_bottom - small_top);
let boxes: Vec<f32> = scene
.nodes
.iter()
.filter_map(|n| match n {
SceneNode::Rect { rect, .. } => Some(rect.y),
_ => None,
})
.collect();
assert_eq!(boxes.len(), 1, "expected the sentinel box");
let row_h = boxes[0];
assert!(
row_h > tallest,
"row should grow beyond its tallest child ({tallest}): {row_h}"
);
assert!(
row_h >= bottom - 1.0,
"shifted children must fit inside the row (bottom {bottom}): {row_h}"
);
assert!(
row_h <= bottom + 1.0,
"row should grow exactly enough (bottom {bottom}): {row_h}"
);
}
#[test]
fn weighted_text_stays_within_lazy_row_bounds() {
use crate::{LazyColumn, LazyColumnConfig};
let description = "supercalifragilisticexpialidocious-supercalifragilisticexpialidocious-supercalifragilisticexpialidocious-supercalifragilisticexpialidocious";
let leading_color = Color::from_rgb(255, 0, 0);
let action_color = Color::from_rgb(0, 0, 255);
let list = LazyColumn(
vec![description],
104.0_f32,
|text: &&str| text.len() as u64,
move |text: &str, _| {
Row(Modifier::new()
.fill_max_width()
.height(Dp(104.0))
.padding(Dp(12.0))
.align_items(AlignItems::CENTER))
.child((
RBox(
Modifier::new()
.size(Dp(40.0), Dp(40.0))
.flex_shrink(0.0)
.background(leading_color),
),
RBox(Modifier::new().width(Dp(12.0)).flex_shrink(0.0)),
Column(Modifier::new().weight(1.0).gap(Dp(3.0))).child((
Text("PackageName").single_line().overflow_ellipsize(),
Text(text)
.max_lines(2)
.overflow_ellipsize()
.modifier(Modifier::new().fill_max_width().min_width(Dp(0.0))),
Text("v1").single_line(),
)),
RBox(Modifier::new().width(Dp(12.0)).flex_shrink(0.0)),
RBox(
Modifier::new()
.width(Dp(92.0))
.height(Dp(40.0))
.flex_shrink(0.0)
.background(action_color),
),
))
},
LazyColumnConfig {
modifier: Modifier::new().fill_max_width().weight(1.0),
..Default::default()
},
);
let root = Row(Modifier::new().fill_max_size()).child((
RBox(Modifier::new().width(Dp(224.0)).fill_max_height()),
RBox(Modifier::new().weight(1.0).fill_max_height()).child(list),
));
let mut engine = make_engine();
let _ = engine.layout_frame(
&root,
(1200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let (scene, _, _) = engine.layout_frame(
&root,
(900, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let texts: Vec<_> = scene
.nodes
.iter()
.filter_map(|node| match node {
SceneNode::Text { text, .. } => Some(text.as_ref()),
_ => None,
})
.collect();
let name_index = texts
.iter()
.position(|text| *text == "PackageName")
.expect("package name text");
let version_index = texts
.iter()
.position(|text| *text == "v1")
.expect("package version text");
let description_lines = &texts[name_index + 1..version_index];
assert_eq!(description_lines.len(), 2, "texts: {texts:?}");
assert!(
description_lines
.last()
.is_some_and(|line| line.ends_with('…')),
"bounded description should be ellipsized: {texts:?}"
);
let action = scene
.nodes
.iter()
.find_map(|node| match node {
SceneNode::Rect {
rect,
brush: Brush::Solid(color),
..
} if *color == action_color => Some(*rect),
_ => None,
})
.expect("fixed action bounds");
assert!(
action.x + action.w <= 900.5,
"fixed action pushed outside shell: {action:?}"
);
for node in &scene.nodes {
if let SceneNode::Text { rect, text, .. } = node
&& description_lines.contains(&text.as_ref())
{
assert!(
rect.x + rect.w <= action.x + 0.5,
"description {:?} exceeds action bounds: {:?} vs {:?}",
text,
rect,
action
);
}
}
}
#[test]
fn test_intrinsic_and_fit_content_sizing() {
let wide = Text("hello world hello world").single_line();
let root = Column(Modifier::new().size(Dp(400.0), Dp(400.0)))
.child(RBox(Modifier::new().intrinsic_width(IntrinsicSize::Max)).child(wide));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
!scene.nodes.is_empty(),
"intrinsic-width subtree should lay out"
);
let narrow = Text("hello world hello world").single_line();
let root = Column(Modifier::new().size(Dp(400.0), Dp(400.0)))
.child(RBox(Modifier::new().fit_content_width(Dp(50.0))).child(narrow));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
!scene.nodes.is_empty(),
"fit-content-width subtree should lay out"
);
}
#[test]
fn test_balanced_flow_row_lays_out() {
use crate::{FlowColumnConfig, FlowRowConfig};
let white = Color::WHITE;
let root = crate::FlowRow(
Modifier::new().size(Dp(300.0), Dp(300.0)),
FlowRowConfig {
balanced: true,
min_lines: 2,
..Default::default()
},
)
.child(RBox(
Modifier::new().size(Dp(100.0), Dp(20.0)).background(white),
))
.child(RBox(
Modifier::new().size(Dp(100.0), Dp(20.0)).background(white),
))
.child(RBox(
Modifier::new().size(Dp(100.0), Dp(20.0)).background(white),
));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(300, 300),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
!scene.nodes.is_empty(),
"balanced flow row should lay out without errors"
);
let root = crate::FlowColumn(
Modifier::new().size(Dp(300.0), Dp(300.0)),
FlowColumnConfig {
balanced: true,
min_lines: 2,
..Default::default()
},
)
.child(RBox(
Modifier::new().size(Dp(20.0), Dp(100.0)).background(white),
));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(300, 300),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(
!scene.nodes.is_empty(),
"balanced flow column should lay out without errors"
);
}
#[test]
fn test_contain_modifiers_lay_out() {
let white = Color::WHITE;
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0)).contain_content())
.child(RBox(
Modifier::new().size(Dp(50.0), Dp(50.0)).background(white),
))
.child(Column(Modifier::new().contain_layout()).child(RBox(
Modifier::new().size(Dp(30.0), Dp(30.0)).background(white),
)));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(!scene.nodes.is_empty(), "contained subtree should lay out");
}
#[test]
fn test_safe_center_alignment_lays_out() {
use crate::Center;
let white = Color::WHITE;
let root = Center(Modifier::new().size(Dp(200.0), Dp(200.0))).child(RBox(
Modifier::new().size(Dp(50.0), Dp(50.0)).background(white),
));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(!scene.nodes.is_empty());
let safe = Text("hi").single_line();
let root = Column(
Modifier::new()
.size(Dp(200.0), Dp(200.0))
.content_alignment_safe(Alignment::Center),
)
.child(safe);
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(!scene.nodes.is_empty());
}
#[test]
fn test_debug_taffy_subtree_and_grid_summary() {
use repose_tree::NodeId;
let white = Color::WHITE;
let root = Column(Modifier::new().size(Dp(300.0), Dp(300.0))).child(
RBox(
Modifier::new()
.size(Dp(300.0), Dp(200.0))
.grid(2, Dp(4.0), Dp(4.0)),
)
.child(RBox(
Modifier::new().size(Dp(50.0), Dp(50.0)).background(white),
))
.child(RBox(
Modifier::new().size(Dp(50.0), Dp(50.0)).background(white),
)),
);
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(300, 300),
&HashMap::new(),
&Interactions::default(),
None,
);
assert!(!scene.nodes.is_empty());
let mut grid_nodes = 0;
let mut other_node = None;
let ids: Vec<NodeId> = eng.taffy_map.keys().copied().collect();
for nid in &ids {
if eng.debug_grid_summary(*nid).is_some() {
grid_nodes += 1;
} else {
other_node = Some(*nid);
}
}
assert_eq!(grid_nodes, 1, "expected exactly one grid container");
assert!(
eng.debug_grid_summary(other_node.expect("leaf node"))
.is_none()
);
let some_id = *ids.first().expect("mapped nodes");
assert!(!eng.debug_taffy_subtree(some_id).is_empty());
}
#[test]
fn test_scrollbar_image_visuals_preserve_thumb_geometry() {
let track = ControlVisual::image(7, Some(ImageSourceRect::new(0, 0, 4, 16)), Color::WHITE)
.image_filter(ImageFilter::Nearest);
let thumb = ControlVisual::image(7, Some(ImageSourceRect::new(4, 0, 8, 16)), Color::WHITE)
.image_filter(ImageFilter::Nearest);
let style = ScrollbarStyle::default()
.with_thickness(Dp(16.0))
.with_track_inset(Dp(0.0))
.with_fixed_thumb_length(Some(Dp(16.0)))
.with_track_visuals(ControlVisualSet::default().with_normal(track))
.with_thumb_visuals(ControlVisualSet::default().with_normal(thumb));
let scroll = crate::scroll::ScrollArea(
Modifier::new()
.size(Dp(100.0), Dp(200.0))
.scrollbar_style(style),
std::rc::Rc::new(ScrollState::new()),
RBox(Modifier::new().size(Dp(100.0), Dp(400.0))),
);
let root = scroll;
let mut engine = LayoutEngine::new();
let (scene, hits, _) = engine.layout_frame(
&root,
(100, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let images: Vec<_> = scene
.nodes
.iter()
.filter_map(|node| match node {
SceneNode::Image { rect, style, .. } => Some((*rect, style.source_rect)),
_ => None,
})
.collect();
assert_eq!(images.len(), 2);
assert_eq!(images[0].1, Some(ImageSourceRect::new(0, 0, 4, 16)));
assert_eq!(images[1].1, Some(ImageSourceRect::new(4, 0, 8, 16)));
assert_eq!(images[1].0.h, 16.0);
assert!(hits.iter().any(|hit| hit.rect.h == 16.0));
let default_scroll = crate::scroll::ScrollArea(
Modifier::new().size(Dp(100.0), Dp(200.0)),
std::rc::Rc::new(ScrollState::new()),
RBox(Modifier::new().size(Dp(100.0), Dp(400.0))),
);
let mut engine = LayoutEngine::new();
let (scene, _, _) = locals::with_density(locals::Density { scale: 2.0 }, || {
engine.layout_frame(
&default_scroll,
(200, 400),
&HashMap::new(),
&Interactions::default(),
None,
)
});
let radii: Vec<_> = scene
.nodes
.iter()
.filter_map(|node| match node {
SceneNode::Rect { radius, .. } => Some(*radius),
_ => None,
})
.collect();
assert_eq!(radii.len(), 2);
assert!(radii.iter().all(|radius| *radius == [Px(4.0); 4]));
}
#[test]
fn image_reports_compose_intrinsic_size() {
const H: u64 = 0xFFFF_FFFF_FFFF;
set_image_intrinsic_size(H, 100, 50);
let sized = |m: Modifier| {
let root = Column(
Modifier::new()
.size(Dp(400.0), Dp(400.0))
.align_items(AlignItems::FLEX_START),
)
.child(crate::Image(m, H));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
scene.nodes.iter().find_map(|n| match n {
SceneNode::Image { rect, .. } => Some(*rect),
_ => None,
})
};
let r = sized(Modifier::new()).expect("image rect");
assert_eq!(
(r.w, r.h),
(100.0, 50.0),
"unsized image is its natural size"
);
let r = sized(Modifier::new().width(Dp(120.0))).expect("image rect");
assert_eq!(
(r.w, r.h),
(120.0, 50.0),
"width(120) keeps the natural height"
);
let r = sized(Modifier::new().width(Dp(30.0))).expect("image rect");
assert_eq!((r.w, r.h), (30.0, 15.0), "width(30) scales the pair down");
let r = sized(Modifier::new().height(Dp(25.0))).expect("image rect");
assert_eq!((r.w, r.h), (50.0, 25.0), "height(25) scales the pair down");
let r = sized(Modifier::new().size(Dp(200.0), Dp(200.0))).expect("image rect");
assert_eq!((r.w, r.h), (200.0, 200.0), "an explicit size wins");
let fill = |m: Modifier| {
let root = Column(Modifier::new().size(Dp(400.0), Dp(400.0))).child(
RBox(
Modifier::new()
.size(Dp(300.0), Dp(150.0))
.align_items(AlignItems::FLEX_START),
)
.child(crate::Image(m, H)),
);
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
scene.nodes.iter().find_map(|n| match n {
SceneNode::Image { rect, .. } => Some(*rect),
_ => None,
})
};
let r = fill(Modifier::new().fill_max_size()).expect("image rect");
assert_eq!((r.w, r.h), (300.0, 150.0), "fill_max fills the parent");
let r = fill(Modifier::new().fill_max_width()).expect("image rect");
assert_eq!(
(r.w, r.h),
(300.0, 50.0),
"fill_max_width keeps natural height"
);
let r = sized(Modifier::new());
assert!(r.is_some(), "node still exists");
let unknown = crate::Image(Modifier::new(), H.wrapping_sub(1));
let root = Column(
Modifier::new()
.size(Dp(400.0), Dp(400.0))
.align_items(AlignItems::FLEX_START),
)
.child(unknown);
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(400, 400),
&HashMap::new(),
&Interactions::default(),
None,
);
let rect = scene.nodes.iter().find_map(|n| match n {
SceneNode::Image { rect, .. } => Some(*rect),
_ => None,
});
assert_eq!(
rect.map(|r| (r.w, r.h)),
Some((0.0, 0.0)),
"unknown natural size measures to zero"
);
}
#[test]
fn children_stretch_on_the_cross_axis_by_default() {
let root = Column(Modifier::new().size(Dp(200.0), Dp(200.0))).child(RBox(
Modifier::new()
.height(Dp(20.0))
.background(Color::from_rgb(255, 0, 0)),
));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&root,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rect = scene.nodes.iter().find_map(|n| match n {
SceneNode::Rect { rect, .. } => Some(*rect),
_ => None,
});
assert_eq!(
rect.map(|r| r.w),
Some(200.0),
"a child must fill the parent's cross axis"
);
let shrink = Column(
Modifier::new()
.size(Dp(200.0), Dp(200.0))
.align_items(AlignItems::FLEX_START),
)
.child(RBox(
Modifier::new()
.height(Dp(20.0))
.background(Color::from_rgb(255, 0, 0)),
));
let mut eng = make_engine();
let (scene, _, _) = eng.layout_frame(
&shrink,
(200, 200),
&HashMap::new(),
&Interactions::default(),
None,
);
let rect = scene.nodes.iter().find_map(|n| match n {
SceneNode::Rect { rect, .. } => Some(*rect),
_ => None,
});
assert_eq!(
rect.map(|r| r.w),
Some(0.0),
"align_items(FLEX_START) must shrink-wrap"
);
}
#[test]
fn animated_visibility_overlay_fills_parent_height() {
use crate::ZStack;
use crate::anim_ext::{
AnimatedVisibility, AnimatedVisibilityConfig, EnterTransition, ExitTransition,
};
use repose_core::animation::{SystemClock, TestClock, set_clock};
use repose_core::animation_driver;
use repose_core::prelude::JustifyContent;
use repose_core::runtime::ComposeGuard;
use repose_core::scope::Scope;
use std::time::Duration;
use web_time::Instant;
let mut clock = TestClock { t: Instant::now() };
set_clock(Box::new(clock.clone()));
let config = AnimatedVisibilityConfig {
key: "overlay".into(),
enter: EnterTransition::ScaleIn { initial: 0.95 },
exit: ExitTransition::ScaleOut { target: 0.95 },
..Default::default()
};
let scope = Scope::new();
let compose = |visible: bool| {
let guard = ComposeGuard::begin();
let view = scope.run(|| {
ZStack(Modifier::new().fill_max_size()).child(AnimatedVisibility(
visible,
Column(
Modifier::new()
.fill_max_size()
.justify_content(JustifyContent::CENTER)
.align_items(AlignItems::CENTER)
.background(Color::from_rgb(0, 0, 0)),
)
.child(RBox(
Modifier::new()
.size(Dp(340.0), Dp(300.0))
.background(Color::from_rgb(255, 255, 255)),
)),
config.clone(),
))
});
drop(guard);
view
};
let _ = compose(false);
let _ = compose(true);
clock.t += Duration::from_secs(5);
set_clock(Box::new(clock.clone()));
animation_driver::tick();
let view = compose(true);
let mut engine = LayoutEngine::new();
let (scene, _, _) = engine.layout_frame(
&view,
(1280, 720),
&HashMap::new(),
&Interactions::default(),
None,
);
let rect_of = |c: Color| {
scene.nodes.iter().find_map(|n| match n {
SceneNode::Rect {
rect,
brush: Brush::Solid(b),
..
} if *b == c => Some(*rect),
_ => None,
})
};
let scrim = rect_of(Color::from_rgb(0, 0, 0)).expect("scrim is painted");
assert_eq!(
(scrim.x, scrim.y, scrim.w, scrim.h),
(0.0, 0.0, 1280.0, 720.0),
"enter wrapper must not shrink a fill_max_size overlay to content height"
);
let panel = rect_of(Color::from_rgb(255, 255, 255)).expect("panel is painted");
assert_eq!(
(panel.x, panel.y, panel.w, panel.h),
(470.0, 210.0, 340.0, 300.0),
"overlay content must stay centered inside the full-height scrim"
);
scope.dispose();
set_clock(Box::new(SystemClock));
}