Stop a scroll asking a question it has already answered

A seventh sweep, over the parts no earlier round named: the tree generator
and the scenario harness, `Fixed`, the headless rig, and once more over the
commit the sixth sweep left, which was itself unreviewed.

`Scroll`'s content box is `answer_px.max(container_len)`, so a scroll whose
content fits has nothing to scroll through and `update_amt` has already put
`amt` at zero. The test choosing between the viewport and a scrolled span
asked `amt != ZERO || content_len != container_len`, where the first
disjunct can never decide it -- the same defect `b7b8d09` removed from the
line above, one operand over. A `debug_assert` of the implication held
across the whole suite, including every scrolling test.

`Fixed::to_scale` and its private `shift_round` arrived on this branch with
no caller and never got one; the only thing that called either was the test
written for them.

`Len::align` wrote `Len` arithmetic out a component at a time, around an
`at.px` that is always zero, where `Len::scale` and the `Add`/`Sub` beside
it say the whole rule in two lines. `LayoutLen::without_leftover` took
`self` where the `apply_leftover` its own doc calls the opposite reading of
the same value takes `&self`.

`run-headless.sh --resize` changed the output's mode but not `out_w`/`out_h`,
which is the extent `replay-touch` scales a recording against -- so
`--resize` with `--replay` put every sample of the gesture somewhere else
and still finished like a run that worked. Both come from one function now.

The generator's plan/build split stranded a comment: "a row takes the height
it is given" describes the size rule `build` derives from `dir`, and it was
left above the `gap` draw, which is the one line it is not about and which
does consume randomness.

Format, clippy with and without layout-diagnostics, and the suite (131 + 19
+ 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical
to b7b8d09 across all 34,492 boxes, and all three seed scans pass: 400 at
depth 5 in 62.75s, 1,000 at depth 6 in 162.37s, 2,000 at depth 4 in 305.25s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-20 02:53:02 -04:00
1 parent b7b8d09e40
commit f8aa0c5cdf
6 files changed
+29 -48

No files matched your search

-27
View File
@@ -131,14 +131,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
self.0 as f32 / Self::one().0 as f32 self.0 as f32 / Self::one().0 as f32
} }
/// The same value on another grid, rounded where the new one is coarser.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT {
true => self.0 << (TO - SHIFT),
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
})
}
pub const fn add(self, rhs: Self) -> Self { pub const fn add(self, rhs: Self) -> Self {
Self(self.0.wrapping_add(rhs.0)) Self(self.0.wrapping_add(rhs.0))
} }
@@ -246,16 +238,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
} }
/// Back to a single step, rounding halves away from zero so that a value and
/// its negation round to the same distance.
const fn shift_round(v: i64, bits: u32) -> i64 {
let half = (1i64 << bits) >> 1;
match v < 0 {
true => -((-v + half) >> bits),
false => (v + half) >> bits,
}
}
const fn div_round(num: i64, den: i64) -> i64 { const fn div_round(num: i64, den: i64) -> i64 {
let (q, rem) = (num / den, num % den); let (q, rem) = (num / den, num % den);
match rem.unsigned_abs() * 2 >= den.unsigned_abs() { match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
@@ -531,15 +513,6 @@ mod tests {
assert_eq!(Px::from_f32(-1e12), Px::MIN); assert_eq!(Px::from_f32(-1e12), Px::MIN);
} }
#[test]
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
// A third, which neither grid holds exactly.
let third = Rel::ONE / Rel::from_int(3);
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
let coarse = Fixed::<6>::from_raw(21);
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
}
#[test] #[test]
fn lerp_takes_the_fraction_as_the_receiver() { fn lerp_takes_the_fraction_as_the_receiver() {
let (from, to) = (Px::from_int(10), Px::from_int(20)); let (from, to) = (Px::from_int(10), Px::from_int(20));
+5 -3
View File
@@ -151,13 +151,15 @@ impl Vec2 {
} }
impl Len { impl Len {
/// This length placed in the box it is measured in: the alignment names a
/// point along that box, and the two ends are that point less the part of
/// the length falling before it and plus the part falling after.
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let rest = Rel::ONE.sub(rel);
let at = Len::from_parts(rel, Px::ZERO); let at = Len::from_parts(rel, Px::ZERO);
UiSpan { UiSpan {
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))), start: at - self.scale(rel),
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))), end: at + self.scale(Rel::ONE.sub(rel)),
} }
} }
} }
+1 -1
View File
@@ -169,7 +169,7 @@ impl LayoutLen {
/// anyone not dividing a box between siblings, where a share is a claim /// anyone not dividing a box between siblings, where a share is a claim
/// on someone else's room rather than a length of its own. /// on someone else's room rather than a length of its own.
/// [`Self::apply_leftover`] is the opposite reading of the same value. /// [`Self::apply_leftover`] is the opposite reading of the same value.
pub const fn without_leftover(self) -> Len { pub const fn without_leftover(&self) -> Len {
Len::from_parts(self.rel, self.px) Len::from_parts(self.rel, self.px)
} }
+11 -6
View File
@@ -106,11 +106,16 @@ export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2 echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
swaymsg output HEADLESS-1 mode "$mode" >/dev/null # The extent `replay-touch` positions against, so a script's coordinates are
# The extent `replay-touch` positions against, so a script's coordinates # the output's own pixels. Set beside every mode change, since a gesture
# are the output's own pixels. # scaled against a mode the output no longer has lands somewhere else and
out_w=${mode%x*} # still looks like a run that worked.
out_h=${mode#*x}; out_h=${out_h%@*} set_mode() {
swaymsg output HEADLESS-1 mode "$1" >/dev/null
out_w=${1%x*}
out_h=${1#*x}; out_h=${out_h%@*}
}
set_mode "$mode"
# Built before the app starts, so a compile error is not reported as a # Built before the app starts, so a compile error is not reported as a
# window that failed to move. # window that failed to move.
@@ -149,7 +154,7 @@ while [ $i -lt "$((seconds * 2))" ]; do
done done
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
swaymsg output HEADLESS-1 mode "$resize" >/dev/null set_mode "$resize"
echo "run-headless: resized to $resize" >&2 echo "run-headless: resized to $resize" >&2
sleep 2 sleep 2
fi fi
+4 -4
View File
@@ -773,10 +773,6 @@ impl Sow<'_> {
self.spans += 1; self.spans += 1;
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default(); let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
let dir = self.rng.below(4); let dir = self.rng.below(4);
// A row takes the height it is given rather than its tallest child,
// which is a rule beside it. Derived from an existing choice and
// consuming no randomness: a seed must keep growing the same tree
// when the generator gains another configuration.
let gap = self.rng.below(3) as i32 * 4; let gap = self.rng.below(3) as i32 * 4;
let grown: Vec<usize> = (0..children.len()).collect(); let grown: Vec<usize> = (0..children.len()).collect();
let order = span_edited(&grown, children.len(), spares.len(), &edit); let order = span_edited(&grown, children.len(), spares.len(), &edit);
@@ -915,6 +911,10 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
gap: Px::from_int(*gap), gap: Px::from_int(*gap),
} }
.add(self.rsc); .add(self.rsc);
// A row takes the height it is given rather than its tallest
// child, which is a rule beside the span rather than anything
// it draws. Derived from `dir` rather than stored, so a plan
// that says the direction says this too.
if dir.axis == Axis::X { if dir.axis == Axis::X {
self.rsc self.rsc
.widgets_mut() .widgets_mut()
+8 -7
View File
@@ -45,13 +45,14 @@ impl Widget for Scroll {
painter.holds(self.axis, Px::MIN..=left); painter.holds(self.axis, Px::MIN..=left);
} }
// Content that fills the viewport and has not been scrolled is the // Content that fills the viewport is the viewport, and is handed back
// viewport, and is handed back as it came. Writing the same box as // as it came -- it has nothing to scroll through, so the clamp above
// its own length in pixels is the same box in another form, and the // has already put `amt` at zero. Writing the same box as its own
// two do not round alike: a part centred in `rel 1` lands a step from // length in pixels is the same box in another form, and the two do
// one centred in `px 900`, since halving a difference is not halving // not round alike: a part centred in `rel 1` lands a step from one
// each part of it. // centred in `px 900`, since halving a difference is not halving each
let content = match self.amt != Px::ZERO || self.content_len != self.container_len { // part of it.
let content = match self.content_len > self.container_len {
true => { true => {
let start = Len::from_parts(Rel::ZERO, -self.amt); let start = Len::from_parts(Rel::ZERO, -self.amt);
UiSpan::new(start, start.offset(self.content_len)).shifted_desc() UiSpan::new(start, start.offset(self.content_len)).shifted_desc()