A box in pixels was composed back up the move chain, on a grid fine enough that the walk rounded once, while a widget's offer was threaded down through its ancestors' offers. Two routes to one length, which is what `Holds::through` allowed for -- and the offer's route broke at a region node. `offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved that against the node's slot entry, which holds the box its parent *placed* the node in. Under a `Scroll` that is as long as the content rather than the viewport, so everything below was re-asked at a width its own answer had produced and the old answer confirmed itself: shrinker seed 220 on `reorder` left a widget 290px out. `ActiveData` now keeps a widget's box as lengths of its parent's box -- `given_len`, and `offer_len` for the box it was first asked about -- and `DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a time: the box its parent gave it, then the part of that box its own answer placed the drawing in, which `placed_lens` states once for both `placed_box` and the walk. `Painter::px_size` and `px_len` read that value, and `UiRenderState::asked_px` takes the same steps back up the parent chain where a local redraw starts part-way down the tree. Neither chain has a coordinate frame in it, so neither can break at a region node, and warm and cold reach every length by the same expression. Three things follow. `Holds::through` is the exact preimage of `px + floor(rel * box)` -- two divisions, no allowance, the whole of a box mapping back to itself. A local redraw asks in the box its parent gave it and only where that box is as long as the offer, which retires `redraw`'s third ask and the region-node exception beside it; `draw_inner` places the answer inside that box itself. And symbolic regions are left to the GPU, hit testing and remaps, where `Moves::resolve` is the only walk: `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region` and `slot_wide` are gone, 252 lines of `core/` net. `px` is deliberately not stored beside those lengths. A resize every widget's `Holds` admits redraws nothing, so a stored pixel length would be stale on every widget in the tree with nothing on it to say so, and refreshing it costs a walk down every reused subtree on the resize path. Instructions:u, medians of 21 runs, seed 1 at depth 8: | phase | before | after | | | --- | ---: | ---: | ---: | | `cold`, 200 frames | 313.1M | 312.9M | -0.04% | | `resize` | 408.1M | 405.6M | -0.61% | | `many` | 1,924M | 1,756M | -8.75% | | `scroll` | 357.3M | 323.4M | -9.49% | | `repaint` | 363.3M | 315.4M | -13.18% | `cold` and `resize` have all twenty-five work counters identical, so those two rows say the draw path costs the same threaded as composed. The other three do less work: `repaint` goes from 23 draw requests and 13 widget draws a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157. Primitive writes are unmoved in every phase. Verified: `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch replay before and after the gesture, and a live resize of `random` to 1280x800 is identical both to the old head's and to a cold render at that size. The oracle passes 100 seeds in release and 120 in debug -- the debug run is the one that exercises the `Holds` assertion -- and the fifteen shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`, which needs both halves of this to fail: the old chain with the old allowance passes it, and the old chain with the exact preimage does not. `AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the resize path re-expressing a part as a fraction of a box that changed length, not a length reached two ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
557 lines
19 KiB
Rust
557 lines
19 KiB
Rust
//! The smallest trees that laid out differently warm than cold, each shrunk
|
|
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
|
|
//! frame that had not settled: a wrapping text shaped at a width it was
|
|
//! measured in rather than the one it was given. The rest are a widget
|
|
//! measured again in a box its own answer had decided, where the old answer
|
|
//! is a fixed point whatever the content now says. The last three are
|
|
//! neither: one box length, composed two ways, landing either side of the
|
|
//! boundary that decided whether a child was drawn at all, and two boxes
|
|
//! reached through a region node's own entry rather than through the offer
|
|
//! that node was given.
|
|
|
|
use iris::harness::Harness;
|
|
use iris::prelude::*;
|
|
use iris::random::Branch;
|
|
|
|
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
|
/// the tree changes -- every widget is marked for redraw and the frame is
|
|
/// taken again -- so no box may move, and a warm frame has to land where a
|
|
/// cold one does.
|
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
|
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
|
let sized = wrapped.width(76).add(&mut h.rsc);
|
|
let aligned = sized;
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
|
let stack = Stack {
|
|
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
|
size: StackSize::Child(0),
|
|
}
|
|
.add(&mut h.rsc);
|
|
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
h.set_root(root);
|
|
vec![
|
|
plain.id(),
|
|
wrapped.id(),
|
|
sized.id(),
|
|
aligned.id(),
|
|
stack.id(),
|
|
root.id(),
|
|
]
|
|
}
|
|
|
|
/// The first frame does not reach the layout a second one does, so "cold" is
|
|
/// not a fixed point and comparing against it compares against a tree that
|
|
/// has not settled.
|
|
#[test]
|
|
fn one_frame_is_enough() {
|
|
let mut h = Harness::new((640, 900));
|
|
let ids = plant(&mut h);
|
|
let first = h.region(&ids[1]).unwrap();
|
|
for _ in 0..3 {
|
|
for &id in &ids {
|
|
h.rsc.widgets_mut().get_dyn_mut(id);
|
|
}
|
|
h.frame();
|
|
}
|
|
let settled = h.region(&ids[1]).unwrap();
|
|
println!(
|
|
"first frame {} tall, settled {} tall",
|
|
first.bot_right.y - first.top_left.y,
|
|
settled.bot_right.y - settled.top_left.y
|
|
);
|
|
assert_eq!(
|
|
first.bot_right.y - first.top_left.y,
|
|
settled.bot_right.y - settled.top_left.y,
|
|
"the first frame had not finished laying out"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn repainting_everything_moves_nothing() {
|
|
let mut warm = Harness::new((640, 900));
|
|
let ids = plant(&mut warm);
|
|
for &id in &ids {
|
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
|
}
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((640, 900));
|
|
let cold_ids = plant(&mut cold);
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|
|
|
|
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
|
/// is the same size whatever the output is, so a resize may not change any of
|
|
/// it -- but the text comes out 3.92px narrower warm than cold.
|
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
|
let aligned = text;
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
|
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
|
vec![
|
|
text.id(),
|
|
aligned.id(),
|
|
inner.id(),
|
|
sized.id(),
|
|
filler.id(),
|
|
root.id(),
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
|
let mut warm = Harness::new((1920, 1200));
|
|
let ids = plant_fixed(&mut warm);
|
|
warm.frame();
|
|
warm.resize((640, 900));
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((640, 900));
|
|
let cold_ids = plant_fixed(&mut cold);
|
|
cold.frame();
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|
|
|
|
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
|
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
|
/// ends up 29.9px from where the other does.
|
|
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
|
let wrapped = wtext("Wrapping shapes one source into as many lines")
|
|
.size(16)
|
|
.wrap(true)
|
|
.add(&mut h.rsc);
|
|
let plain = wtext("one line, overflowing whatever it is given")
|
|
.size(16)
|
|
.wrap(false)
|
|
.add(&mut h.rsc);
|
|
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
|
|
let second: StrongWidget = plain.add_strong(&mut h.rsc);
|
|
let children = match swapped {
|
|
true => vec![second, first],
|
|
false => vec![first, second],
|
|
};
|
|
let span = Span {
|
|
children,
|
|
dir: Dir::RIGHT,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let span_handle = span;
|
|
let aligned = span;
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
|
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
|
(
|
|
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
|
span_handle,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
|
let mut warm = Harness::new((640, 900));
|
|
let (ids, span) = plant_pair(&mut warm, false);
|
|
warm.frame();
|
|
warm.rsc[span].children.rotate_left(1);
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((640, 900));
|
|
let (cold_ids, _) = plant_pair(&mut cold, true);
|
|
cold.frame();
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|
|
|
|
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
|
/// content from what the content says, and hands that box down through a
|
|
/// pass-through; the span under it was given that box once, so nothing at its
|
|
/// own edge says the box was its own answer.
|
|
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
|
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
|
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
|
let mut inner_children: Vec<StrongWidget> =
|
|
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
|
|
if swapped {
|
|
inner_children.rotate_left(1);
|
|
}
|
|
let inner = Span {
|
|
children: inner_children,
|
|
dir: Dir::RIGHT,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let block = rect(Color::RED).add(&mut h.rsc);
|
|
let fixed = block.width(87).add(&mut h.rsc);
|
|
let mut outer_children: Vec<StrongWidget> =
|
|
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
|
if swapped {
|
|
outer_children.rotate_left(1);
|
|
}
|
|
let outer = Span {
|
|
children: outer_children,
|
|
dir: Dir::RIGHT,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc);
|
|
// Carried no rule even before rules were a property: it is here to be a
|
|
// widget between the span and the scroll, not to declare anything.
|
|
let through = (outer,).span(Dir::RIGHT).add(&mut h.rsc);
|
|
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
|
h.state.root = Some(scroll.add_strong(&mut h.rsc));
|
|
(
|
|
vec![
|
|
text.id(),
|
|
filler.id(),
|
|
inner.id(),
|
|
block.id(),
|
|
fixed.id(),
|
|
outer.id(),
|
|
through.id(),
|
|
scroll.id(),
|
|
],
|
|
[inner, outer],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
|
let mut warm = Harness::new((640, 900));
|
|
let (ids, spans) = plant_scrolled(&mut warm, false);
|
|
warm.frame();
|
|
for span in spans {
|
|
warm.rsc[span].children.rotate_left(1);
|
|
}
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((640, 900));
|
|
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
|
cold.frame();
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|
|
|
|
/// Reports a width derived from the box it is asked in. Reading through the
|
|
/// painter is its declaration that the answer holds for that width only.
|
|
struct Wider {
|
|
extra: f32,
|
|
}
|
|
|
|
impl Widget for Wider {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
Size {
|
|
x: LayoutLen {
|
|
px: painter.px_len(Axis::X) + Px::from_f32(self.extra),
|
|
..LayoutLen::ZERO
|
|
},
|
|
y: LayoutLen::LEFTOVER,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
|
let content = Wider { extra }.add(&mut h.rsc);
|
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
|
let root = scroll;
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
|
h.set_root(root);
|
|
(content, scroll.id())
|
|
}
|
|
|
|
#[test]
|
|
fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() {
|
|
let mut warm = Harness::new((100, 100));
|
|
let (content, scroll) = plant_wider(&mut warm, 50.0);
|
|
warm.rsc[content].extra = 70.0;
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((100, 100));
|
|
let (_, cold_scroll) = plant_wider(&mut cold, 70.0);
|
|
|
|
assert_eq!(warm.region(&scroll), cold.region(&cold_scroll));
|
|
}
|
|
|
|
/// Six widgets, shrunk from 266. `measured`'s box is exactly the height of its
|
|
/// one fixed child, which is the box a parent sizing itself from that answer
|
|
/// hands back -- so whether its leftover-only child was drawn at all came down
|
|
/// to the 0.00003 px the composed length differs by, one way warm and the
|
|
/// other cold.
|
|
fn plant_boundary(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
|
let plain = wtext("one line, overflowing whatever it is given")
|
|
.size(16)
|
|
.wrap(false)
|
|
.add(&mut h.rsc);
|
|
let mut pair: Vec<StrongWidget> =
|
|
vec![filler.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
|
if swapped {
|
|
pair.rotate_left(1);
|
|
}
|
|
let measured = Span {
|
|
children: pair,
|
|
dir: Dir::DOWN,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc);
|
|
// Takes the whole box on its own, so the span above has nothing left to
|
|
// divide and `measured` is given exactly the text's height.
|
|
let whole = rect(Color::RED).add(&mut h.rsc);
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_size_rules(whole, None, Some(LayoutLen::rel(1.0)));
|
|
let mut inner_children: Vec<StrongWidget> = vec![
|
|
measured.add_strong(&mut h.rsc),
|
|
whole.add_strong(&mut h.rsc),
|
|
];
|
|
if swapped {
|
|
inner_children.rotate_left(1);
|
|
}
|
|
let inner = Span {
|
|
children: inner_children,
|
|
dir: Dir::DOWN,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.rsc
|
|
.widgets_mut()
|
|
.set_size_rules(inner, None, Some(LayoutLen::px(198.0)));
|
|
// One more span above it: without a box composed through it, both trees
|
|
// round the same way and the boundary is never crossed.
|
|
let outer = (inner,).span(Dir::DOWN).add(&mut h.rsc);
|
|
h.set_root(outer);
|
|
(
|
|
vec![
|
|
filler.id(),
|
|
plain.id(),
|
|
measured.id(),
|
|
whole.id(),
|
|
inner.id(),
|
|
outer.id(),
|
|
],
|
|
[measured, inner],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
|
let mut warm = Harness::new((640, 900));
|
|
let (ids, spans) = plant_boundary(&mut warm, false);
|
|
warm.frame();
|
|
for span in spans {
|
|
warm.rsc[span].children.rotate_left(1);
|
|
}
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((640, 900));
|
|
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
|
cold.frame();
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|
|
|
|
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
|
|
/// 18 grows at depth 6. A scroll inside a scroll, the inner one owning a
|
|
/// movable region of its own, and only its text marked for redraw. Nothing
|
|
/// about the tree changes, so no box may.
|
|
fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
|
|
let text = wtext("one line, overflowing whatever it is given")
|
|
.size(16)
|
|
.wrap(false)
|
|
.add(&mut h.rsc);
|
|
let inner = Scroll::new(text.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_region_node(inner.id(), true);
|
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_size_rules(
|
|
filler.id(),
|
|
Some(LayoutLen::px(87.0)),
|
|
Some(LayoutLen::px(24.0)),
|
|
);
|
|
let span = Span {
|
|
children: vec![inner.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)],
|
|
dir: Dir::DOWN,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let root = Scroll::new(span.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
|
h.set_root(root);
|
|
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
|
|
}
|
|
|
|
/// A local redraw asks a dirty widget in the box its parent gave it, and only
|
|
/// where that box is as long as the one it was offered; anything else is a
|
|
/// question its parent has to ask. This inner scroll's offer is the outer
|
|
/// scroll's whole viewport and the box it was given is 24px shorter -- the
|
|
/// height of the sized child the outer scroll snaps to the end of -- so what
|
|
/// it must not do is settle itself. It was drawn at its offer once, and the
|
|
/// inner scroll and its text stayed 24px too low.
|
|
#[test]
|
|
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
|
let mut warm = Harness::new((900, 1200));
|
|
let ids = plant_nested_scrolls(&mut warm);
|
|
warm.rsc.widgets_mut().get_dyn_mut(ids[0]);
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((900, 1200));
|
|
let cold_ids = plant_nested_scrolls(&mut cold);
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|
|
|
|
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
|
/// to. The pad owns a movable region and is the scroll's content, so the box
|
|
/// the scroll places it in is as long as that content while the box it was
|
|
/// offered is the viewport -- and with no padding to tell those two apart,
|
|
/// the span inside it looked like it was still at its offer. So everything
|
|
/// under the pad was asked again in the *placed* box, the offer resolving
|
|
/// against the node's own entry, which holds that box: the texts kept the
|
|
/// widths they had, the content stayed the length those widths make, and the
|
|
/// old answer confirmed itself. What the branch adds is a tree that differs
|
|
/// rather than a box that moved, since a probe measured at the wrong width
|
|
/// takes the other side.
|
|
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
|
let wide = rect(Color::GREEN).add(&mut h.rsc);
|
|
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
|
let branch = Branch {
|
|
probe: probe.add_strong(&mut h.rsc),
|
|
wide: wide.add_strong(&mut h.rsc),
|
|
narrow: narrow.add_strong(&mut h.rsc),
|
|
threshold: 213.0,
|
|
}
|
|
.add(&mut h.rsc);
|
|
let wrapped = wtext(
|
|
"Wrapping shapes one source into as many lines as the box \
|
|
leaves room for, so a paragraph's height is an answer and not a setting.",
|
|
)
|
|
.size(16)
|
|
.wrap(true)
|
|
.add(&mut h.rsc);
|
|
let plain = wtext("one line, overflowing whatever it is given")
|
|
.size(16)
|
|
.wrap(false)
|
|
.add(&mut h.rsc);
|
|
let row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
|
|
if swapped {
|
|
children.rotate_left(1);
|
|
}
|
|
Span {
|
|
children,
|
|
dir: Dir::RIGHT,
|
|
gap: Px::ZERO,
|
|
}
|
|
.add(&mut h.rsc)
|
|
};
|
|
let texts: Vec<StrongWidget> =
|
|
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
|
let inner = row(h, texts);
|
|
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
|
let outer = row(h, pair);
|
|
let pad = Pad {
|
|
padding: Padding::ZERO,
|
|
inner: outer.add_strong(&mut h.rsc),
|
|
}
|
|
.add(&mut h.rsc);
|
|
h.rsc.widgets_mut().set_region_node(pad.id(), true);
|
|
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
|
h.set_root(root);
|
|
(
|
|
vec![
|
|
probe.id(),
|
|
wide.id(),
|
|
narrow.id(),
|
|
branch.id(),
|
|
wrapped.id(),
|
|
plain.id(),
|
|
inner.id(),
|
|
outer.id(),
|
|
pad.id(),
|
|
root.id(),
|
|
],
|
|
[outer, inner],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
|
let mut warm = Harness::new((900, 1200));
|
|
let (ids, spans) = plant_under_a_node(&mut warm, false);
|
|
warm.frame();
|
|
for span in spans {
|
|
warm.rsc[span].children.rotate_left(1);
|
|
}
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((900, 1200));
|
|
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
|
|
cold.frame();
|
|
|
|
let mut wrong = Vec::new();
|
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
if got != want {
|
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
}
|
|
}
|
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
}
|