Files
iris/tests/drift.rs
T
iris-ai 8220a78d4a Carry a length as a rule beside a widget, not a widget around it
`.width()` built a `SetSize` whose whole job was to answer `size_hint`, so
every declared length cost a widget, an `ActiveData` and a link of chain to
say one number. It is now a `SizeRule` per axis on `WidgetData`, beside
`region_node`, resolved by `Painter` where the widget is drawn. `SetSize`
and `MaxSize` are gone; `MaxSize` had no caller but its own builders.

That settles which of two answers is the size. A rule wins on the axis it
names and the `Size` returned by `draw` answers the rest, applied once in
`draw_inner` rather than by each widget that could carry one -- so the
widget under a rule never learns of it. `Painter::size_hint` reads the rule
first for the same reason: a rule that beats what a widget would draw has
to beat what it says about itself.

`declared_lens` still falls back to a non-leftover `size_hint`, which is
how an image or a gap gets its own pixel size rather than the whole offer.
That is the offer's business rather than a declaration's, and it falls away
when a widget occupies its reported size inside the box it was offered.

`known` and `declared` are separate because a share is a length to whoever
divides one and not to whoever composes a box: `.width(leftover(3))` is
known without drawing but cannot narrow anything.

Checked: fmt, clippy, 85 tests, and 100 generated seeds agreeing warm
against cold in 67.6 s. `minimal`, `text` and `view` render byte-identical
at 1920x1200; `tabs` differs only in the widget count it prints about
itself, which is two wrapper types smaller.
2026-09-15 19:44:21 -04:00

46 lines
2.0 KiB
Rust

//! What a retained drawing costs in accuracy when it is moved instead of made
//! again. A subtree's stored regions are the only record of where it is, so a
//! move that works from the last answer rather than from the box it is now in
//! integrates its own rounding, and nothing later recomputes it. Re-expressing
//! each part as the same fraction of the new box is what keeps a long-lived
//! layout on the one a cold start produces.
use iris::harness::Harness;
use iris::prelude::*;
/// A row of a fixed height under a bar, so changing the bar's height moves the
/// row without changing the box it is given: the move path, repeatedly.
fn plant(h: &mut Harness, bar_height: f32) -> (WeakWidget<Rect>, WeakWidget<Rect>) {
let bar = rect(Color::RED).height(bar_height).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = (inner, rect(Color::GREEN)).span(Dir::RIGHT).height(100);
h.set_root((bar, row).span(Dir::DOWN));
(bar, inner)
}
/// Enough moves to pass the 0.05 physical pixels layout treats as the same
/// place, for a move that adds an offset to the last answer. Measured on this
/// fixture on 2026-09-15: adding the offset to both ends of a span shortened
/// the row by 0.071 over this many moves and by 0.712 over ten times as many,
/// growing with the count rather than settling. Placing the far end from the
/// near one instead left 0.069, because the length is re-derived either way.
const MOVES: usize = 20_000;
#[test]
fn a_subtree_moved_many_times_stays_where_a_cold_layout_puts_it() {
let mut warm = Harness::new((640, 900));
let (bar, inner) = plant(&mut warm, 40.0);
let mut height = 40.0;
for step in 0..MOVES {
height = 40.0 + (step % 300) as f32 * 0.37;
warm.set_len(bar, Axis::Y, height);
warm.frame();
}
let mut cold = Harness::new((640, 900));
let (_, cold_inner) = plant(&mut cold, height);
cold.frame();
assert_eq!(warm.region(&inner), cold.region(&cold_inner));
}