A widget's region is now held in the coordinates of the slot it draws in rather than the window's, and `Painter::place` is how a container asks for a slot: it draws a child it decides the box of and may decide again. Everything under that slot is a fraction of its box, so placing the child a second time is one entry to write whether it moved or changed length. A child drawn any other way has no slot and shares its nearest ancestor's. That is what keeps the chain short. `chain_cost` measured depth as the cost -- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs past that for nothing, since almost every slot was zero. `Span`, `Aligned` and `Scroll` are the containers that re-place a child after drawing it, and `tests/layout.rs` pins that four widgets between a span and a leaf leave the leaf's chain one deep. `UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are gone. Nothing is inverted any more: a box that changed length is written to its slot, and the descendants recompose against it in the shader. That also retires the case the guard existed for, where a fixed length has no fraction to recover -- `tests/layout.rs` now stretches a 40-tall row on its other axis, which `stretchable` refused outright. What still walks the CPU is deciding who must draw again, which no chain can answer: `mark_resized` descends from the widget whose box changed and marks anything whose own box changed length and whose drawing reads it. A part of a box with no relative extent on an axis is a fixed length, and composing into it leaves none either, so the walk stops where a length did not change -- an 80-wide child in a widened row is not redrawn though it says `Redraw`. `Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`: each places in fractions and offsets of its own box and none reads the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay `Redraw`. 45 tests pass, five of them new. Render verification comes after the CPU side, per the owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
6.5 KiB
Rust
185 lines
6.5 KiB
Rust
//! Where a frame puts things, with no window to put them in.
|
|
|
|
use iris::harness::{Harness, assert_corners};
|
|
use iris::prelude::*;
|
|
|
|
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
|
|
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
|
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
let right = rect(Color::BLUE).add(&mut h.rsc);
|
|
h.set_root((left, right).span(Dir::RIGHT));
|
|
(left.id(), right.id())
|
|
}
|
|
|
|
#[test]
|
|
fn a_span_gives_each_child_the_width_it_asked_for() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (left, right) = two_rects(&mut h);
|
|
|
|
assert_corners!(h, left, (0, 0), (100, 200));
|
|
assert_corners!(h, right, (100, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn resizing_relays_out_against_the_new_output() {
|
|
let mut h = Harness::new((400, 200));
|
|
let (left, right) = two_rects(&mut h);
|
|
|
|
h.resize((800, 100));
|
|
assert!(h.needs_redraw());
|
|
h.frame();
|
|
|
|
assert_corners!(h, left, (0, 0), (100, 100));
|
|
assert_corners!(h, right, (100, 0), (800, 100));
|
|
}
|
|
|
|
#[test]
|
|
fn an_empty_widget_takes_a_share_of_a_span() {
|
|
let mut h = Harness::new((400, 200));
|
|
let gap = ().add(&mut h.rsc);
|
|
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
|
h.set_root((gap, right).span(Dir::RIGHT));
|
|
|
|
assert_corners!(h, gap, (0, 0), (300, 200));
|
|
assert_corners!(h, right, (300, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_child_drawn_twice_moves_once() {
|
|
let mut h = Harness::new((400, 200));
|
|
// `Aligned` draws its child twice; listing it twice would move it twice.
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let centered = inner.center().width(200).add(&mut h.rsc);
|
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((left, centered).span(Dir::RIGHT));
|
|
assert_corners!(h, inner, (100, 0), (300, 200));
|
|
|
|
h.rsc[left].x = Some(Len::abs(150));
|
|
h.frame();
|
|
|
|
assert_corners!(h, inner, (150, 0), (350, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_resize_lands_where_a_cold_start_would() {
|
|
let build = |h: &mut Harness| {
|
|
let para = wtext(
|
|
"Wrapping shapes one source into as many lines as its container leaves room \
|
|
for, so the height of a paragraph is an answer rather than a setting.",
|
|
)
|
|
.size(20)
|
|
.wrap(true)
|
|
.pad(16)
|
|
.add(&mut h.rsc);
|
|
let below = rect(Color::RED).add(&mut h.rsc);
|
|
let root = (para, below).span(Dir::DOWN).pad(12);
|
|
h.set_root(root);
|
|
(para, below)
|
|
};
|
|
|
|
let mut cold = Harness::new((900, 1200));
|
|
let (cold_para, cold_below) = build(&mut cold);
|
|
|
|
let mut resized = Harness::new((1920, 1200));
|
|
let (para, below) = build(&mut resized);
|
|
resized.resize((900, 1200));
|
|
resized.frame();
|
|
|
|
assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph");
|
|
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
|
|
}
|
|
|
|
#[test]
|
|
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
|
|
let mut h = Harness::new((400, 400));
|
|
// The panel fills a stack sized by its sibling, so it is drawn in the
|
|
// whole box and then placed in the shorter one. Reusing it in that fixed
|
|
// box afterwards would leave it whatever height it happened to have.
|
|
let panel = rect(Color::BLUE).add(&mut h.rsc);
|
|
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
|
|
let stack = (panel, leaf)
|
|
.stack()
|
|
.size(StackSize::Child(1))
|
|
.add(&mut h.rsc);
|
|
h.set_root(stack.align(Align::TOP));
|
|
assert_corners!(h, panel, (0, 0), (400, 100));
|
|
|
|
h.rsc[leaf].y = Some(Len::abs(250));
|
|
h.frame();
|
|
|
|
assert_corners!(h, panel, (0, 0), (400, 250));
|
|
}
|
|
|
|
#[test]
|
|
fn a_moved_subtree_takes_its_children_with_it() {
|
|
let mut h = Harness::new((400, 400));
|
|
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
|
h.set_root((first, row).span(Dir::DOWN));
|
|
assert_corners!(h, inner, (10, 50), (390, 70));
|
|
|
|
h.rsc[first].y = Some(Len::abs(80));
|
|
h.frame();
|
|
|
|
// The row is the same shape somewhere else, so one slot moved it and
|
|
// `inner`'s own region was never rewritten.
|
|
assert_corners!(h, inner, (10, 90), (390, 110));
|
|
}
|
|
|
|
#[test]
|
|
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
|
|
let mut h = Harness::new((400, 200));
|
|
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
|
|
let rest = rect(Color::GREEN).add(&mut h.rsc);
|
|
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
|
// Changing the bar's width is the only thing that changes the box the
|
|
// panel and everything under it was drawn for.
|
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((bar, panel).span(Dir::RIGHT));
|
|
assert_corners!(h, fixed, (100, 0), (150, 200));
|
|
assert_corners!(h, rest, (150, 0), (400, 200));
|
|
|
|
h.rsc[bar].x = Some(Len::abs(200));
|
|
h.frame();
|
|
|
|
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
|
|
// against its new start and the one taking the rest absorbs the change.
|
|
assert_corners!(h, fixed, (200, 0), (250, 200));
|
|
assert_corners!(h, rest, (250, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
|
let mut h = Harness::new((400, 200));
|
|
// The row is 40 tall whatever happens, which used to make its drawing
|
|
// impossible to take out of: recovering a fraction of a box needs a
|
|
// relative extent, and it has none on that axis.
|
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
|
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
|
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
|
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((bar, column).span(Dir::RIGHT));
|
|
assert_corners!(h, inner, (110, 10), (390, 30));
|
|
|
|
h.rsc[bar].x = Some(Len::abs(200));
|
|
h.frame();
|
|
|
|
assert_corners!(h, inner, (210, 10), (390, 30));
|
|
}
|
|
|
|
#[test]
|
|
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
|
let mut h = Harness::new((400, 200));
|
|
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
|
// Four widgets between the span and the leaf, none of which places what
|
|
// it draws, so all of them share the span's slot.
|
|
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
|
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
h.set_root((bar, buried).span(Dir::RIGHT));
|
|
|
|
let slot = h.render.active[&leaf.id()].parent_move;
|
|
assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf");
|
|
}
|