Skip to main content

kui_core/
scroll.rs

1//! Retained scroll state: the offset of every scroll container, keyed by
2//! node `Key`, and the geometry the last layout resolved for it.
3//!
4//! A view makes a scroll container with `NodeSpec::scroll_y` /
5//! `scroll_x` and never touches this store directly: the wheel, the
6//! scrollbar and the keys move the offset, `Core::set_scroll` and
7//! `Core::reveal` move it from the app, and layout clamps it to the
8//! content every frame. What a view does read is [`ScrollGeometry`]
9//! through `Ui::scroll_geometry` / `Core::scroll_geometry`: where the
10//! container is, how big its content came out and where it is scrolled
11//! to, which is what a virtual list needs to build only the visible rows.
12//!
13//! ```rust
14//! use kui_core::{Core, NodeSpec, Size, TextStyle};
15//!
16//! let mut core = Core::new();
17//! let mut ui = core.frame(Size::new(200.0, 100.0), 1.0);
18//! ui.configure_root(NodeSpec::column().fill());
19//! let list = ui.with_keyed("list", NodeSpec::column().fill().scroll_y(), |ui| {
20//!     for i in 0..50 {
21//!         ui.text(&format!("row {i}"), TextStyle::new(14.0));
22//!     }
23//! });
24//! ui.finish();
25//!
26//! // Read during the next frame's build (or between frames): last layout's numbers.
27//! let g = core.scroll_geometry(list).expect("laid out as a scroll container");
28//! assert!(g.content.h > g.rect.h);
29//! assert!(g.max_offset.y > 0.0 && g.offset.y == 0.0);
30//! ```
31
32use rustc_hash::FxHashMap;
33
34use crate::geom::{Rect, Size, Vec2};
35use crate::key::Key;
36
37/// What the last layout resolved for a scroll container: where the container
38/// landed, how big its content came out, and the offset it clamped. All
39/// logical px, in the same viewport coordinates `on_layout` reports.
40///
41/// This is the geometry a view needs to build only the rows that can be seen
42/// — it is read during the *next* frame's build, so it describes the frame
43/// before. See `Core::scroll_geometry` for what that costs and
44/// `widgets::uniform_list` for the uniform-row case done for you.
45///
46/// The four numbers describe one moment, which is what makes arithmetic on
47/// them safe: `offset` is always within `max_offset`, even immediately after
48/// a `set_scroll` wrote something wilder (the documented "a huge value jumps
49/// to the end" would otherwise hand a view an index a million rows past its
50/// data).
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct ScrollGeometry {
53    /// The container's own box, as the last layout placed and sized it —
54    /// the same rect an `on_layout` on that node would have reported.
55    pub rect: Rect,
56    /// Its laid-out content, padding included. Bigger than `rect` on an axis
57    /// exactly when that axis has somewhere left to scroll.
58    pub content: Size,
59    /// Where the container is scrolled to (positive = content moved up /
60    /// left): the retained offset clamped to `max_offset`, which is what the
61    /// next layout will resolve it to unless the content changes size in the
62    /// same frame. `Core::scroll_offset` is the unclamped retained number.
63    pub offset: Vec2,
64    /// How far `offset` can travel on each axis — zero on an axis that does
65    /// not scroll, or whose content fits. `offset == max_offset` is "at the
66    /// end", which is how a log view asks whether it is still tailing.
67    pub max_offset: Vec2,
68}
69
70impl ScrollGeometry {
71    /// `{x, y, w, h, content_w, content_h, offset: {x, y}, max_offset:
72    /// {x, y}}` — the box's rect flattened, its content's size beside it.
73    pub fn to_value(&self) -> crate::value::Value {
74        use crate::value::Value;
75        Value::map([
76            ("x", Value::float(self.rect.x)),
77            ("y", Value::float(self.rect.y)),
78            ("w", Value::float(self.rect.w)),
79            ("h", Value::float(self.rect.h)),
80            ("content_w", Value::float(self.content.w)),
81            ("content_h", Value::float(self.content.h)),
82            ("offset", self.offset.to_value()),
83            ("max_offset", self.max_offset.to_value()),
84        ])
85    }
86}
87
88/// One container's retained state. The offset exists from the moment anything
89/// writes one; the rest only once a layout has resolved the key *as a scroll
90/// container*, which is why it is optional and the offset is not.
91#[derive(Default, Clone, Copy)]
92struct Entry {
93    offset: Vec2,
94    geom: Option<(Rect, Size, Vec2)>,
95    /// The offset the last layout *placed the content at* — `offset` as
96    /// `resolve` clamped it, before anything wrote a newer one. A wheel
97    /// notch between two frames moves `offset` at once and this only at
98    /// the next layout, which is the difference a drag following its
99    /// scroller reads: the frame on screen is at
100    /// this offset, whatever the store already holds.
101    laid: Vec2,
102    /// The frame a layout last resolved this key, or an offset was last
103    /// written at it. Only the budget reads it (see
104    /// [`MAX_UNDECLARED_SCROLLS`]).
105    last_declared: u64,
106    /// For an `auto` bar: the scroll state (offset, max) the bar was last
107    /// emitted for, and the clock reading it last changed at — or was
108    /// otherwise active — so the bar knows how long it has been quiet.
109    /// `None` until the bar is first emitted.
110    bar: Option<(Vec2, Vec2, f64)>,
111    /// For an `anchor` container: the child first in view at the last
112    /// layout and where its leading edge was in the content, on the main
113    /// axis. What the next layout keeps still.
114    anchor: Option<(Key, f32)>,
115    /// A programmatic offset change waiting to be eased — a `set_scroll`
116    /// or a `reveal` since the last layout. The pointer's own
117    /// writes leave it false: a scroll under the thumb or the wheel is
118    /// the hand's, and easing it would lag behind the finger.
119    asked_smooth: bool,
120    /// The leg an eased offset is on: where the content was when it
121    /// started, when that was, and the container's transition. The
122    /// target is `offset`, and `resolve` samples between the two while
123    /// it runs — as does `geometry`, so a view slicing rows during the
124    /// leg slices by the place this frame will draw.
125    smooth: Option<(Vec2, f64, crate::anim::Transition)>,
126    /// The frame a layout last resolved this key as a scroll container
127    /// — unlike `last_declared`, which a write also stamps. What
128    /// `take_resliced` asks before it compares a read with a layout.
129    laid_frame: u64,
130}
131
132/// How many *undeclared* scroll entries the store keeps before the longest
133/// undeclared one is dropped. An entry a layout resolved in the frame
134/// that just ended is never evicted, however many there are. An entry is
135/// about 64 bytes, so a full budget is about 64 KB.
136pub const MAX_UNDECLARED_SCROLLS: usize = 1024;
137
138/// The retained scroll state of one window, owned by `Core`. Reach it
139/// through `Core::scroll_offset`, `Core::set_scroll` and
140/// `Core::scroll_geometry` rather than directly.
141#[derive(Default)]
142pub struct ScrollStore {
143    entries: FxHashMap<Key, Entry>,
144    /// The clock an eased offset reads (`Core::set_time`); `None` until
145    /// a driver sets one, which is a driver that shows no animation.
146    now: Option<f64>,
147    /// The containers whose eased offset the last layout left
148    /// mid-flight, kept as that layout found them: a wheel between two
149    /// frames ends a leg in its entry, but the frame after is still owed
150    /// by that container, and a trace names it.
151    owing: Vec<Key>,
152    /// The frame being built, stamped onto every entry touched.
153    frame_no: u64,
154    /// The geometries read during this build ([`Self::geometry`]) — what
155    /// a view sliced its rows by — as the box size and the clamped
156    /// offset it was handed, so that after layout the core can tell
157    /// whether the frame came out as the view assumed
158    /// ([`Self::resliced`]). Interior mutability because a read is a
159    /// read.
160    reads: std::cell::RefCell<Vec<(Key, Size, Vec2)>>,
161}
162
163impl ScrollStore {
164    /// How many entries are retained, declared and undeclared together.
165    pub fn len(&self) -> usize {
166        self.entries.len()
167    }
168
169    pub fn is_empty(&self) -> bool {
170        self.entries.is_empty()
171    }
172
173    /// Stamps the frame being built and, if the map has grown past the
174    /// budget, drops the longest-undeclared entries
175    /// ([`MAX_UNDECLARED_SCROLLS`]). Same rule as the editors': an entry
176    /// the frame that just ended resolved (or that was written at since)
177    /// is never evicted, so retention across absence still holds — this is
178    /// a ceiling, not a prune. A store inside the budget never walks
179    /// itself.
180    pub(crate) fn begin_frame(&mut self, frame_no: u64) {
181        self.frame_no = frame_no;
182        // Not the reads: a binding that runs its view before the frame
183        // begins — Node's, which calls `scrollGeometry` while the JS
184        // view builds its tree — reads between two frames, and those
185        // reads are this frame's. `take_resliced` drains them.
186        self.owing.clear();
187        if self.entries.len() > MAX_UNDECLARED_SCROLLS {
188            self.evict(frame_no.saturating_sub(1));
189        }
190    }
191
192    fn evict(&mut self, declared_at: u64) {
193        crate::retain::evict_undeclared(
194            &mut self.entries,
195            MAX_UNDECLARED_SCROLLS,
196            declared_at,
197            |e| e.last_declared,
198            |_| false,
199            |_| {},
200        );
201    }
202
203    pub fn offset(&self, key: Key) -> Vec2 {
204        self.entries.get(&key).map_or(Vec2::ZERO, |e| e.offset)
205    }
206
207    /// Where `key`'s content is drawn: the offset the last layout placed
208    /// it at, for a key a layout has resolved, else the stored offset.
209    /// What the thumb, the access tree and the inspector show — during
210    /// an eased leg the target is somewhere the content is not
211    /// yet.
212    pub(crate) fn drawn(&self, key: Key) -> Vec2 {
213        self.entries.get(&key).map_or(
214            Vec2::ZERO,
215            |e| {
216                if e.geom.is_some() { e.laid } else { e.offset }
217            },
218        )
219    }
220
221    /// Where a write that takes the content "where it stands" starts
222    /// from: the drawn place while a leg is in flight, else the stored
223    /// offset — which between two frames may already hold a wheel
224    /// notch the frame has not drawn, and that notch must add up.
225    pub(crate) fn standing(&self, key: Key) -> Vec2 {
226        self.entries.get(&key).map_or(Vec2::ZERO, |e| {
227            if e.smooth.is_some() { e.laid } else { e.offset }
228        })
229    }
230
231    /// Moves `key`'s scroll state by the content that moved under it —
232    /// `drawn` for the drawn place and a leg's start, `target` for the
233    /// offset — without asking for an ease or ending one: each means the
234    /// same content it did, in a coordinate space that moved under it.
235    /// Two deltas because mid-leg they are two places in the content, and
236    /// rows measured between them move one and not the other. What
237    /// `widgets::list`'s height correction is: a `set_scroll` there,
238    /// eased on a container with a `transition`, showed the uncorrected
239    /// frame the correction exists to hide, and mid-glide it moved the
240    /// target to where the content stood and ended a long jump a screen
241    /// along.
242    pub(crate) fn shift(&mut self, key: Key, drawn: Vec2, target: Vec2) {
243        let e = self.entries.entry(key).or_default();
244        // From the offset as the last layout clamped it — the number the
245        // view read — or a raw "jump to the end" (`f32::MAX`) plus a
246        // shift would still be the end, and the rows would not hold.
247        if let Some((_, _, max)) = e.geom {
248            e.offset.x = e.offset.x.clamp(0.0, max.x);
249            e.offset.y = e.offset.y.clamp(0.0, max.y);
250        }
251        e.offset.x += target.x;
252        e.offset.y += target.y;
253        e.laid.x += drawn.x;
254        e.laid.y += drawn.y;
255        if let Some((from, _, _)) = &mut e.smooth {
256            from.x += drawn.x;
257            from.y += drawn.y;
258        }
259        e.last_declared = self.frame_no;
260    }
261
262    /// The last layout's geometry for `key`, or `None` for a key no layout
263    /// has ever resolved as a scroll container — including one that only
264    /// ever had an offset written at it.
265    /// The offset the last layout placed `key`'s content at, or `None`
266    /// for a key no layout has resolved as a scroll container. Unlike
267    /// [`Self::geometry`]'s `offset`, an offset written since is not in
268    /// it: this is where the frame on screen *is*.
269    pub(crate) fn laid_offset(&self, key: Key) -> Option<Vec2> {
270        let e = self.entries.get(&key)?;
271        e.geom?;
272        Some(e.laid)
273    }
274
275    pub fn geometry(&self, key: Key) -> Option<ScrollGeometry> {
276        let e = self.entries.get(&key)?;
277        let (rect, content, max_offset) = e.geom?;
278        // Clamped here, not just at layout: a `set_scroll` between two
279        // frames leaves a raw number in the store, and a view slicing
280        // its data by it would index far off the end.
281        //
282        // While an offset is easing this is where the content
283        // *is*, not where it is going: the question this answers is
284        // which rows can be seen, and during the leg that is the ones
285        // around the drawn offset. The target is `Core::scroll_offset`.
286        //
287        // And where it *will be* drawn this frame, not where the last
288        // frame drew it: the leg is sampled at the clock the coming
289        // layout reads, so a view slices the rows that frame shows
290        // rather than a frame behind with a blank band on every frame of
291        // a long glide.
292        let clamp = |v: Vec2| Vec2::new(v.x.clamp(0.0, max_offset.x), v.y.clamp(0.0, max_offset.y));
293        let offset = match (e.smooth, self.now) {
294            (Some((from, start, t)), Some(now)) => {
295                sample((clamp(from), start, t), clamp(e.offset), now).0
296            }
297            (Some(_), None) => clamp(e.laid),
298            (None, _) => clamp(e.offset),
299        };
300        if let Ok(mut reads) = self.reads.try_borrow_mut() {
301            reads.push((key, Size::new(rect.w, rect.h), offset));
302        }
303        Some(ScrollGeometry {
304            rect,
305            content,
306            offset,
307            max_offset,
308        })
309    }
310
311    /// Whether a container whose geometry this build read came out of
312    /// layout with another box size or another placed offset than the
313    /// one it was handed: the view sliced by the frame before, and that
314    /// frame is not this one — a resize, a split sliding open, a reveal
315    /// — so one more frame is owed, built against what is on screen now.
316    /// Without it the rows a virtual list built for the old box stay on
317    /// screen until the next event, a screenful short.
318    ///
319    /// Drains the reads: the next frame's are whatever is read from here
320    /// on. A container read and then not laid out this frame — a pane in
321    /// a hidden tab — owes nothing: there is no frame of it on screen to
322    /// correct, and comparing its stale placement with a `set_scroll`
323    /// written since asked for a frame on every frame until the tab came
324    /// back.
325    pub(crate) fn take_resliced(&mut self) -> bool {
326        let reads = std::mem::take(self.reads.get_mut());
327        let frame_no = self.frame_no;
328        reads.iter().any(|(key, size, offset)| {
329            match self
330                .entries
331                .get(key)
332                .filter(|e| e.laid_frame == frame_no)
333                .and_then(|e| e.geom.map(|g| (g, e.laid)))
334            {
335                Some(((rect, _, _), laid)) => {
336                    (rect.w - size.w).abs() > 0.5
337                        || (rect.h - size.h).abs() > 0.5
338                        || (laid.x - offset.x).abs() > 0.5
339                        || (laid.y - offset.y).abs() > 0.5
340                }
341                None => false,
342            }
343        })
344    }
345
346    /// Where `key` stands against its travel, for a scroll gesture asking
347    /// whether it can still move a way: the place a wheel's
348    /// delta would be added to — the drawn place while an eased leg is in
349    /// flight, which the delta ends, else the stored offset with
350    /// any notch since the last frame in it — clamped to the last
351    /// layout's `max_offset`, and that `max_offset`. `None` for a key no
352    /// layout has resolved as a scroll container. A read that records
353    /// nothing, unlike [`Self::geometry`]'s: a wheel asking is not a view
354    /// slicing rows.
355    pub(crate) fn room(&self, key: Key) -> Option<(Vec2, Vec2)> {
356        let e = self.entries.get(&key)?;
357        let (_, _, max) = e.geom?;
358        let from = if e.smooth.is_some() { e.laid } else { e.offset };
359        let at = Vec2::new(from.x.clamp(0.0, max.x), from.y.clamp(0.0, max.y));
360        Some((at, max))
361    }
362
363    /// Adds a delta (positive = scroll content further down/right).
364    /// Clamping happens in the next layout pass.
365    pub fn scroll_by(&mut self, key: Key, delta: Vec2) {
366        self.scroll_by_from(key, delta, false);
367    }
368
369    /// The same, from a programmatic source — a `reveal`, an app's
370    /// `set_scroll` — which a container declaring a `transition` eases
371    /// into rather than jumping.
372    pub fn scroll_by_smooth(&mut self, key: Key, delta: Vec2) {
373        self.scroll_by_from(key, delta, true);
374    }
375
376    fn scroll_by_from(&mut self, key: Key, delta: Vec2, smooth: bool) {
377        let frame_no = self.frame_no;
378        let e = self.entries.entry(key).or_default();
379        e.asked_smooth = smooth;
380        // A delta is measured against what is on screen — the wheel's
381        // notch, a reveal's gap from the drawn node — so mid-leg it
382        // moves from the drawn place, not from the target: adding it to
383        // the target jumped the content past where the hand or the
384        // reveal meant. Taking the leg's place ends the leg; a
385        // programmatic ask starts a new one from there at the next
386        // layout, and a second delta before then adds up as it did.
387        if e.smooth.take().is_some() {
388            e.offset = e.laid;
389        }
390        e.offset.x += delta.x;
391        e.offset.y += delta.y;
392        // An offset written between two frames counts as a declaration: it
393        // is usually a `set_scroll` at a key this frame is about to build.
394        e.last_declared = frame_no;
395    }
396
397    pub fn set(&mut self, key: Key, offset: Vec2) {
398        self.set_from(key, offset, false);
399    }
400
401    /// The same, from a programmatic source; see
402    /// [`scroll_by_smooth`](Self::scroll_by_smooth).
403    pub fn set_smooth(&mut self, key: Key, offset: Vec2) {
404        self.set_from(key, offset, true);
405    }
406
407    fn set_from(&mut self, key: Key, offset: Vec2, smooth: bool) {
408        let frame_no = self.frame_no;
409        let e = self.entries.entry(key).or_default();
410        e.last_declared = frame_no;
411        // Asked again for where a leg under way is already going — a view
412        // that calls `reveal_row` every frame until the row shows — the
413        // leg goes on. Asked anew, it would start over from where it is
414        // drawn, sampled at the same instant: the content never moved and
415        // every frame asked for the next.
416        if smooth && e.smooth.is_some() && e.offset == offset {
417            return;
418        }
419        e.asked_smooth = smooth;
420        if !smooth {
421            e.smooth = None;
422        }
423        e.offset = offset;
424    }
425
426    /// The clock the eased offsets read, fed by `Core::set_time` beside
427    /// the animation store's.
428    pub(crate) fn set_time(&mut self, now: f64) {
429        self.now = Some(now);
430    }
431
432    /// Whether an eased offset was still mid-flight at the last layout,
433    /// so the driver owes another frame.
434    pub fn animating(&self) -> bool {
435        !self.owing.is_empty()
436    }
437
438    /// The containers whose eased offset the last layout left mid-flight
439    /// — what [`Self::animating`] is made of, for a trace.
440    /// As that layout left them, not as the entries read now: a leg a
441    /// wheel ended since still owes the frame it was mid-flight for.
442    pub(crate) fn easing(&self) -> impl Iterator<Item = Key> + '_ {
443        self.owing.iter().copied()
444    }
445
446    /// How long `key`'s scroll state has been quiet, in seconds of the
447    /// driver's clock, as of `now` — zero on the frame the offset or the
448    /// travel moved, on the first frame the bar is asked about, and
449    /// whenever `active` says the pointer or a drag is holding it. What an
450    /// `auto` scrollbar fades by (`crate::spec::ScrollbarMode::Auto`).
451    pub(crate) fn bar_idle(&mut self, key: Key, now: f64, active: bool) -> f64 {
452        let e = self.entries.entry(key).or_default();
453        let max = e.geom.map_or(Vec2::ZERO, |(_, _, m)| m);
454        // The drawn place: an eased leg is the content moving, and a bar
455        // that faded while it moved would be quiet through a scroll.
456        let state = (if e.geom.is_some() { e.laid } else { e.offset }, max);
457        match e.bar {
458            Some((off, m, at)) if !active && (off, m) == state => (now - at).max(0.0),
459            _ => {
460                e.bar = Some((state.0, state.1, now));
461                0.0
462            }
463        }
464    }
465
466    /// The anchor the last layout recorded for `key`: the child first in
467    /// view and its leading edge's content position on the main axis.
468    pub(crate) fn anchor(&self, key: Key) -> Option<(Key, f32)> {
469        self.entries.get(&key).and_then(|e| e.anchor)
470    }
471
472    /// Records this layout's anchor for `key` (see [`Self::anchor`]);
473    /// `None` when the container had nothing in view.
474    pub(crate) fn set_anchor(&mut self, key: Key, anchor: Option<(Key, f32)>) {
475        self.entries.entry(key).or_default().anchor = anchor;
476    }
477
478    /// What layout calls on a scroll container: records the geometry it
479    /// resolved, clamps the stored offset to `[0, max]` per axis, and
480    /// returns it. `max` is passed rather than derived from `rect` and
481    /// `content` because an axis that does not scroll has no travel however
482    /// far its content overflows.
483    pub fn resolve(
484        &mut self,
485        key: Key,
486        rect: Rect,
487        content: Size,
488        max: Vec2,
489        smooth: Option<crate::anim::Transition>,
490    ) -> Vec2 {
491        let frame_no = self.frame_no;
492        let now = self.now;
493        let e = self.entries.entry(key).or_default();
494        e.last_declared = frame_no;
495        e.geom = Some((rect, content, Vec2::new(max.x.max(0.0), max.y.max(0.0))));
496        e.offset.x = e.offset.x.clamp(0.0, max.x.max(0.0));
497        e.offset.y = e.offset.y.clamp(0.0, max.y.max(0.0));
498        // Where the content is drawn: the offset itself, unless the
499        // container asked for a transition and the move was a
500        // programmatic one — then it starts where the last frame left it
501        // and eases to the offset over the leg.
502        e.laid_frame = frame_no;
503        let drawn = match (smooth, now) {
504            (Some(t), Some(now)) if t.duration_ms > 0.0 => {
505                if std::mem::take(&mut e.asked_smooth) && e.laid != e.offset {
506                    // From where the content is *now*, so a second
507                    // reveal mid-flight carries on from what is on
508                    // screen rather than snapping back to start.
509                    e.smooth = Some((e.laid, now, t));
510                }
511                match e.smooth {
512                    Some((from, start, _)) => {
513                        // The start clamped too: content that shrank
514                        // under a leg must not be drawn past its new end
515                        // for the rest of the leg.
516                        let from = Vec2::new(
517                            from.x.clamp(0.0, max.x.max(0.0)),
518                            from.y.clamp(0.0, max.y.max(0.0)),
519                        );
520                        let (at, done) = sample((from, start, t), e.offset, now);
521                        if done {
522                            e.smooth = None;
523                        } else {
524                            e.smooth = Some((from, start, t));
525                            if !self.owing.contains(&key) {
526                                self.owing.push(key);
527                            }
528                        }
529                        at
530                    }
531                    None => e.offset,
532                }
533            }
534            _ => {
535                e.asked_smooth = false;
536                e.smooth = None;
537                e.offset
538            }
539        };
540        e.laid = drawn;
541        drawn
542    }
543}
544
545/// A leg sampled at `now`: the drawn place, and whether the leg is over.
546fn sample(
547    (from, start, t): (Vec2, f64, crate::anim::Transition),
548    to: Vec2,
549    now: f64,
550) -> (Vec2, bool) {
551    let p = (((now - start) / (t.duration_ms as f64 / 1000.0)) as f32).clamp(0.0, 1.0);
552    let k = t.curve().apply(p);
553    (
554        Vec2::new(from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k),
555        p >= 1.0,
556    )
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    fn resolve(s: &mut ScrollStore, k: Key, max: Vec2) -> Vec2 {
564        // The geometry a 100x300 container with `max` of overflow would have.
565        s.resolve(
566            k,
567            Rect::new(0.0, 0.0, 100.0, 300.0),
568            Size::new(100.0 + max.x, 300.0 + max.y),
569            max,
570            None,
571        )
572    }
573
574    #[test]
575    fn accumulates_and_clamps() {
576        let mut s = ScrollStore::default();
577        let k = Key::ROOT.str("list");
578        s.scroll_by(k, Vec2::new(0.0, 120.0));
579        s.scroll_by(k, Vec2::new(0.0, 500.0));
580        assert_eq!(
581            resolve(&mut s, k, Vec2::new(0.0, 300.0)),
582            Vec2::new(0.0, 300.0)
583        );
584        s.scroll_by(k, Vec2::new(0.0, -1000.0));
585        assert_eq!(
586            resolve(&mut s, k, Vec2::new(0.0, 300.0)),
587            Vec2::new(0.0, 0.0)
588        );
589        // Content shrinking re-clamps existing offsets.
590        s.scroll_by(k, Vec2::new(0.0, 250.0));
591        assert_eq!(
592            resolve(&mut s, k, Vec2::new(0.0, 100.0)),
593            Vec2::new(0.0, 100.0)
594        );
595    }
596
597    #[test]
598    fn unknown_key_is_zero() {
599        let mut s = ScrollStore::default();
600        assert_eq!(s.offset(Key::ROOT.str("x")), Vec2::ZERO);
601        assert_eq!(
602            resolve(&mut s, Key::ROOT.str("x"), Vec2::new(0.0, 100.0)),
603            Vec2::ZERO
604        );
605    }
606
607    /// An offset written at a key is not evidence that the key is a
608    /// container: only a layout resolving it fills the geometry in.
609    #[test]
610    fn geometry_needs_a_layout_not_just_an_offset() {
611        let mut s = ScrollStore::default();
612        let k = Key::ROOT.str("list");
613        assert_eq!(s.geometry(k), None);
614        s.set(k, Vec2::new(0.0, 40.0));
615        assert_eq!(s.offset(k), Vec2::new(0.0, 40.0));
616        assert_eq!(s.geometry(k), None);
617
618        resolve(&mut s, k, Vec2::new(0.0, 700.0));
619        let g = s.geometry(k).expect("resolved");
620        assert_eq!(g.rect, Rect::new(0.0, 0.0, 100.0, 300.0));
621        assert_eq!(g.content, Size::new(100.0, 1000.0));
622        // The geometry carries the clamped offset, not the written one.
623        assert_eq!(g.offset, Vec2::new(0.0, 40.0));
624        assert_eq!(g.max_offset, Vec2::new(0.0, 700.0));
625    }
626
627    /// An axis that does not scroll has no travel however far its content
628    /// overflows — which is why `max` is recorded rather than derived from
629    /// the two sizes.
630    #[test]
631    fn max_offset_is_the_axis_travel_not_the_overflow() {
632        let mut s = ScrollStore::default();
633        let k = Key::ROOT.str("list");
634        // Content overflows both axes; only y scrolls.
635        s.resolve(
636            k,
637            Rect::new(0.0, 0.0, 100.0, 300.0),
638            Size::new(400.0, 1000.0),
639            Vec2::new(0.0, 700.0),
640            None,
641        );
642        let g = s.geometry(k).unwrap();
643        assert_eq!(g.max_offset, Vec2::new(0.0, 700.0));
644        assert!(
645            g.content.w > g.rect.w,
646            "the x overflow is real, the travel is not"
647        );
648    }
649
650    /// A `set_scroll` of "a huge value" means "the end"; a view slicing by
651    /// the geometry sees the end, not the huge value.
652    #[test]
653    fn geometry_clamps_an_offset_no_layout_has_seen_yet() {
654        let mut s = ScrollStore::default();
655        let k = Key::ROOT.str("list");
656        resolve(&mut s, k, Vec2::new(0.0, 700.0));
657        s.set(k, Vec2::new(0.0, 1e9));
658        // The raw retained number is what was written...
659        assert_eq!(s.offset(k), Vec2::new(0.0, 1e9));
660        // ...but the geometry is coherent.
661        let g = s.geometry(k).unwrap();
662        assert_eq!(g.offset, g.max_offset);
663        assert_eq!(g.offset.y, 700.0);
664    }
665}