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}