Make the frame a length of the window and the box a region

There is one coordinate unit, the window. Every box in the tree is a
region in window units and a widget's frame is a length in the same
units, which is only what fractions resolve against, so the box need not
be the frame and padding can take from both without either becoming the
other. A region node's entry is a translation -- a rel 1 region anchored
where its box starts -- rather than a box, so nothing composes a frame
back up a chain and a node that moves is one entry write.

Padding is then an inset of both: its pixels come off the frame, so
rel(1.0) under it fills the padded widget rather than overflowing it,
and off the box, so what is drawn sits inside. A length a container
decides for a child's frame is a length of the window like everything
else here -- a row's slot, padding's frame less its pixels, or the box a
stack's sizing child decided, which arrives as Part::Sized -- because a
slot of a row is not a fraction of anything the row can name, the same
reason a node entry is a translation. A declaration is a fraction of
whichever of those reached it, and is the only one that also places the
box.

Frame validity is a pin beside the box's, not a range: a range of window
pixels cannot say which frame an answer is a fraction of, since two
frames are different lengths at the same window size. A widget pins its
frame by reading it or by being answered with it under a fractional
rule, and the pin composes up wherever a length of this frame is what
reached the child.

Also here, because the diagnosis needed them: the shrinker reports the
shrunk tree's own divergence with each level's frame, ask, box and size
warm against cold, and there is a size-resize case -- a change and then a
resize, the order that shows an answer kept as a fraction of the wrong
length, which every other case compares at the window it was made at.

Three defects the reports found, each pinned: a rule changed over two
pads relocated the column under them instead of dividing it again (seed
59, depth 5, resize-size), a share inside padding had the padding taken
off twice, and a root resolved its own rule twice.

fmt and clippy clean with and without layout-diagnostics, 121 suite, 20
core, 11 generated, the 400-seed depth-5 shrinker over all sixteen cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-19 00:14:46 -04:00
1 parent 0ef87ebfcf
commit 1512d8418b
15 files changed
+680 -444

No files matched your search

+3 -1
View File
@@ -26,7 +26,9 @@ impl Widget for BranchesOnMeasurement {
let measured = painter
.widget_at(&self.probe, [None; 2], [Place::Within(Part::All), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let px = measured
.apply_leftover()
.to_px(painter.window_px_len(Axis::X));
let below = Place::Within(Part::From(UiSpan::new(cut, painter.extent_len(Axis::Y))));
let place = [Place::Within(Part::All), below];
+90 -9
View File
@@ -83,10 +83,8 @@ fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
assert!(crowded > whole_row, "{crowded} against {whole_row}");
}
/// The same reading through a pad: padding goes around what it pads and
/// does not narrow what a fraction under it is a fraction of, so half of the
/// window plus the padding is what the pad takes and where the next child
/// starts.
/// Padding is an inset: it narrows the frame a fraction resolves against and
/// adds itself back to the padded widget's reported length.
#[test]
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100));
@@ -97,9 +95,83 @@ fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
// placed inside it by its own alignment, which is not what is under test.
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
assert_corners!(h, inner, (10, 10), (210, 90));
assert_corners!(h, padded, (0, 0), (220, 100));
assert_corners!(h, tail, (220, 0), (320, 100));
assert_corners!(h, inner, (10, 10), (200, 90));
assert_corners!(h, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
}
const PARAGRAPH: &str = "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.";
/// The worked example of what padding insets: in a 900 px row after a 24 px
/// icon, a `rel(1.0)` inside `pad(16)` is 900 - 32 and overflows the row by
/// the icon's width, while a wrapping text beside it is asked in the room
/// left, 900 - 24 - 32, and wraps there.
#[test]
fn padding_keeps_the_frame_distinct_from_the_room_left_in_a_row() {
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let fill_width = h.region(&fill).unwrap().size().x;
assert_eq!(fill_width, Px::from_int(868));
let mut h = Harness::new((900, 200));
let icon = rect(Color::RED).width(24).add(&mut h.rsc);
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).add(&mut h.rsc);
h.set_root((icon, padded).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
let asked = active.offer_part.x.len().to_px(window);
assert_eq!(active.frame.x.to_px(window), Px::from_int(868));
assert_eq!(asked, Px::from_int(844));
}
/// The other way round: a share inside padding. A slot is a length of the
/// row, which is already the padded width, so what the span decided reaches
/// the child as it stands -- taking the padding off a second time would make
/// `rel(1.0)` in the slot shorter than the slot.
#[test]
fn a_share_inside_padding_fills_the_slot_it_was_given() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let first = Span {
children: vec![fill.add_strong(&mut h.rsc)],
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.width(leftover(1))
.add(&mut h.rsc);
let second = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
let row = (first, second).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row.pad(16));
assert_eq!(h.region(&first).unwrap().size().x, Px::from_int(434));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(434));
}
/// The same padding in a share instead: the slot is 450, so both the
/// fraction and the wrap are the slot less the padding, and the two agree.
#[test]
fn padding_narrows_both_frame_and_box_inside_a_share() {
let mut h = Harness::new((900, 200));
let fill = rect(Color::GREEN).width(rel(1.0)).add(&mut h.rsc);
let padded = fill.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
assert_eq!(h.region(&fill).unwrap().size().x, Px::from_int(418));
let mut h = Harness::new((900, 200));
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let padded = text.pad(16).width(leftover(1)).add(&mut h.rsc);
let other = rect(Color::BLUE).width(leftover(1)).add(&mut h.rsc);
h.set_root((padded, other).span(Dir::RIGHT).width(rel(1.0)));
let active = &h.render.active[&text.id()];
let window = h.render.output_size().x;
assert_eq!(active.frame.x.to_px(window), Px::from_int(418));
assert_eq!(active.offer_part.x.len().to_px(window), Px::from_int(418));
}
#[test]
@@ -428,8 +500,7 @@ fn a_row_of_equal_shares_fills_it_exactly() {
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
let active = &h.render.active[&id];
let drawn = active.extent.within(&active.frame_abs);
let region = h.render.moves.resolve(active.parent_move, drawn);
let region = h.render.moves.resolve(active.move_idx, active.extent);
let dim = h.size().axis(axis);
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
@@ -740,3 +811,13 @@ fn a_fixed_child_is_centered_in_its_wrappers_share() {
assert_corners!(h, wrapper, (200, 0), (900, 400));
assert_corners!(h, leaf, (500, 150), (600, 250));
}
/// The root's frame is the window and its rule is a fraction of that, which
/// is one resolution and not two: nothing above it narrowed anything.
#[test]
fn a_root_with_a_fraction_rule_is_that_fraction_of_the_window() {
let mut h = Harness::new((900, 200));
let root = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&root).unwrap().size().x, Px::from_int(450));
}
+96
View File
@@ -733,6 +733,51 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
const PARAGRAPH: &str = "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.";
fn plant_stack_resized_from_free(h: &mut Harness, fixed: bool) -> (Vec<WidgetId>, WidgetId) {
let sizing = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rules(sizing.id(), None, None);
if fixed {
h.rsc.widgets_mut().set_size_rules(
sizing.id(),
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
}
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
let pad = Pad {
padding: Padding::ZERO,
inner: text.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![sizing.add_strong(&mut h.rsc), pad.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
h.set_root(stack);
(
vec![sizing.id(), text.id(), pad.id(), stack.id()],
sizing.id(),
)
}
#[test]
fn fixing_a_stacks_sizing_child_repositions_its_overlay() {
let mut warm = Harness::new((900, 1200));
let (ids, sizing) = plant_stack_resized_from_free(&mut warm, false);
warm.frame();
warm.rsc.widgets_mut().set_size_rules(
sizing,
Some(LayoutLen::px(112)),
Some(LayoutLen::px(101)),
);
warm.frame();
let mut cold = Harness::new((900, 1200));
let (cold_ids, _) = plant_stack_resized_from_free(&mut cold, true);
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
/// the span takes its width from the longest line of the texts in it, and
@@ -927,3 +972,54 @@ fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}
/// Six widgets, shrunk from seed 59 at depth 5 (`resize-size`). The column
/// divides the box it is given between two shares, so its drawing holds for
/// that box's length alone, and the pads above it pass that dependency up:
/// each one's box is a part of the box it was asked in. Padding narrowing
/// the frame it hands down does not change that, and while it was taken to,
/// changing the rule over the pads relocated the column's drawing into the
/// new box instead of dividing it again.
fn plant_two_shares_under_two_pads(h: &mut Harness, height: f32) -> Vec<WidgetId> {
let top = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
let bottom = rect(Color::RED).add(&mut h.rsc);
let column = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
let inner = Pad {
padding: Padding::ZERO,
inner: column.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
let outer = Pad {
padding: Padding::ZERO,
inner: inner.add_strong(&mut h.rsc),
}
.height(height)
.add(&mut h.rsc);
let beside = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((outer, beside).span(Dir::RIGHT));
vec![
top.id(),
bottom.id(),
column.id(),
inner.id(),
outer.id(),
beside.id(),
]
}
#[test]
fn changing_a_rule_over_two_pads_divides_the_column_again() {
let mut warm = Harness::new((900, 1200));
let ids = plant_two_shares_under_two_pads(&mut warm, 88.0);
warm.frame();
warm.rsc
.widgets_mut()
.set_size_rules(ids[4], None, Some(LayoutLen::px(105)));
warm.frame();
let mut cold = Harness::new((900, 1200));
let cold_ids = plant_two_shares_under_two_pads(&mut cold, 105.0);
cold.frame();
assert_same_regions(&warm, &ids, &cold, &cold_ids);
}