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>
105 lines
4.2 KiB
Rust
105 lines
4.2 KiB
Rust
use crate::prelude::*;
|
|
|
|
pub struct Scroll {
|
|
inner: StrongWidget,
|
|
axis: Axis,
|
|
amt: Px,
|
|
snap_end: bool,
|
|
container_len: Px,
|
|
content_len: Px,
|
|
}
|
|
|
|
impl Widget for Scroll {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let container_len = painter.px_len(self.axis);
|
|
// Asked in the whole viewport, then put at the scrolled offset.
|
|
let answer_len = painter
|
|
.widget_at(&self.inner, PlaceDesc::WHOLE.fills())
|
|
.len(self.axis);
|
|
let answer_px = painter.to_px(answer_len.without_leftover(), self.axis);
|
|
self.container_len = container_len;
|
|
self.content_len = answer_px.max(container_len);
|
|
|
|
if self.snap_end {
|
|
self.amt = self.content_len - self.container_len;
|
|
}
|
|
self.update_amt();
|
|
// Reading the box in pixels above holds this drawing to that one
|
|
// length, so these two say where it holds more widely.
|
|
//
|
|
// Content of a fixed length that fits is handed the whole box below,
|
|
// and nothing here reads the box again, so every longer box gives the
|
|
// same drawing: it holds from the length the content needs upwards,
|
|
// and shrinking past that is what changes it. Where it sits in a box
|
|
// longer than itself is not this widget's to say -- placing its
|
|
// answer in the whole box is its own alignment, and that placement is
|
|
// a fraction of the box, so it holds at every length too.
|
|
//
|
|
// One scrolled part way sits where it is until the box shrinks past
|
|
// what is left of it. Kept to the end, it moves with every length.
|
|
let answer_is_px = answer_len.is_px();
|
|
if answer_is_px && self.content_len <= self.container_len {
|
|
painter.holds(self.axis, answer_px..=Px::MAX);
|
|
} else if answer_is_px && !self.snap_end {
|
|
let left = self.content_len - self.amt;
|
|
painter.holds(self.axis, Px::MIN..=left);
|
|
}
|
|
|
|
// Content that fills the viewport is the viewport, and is handed back
|
|
// as it came -- it has nothing to scroll through, so the clamp above
|
|
// has already put `amt` at zero. Writing the same box as its own
|
|
// length in pixels is the same box in another form, and the two do
|
|
// not round alike: a part centred in `rel 1` lands a step from one
|
|
// centred in `px 900`, since halving a difference is not halving each
|
|
// part of it.
|
|
let content = match self.content_len > self.container_len {
|
|
true => {
|
|
let start = Len::from_parts(Rel::ZERO, -self.amt);
|
|
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
|
}
|
|
false => PlaceDescAxis::WHOLE,
|
|
};
|
|
// The viewport is the inner's rel base, so a fraction it declares or
|
|
// reports is a fraction of what is on screen rather than of the
|
|
// content box its own answer decided. Where it goes is the content
|
|
// box, scrolled: its drawing moved there, not made again there.
|
|
painter.place_at(&self.inner, content.on_axis(self.axis).fills());
|
|
// What it occupies is its box, on both axes: it clips its content to
|
|
// that box, so it can neither take less of one nor honestly ask for
|
|
// more. The content's length is what it scrolls through, not what it
|
|
// is.
|
|
Size::LEFTOVER
|
|
}
|
|
|
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
|
Some(LayoutLen::LEFTOVER)
|
|
}
|
|
}
|
|
|
|
impl Scroll {
|
|
pub fn new(inner: StrongWidget, axis: Axis) -> Self {
|
|
Self {
|
|
inner,
|
|
axis,
|
|
amt: Px::ZERO,
|
|
snap_end: true,
|
|
container_len: Px::ZERO,
|
|
content_len: Px::ZERO,
|
|
}
|
|
}
|
|
|
|
pub fn update_amt(&mut self) {
|
|
self.amt = self.amt.max(Px::ZERO);
|
|
let len = (self.content_len - self.container_len).max(Px::ZERO);
|
|
self.amt = self.amt.min(len);
|
|
self.snap_end = self.amt == len;
|
|
}
|
|
|
|
/// Scrolled by a distance the platform measures, which is the last place
|
|
/// a wheel notch or a finger is a float.
|
|
pub fn scroll(&mut self, amt: f32) {
|
|
self.amt -= Px::from_f32(amt);
|
|
self.update_amt();
|
|
}
|
|
}
|