A local redraw drew a dirty widget in the box it was placed in. When a reader decided that box from the widget's own answer -- an aligned span sized to its children, a text at the tail of a row, a scroll's content -- the old answer is a fixed point of measuring there whatever the content now says, so the layout had two stable answers and which one it reached depended on the tree's history. `tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span, and a scroll placing a pass-through `SetSize` in a box the content decided, where the span under it was placed once and nothing at its own edge said so. `ActiveData::offered_px` keeps the pixel size of the box the parent first asked about the child in, whether through `known_len` or a first `place`, beside `px`, the box it drew against. A dirty widget whose size reads an axis on which some reader up its chain gave what it read a box other than the one it asked in is not drawn locally: the chain is marked and the parent of the highest such placement draws, since above it every box is a constraint rather than an answer. The walk goes up the whole reader chain because a pass-through hands a derived box down unchanged. `Scroll` read its box's length for the clamp through `px_len`, which records the reported size as depending on it, and it does not: its size is its content's. That made every scroll tick a size question asked in a derived box, at 34x the instructions. `Painter::px_len_for_draw` is the read that records nothing. Instructions per frame on the depth-8 rig against the previous head: `many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes 200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the five reference renders and the resize render are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
66 lines
2.1 KiB
Rust
66 lines
2.1 KiB
Rust
use crate::prelude::*;
|
|
|
|
pub struct Scroll {
|
|
inner: StrongWidget,
|
|
axis: Axis,
|
|
amt: f32,
|
|
snap_end: bool,
|
|
container_len: f32,
|
|
content_len: f32,
|
|
}
|
|
|
|
impl Widget for Scroll {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
let output_len = painter.output_len(self.axis);
|
|
// Its size is its content's, whatever box that is scrolled within.
|
|
let container_len = UiScalar::px(painter.px_len_for_draw(self.axis));
|
|
// Draw in the whole container only when its scrolling-axis length is
|
|
// not already known, then place it at the scrolled offset.
|
|
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
|
let measured = known_len.is_none();
|
|
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
|
|
let content_len = known_len
|
|
.unwrap_or_else(|| child.unwrap().axis(self.axis))
|
|
.apply_rest()
|
|
.within_len(container_len)
|
|
.to_px(output_len);
|
|
self.container_len = container_len.to_px(output_len);
|
|
self.content_len = content_len;
|
|
|
|
if self.snap_end {
|
|
self.amt = self.content_len - self.container_len;
|
|
}
|
|
self.update_amt();
|
|
|
|
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
|
let placed = painter.place(&self.inner, region).size();
|
|
child.unwrap_or(placed)
|
|
}
|
|
}
|
|
|
|
impl Scroll {
|
|
pub fn new(inner: StrongWidget, axis: Axis) -> Self {
|
|
Self {
|
|
inner,
|
|
axis,
|
|
amt: 0.0,
|
|
snap_end: true,
|
|
container_len: 0.0,
|
|
content_len: 0.0,
|
|
}
|
|
}
|
|
|
|
pub fn update_amt(&mut self) {
|
|
self.amt = self.amt.max(0.0);
|
|
let len = (self.content_len - self.container_len).max(0.0);
|
|
self.amt = self.amt.min(len);
|
|
self.snap_end = self.amt == len;
|
|
}
|
|
|
|
pub fn scroll(&mut self, amt: f32) {
|
|
self.amt -= amt;
|
|
self.update_amt();
|
|
}
|
|
}
|