use lieui_geom::{Point, Rect, Size};
use lieui_layout::types::Direction;
use lieui_layout::{ComputedLayout, FlexDirection, FlexNode, VALUE_UNDEFINED};
use lieui_text::TextEngine;
use crate::track::{Kind, NodeId, Placement, Track, Visibility};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LayoutStats {
pub ran: bool,
pub boundaries: usize,
pub nodes: usize,
pub moved: usize,
pub scroll_containers: usize,
pub clamped_scroll: usize,
}
pub fn needs_layout(track: &Track) -> bool {
track.has_layout_dirty()
}
pub fn layout(track: &mut Track, window: Size) -> LayoutStats {
let boundaries = track.layout_boundaries();
if boundaries.is_empty() {
return LayoutStats::default();
}
let mut st = LayoutStats {
ran: true,
boundaries: boundaries.len(),
..Default::default()
};
let root_nodes: Vec<NodeId> = track.roots().iter().map(|r| r.node).collect();
let anchored_roots: Vec<NodeId> = track
.roots()
.iter()
.filter(|r| r.opts.anchor.is_some())
.map(|r| r.node)
.collect();
for b in boundaries {
if !track.contains(b) {
continue;
}
let is_root = root_nodes.contains(&b);
let (avail, base) = if is_root {
if anchored_roots.contains(&b) {
(
Size::new(VALUE_UNDEFINED, VALUE_UNDEFINED),
None,
)
} else {
(window, None)
}
} else {
let r = track.get(b).map(|n| n.rect()).unwrap_or_default();
(
Size::new(r.width.max(0.0), r.height.max(0.0)),
Some((r.x, r.y)),
)
};
let mut flex = build(track, b);
flex.layout(avail.width, avail.height, Direction::Ltr);
let (ox, oy) = match base {
Some((bx, by)) => (bx - flex.get_left(), by - flex.get_top()),
None => (0.0, 0.0),
};
write_back(track, b, &flex, ox, oy, &mut st);
}
track.clear_layout_flags();
st
}
pub const ANCHOR_GAP: f32 = 4.0;
pub fn place_anchored_layers(track: &mut Track, window: Size) -> usize {
let anchored: Vec<(NodeId, crate::track::Anchor)> = track
.roots()
.iter()
.filter_map(|r| r.opts.anchor.clone().map(|a| (r.node, a)))
.collect();
let mut moved = 0;
for (node, anchor) in anchored {
let Some(target) = anchored_origin(track, node, &anchor, window) else {
continue;
};
let Some(cur) = track.get(node).map(|n| n.rect()) else {
continue;
};
let (dx, dy) = (target.0 - cur.x, target.1 - cur.y);
if dx.abs() < 0.01 && dy.abs() < 0.01 {
continue;
}
for id in track.descendants(node) {
if let Some(n) = track.get_mut(id) {
n.computed.x += dx;
n.computed.y += dy;
}
}
track.damage_rect(cur);
track.damage_rect(Rect::new(cur.x + dx, cur.y + dy, cur.width, cur.height));
moved += 1;
}
moved
}
fn anchored_origin(
track: &Track,
layer: NodeId,
anchor: &crate::track::Anchor,
window: Size,
) -> Option<(f32, f32)> {
let lr = track.get(layer).map(|n| n.rect()).unwrap_or_default();
let (sw, sh) = (lr.width, lr.height);
let a = match &anchor.target {
crate::track::AnchorTarget::Key(key) => track.find_by_key(key)?,
crate::track::AnchorTarget::Node(id) => *id,
};
let ar = track.get(a).map(|n| n.rect()).unwrap_or_default();
let mut p = anchor.placement;
match p {
Placement::Below
if ar.bottom() + ANCHOR_GAP + sh > window.height
&& ar.y - ANCHOR_GAP - sh >= 0.0 =>
{
p = Placement::Above
}
Placement::Above
if ar.y - ANCHOR_GAP - sh < 0.0
&& ar.bottom() + ANCHOR_GAP + sh <= window.height =>
{
p = Placement::Below
}
Placement::RightOf
if ar.right() + ANCHOR_GAP + sw > window.width
&& ar.x - ANCHOR_GAP - sw >= 0.0 =>
{
p = Placement::LeftOf
}
Placement::LeftOf
if ar.x - ANCHOR_GAP - sw < 0.0
&& ar.right() + ANCHOR_GAP + sw <= window.width =>
{
p = Placement::RightOf
}
_ => {}
}
let (mut x, mut y) = match p {
Placement::Below => (ar.x, ar.bottom() + ANCHOR_GAP),
Placement::Above => (ar.x, ar.y - ANCHOR_GAP - sh),
Placement::RightOf => (ar.right() + ANCHOR_GAP, ar.y),
Placement::LeftOf => (ar.x - ANCHOR_GAP - sw, ar.y),
Placement::ScreenCenter => ((window.width - sw) * 0.5, (window.height - sh) * 0.5),
Placement::Fixed { x, y } => (x, y),
};
if !matches!(p, Placement::Fixed { .. }) {
x = x.clamp(0.0, (window.width - sw).max(0.0));
y = y.clamp(0.0, (window.height - sh).max(0.0));
}
Some((x, y))
}
pub fn layout_subtree(track: &mut Track, id: NodeId, available: Size) -> LayoutStats {
if !track.contains(id) {
return LayoutStats::default();
}
let mut flex = build(track, id);
flex.layout(available.width, available.height, Direction::Ltr);
let r = track.get(id).map(|n| n.rect()).unwrap_or_default();
let (ox, oy) = (r.x - flex.get_left(), r.y - flex.get_top());
let mut st = LayoutStats {
ran: true,
boundaries: 1,
..Default::default()
};
write_back(track, id, &flex, ox, oy, &mut st);
track.clear_layout_flags();
st
}
pub(crate) fn layout_children(track: &Track, id: NodeId) -> Vec<NodeId> {
track
.children(id)
.iter()
.copied()
.filter(|c| {
track
.get(*c)
.map(|n| n.visibility != Visibility::Collapsed)
.unwrap_or(false)
})
.collect()
}
pub(crate) fn build(track: &Track, id: NodeId) -> FlexNode {
let Some(n) = track.get(id) else {
return FlexNode::new(id.to_u64(), Default::default());
};
let mut flex = FlexNode::new(id.to_u64(), n.layout.clone());
match &n.kind {
Kind::Text(s) => flex.measure_text = Some((s.clone(), n.text.spec.clone())),
Kind::Button { label } => flex.measure_text = Some((label.clone(), n.text.spec.clone())),
Kind::Input {
text, placeholder, ..
} => {
let s = if text.is_empty() { placeholder } else { text };
flex.measure_text = Some((s.clone(), n.text.spec.clone()));
}
Kind::Image(img) => flex.intrinsic_size = Some((img.width as f32, img.height as f32)),
Kind::Custom(cell) => {
let s = cell.intrinsic_size();
if s.width > 0.0 || s.height > 0.0 {
flex.intrinsic_size = Some((s.width, s.height));
}
}
_ => {}
}
for c in layout_children(track, id) {
flex.children.push(build(track, c));
}
if n.layout.overflow_scroll {
for c in flex.children.iter_mut() {
c.style.flex_shrink = 0.0;
c.style.flex_grow = 0.0;
}
}
flex
}
fn write_back(
track: &mut Track,
id: NodeId,
flex: &FlexNode,
ox: f32,
oy: f32,
st: &mut LayoutStats,
) {
let x = ox + flex.get_left();
let y = oy + flex.get_top();
let w = flex.get_width();
let h = flex.get_height();
let is_scroll = track
.get(id)
.map(|n| n.layout.overflow_scroll)
.unwrap_or(false);
let children = layout_children(track, id);
let old_rect = track.get(id).map(|n| n.rect()).unwrap_or_default();
let desired = desired_size(track, id, flex, w, h);
if let Some(n) = track.get_mut(id) {
n.computed = ComputedLayout {
x,
y,
width: w,
height: h,
overflow_scroll: is_scroll,
};
n.desired = desired;
if matches!(&n.kind, Kind::Text(_) | Kind::Button { .. }) {
n.text_wrap = flex.measured_wrap_width;
}
}
let new_rect = Rect::new(x, y, w, h);
if !rect_eq(old_rect, new_rect) {
st.moved += 1;
track.damage_rect(old_rect);
track.damage_rect(new_rect);
}
st.nodes += 1;
let (child_ox, child_oy) = if is_scroll {
st.scroll_containers += 1;
let mut cw = 0.0f32;
let mut ch = 0.0f32;
for c in flex.children.iter() {
cw = cw.max(c.get_left() + c.get_width() + c.get_layout_end_margin(FlexDirection::Row));
ch = ch.max(c.get_top() + c.get_height() + c.get_layout_end_margin(FlexDirection::Column));
}
let content = Size::new(cw, ch);
let (sx, sy) = track.scroll_offset(id);
let nx = sx.clamp(0.0, (cw - w).max(0.0));
let ny = sy.clamp(0.0, (ch - h).max(0.0));
if (nx - sx).abs() > 1e-4 || (ny - sy).abs() > 1e-4 {
st.clamped_scroll += 1;
track.set_scroll_offset(id, (nx, ny));
}
if let Some(n) = track.get_mut(id) {
n.content_size = content;
}
(x - nx, y - ny)
} else {
(x, y)
};
for (c, fc) in children.iter().zip(flex.children.iter()) {
write_back(track, *c, fc, child_ox, child_oy, st);
}
}
fn desired_size(track: &Track, id: NodeId, flex: &FlexNode, w: f32, h: f32) -> Size {
if !flex.children.is_empty() {
return Size::new(w, h);
}
match track.get(id).map(|n| &n.kind) {
Some(Kind::Text(s)) => {
let spec = track.get(id).map(|n| n.text.spec.clone()).unwrap_or_default();
let (mw, mh) = TextEngine::measure_text(s, &spec);
Size::new(mw as f32, mh as f32)
}
Some(Kind::Input {
text, placeholder, ..
}) => {
let spec = track.get(id).map(|n| n.text.spec.clone()).unwrap_or_default();
let s = if text.is_empty() { placeholder } else { text };
let (mw, mh) = TextEngine::measure_text(s, &spec);
Size::new(mw as f32, mh as f32)
}
Some(Kind::Image(img)) => Size::new(img.width as f32, img.height as f32),
Some(Kind::Custom(cell)) => {
let s = cell.intrinsic_size();
if s.width > 0.0 || s.height > 0.0 {
Size::new(s.width, s.height)
} else {
Size::new(w, h)
}
}
_ => Size::new(w, h),
}
}
fn rect_eq(a: Rect, b: Rect) -> bool {
(a.x - b.x).abs() < 1e-3
&& (a.y - b.y).abs() < 1e-3
&& (a.width - b.width).abs() < 1e-3
&& (a.height - b.height).abs() < 1e-3
}
pub fn rect_of(track: &Track, id: NodeId) -> Rect {
track.get(id).map(|n| n.rect()).unwrap_or_default()
}
pub fn contains(track: &Track, id: NodeId, p: Point) -> bool {
rect_of(track, id).contains(p)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::track::{Anchor, Flags, Key, Kind, Layer};
use lieui_layout::FlexDirection;
fn text(t: &mut Track, s: &str, size: f64) -> NodeId {
let id = t.create(Kind::Text(s.to_string()), None);
t.get_mut(id).unwrap().text.spec.font_size = size;
id
}
fn fixed(t: &mut Track, w: f32, h: f32) -> NodeId {
let id = t.create(Kind::Box, None);
t.get_mut(id).unwrap().layout.dim = [w, h];
id
}
const WIN: Size = Size::new(300.0, 200.0);
fn anchored(ax: f32, ay: f32, placement: Placement) -> (Track, NodeId, NodeId) {
let mut t = Track::new();
let content = t.create(Kind::Box, None);
t.add_root(Layer::Content, None, content);
let anchor = fixed(&mut t, 100.0, 30.0);
{
let n = t.get_mut(anchor).unwrap();
n.key = Some(Key::from("anchor"));
n.layout = n.layout.clone().margin_left(ax).margin_top(ay);
}
t.append_child(content, anchor);
let popup = t.create(Kind::Box, None);
let txt = text(&mut t, "菜单项", 14.0);
t.append_child(popup, txt);
let rid = t.add_root(Layer::Popup, None, popup);
t.root_mut(rid).unwrap().opts.anchor = Some(Anchor {
target: crate::track::AnchorTarget::Key(Key::from("anchor")),
placement,
});
(t, anchor, popup)
}
fn place(t: &mut Track) -> (Rect, Rect) {
layout(t, WIN);
place_anchored_layers(t, WIN);
let anchor = t.find_by_key(&Key::from("anchor")).unwrap();
let popup = t
.roots()
.iter()
.find(|r| r.layer == Layer::Popup)
.unwrap()
.node;
(t.get(anchor).unwrap().rect(), t.get(popup).unwrap().rect())
}
#[test]
fn popup_places_below_the_anchor_with_a_gap() {
let (mut t, _, popup) = anchored(50.0, 40.0, Placement::Below);
let (ar, pr) = place(&mut t);
assert_eq!(pr.x, ar.x, "Below 与锚点左对齐");
assert!((pr.y - (ar.bottom() + ANCHOR_GAP)).abs() < 0.5, "{ar:?} -> {pr:?}");
let _ = popup;
}
#[test]
fn popup_shrink_wraps_instead_of_filling_the_window() {
let (mut t, _, _) = anchored(50.0, 40.0, Placement::Below);
let (_, pr) = place(&mut t);
assert!(pr.width < WIN.width && pr.height < WIN.height, "内容自适应:{pr:?}");
}
#[test]
fn popup_flips_above_when_below_overflows() {
let (mut t, ar_id, _) = anchored(20.0, 160.0, Placement::Below);
let (_, pr) = place(&mut t);
let ar = t.get(ar_id).unwrap().rect();
assert!(pr.bottom() <= ar.y + 0.5, "层应在锚点上方:{ar:?} -> {pr:?}");
assert!(pr.y >= 0.0);
}
#[test]
fn popup_clamps_inside_the_window_when_neither_side_fits() {
let (mut t, _, popup) = anchored(20.0, 160.0, Placement::Below);
t.get_mut(popup).unwrap().layout.dim = [80.0, 190.0];
let (_, pr) = place(&mut t);
assert!((pr.y - (WIN.height - pr.height)).abs() < 0.5, "钳到视口内:{pr:?}");
assert!(pr.y >= 0.0 && pr.bottom() <= WIN.height);
}
#[test]
fn right_of_flips_to_left_of_near_the_right_edge() {
let (mut t, ar_id, _) = anchored(180.0, 20.0, Placement::RightOf);
let (_, pr) = place(&mut t);
let ar = t.get(ar_id).unwrap().rect();
assert!(pr.right() <= ar.x + 0.5, "层应在锚点左侧:{ar:?} -> {pr:?}");
}
#[test]
fn screen_center_centers_the_layer() {
let (mut t, _, _) = anchored(50.0, 40.0, Placement::ScreenCenter);
let (_, pr) = place(&mut t);
assert!((pr.x - (WIN.width - pr.width) * 0.5).abs() < 0.5, "{pr:?}");
assert!((pr.y - (WIN.height - pr.height) * 0.5).abs() < 0.5, "{pr:?}");
}
#[test]
fn missing_anchor_key_keeps_the_layer_in_place() {
let (mut t, _, popup) = anchored(50.0, 40.0, Placement::Below);
if let Some(n) = t.get_mut(t.find_by_key(&Key::from("anchor")).unwrap()) {
n.key = Some(Key::from("other"));
}
layout(&mut t, WIN);
let before = t.get(popup).unwrap().rect();
let moved = place_anchored_layers(&mut t, WIN);
assert_eq!(moved, 0);
assert_eq!(t.get(popup).unwrap().rect(), before);
}
#[test]
fn moving_the_layer_registers_old_and_new_damage() {
let (mut t, _, popup) = anchored(50.0, 40.0, Placement::Below);
layout(&mut t, WIN);
let _ = t.take_damage(); let moved = place_anchored_layers(&mut t, WIN);
assert_eq!(moved, 1);
let (rects, all) = t.take_damage();
assert!(!all);
assert!(rects.len() >= 2, "旧 ∪ 新位置都要重绘:{rects:?}");
let pr = t.get(popup).unwrap().rect();
assert!(rects.iter().any(|r| r.intersects(&pr)), "新位置在脏区里");
}
#[test]
fn fixed_placement_is_taken_literally() {
let (mut t, _, _) = anchored(50.0, 40.0, Placement::Fixed { x: 123.0, y: 77.0 });
let (_, pr) = place(&mut t);
assert!((pr.x - 123.0).abs() < 0.5 && (pr.y - 77.0).abs() < 0.5, "{pr:?}");
}
#[test]
fn column_with_padding_and_gap_positions_children() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
{
let n = t.get_mut(root).unwrap();
n.layout = n
.layout
.clone()
.padding_all(10.0)
.gap(5.0);
n.layout.flex_direction = FlexDirection::Column;
}
t.add_root(Layer::Content, None, root);
let a = text(&mut t, "hello", 16.0);
let b = text(&mut t, "world", 16.0);
t.append_child(root, a);
t.append_child(root, b);
layout(&mut t, Size::new(300.0, 200.0));
assert_eq!(rect_of(&t, root), Rect::new(0.0, 0.0, 300.0, 200.0), "自适应根填满窗口");
assert_eq!(rect_of(&t, a).x, 10.0, "padding 生效");
assert_eq!(rect_of(&t, a).y, 10.0);
assert_eq!(
rect_of(&t, b).y,
rect_of(&t, a).height + 10.0 + 5.0,
"gap 生效"
);
assert!(t.get(a).unwrap().desired.width > 0.0);
assert!(t.get(a).unwrap().desired.height > 0.0);
}
#[test]
fn row_children_share_width_by_flex_grow() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.get_mut(root).unwrap().layout.flex_direction = FlexDirection::Row;
t.get_mut(root).unwrap().layout.dim = [300.0, 100.0];
t.add_root(Layer::Content, None, root);
let a = fixed(&mut t, 100.0, 100.0);
let b = t.create(Kind::Box, None);
t.get_mut(b).unwrap().layout.flex_grow = 1.0;
t.append_child(root, a);
t.append_child(root, b);
layout(&mut t, Size::new(300.0, 100.0));
assert_eq!(rect_of(&t, a), Rect::new(0.0, 0.0, 100.0, 100.0));
assert_eq!(rect_of(&t, b), Rect::new(100.0, 0.0, 200.0, 100.0), "吃掉剩余宽");
}
#[test]
fn wrapped_text_draws_with_the_same_wrap_width_as_measure() {
use vello_cpu::color::PremulRgba8;
let mut t = Track::new();
let root = t.create(Kind::Box, None);
{
let n = t.get_mut(root).unwrap();
n.layout.flex_direction = FlexDirection::Column;
n.layout.dim = [120.0, 120.0];
}
t.add_root(Layer::Content, None, root);
let text = t.create(
Kind::Text("这是一段足够长的中文文本,用来验证换行测度与绘制一致".into()),
None,
);
{
let n = t.get_mut(text).unwrap();
n.text.spec.font_size = 14.0;
n.text.color = lieui_geom::Color::BLACK;
}
t.append_child(root, text);
layout(&mut t, Size::new(120.0, 120.0));
let rect = rect_of(&t, text);
assert!(rect.height > 30.0, "应当换行成多行:h={}", rect.height);
let mut r = crate::render::Renderer::new(Size::new(120.0, 120.0), lieui_geom::Color::WHITE);
r.render(&t, &[], true);
let pix = r.pixmap();
let pw = usize::from(pix.width());
let (mut top, mut bottom) = (usize::MAX, 0usize);
for y in 0..120usize {
for x in 0..120usize {
let p: PremulRgba8 = pix.data()[y * pw + x];
if p.r < 240 || p.g < 240 || p.b < 240 {
top = top.min(y);
bottom = bottom.max(y);
}
}
}
let drawn = (bottom - top + 1) as f32;
assert!(
drawn > rect.height * 0.6,
"画出的墨迹带要覆盖盒子的大部分(不再是只画第一行):\
墨迹 {top}..{bottom}({drawn}px) vs 盒子高 {}",
rect.height
);
}
#[test]
fn optical_align_changes_the_measured_height() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
{
let n = t.get_mut(root).unwrap();
n.layout.dim = [300.0, 100.0];
n.layout.flex_direction = FlexDirection::Row;
n.layout.align_items = lieui_layout::FlexAlign::Center;
}
t.add_root(Layer::Content, None, root);
let plain = t.create(Kind::Text("abc".into()), None);
let optical = t.create(Kind::Text("abc".into()), None);
t.get_mut(plain).unwrap().text.spec.font_size = 13.0;
{
let n = t.get_mut(optical).unwrap();
n.text.spec.font_size = 13.0;
n.text.spec.optical_align = true;
}
t.append_child(root, plain);
t.append_child(root, optical);
layout(&mut t, Size::new(300.0, 100.0));
let h_plain = rect_of(&t, plain).height;
let h_optical = rect_of(&t, optical).height;
assert!(
h_optical < h_plain,
"光学高度 = 墨迹高 < 行盒高:{h_optical} < {h_plain}"
);
assert!((rect_of(&t, plain).center().y - 50.0).abs() < 0.01);
assert!((rect_of(&t, optical).center().y - 50.0).abs() < 0.01);
}
#[test]
fn fixed_size_subtree_is_a_boundary() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.add_root(Layer::Content, None, root);
let card = fixed(&mut t, 300.0, 200.0);
t.append_child(root, card);
let label = text(&mut t, "hi", 16.0);
t.append_child(card, label);
let st = layout(&mut t, Size::new(500.0, 400.0));
assert_eq!(st.boundaries, 1, "只有内容根是边界");
assert_eq!(rect_of(&t, card), Rect::new(0.0, 0.0, 300.0, 200.0));
let old_desired = t.get(label).unwrap().desired.width;
t.get_mut(label).unwrap().kind = Kind::Text("hi there, this is a much longer text".into());
t.mark_layout_dirty(label);
let st2 = layout(&mut t, Size::new(500.0, 400.0));
assert_eq!(st2.boundaries, 1, "边界收敛到 card");
assert_eq!(st2.nodes, 2, "只重建了 card 子树(不含 root)");
assert_eq!(rect_of(&t, card), Rect::new(0.0, 0.0, 300.0, 200.0), "card 尺寸不变");
assert!(
t.get(label).unwrap().desired.width > old_desired,
"文本被重新测度"
);
}
#[test]
fn window_resize_marks_all_roots_as_boundaries() {
let mut t = Track::new();
let content = t.create(Kind::Box, None);
t.add_root(Layer::Content, None, content);
let modal = t.create(Kind::Box, None);
t.add_root(Layer::Modal, None, modal);
layout(&mut t, Size::new(300.0, 100.0));
assert_eq!(rect_of(&t, content), Rect::new(0.0, 0.0, 300.0, 100.0));
t.mark_all_layout_dirty();
let st = layout(&mut t, Size::new(400.0, 200.0));
assert_eq!(st.boundaries, 2, "两个层根");
assert_eq!(rect_of(&t, content), Rect::new(0.0, 0.0, 400.0, 200.0));
assert_eq!(rect_of(&t, modal), Rect::new(0.0, 0.0, 400.0, 200.0));
}
#[test]
fn collapsed_children_are_excluded_from_the_flex_tree() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.get_mut(root).unwrap().layout.flex_direction = FlexDirection::Row;
t.get_mut(root).unwrap().layout.dim = [300.0, 100.0];
t.add_root(Layer::Content, None, root);
let a = fixed(&mut t, 100.0, 100.0);
let b = fixed(&mut t, 100.0, 100.0);
let c = fixed(&mut t, 100.0, 100.0);
for k in [a, b, c] {
t.append_child(root, k);
}
layout(&mut t, Size::new(300.0, 100.0));
assert_eq!(rect_of(&t, b).x, 100.0);
crate::cmd::apply_cmds(
&mut t,
&[crate::cmd::Cmd::SetVisibility {
id: b,
visibility: crate::track::Visibility::Collapsed,
}],
);
let st = layout(&mut t, Size::new(300.0, 100.0));
assert!(st.ran);
assert_eq!(rect_of(&t, b).x, 100.0, "收起节点不重排(保留旧 rect)");
assert_eq!(rect_of(&t, c).x, 100.0, "兄弟补位到 b 的位置");
}
#[test]
fn scroll_container_reports_content_size_and_shifts_children() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
{
let n = t.get_mut(root).unwrap();
n.layout.dim = [100.0, 100.0];
n.layout.overflow_scroll = true;
}
t.add_root(Layer::Content, None, root);
let child = fixed(&mut t, 80.0, 300.0);
t.append_child(root, child);
layout(&mut t, Size::new(100.0, 100.0));
assert_eq!(t.get(root).unwrap().content_size, Size::new(80.0, 300.0));
assert_eq!(rect_of(&t, child), Rect::new(0.0, 0.0, 80.0, 300.0));
t.set_scroll_offset(root, (0.0, 50.0));
t.mark_layout_dirty(root);
layout(&mut t, Size::new(100.0, 100.0));
assert_eq!(rect_of(&t, child).y, -50.0);
t.set_scroll_offset(root, (0.0, 1000.0));
t.mark_layout_dirty(root);
let st = layout(&mut t, Size::new(100.0, 100.0));
assert_eq!(st.clamped_scroll, 1);
assert_eq!(t.scroll_offset(root), (0.0, 200.0));
assert_eq!(rect_of(&t, child).y, -200.0);
}
#[test]
fn scroll_children_do_not_shrink_to_the_viewport() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
{
let n = t.get_mut(root).unwrap();
n.layout.dim = [100.0, 100.0];
n.layout.flex_direction = FlexDirection::Column;
n.layout.overflow_scroll = true;
}
t.add_root(Layer::Content, None, root);
let child = fixed(&mut t, 100.0, 300.0);
t.append_child(root, child);
layout(&mut t, Size::new(100.0, 100.0));
assert_eq!(rect_of(&t, child).height, 300.0, "滚动内容保持自然高度");
assert_eq!(t.get(root).unwrap().content_size.height, 300.0);
}
#[test]
fn layout_is_a_no_op_when_nothing_is_dirty() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.add_root(Layer::Content, None, root);
layout(&mut t, Size::new(300.0, 100.0));
let st = layout(&mut t, Size::new(300.0, 100.0));
assert!(!st.ran);
assert_eq!(st.boundaries, 0);
}
#[test]
fn layout_reports_old_and_new_bounds_as_damage() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.get_mut(root).unwrap().layout.flex_direction = FlexDirection::Column;
t.add_root(Layer::Content, None, root);
let a = text(&mut t, "a", 16.0);
t.append_child(root, a);
layout(&mut t, Size::new(300.0, 100.0));
let _ = t.take_damage();
t.get_mut(a).unwrap().flags = Flags::EMPTY;
let b = text(&mut t, "b", 16.0);
t.append_child(root, b);
let st = layout(&mut t, Size::new(300.0, 100.0));
assert!(st.ran);
let (rects, _all) = t.take_damage();
assert!(!rects.is_empty(), "布局变化应登记脏矩形");
}
#[test]
fn layout_subtree_helper_lays_out_one_subtree() {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.add_root(Layer::Content, None, root);
let card = fixed(&mut t, 120.0, 40.0);
t.append_child(root, card);
let inner = text(&mut t, "x", 12.0);
t.append_child(card, inner);
let st = layout_subtree(&mut t, card, Size::new(120.0, 40.0));
assert!(st.ran);
assert_eq!(st.nodes, 2);
assert_eq!(rect_of(&t, card), Rect::new(0.0, 0.0, 120.0, 40.0));
assert!(contains(&t, inner, Point::new(1.0, 1.0)));
}
}