Skip to main content

kui_core/runtime/
scrolling.rs

1//! Retained scroll offsets: the outside API (`reveal`, `set_scroll`, the
2//! geometry a virtual list reads) and the after-layout nudges that keep a
3//! caret or a revealed node in view. The wheel and the scrollbars move the
4//! same offsets from `dispatch`.
5
6use super::*;
7
8impl Core {
9    /// Sets one axis of a container's scroll offset, keeping the other
10    /// where it stands — mid-leg, the drawn place and not the target, so
11    /// a thumb dragged on one axis does not jump the other to where an
12    /// eased leg was going.
13    pub(crate) fn set_scroll_axis(&mut self, key: Key, axis: ScrollAxis, value: f32) {
14        let mut off = self.scroll.standing(key);
15        match axis {
16            ScrollAxis::X => off.x = value,
17            ScrollAxis::Y => off.y = value,
18        }
19        self.scroll.set(key, off);
20    }
21
22    // -- Scrolling ------------------------------------------------------
23    // Scroll offsets are retained per node key, clamped to the overflow the
24    // last layout found. The wheel, the scrollbars, Tab and the caret all
25    // move them from inside; these three are the same moves from outside,
26    // so "scroll to the selected row", "jump to the top" and "restore the
27    // position I saved" need no layout arithmetic in the app.
28
29    /// Scrolls the nearest scrolling ancestor of `key` so the node is
30    /// inside it — what Tab does to the control it lands on, asked for by
31    /// name. Already-visible nodes stay put.
32    ///
33    /// The request resolves at the next `finish_frame`, against the frame
34    /// that one lays out — the frame being built if this is called from a
35    /// view, the one after it if from an event handler (a frame is
36    /// requested, so one comes). That is what lets a view reveal a row it
37    /// is declaring for the first time. If that frame does not declare
38    /// `key`, or nothing above it scrolls, it is a no-op — the request is
39    /// spent, not held for the frame that might. Two reveals before one
40    /// frame into the *same* container are contradictory, so the last one
41    /// wins there; reveals into different containers — a tab strip and
42    /// the pane list under it — are not, and each lands.
43    ///
44    /// Traced, the ask is `"reveal"` at the caller's line.
45    #[track_caller]
46    pub fn reveal(&mut self, key: Key) {
47        self.pending_reveal.push(key);
48        self.owe_frame("reveal");
49    }
50
51    /// [`Self::reveal`] by the label a node declares, resolved when the
52    /// frame finishes — against the frame being built, or the next one
53    /// when none is — so a view may name a row it is declaring right now,
54    /// or one the frame after declares. A label that frame
55    /// does not declare raises `label-without-node` and moves nothing.
56    #[track_caller]
57    pub fn reveal_label(&mut self, label: &str) {
58        self.pending_reveal_labels
59            .push((label.to_string(), self.origin, self.ns_key));
60        self.owe_frame("reveal_label");
61    }
62
63    /// [`Self::set_scroll`] by label, resolved like [`Self::reveal_label`]
64    /// but before layout, so the frame that resolves it lays out at the
65    /// offset.
66    #[track_caller]
67    pub fn set_scroll_label(&mut self, label: &str, offset: Vec2) {
68        self.pending_scroll_labels
69            .push((label.to_string(), self.origin, self.ns_key, offset));
70        if !self.building {
71            self.owe_frame("set_scroll_label");
72        }
73    }
74
75    /// A deferred label, found as the fill that asked would find it
76    /// (`find_label` answers per origin, and within it per fill), in this
77    /// frame alone.
78    fn find_label_as(
79        &mut self,
80        label: &str,
81        origin: crate::tree::OriginId,
82        fill: Key,
83    ) -> Option<Key> {
84        let at = std::mem::replace(&mut self.origin, origin);
85        let ns = std::mem::replace(&mut self.ns_key, fill);
86        let key = self.find_label(label, false);
87        self.origin = at;
88        self.ns_key = ns;
89        key
90    }
91
92    /// Before layout: the `set_scroll_label` asks, as `set_scroll`s.
93    pub(crate) fn resolve_scroll_labels(&mut self) {
94        for (label, origin, fill, offset) in std::mem::take(&mut self.pending_scroll_labels) {
95            match self.find_label_as(&label, origin, fill) {
96                Some(key) => self.scroll.set_smooth(key, offset),
97                None => self
98                    .diag
99                    .raise(crate::diag::label_without_node("set_scroll", &label)),
100            }
101        }
102    }
103
104    /// The retained scroll offset of the container `key`, as the last
105    /// layout clamped it (positive = content moved up / left) — the
106    /// target: while a container with a `transition` eases to it the
107    /// content is drawn short of it, where [`Self::scroll_geometry`]
108    /// says. Zero for a node that never scrolled, and for one that is not
109    /// a container at all — the store keeps offsets, not membership.
110    pub fn scroll_offset(&self, key: Key) -> Vec2 {
111        let v = self.scroll.offset(key);
112        self.note_read(|| replay::Read::Scroll(key, v));
113        v
114    }
115
116    /// Everything the last layout resolved for the container `key`: its own
117    /// box, its content size, and the clamped offset — `None` for a key no
118    /// layout has ever resolved as a scroll container. While an eased
119    /// leg runs the offset is where the content is drawn rather than
120    /// the target, sampled at the clock the coming frame reads, so a view
121    /// slices the rows that frame shows.
122    ///
123    /// This is what makes a long list affordable. The core culls glyphs by
124    /// viewport but builds every child a view declares, so ten thousand rows
125    /// cost ten thousand rows; with the offset and the container's height a
126    /// view can declare only the rows that can be seen and two spacers, and
127    /// pay for a screenful. `widgets::uniform_list` is that, done.
128    ///
129    /// Read during a build, it describes the frame before — the tree it came
130    /// from is already cleared. That is one frame of lag on the size, so the
131    /// frame after a resize slices to the old height; a row or two of
132    /// overscan covers it, which is what the widget does. The rect is the
133    /// same one an `on_layout` on that node would post, without the round
134    /// trip through the app's model, and without firing every time an
135    /// enclosing container scrolls the whole list past.
136    pub fn scroll_geometry(&self, key: Key) -> Option<crate::scroll::ScrollGeometry> {
137        let shift = self.dt_shift();
138        let g = self.scroll.geometry(key).map(|mut g| {
139            g.rect.x -= shift.x;
140            g.rect.y -= shift.y;
141            g
142        });
143        self.note_read(|| replay::Read::ScrollGeom(key, g));
144        g
145    }
146
147    /// The rect the last frame laid `key` out at, in logical viewport px —
148    /// for a node that declared `on_layout`, whose rect the core keeps for
149    /// the event's edge trigger anyway. The query
150    /// shape of the `layout` event: the same numbers, read during the next
151    /// build with no event, no tag and no model field. `None` for a key
152    /// that did not declare `on_layout` last frame; read during a build it
153    /// describes the previous frame, like [`Self::scroll_geometry`].
154    pub fn layout_of(&self, key: Key) -> Option<Rect> {
155        let r = self.layout_of_raw(key);
156        self.note_read(|| replay::Read::Layout(key, r));
157        r
158    }
159
160    pub(crate) fn layout_of_raw(&self, key: Key) -> Option<Rect> {
161        let shift = self.dt_shift();
162        // "Last frame" is the one before this build while a build is on,
163        // and the one just finished between two — `frame_no` has already
164        // moved on in the first case and not in the second.
165        let last = self.frame_no - u64::from(self.building);
166        self.layouts
167            .get(&key)
168            .filter(|(_, seen)| *seen == last)
169            .map(|(r, _)| Rect::new(r.x - shift.x, r.y - shift.y, r.w, r.h))
170    }
171
172    /// Sets the container `key`'s retained offset, the way the wheel would.
173    /// Takes effect at the next layout, which clamps it to that frame's
174    /// overflow: `Vec2::ZERO` is "jump to the top", and a large value is
175    /// "jump to the end" without knowing the content height. Writing an
176    /// offset for a key that never scrolls is harmless; it just never
177    /// reads back. Between two frames the write asks for the frame that
178    /// lands it; from inside a view it asks for nothing, because the
179    /// frame being built is that frame — the positions pass reads the
180    /// store after the view has run — and a view writing every frame
181    /// would otherwise be a window that never idles (the devtools' events
182    /// list and `widgets::list` both write this way).
183    #[track_caller]
184    pub fn set_scroll(&mut self, key: Key, offset: Vec2) {
185        // Programmatic, so a container with a `transition` eases into it
186        // (F80); the wheel and the thumb go through `scroll_by` and
187        // `set_scroll_axis`, which do not.
188        self.scroll.set_smooth(key, offset);
189        if !self.building {
190            self.owe_frame("set_scroll");
191        }
192    }
193
194    /// Moves `key`'s scroll state by the content that moved under it:
195    /// `drawn` for where the content is drawn (and an eased leg's start),
196    /// `target` for the retained offset. No ease is asked or ended, and no
197    /// frame is asked for: it is a correction to the frame about to be
198    /// laid out — the rows above the window were measured and came out
199    /// another height — so it belongs to that frame, whoever calls it. A
200    /// variable-height list's anchor: `widgets::list` from its view,
201    /// the Node and Lua ports from theirs, just before the tree they
202    /// return is laid out.
203    pub fn shift_scroll(&mut self, key: Key, drawn: Vec2, target: Vec2) {
204        self.scroll.shift(key, drawn, target);
205    }
206
207    /// After layout: if the focused edit's caret moved this frame, nudge the
208    /// nearest scrollable ancestor so the caret stays visible, then re-run
209    /// the positions pass with the adjusted offset (positions is the only
210    /// pass scroll offsets feed into, so nothing else needs recomputing).
211    pub(crate) fn scroll_caret_into_view(&mut self) {
212        let Some(key) = self.edit.caret_moved else {
213            return;
214        };
215        // Not under a held pointer drag: the caret is where the pointer
216        // is, past the edge or not, and what moves the scroller then is
217        // the drag's own rate (ADR 0029, decision 2) — a reveal there
218        // would jump it by the whole distance past, every frame. The move
219        // stays noted, so the release reveals where the caret landed;
220        // a field still scrolls its own text below, at once.
221        let held = self.edit.dragging.is_some_and(|(k, _)| k == key);
222        if !held {
223            self.edit.caret_moved = None;
224        }
225        if self.edit.focused() != Some(key) {
226            return;
227        }
228        let Some(i) =
229            (0..self.tree.len()).find(|&i| self.tree.content[i] == NodeContent::Edit(key))
230        else {
231            return;
232        };
233        let pad = self.tree.specs[i].layout.padding;
234        // A single-line field scrolls its own text, and the box it does
235        // that in is final now that layout has run — so settle its offset
236        // before asking where the caret is. Without this the ancestor
237        // scrolls to reveal a caret the field is about to bring into view
238        // itself (F41); emission recomputes the same number.
239        let inner_w = (self.tree.size[i].w - pad.x()).max(0.0) * self.scale;
240        self.edit_with_fonts(|edit, fs| edit.line_offset(key, inner_w, fs));
241        if held {
242            return;
243        }
244        let Some((_, caret)) = self.stock_caret_viewport_rect(key) else {
245            return;
246        };
247        self.scroll_rect_into_view(i, caret, true);
248    }
249
250    /// After layout: resolves a pending `reveal(key)` against the frame
251    /// just laid out, then forgets it either way — a key this frame did
252    /// not declare is a no-op, not a request that waits for the frame that
253    /// declares it. Like the caret, the positions pass re-runs, so this
254    /// frame already draws the node in view.
255    pub(crate) fn apply_pending_reveal(&mut self) {
256        for (label, origin, fill) in std::mem::take(&mut self.pending_reveal_labels) {
257            match self.find_label_as(&label, origin, fill) {
258                Some(key) => self.pending_reveal.push(key),
259                None => self
260                    .diag
261                    .raise(crate::diag::label_without_node("reveal", &label)),
262            }
263        }
264        let asks = std::mem::take(&mut self.pending_reveal);
265        if asks.is_empty() {
266            return;
267        }
268        // Each ask by the container it would move — its nearest
269        // scrolling ancestor, which is the only one a reveal nudges —
270        // the last ask per container kept, in the order asked.
271        let mut kept: Vec<(u32, usize)> = Vec::new();
272        for key in asks {
273            let Some(i) = (0..self.tree.len()).find(|&i| self.tree.keys[i] == key) else {
274                continue;
275            };
276            let mut a = self.tree.parent[i];
277            while a != NIL {
278                let spec = self.tree.specs[a as usize].layout;
279                if spec.scroll_x || spec.scroll_y {
280                    break;
281                }
282                a = self.tree.parent[a as usize];
283            }
284            if a == NIL {
285                continue;
286            }
287            kept.retain(|(c, _)| *c != a);
288            kept.push((a, i));
289        }
290        for (_, i) in kept {
291            let rect = Rect::from_pos_size(self.tree.pos[i], self.tree.size[i]);
292            self.scroll_rect_into_view_smooth(i, rect, true);
293        }
294    }
295
296    /// Nudges the nearest scrolling ancestor of node `i` so `rect`
297    /// (viewport coordinates) is inside it. With `relayout`, re-runs the
298    /// positions pass so this frame already shows the new offset
299    /// (positions is the only pass scroll offsets feed into); without it
300    /// the next frame does.
301    pub(crate) fn scroll_rect_into_view(&mut self, i: usize, rect: Rect, relayout: bool) {
302        self.scroll_rect_into_view_from(i, rect, relayout, false);
303    }
304
305    /// The same, easing the nudge on a container that declares a
306    /// `transition`: what `reveal` asks for, where a caret nudge
307    /// and a focus move take the content there at once.
308    pub(crate) fn scroll_rect_into_view_smooth(&mut self, i: usize, rect: Rect, relayout: bool) {
309        self.scroll_rect_into_view_from(i, rect, relayout, true);
310    }
311
312    fn scroll_rect_into_view_from(&mut self, i: usize, rect: Rect, relayout: bool, smooth: bool) {
313        // Slack so the target isn't glued to the container edge.
314        const MARGIN: f32 = 4.0;
315        let mut a = self.tree.parent[i];
316        while a != NIL {
317            let spec = self.tree.specs[a as usize].layout;
318            if spec.scroll_x || spec.scroll_y {
319                let view =
320                    Rect::from_pos_size(self.tree.pos[a as usize], self.tree.size[a as usize]);
321                let mut delta = Vec2::ZERO;
322                if spec.scroll_y {
323                    if rect.y < view.y + MARGIN {
324                        delta.y = rect.y - (view.y + MARGIN);
325                    } else if rect.y + rect.h > view.y + view.h - MARGIN {
326                        delta.y = rect.y + rect.h - (view.y + view.h - MARGIN);
327                    }
328                }
329                if spec.scroll_x {
330                    if rect.x < view.x + MARGIN {
331                        delta.x = rect.x - (view.x + MARGIN);
332                    } else if rect.x + rect.w > view.x + view.w - MARGIN {
333                        delta.x = rect.x + rect.w - (view.x + view.w - MARGIN);
334                    }
335                }
336                if delta.x != 0.0 || delta.y != 0.0 {
337                    let key = self.tree.keys[a as usize];
338                    if smooth {
339                        self.scroll.scroll_by_smooth(key, delta);
340                    } else {
341                        self.scroll.scroll_by(key, delta);
342                    }
343                    if relayout {
344                        layout::reposition(
345                            &mut self.tree,
346                            &mut self.scroll,
347                            self.viewport,
348                            self.scale,
349                        );
350                    }
351                }
352                return;
353            }
354            a = self.tree.parent[a as usize];
355        }
356    }
357}