`.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.
46 lines
2.0 KiB
Rust
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));
|
|
}
|