Files
iris/src/widget/position/scroll.rs
T
iris-aiandClaude Fable 5.1 5b7800264d Read a child's answer in the asker's frame, and drop the root move entry
A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.

That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.

The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.

Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against 5f16617, a live resize to 1280x800 is identical to a
cold render, and the 100-seed oracle, all fifteen shrinker cases at 400
seeds of depth 5, and 1000 seeds of depth 6 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:00:15 -04:00

101 lines
4.0 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);
// Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset.
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) {
Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
let content = answer_len.apply_leftover();
self.container_len = container_len;
self.content_len = content.to_px(container_len);
if self.snap_end {
self.amt = self.content_len - self.container_len;
}
self.update_amt();
let align = painter.alignment().axis(self.axis);
// Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. 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 fixed_len = content.rel == Rel::ZERO;
if fixed_len && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=Px::MAX);
} else if fixed_len && !self.snap_end {
let left = self.content_len - self.amt;
painter.holds(self.axis, Px::MIN..=left);
}
// Content shorter than the viewport has room to sit in, and where it
// sits is this widget's own alignment -- the same property that would
// have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(Px::ZERO);
let anchor = slack.mul(align.rel());
let mut region = UiRegion::FULL;
// Content that fills the viewport and has not been scrolled is the
// viewport, and is handed back as it came. 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 moved = anchor != Px::ZERO || self.amt != Px::ZERO;
if moved || self.content_len != self.container_len {
let offset = UiVec2::from_axis(
self.axis,
Len::from_parts(Rel::ZERO, anchor - self.amt),
Len::ZERO,
);
region = region.offset(offset);
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
}
painter.widget_decided(&self.inner, region, [true; 2]);
// 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
}
}
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();
}
}