Skip to main content

kui_core/
scroll.rs

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