Lay out in a frame that passes through and a box placed in it

A widget is asked in two boxes rather than one. Its frame is what a fraction
it declares or reports is a fraction of, and it passes through a span, a
stack and a scroll unchanged, so `rel(0.5)` is half the same area however
many containers sit between: a frame is narrowed only by what is decided
above the widget -- a declared length, the root. Its extent is where the
drawing goes, given as a `Place` per axis: a part of the parent's own box,
measured in frame lengths from where that box starts, which the child either
fills or has its answer placed inside.

What that buys is that nothing under a container depends on where the
container sits. A container reads `extent_len` for the length it divides and
nothing about the start, so moving it re-places its children by re-adding
that start and draws nobody again; and a fraction is resolved once, against
the frame, rather than once per box it is composed through -- a stack sized
by a child that reports `rel(0.5)` no longer takes half of half.

`Place` replaces `DrawRegion`, `ExtentPlacement`, `widget_within`,
`measure_len`, `region()`, `placement()` and `box_of`. Primitives and masks
are written in the widget's own box's coordinates alone, so the drawing has
one reference rather than two. The placement pin goes with them: reading the
extent's length pins that length symbolically, and pins compose only where a
child's box is its parent's own.

Placing an answer waits for the end of the parent's draw or for the next ask
of that child in it, so a span child is one drawing and one move rather than
two moves.

`Pad` is transparent: its padding goes around what it pads and its child
keeps the outer frame, which is where `Outset` was going anyway. A fraction
under a pad is now a fraction of the frame rather than of the inset box.

Checked: fmt, clippy with -D warnings, 109 suite tests and 20 core tests in
debug, the 11 generated cases, and the shrinker at 400 trees of depth 5 over
all fifteen cases -- which still finds seed 108 under `reorder`, where a
wrapping text measured in one box and drawn in another settles differently
warm than cold. `redraw` therefore keeps the baseline's deferral for a box
that is not as long as the one the widget was measured in; the plan's step
6 is not done, and the next commit message or the handoff says why.
This commit is contained in:
iris-ai committed 2026-09-18 00:40:59 -04:00
1 parent 34cafb6edc
commit 1956be3f3d
19 files changed
+989 -943

No files matched your search

+10 -7
View File
@@ -21,16 +21,19 @@ struct BranchesOnMeasurement {
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(Px::from_int(40));
let measured = painter.widget_within(&self.probe, top).len(Axis::X);
let len = painter.extent_len();
let cut = Len::from_parts(Rel::ZERO, Px::from_int(40));
let top = Place::Within(Some(UiSpan::new(Len::ZERO, cut)));
let measured = painter
.widget_at(&self.probe, UiRegion::FULL, [Place::Within(None), top])
.len(Axis::X);
let px = measured.apply_leftover().to_px(painter.px_len(Axis::X));
let mut below = UiRegion::FULL;
below.y.start = below.y.start.offset(Px::from_int(40));
let below = Place::Within(Some(UiSpan::new(cut, len.y)));
let place = [Place::Within(None), below];
match px > Px::from_f32(self.threshold) {
true => painter.widget_within(&self.wide, below),
false => painter.widget_within(&self.narrow, below),
true => painter.widget_at(&self.wide, UiRegion::FULL, place),
false => painter.widget_at(&self.narrow, UiRegion::FULL, place),
};
Size::LEFTOVER
}
+10 -7
View File
@@ -83,11 +83,12 @@ 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: its inset is the whole box less the
/// padding, so half of the inset plus the padding is half the box plus one
/// padding, not two.
/// 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.
#[test]
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
fn a_pad_puts_its_padding_around_a_fraction_of_the_whole_box() {
let mut h = Harness::new((400, 100));
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
@@ -96,8 +97,9 @@ fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_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, padded, (0, 0), (210, 100));
assert_corners!(h, tail, (210, 0), (310, 100));
assert_corners!(h, inner, (10, 10), (210, 90));
assert_corners!(h, padded, (0, 0), (220, 100));
assert_corners!(h, tail, (220, 0), (320, 100));
}
#[test]
@@ -426,7 +428,8 @@ 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 region = h.render.moves.resolve(active.parent_move, active.region);
let drawn = active.extent.within(&active.frame_abs);
let region = h.render.moves.resolve(active.parent_move, drawn);
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());
+96 -51
View File
@@ -213,9 +213,12 @@ struct FromHint {
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.px);
painter.widget_within(&self.inner, region);
let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px));
painter.widget_at(
&self.inner,
UiRegion::FULL,
[Place::Within(None), Place::Within(Some(top))],
);
Size::LEFTOVER
}
}
@@ -305,6 +308,51 @@ fn a_span_ruled_across_itself_moves_its_child_without_redrawing_it() {
assert_eq!(h.render.active[&span.id()].size.y, LayoutLen::rel(1.0));
}
/// A row places its children as lengths from where its own box starts, so a
/// child that grew moves the ones after it and nothing else: each of them is
/// the same box in a new place, which the retained drawing follows without
/// being made again. Both kinds of length: one the row resolves from a rule,
/// and one it takes from what the child reported.
#[test]
fn a_row_moves_what_follows_a_child_that_grew_rather_than_drawing_it() {
for declared in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::RED).width(50).add(&mut h.rsc);
let ruled = Rc::new(Cell::new(0));
let second = Counted {
draws: ruled.clone(),
size: Size::LEFTOVER,
reads_box: false,
};
let second = match declared {
true => second.width(rel(0.25)).add(&mut h.rsc),
false => second.width(60).add(&mut h.rsc),
};
let (third, reported) = counted(&mut h, Size::from((70, 20)), false);
h.set_root((first, second, third).span(Dir::RIGHT).width(rel(1.0)));
let (was_ruled, was_reported) = (ruled.get(), reported.get());
// A quarter of the row is a quarter of the row, wherever it sits in
// it and whatever the first child takes.
let width = match declared {
true => 100,
false => 60,
};
assert_corners!(h, second, (50, 0), (50 + width, 200));
h.set_len(first, Axis::X, 80);
h.frame();
assert_eq!(ruled.get(), was_ruled, "the ruled child was drawn again");
assert_eq!(
reported.get(),
was_reported,
"the reported child was drawn again"
);
assert_corners!(h, second, (80, 0), (80 + width, 200));
assert_corners!(h, third, (80 + width, 90), (150 + width, 110));
}
}
/// The output is the root of the box chain, so a resize is a box that changed
/// length like any other -- there is not a second rule for the window. A
/// drawing that holds for one length is drawn again whichever box moved.
@@ -749,36 +797,6 @@ fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
.collect()
}
#[test]
fn frame_geometry_and_extent_geometry_keep_their_references() {
struct Both(Rc<Cell<usize>>);
impl Widget for Both {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.primitive_within(RectPrimitive::color(Color::RED), UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
}
for node in [false, true] {
let mut h = Harness::new((400, 200));
let first = rect(Color::GREEN).width(100).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let both = Both(draws.clone()).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(both, node);
h.set_root((first, both).span(Dir::RIGHT));
let count = draws.get();
h.set_len(first, Axis::X, 200);
h.frame();
assert_eq!(draws.get(), count);
let bounds = primitive_bounds(&h, both.id());
assert_eq!(bounds[0].top_left.x, Px::ZERO);
assert_eq!(bounds[0].bot_right.x, Px::from_int(400));
assert_eq!(bounds[1].top_left.x, Px::from_int(200));
assert_eq!(bounds[1].bot_right.x, Px::from_int(400));
}
}
#[test]
fn changing_an_inherited_extent_keeps_the_original_measurement_offer() {
fn build(h: &mut Harness, width: i32, text: &str) -> (WeakWidget<Text>, WeakWidget<Rect>) {
@@ -858,7 +876,14 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
}
impl Widget for Frame {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.widget_within(&self.child, self.region);
painter.widget_at(
&self.child,
UiRegion::FULL,
[
Place::Within(Some(self.region.x)),
Place::Within(Some(self.region.y)),
],
);
Size::LEFTOVER
}
}
@@ -866,7 +891,7 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
impl Widget for Painted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.0.set(self.0.get() + 1);
painter.set_mask(DrawRegion::Extent(UiRegion::FULL));
painter.set_mask(UiRegion::FULL);
painter.primitive(RectPrimitive::color(Color::BLUE));
Size::LEFTOVER
}
@@ -966,8 +991,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
UiSpan::new(Len::rel(0.23) + Len::px(-7.125), Len::FULL),
UiSpan::new(Len::rel(0.37) + Len::px(3.25), Len::FULL),
);
painter.glyphs(text, DrawRegion::Frame(origin));
painter.glyphs(text, DrawRegion::Extent(origin));
painter.glyphs(text, origin);
Size::LEFTOVER
}
}
@@ -981,7 +1005,10 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
painter.widget_at(
&self.child,
self.region,
[Some(self.extent.x), Some(self.extent.y)],
[
Place::Fill(Some(self.extent.x)),
Place::Fill(Some(self.extent.y)),
],
);
Size::LEFTOVER
}
@@ -1128,7 +1155,7 @@ fn widening_and_restoring_a_contract_does_not_invalidate_its_reader() {
assert_eq!(leaf_draws.get(), settled + 1);
}
#[test]
fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
fn padding_and_stack_boxes_follow_the_extent_without_drawing_again() {
struct Observed<W> {
widget: W,
draws: Rc<Cell<usize>>,
@@ -1148,7 +1175,10 @@ fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
painter.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
[
Place::Fill(Some(self.extent.x)),
Place::Fill(Some(self.extent.y)),
],
);
Size::LEFTOVER
}
@@ -1179,13 +1209,18 @@ fn padding_and_stack_frames_follow_the_extent_without_drawing_again() {
h.set_root(root);
(root, leaf, fixed, draws)
};
// The same box in three places. A pad places its child as lengths of
// its own box measured from where that box starts, so moving it is
// nothing to the pad -- where changing its length is a different
// question, and does draw it again.
let at = |start: f32| {
let span = |start: Len| UiSpan::new(start, start + Len::rel(0.4));
UiRegion::new(span(Len::rel(start) + Len::px(3.125)), span(Len::px(11.25)))
};
let mut warm = Harness::new((403, 211));
let (root, leaf, fixed, draws) = plant(&mut warm, UiRegion::FULL);
for (start, end) in [(0.13, 0.83), (-0.17, 1.23), (0.31, 0.67)] {
let extent = UiRegion::new(
UiSpan::new(Len::rel(start) + Len::px(3.125), Len::rel(end)),
UiSpan::new(Len::px(11.25), Len::rel(end)),
);
let (root, leaf, fixed, draws) = plant(&mut warm, at(0.13));
for start in [0.13, -0.17, 0.31] {
let extent = at(start);
let before = draws.get();
warm.rsc[root].extent = extent;
warm.frame();
@@ -1229,11 +1264,11 @@ fn moving_an_extent_child_preserves_the_slot_chosen_from_its_measurement() {
&self.child,
UiRegion::FULL,
[
Some(UiSpan::new(
Place::Fill(Some(UiSpan::new(
Len::px(self.start),
Len::px(self.start + 200.0),
)),
Some(UiSpan::FULL),
))),
Place::Fill(Some(UiSpan::FULL)),
],
);
Size::LEFTOVER
@@ -1267,7 +1302,14 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
impl Widget for Container {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter
.widget_within(&self.child, DrawRegion::Extent(self.region))
.widget_at(
&self.child,
UiRegion::FULL,
[
Place::Within(Some(self.region.x)),
Place::Within(Some(self.region.y)),
],
)
.size()
}
}
@@ -1283,7 +1325,10 @@ fn extent_frames_keep_fractional_reports_and_numeric_dependencies_valid() {
.widget_at(
&self.child,
UiRegion::FULL,
[Some(self.extent.x), Some(self.extent.y)],
[
Place::Fill(Some(self.extent.x)),
Place::Fill(Some(self.extent.y)),
],
)
.size(),
);
+1 -1
View File
@@ -71,7 +71,7 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
impl Widget for Clipper {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.set_mask(UiRegion::FULL);
painter.widget(&self.0).size()
}
}