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}