Compare commits

..
4 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 e166e005dc Pin that a length in pixels is that many pixels
Asked of the `tabs` render: does a gap come out the same number of pixels
wherever it appears? For a length in pixels it does, and structurally rather
than by luck -- `Len::within` adds a part's own pixels rather than scaling
them, and both ends of a gap carry the same fraction, so the multiply that
rounds is the same on each and cancels. The test buries a row of five under
three containers that are each a fraction of their parent, so nothing
reaches the window without being composed and rounded, and checks every gap
and every declared width at five box widths. Swept over 2,100 widths when it
was written and exact at every one.

For a share it does not, and the second test pins by how much rather than
pretending otherwise: one or two steps between children that asked for the
same fraction, 0.001 to 0.002 px. A position is the quantity that gets
rounded so the row fills exactly and no two children leave a seam, and that
is what costs it. Exact composition would shrink the spread, not remove it:
five equal lengths cannot fill a row whose step count is not a multiple of
five.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:16:18 -04:00
iris-aiandClaude Opus 5 38eba543f6 Tighten a validity range to what the arithmetic needs
`Holds::through`'s allowance for the two routes to a length was four half
steps either side, from a derivation that said each rounding now drops a
whole step where it used to drop half of one. That overshot: three is the
floor, two fires the `Holds` assertion in `draw_at` on eleven generated
cases, and four was never measured as necessary. Tightening both ends did
not move one of the rig's twenty-five work counters, so the extra half step
was not buying any reuse either.

It cannot go to zero. The range has to contain the box a drawing was made
in, which the assertion checks, and it must not contain a box the drawing
does not hold for, which the warm-against-cold oracle checks -- and those
two only coincide where a length reached two ways is the same number. It is
not, yet; composing in `i64` and narrowing once is the queued change that
would make it so, and shrinking this allowance is how to tell whether that
worked.

Checked: fmt, clippy, 81 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` and the tab
replay byte-identical at 1920x1200 against `2bc6bdf`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:15:44 -04:00
iris-aiandClaude Opus 5 2bc6bdfc77 Let a child with room to move use its own alignment
`Stack` and `Pad` forced the near edge on every child. That override exists
so a container that reports a child's size and then hands it the box derived
from that report does not place its content twice -- and it is owed only
where the box really is the child's own answer.

`Stack` gives every child the box its sizing child defines. That box is
`box_of(child.size())`, so the sizing child has no room in it and needs the
override; every other child is handed a box that owes nothing to it, and
where it sits in one bigger than itself is its own business. With the
override it could not be aligned at all.

`Pad` reports its inner's size plus the padding, so where its box is that
answer the inset box is exactly the inner and alignment has nowhere to move
it. Where the box is bigger -- a share of a row, a rule over the pad -- the
slack belongs to the inner, and the override pinned it to a corner.

The `tabs` example is the visible case both ways: its counters asked for
`Align::RIGHT` inside a stack and sat at the left, and `text`'s narrow panel
filled a row it had asked to sit at the top of. Both match canonical `main`
again. Neither was noticed when `d3b0ebf` made alignment a property, and the
handoff's claim that `tabs` then "differs only in the widget count it prints
about itself" was wrong -- it was checked at `8220a78` and not re-checked
after the next commit.

Checked: fmt, clippy, 81 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, and all fifteen shrinker cases at
400 seeds of depth 5. `tabs`, `text` and `random` change exactly where a
child now honours its own alignment; `view` and `minimal` are unchanged.

`tabs` is still not `main`'s render: `.sized((100, 100)).center().width(
leftover(2))` on one widget no longer means a square centred in a two-share
box, because one widget carries one length per axis and `.width` overwrites
what `.sized` set. That one is an API question, not a bug, and is open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:03:09 -04:00
iris-aiandClaude Opus 5 d8ae9c3bdd Place a locally redrawn widget once, in the box already chosen for it
`redraw` asks a dirty widget at its offer, and then again in the final box
its parent chose from that answer. The second ask handed that box over as if
it were an offer, so `draw_inner` ran `placed_box` on it and applied the
widget's own alignment to a box that had already been placed -- a second
placement on every local redraw of a widget that is not near-aligned. It
only showed where the widget's alignment was its own to apply: a container
override makes `draw_inner` take the box as given, and `Stack`, `Pad` and
`Scroll` override every child they hand a box to.

It is the fix for both of the handoff's standing warm-against-cold failures.
Shrinker seed 288 on `region-node` was an 8.8px inset at each end of a `Text`
under a `Span(Y-)` under two `Stack`s; oracle seed 326 at depth 6 was 88px on
a `Text` under two `Branch`es. Neither reduced below 11 and 43 widgets, and
both are this.

Checked: fmt, clippy, 80 suite tests, 17 core unit tests, the release oracle
at 100 seeds, **all fifteen shrinker cases at 400 seeds of depth 5**, and
**1000 seeds of depth 6** -- the last two for the first time. `tabs`, `text`,
`random` and the tab replay render byte-identical at 1920x1200 against
`08c9d5a`, since nothing about a cold layout changes.

Generated seed 20 at depth 4 catches it and joins the ordinary set, so
`cargo test` fails without this rather than only the ignored long run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:58:07 -04:00
6 changed files with 176 additions and 28 deletions

No files matched your search

+24 -20
View File
@@ -55,23 +55,26 @@ impl Holds {
if rel == 0 { if rel == 0 {
return Self::ANY; return Self::ANY;
} }
// In half steps. The box a length was composed down the chain from // In half steps, and no more than the arithmetic needs: too wide a
// and the box the same length is measured against the window in are // range admits reusing a drawing where it does not hold, and too
// two routes to one number, each rounding where the other does not, // narrow a one leaves out the box a drawing was made in, which the
// and each rounding drops a whole step since `Fixed::mul` truncates: // `Holds` assertion in `draw_at` catches. `ROUTES` is at that floor
// two steps either side. The multiply on the way in drops a step of // -- two half steps fires it -- and tightening both ends moved not
// its own, and only downward, so it is one more step at the top and // one of the rig's work counters, so the slack is not buying reuse.
// nothing at the bottom -- and the whole of a box has no multiply in //
// it, however many pixels were added to it, since multiplying by one // `ROUTES` covers a box composed down the chain against the same box
// is exact and taking the pixels off again is too. Allowing for it // measured against the window: two routes to one number, each
// there anyway compounded, a step a level down a chain of widgets // rounding where the other does not. `way_in` covers the multiply
// each taking the whole of its parent, which is the unsound // this inverts, which drops a step and only ever downward, so it
// direction: a range wider than what a drawing holds for admits // belongs at the top of the range alone -- and the whole of a box has
// reusing it where it does not hold. // no multiply in it, however many pixels were added to it, since
// multiplying by one is exact and taking the pixels off again is too.
// Allowing for it there anyway compounded, a step a level down a
// chain of widgets each taking the whole of its parent.
// //
// Shifted by half of what a `Rel` counts in, to divide by the // Shifted by half of what a `Rel` counts in, to divide by the
// fraction: exact until the division takes it back to the grid. // fraction: exact until the division takes it back to the grid.
const ROUTES: i64 = 4; const ROUTES: i64 = 3;
let px = len.px.raw() as i64; let px = len.px.raw() as i64;
let half_rel = REL_SHIFT - 1; let half_rel = REL_SHIFT - 1;
let way_in = match rel == Rel::ONE.raw() as i64 { let way_in = match rel == Rel::ONE.raw() as i64 {
@@ -136,19 +139,20 @@ mod tests {
/// taken off it, brings no multiply of its own: only the two routes to /// taken off it, brings no multiply of its own: only the two routes to
/// the same length are left to allow for, and not a rounding that did /// the same length are left to allow for, and not a rounding that did
/// not happen. Widening for it as well grew the interval a level at a /// not happen. Widening for it as well grew the interval a level at a
/// time down a chain of them. /// time down a chain of them. Three half steps come back as one whole
/// one, since dividing by a whole box is dividing by one.
#[test] #[test]
fn the_whole_of_a_box_widens_by_the_routes_alone() { fn the_whole_of_a_box_widens_by_the_routes_alone() {
let at = Px::from_int(956); let at = Px::from_int(956);
let two_steps = |len: Px| Holds { let one_step = |len: Px| Holds {
lo: len - Px::from_raw(2), lo: len - Px::STEP,
hi: len + Px::from_raw(2), hi: len + Px::STEP,
}; };
assert_eq!(Holds::at(at).through(Len::FULL), two_steps(at)); assert_eq!(Holds::at(at).through(Len::FULL), one_step(at));
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8)); let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
assert_eq!( assert_eq!(
Holds::at(at).through(less_eight), Holds::at(at).through(less_eight),
two_steps(at + Px::from_int(8)) one_step(at + Px::from_int(8))
); );
} }
+10 -3
View File
@@ -1013,9 +1013,16 @@ impl UiRenderState {
if at_offer { if at_offer {
return; return;
} }
// Then in the final box its parent chose from that answer. It is kept // Then in the final box its parent chose from that answer. That box
// if it holds there; otherwise its result is the parent's business. // is already placed, so the near edge goes with it: applying the
self.draw_inner(id, region, info, None, rsc); // widget's own alignment to it again would place its content twice,
// the way it did for a region node under a `Stack` once the stack
// stopped overriding every child's alignment.
let placed_info = DrawInfo {
align: Some(RegionAlign::NEAR),
..info
};
self.draw_inner(id, region, placed_info, None, rsc);
let active = &self.active[&id]; let active = &self.active[&id];
if (active.size, active.holds) != was { if (active.size, active.holds) != was {
rsc.widgets_mut().needs_redraw.insert(parent); rsc.widgets_mut().needs_redraw.insert(parent);
+7 -1
View File
@@ -7,8 +7,14 @@ pub struct Pad {
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// The inner's own alignment, not the near edge. This reports the
// inner's size plus the padding, so where the box is that answer the
// inset box is exactly the inner and alignment has no room to move
// it; where the box is bigger -- a share of a row, a rule over this
// widget -- the slack is the inner's to sit in, and forcing the near
// edge pinned it to a corner it had not asked for.
let inner = painter let inner = painter
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR) .widget_within(&self.inner, self.padding.region())
.size(); .size();
Size { Size {
x: LayoutLen { x: LayoutLen {
+9 -1
View File
@@ -29,7 +29,15 @@ impl Widget for Stack {
let region = painter.box_of(size); let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() { for (i, child) in self.children.iter().enumerate() {
painter.child_layer_at(i); painter.child_layer_at(i);
painter.widget_aligned(child, region, RegionAlign::NEAR); // The sizing child placed its own content in the box its answer
// decided, and this box was derived from that answer, so applying
// its alignment again here would place it twice. Every other
// child is handed a box that owes nothing to its own answer, and
// where it sits in one bigger than itself is its own business.
match sizing == Some(i) {
true => painter.widget_aligned(child, region, RegionAlign::NEAR),
false => painter.widget_within(child, region),
};
} }
size size
} }
+121
View File
@@ -499,3 +499,124 @@ fn leftover_children_disappear_at_the_exact_fixed_content_boundary() {
assert!(h.region(&a).is_none()); assert!(h.region(&a).is_none());
assert!(h.region(&b).is_none()); assert!(h.region(&b).is_none());
} }
/// **A stack child smaller than the stack sits where its own alignment
/// says.** `Stack` gives every child the box its sizing child defines and
/// used to force the near edge on all of them; that override is owed only to
/// the sizing child, which has already placed its own content in the box the
/// stack derived from its answer. Every other child is handed a box that owes
/// nothing to it, so where it sits in one bigger than itself is its own
/// business -- and with the override it could not be aligned at all, which is
/// what moved the `tabs` example's counters to the wrong corner.
#[test]
fn a_stack_child_smaller_than_the_stack_keeps_its_own_alignment() {
let mut h = Harness::new((400, 200));
let big = rect(Color::BLUE).add(&mut h.rsc);
let small = rect(Color::RED).sized((50, 50)).add(&mut h.rsc);
h.rsc
.widgets_mut()
.set_alignment(small.id(), Axis::X, AxisAlign::POS);
let (a, b) = (big.add_strong(&mut h.rsc), small.add_strong(&mut h.rsc));
let children: Vec<StrongWidget> = vec![a, b];
h.set_root(Stack {
children,
size: StackSize::Default,
});
assert_corners!(h, big, (0, 0), (400, 200));
// The far edge on X because it asked for it, the middle on Y because
// that is the default.
assert_corners!(h, small, (350, 75), (400, 125));
}
/// Five children of one span, buried under three containers that are each a
/// fraction of their parent so no length reaches the window without being
/// composed and rounded on the way. Returns each child's drawn width and
/// each gap between them, in pixels.
fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>, Vec<Px>) {
let mut h = Harness::new((box_w, 400.0));
let mut ids = Vec::new();
let mut kids: Vec<StrongWidget> = Vec::new();
for _ in 0..5 {
let r = rect(Color::RED).add(&mut h.rsc);
if let Some(len) = kid {
h.rsc
.widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
}
ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc));
}
let span = Span {
children: kids,
dir: Dir::RIGHT,
gap: Px::from_f32(gap),
}
.add(&mut h.rsc);
let a = (span.width(rel(0.9)),).span(Dir::RIGHT).add(&mut h.rsc);
let b = (a.width(rel(0.8)),).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((b.width(rel(0.7)),).span(Dir::RIGHT));
let boxes: Vec<_> = ids
.iter()
.map(|id| h.region(id).expect("a child drew nothing"))
.collect();
(
boxes.iter().map(|b| b.bot_right.x - b.top_left.x).collect(),
boxes
.windows(2)
.map(|p| p[1].top_left.x - p[0].bot_right.x)
.collect(),
)
}
/// **A length given in pixels is that many pixels, wherever it ends up.** A
/// gap and a declared width compose additively -- `Len::within` adds a part's
/// own pixels rather than scaling them, and both ends of a gap carry the same
/// fraction, so the multiply that rounds is the same on each -- which is why
/// nesting the row inside fractions of fractions cannot move them. Swept over
/// 2,100 box widths when this was written and exact at every one; five here,
/// including widths that divide badly by five.
#[test]
fn a_length_in_pixels_is_that_many_pixels_however_it_is_nested() {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let want = Px::from_int(7);
let (_, gaps) = row_under_fractions(None, 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between leftover children are {gaps:?}"
);
let (widths, gaps) = row_under_fractions(Some(LayoutLen::px(100.0)), 7.0, box_w);
assert!(
gaps.iter().all(|g| *g == want),
"box {box_w}: gaps between fixed children are {gaps:?}"
);
assert!(
widths.iter().all(|w| *w == Px::from_int(100)),
"box {box_w}: declared widths came out {widths:?}"
);
}
}
/// **Children asking for the same share of a row are not the same length**,
/// and this pins by how much rather than claiming they are equal. A position
/// is the quantity that gets rounded, so the row fills exactly and no two
/// children leave a seam; what that costs is a step or two between lengths
/// that were asked for identically. Exact composition would shrink the
/// spread, not remove it: five equal lengths cannot fill a row whose step
/// count is not a multiple of five.
#[test]
fn equal_shares_differ_by_at_most_two_steps_and_fill_the_row() {
for kid in [None, Some(LayoutLen::rel(0.2))] {
for box_w in [300.0, 1000.0, 1001.0, 1003.0, 1920.0] {
let (widths, gaps) = row_under_fractions(kid, 0.0, box_w);
let spread = *widths.iter().max().unwrap() - *widths.iter().min().unwrap();
assert!(
spread <= Px::from_raw(2),
"box {box_w}, {kid:?}: widths {widths:?} spread {spread:?}"
);
assert!(
gaps.iter().all(|g| *g == Px::ZERO),
"box {box_w}, {kid:?}: children left seams {gaps:?}"
);
}
}
}
+5 -3
View File
@@ -25,9 +25,11 @@ fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4) env("IRIS_GENERATED_DEPTH", 4)
} }
/// The seeds the ordinary tests take. Eight that have never failed and one, /// The seeds the ordinary tests take. Seven that have never failed; 86,
/// 86, that a `Scroll` fixed point once settled differently on. /// which a `Scroll` fixed point once settled differently on; and 20, which
const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98]; /// caught a locally redrawn widget being placed twice in the box its parent
/// had already placed it in.
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
fn check(seed: u64, depth: usize, case: Case) { fn check(seed: u64, depth: usize, case: Case) {
let grown = plan(seed, depth, &Edits::default()); let grown = plan(seed, depth, &Edits::default());