Skip to main content

kui_core/runtime/
follow.rs

1//! A held drag following its scroller.
2//!
3//! Two things a caret drag or a drag-select needs beyond the press, the
4//! motion and the release, both of them about the frame moving under a
5//! pointer that did not: when the layout under the held pointer changes
6//! — a wheel notch, a scroller nudged, a virtual list re-sliced — the
7//! live end is placed again where the pointer is; and when
8//! the pointer is held past the scroller's edge, the core is what changes
9//! the layout, at a rate from how far past. Both run at the
10//! start of every frame against the frame that finished, so the frame
11//! being built paints the result.
12//!
13//! The scroller is found once, at the press: the nearest ancestor-or-self
14//! of the pressed node that scrolls, or that declared `on_scroll` — the
15//! second kind is nudged as an event, since the core holds nothing of
16//! what it scrolls (a `cells` grid is one screenful of the app's history;
17//! decision 4).
18
19use super::*;
20
21/// Past this many logical px beyond the edge the step stops speeding up.
22pub const AUTOSCROLL_REACH: f32 = 100.0;
23/// Logical px per second of scrolling, per logical px past the edge: 60
24/// px past is 600 px/s, the reach is 1000 px/s.
25pub const AUTOSCROLL_RATE: f32 = 10.0;
26/// The frame a core with no clock takes, in seconds — a sixtieth, so a
27/// headless drive steps by a knowable amount. A clockless core does not
28/// snap here: there is no end state to snap to.
29pub const CLOCKLESS_FRAME: f64 = 1.0 / 60.0;
30/// The longest frame the rate is applied over: a window that sat hidden
31/// for a second must not scroll a second's worth when it comes back.
32const MAX_FRAME: f64 = 0.1;
33
34/// What a held drag scrolls, and what its live end was last placed
35/// against.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub(crate) enum Scroller {
38    /// A scroll container: the nudge is an offset the layout clamps, and
39    /// the gate is the offset the last layout placed the content at.
40    Container { key: Key, seen: Vec2 },
41    /// An `on_scroll` node: the nudge is a `scroll` event the app answers
42    /// by re-declaring. For a `cells` grid the gate is the `origin_line`
43    /// it declared; any other handler moves what it likes, so the gate
44    /// is every frame while the drag is held.
45    Handler { key: Key, seen: Option<u64> },
46}
47
48impl Scroller {
49    fn key(self) -> Key {
50        match self {
51            Scroller::Container { key, .. } | Scroller::Handler { key, .. } => key,
52        }
53    }
54}
55
56/// A held drag and what it follows: the pointer's last position, kept
57/// across a `CursorLeft` (a press dragged out of the window gets one on
58/// some platforms, and the drag is still held); the scroller; and
59/// whether the last frame stepped it, which is what asks for the next
60/// frame (`Core::animating`).
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub(crate) struct DragFollow {
63    pub point: Vec2,
64    pub scroller: Option<Scroller>,
65    pub stepped: bool,
66}
67
68impl Core {
69    /// Arms the follow for a drag a press just started on `node` at
70    /// `point`: finds the scroller and records where it is now.
71    pub(crate) fn arm_follow(&mut self, node: Key, point: Vec2) {
72        let scroller = self.scroller_of(node);
73        self.drag_follow = Some(DragFollow {
74            point,
75            scroller,
76            stepped: false,
77        });
78    }
79
80    /// The pointer moved and the caller placed the live end there against
81    /// the finished frame: the scroller's reading is taken again, so the
82    /// next frame does not place the end a second time for a move the
83    /// frame already answered.
84    pub(crate) fn follow_point(&mut self, point: Vec2) {
85        let Some(mut f) = self.drag_follow else {
86            return;
87        };
88        f.point = point;
89        f.scroller = f.scroller.map(|s| self.reading_of(s.key(), s));
90        self.drag_follow = Some(f);
91    }
92
93    /// The drag ended: the live end is placed once more where the pointer
94    /// is, against the finished frame — a release right after a nudge
95    /// lands where the pointer is, not a frame behind — and the follow
96    /// is dropped. The drags themselves are the caller's to clear.
97    pub(crate) fn end_follow(&mut self) {
98        if let Some(f) = self.drag_follow.take() {
99            self.rehit(f.point);
100        }
101    }
102
103    /// Whether a held drag stepped its scroller this frame — one more
104    /// frame is wanted with nothing in the queue.
105    pub(crate) fn autoscrolling(&self) -> bool {
106        self.drag_follow.is_some_and(|f| f.stepped)
107    }
108
109    /// The scroller a held drag stepped this frame, when it did: who
110    /// [`Self::autoscrolling`] names in a trace.
111    pub(crate) fn autoscroller(&self) -> Option<Key> {
112        self.drag_follow
113            .filter(|f| f.stepped)
114            .and_then(|f| f.scroller)
115            .map(Scroller::key)
116    }
117
118    /// The nearest ancestor-or-self of `node` that a held drag scrolls,
119    /// with its current reading. Self first, because a `uniform_list`
120    /// is both the selection's scope and its scroller. A handler beats
121    /// a container on the same node: the app asked to hear the wheel
122    /// (`ScrollRegion::handler`). Read off the finished frame's tree,
123    /// which is what a press between two frames sees; a press during a
124    /// build has no scroller.
125    fn scroller_of(&self, node: Key) -> Option<Scroller> {
126        if self.building {
127            return None;
128        }
129        let mut i = self.tree.index_of(node)?;
130        loop {
131            let key = self.tree.keys[i];
132            let spec = &self.tree.specs[i];
133            if spec.events().on_scroll.is_some() {
134                return Some(self.reading_of(key, Scroller::Handler { key, seen: None }));
135            }
136            if spec.layout.scroll_x || spec.layout.scroll_y {
137                return Some(self.reading_of(
138                    key,
139                    Scroller::Container {
140                        key,
141                        seen: Vec2::ZERO,
142                    },
143                ));
144            }
145            match self.tree.parent[i] {
146                NIL => return None,
147                p => i = p as usize,
148            }
149        }
150    }
151
152    /// The scroller `key`, of `kind`'s kind, at what the finished frame
153    /// placed it: the one reading `scroller_of` takes, the gate compares
154    /// against, and a placement refreshes.
155    fn reading_of(&self, key: Key, kind: Scroller) -> Scroller {
156        match kind {
157            Scroller::Container { .. } => Scroller::Container {
158                key,
159                seen: self.scroll.laid_offset(key).unwrap_or(Vec2::ZERO),
160            },
161            Scroller::Handler { .. } => Scroller::Handler {
162                key,
163                seen: self.grid_line(key),
164            },
165        }
166    }
167
168    /// The `origin_line` the finished frame's grid `key` declared, if the
169    /// key is a grid.
170    fn grid_line(&self, key: Key) -> Option<u64> {
171        self.cells_id_of_ref(key)
172            .map(|id| self.cells.origin_line(id, self.building))
173    }
174
175    /// The row height of the grid `key`, logical px — what a wheel delta
176    /// or an edge step is divided by to become lines.
177    fn grid_line_height(&mut self, key: Key) -> Option<f32> {
178        let id = self.cells_id_of_ref(key)?;
179        let sess = &mut *self.session.state();
180        let cell = self
181            .cells
182            .cell_size(id, self.building, &sess.resources, &mut sess.fonts);
183        (cell.h > 0.0).then_some(cell.h)
184    }
185
186    /// Places the held drag's live end at `p` against the finished frame,
187    /// exactly as a `CursorMoved` there would: the caret for a caret
188    /// drag, the selection's focus for a drag-select, in whichever
189    /// geometry its scope has.
190    pub(crate) fn rehit(&mut self, p: Vec2) {
191        if let Some((key, _)) = self.edit.dragging {
192            self.edit_drag_to(key, p);
193        }
194        if let Some(drag) = self.select_dragging {
195            self.extend_select_drag(drag, p);
196        }
197    }
198
199    /// Moves the caret drag in editor `key` to the viewport point `p`,
200    /// against the origin the editor was drawn at this frame — not the
201    /// press's, which a nudge has moved.
202    pub(crate) fn edit_drag_to(&mut self, key: Key, p: Vec2) {
203        let origin = self
204            .interaction
205            .edit_origin_of(key)
206            .or_else(|| self.edit.dragging.map(|(_, o)| o));
207        if let Some(origin) = origin {
208            let local = Vec2::new(p.x - origin.x, p.y - origin.y);
209            self.edit_with_fonts(|edit, fs| edit.drag(key, local, fs));
210        }
211    }
212
213    /// At the start of a frame, against the one that finished: re-places
214    /// the live end if the scroller moved since it was last placed, then
215    /// steps the scroller if the pointer is held past its edge. Nothing
216    /// while no drag is held.
217    pub(crate) fn follow_drag(&mut self) {
218        let now = self.anim.time();
219        let dt = match (now, self.last_frame_time) {
220            (Some(n), Some(l)) => (n - l).clamp(0.0, MAX_FRAME),
221            (Some(_), None) | (None, _) => CLOCKLESS_FRAME,
222        } as f32;
223        self.last_frame_time = now;
224        let Some(mut f) = self.drag_follow else {
225            return;
226        };
227        f.stepped = false;
228        if let Some(scroller) = f.scroller {
229            // 1. The frame under the pointer moved: place the end again.
230            let key = scroller.key();
231            let now = self.reading_of(key, scroller);
232            let moved = match scroller {
233                Scroller::Container { .. } => now != scroller,
234                Scroller::Handler { seen, .. } => seen.is_none() || now != scroller,
235            };
236            if moved {
237                self.rehit(f.point);
238                f.scroller = Some(now);
239            }
240            // 2. The pointer is past the edge: step the scroller.
241            if let Some(i) = self.tree.index_of(key) {
242                let rect = Rect::from_pos_size(self.tree.pos[i], self.tree.size[i]);
243                f.stepped = match scroller {
244                    Scroller::Container { .. } => {
245                        let spec = self.tree.specs[i].layout;
246                        let delta = edge_step(rect, f.point, spec.scroll_x, spec.scroll_y, dt);
247                        // A step the clamp would undo is no step: a
248                        // scroller at its end under a held pointer is
249                        // idle, not asking for frames until the release.
250                        let moves = self.scroll.geometry(key).is_some_and(|g| {
251                            let can =
252                                |o: f32, d: f32, m: f32| (o + d).clamp(0.0, m) != o.clamp(0.0, m);
253                            can(g.offset.x, delta.x, g.max_offset.x)
254                                || can(g.offset.y, delta.y, g.max_offset.y)
255                        });
256                        if moves {
257                            self.scroll.scroll_by(key, delta);
258                        }
259                        moves
260                    }
261                    Scroller::Handler { .. } => {
262                        // A grid scrolls lines and has no columns to
263                        // scroll to; any other handler hears both axes.
264                        let grid = self.grid_line(key).is_some();
265                        let delta = edge_step(rect, f.point, !grid, true, dt);
266                        if delta != Vec2::ZERO {
267                            // The event's delta is the wheel's: positive
268                            // `dy` is earlier content, so a step toward
269                            // later content — the pointer below — is
270                            // negative.
271                            let wheel = Vec2::new(-delta.x, -delta.y);
272                            if let Some(ev) = self.scroll_event(key, f.point, wheel) {
273                                self.pending.push(ev);
274                            }
275                            true
276                        } else {
277                            false
278                        }
279                    }
280                };
281            }
282        }
283        self.drag_follow = Some(f);
284    }
285
286    /// The `scroll` event a delta over the `on_scroll` node `key` makes —
287    /// the wheel's, or an edge step's — with the whole lines it covers
288    /// on a grid. The fraction left over is carried to the next delta on
289    /// the same grid, whichever of the two made it (`line_carry`): a
290    /// notch's half line and an edge step's add up. `None` when the node
291    /// is not in the finished frame or declared no tag.
292    pub(crate) fn scroll_event(&mut self, key: Key, p: Vec2, delta: Vec2) -> Option<UiEvent> {
293        let i = self.tree.index_of(key)?;
294        self.tree.specs[i].events().on_scroll.as_ref()?;
295        let lines = match self.grid_line_height(key) {
296            Some(h) => {
297                let carry = match self.line_carry {
298                    Some((k, c)) if k == key => c,
299                    _ => 0.0,
300                };
301                // Later history is the wheel rolling down: negative `dy`.
302                let total = carry - delta.y / h;
303                let whole = total.trunc();
304                self.line_carry = Some((key, total - whole));
305                Value::Int(whole as i64)
306            }
307            None => Value::Null,
308        };
309        let origin = self.tree.origins[i];
310        let tag = self.tree.specs[i].events().on_scroll.as_ref();
311        let payload = Value::map([
312            ("kind", Value::str("scroll")),
313            ("x", Value::Float(p.x as f64)),
314            ("y", Value::Float(p.y as f64)),
315            ("dx", Value::Float(delta.x as f64)),
316            ("dy", Value::Float(delta.y as f64)),
317            ("lines", lines),
318        ]);
319        Some(UiEvent::on(origin, key, payload).tagged(tag))
320    }
321}
322
323/// How far a scroller steps this frame for a pointer at `p` against its
324/// `rect`: on each axis it scrolls, the distance past that edge (zero
325/// inside), capped at the reach, times the rate, times the frame. A
326/// positive step is toward later content, the sign `ScrollStore::scroll_by`
327/// takes.
328pub(crate) fn edge_step(rect: Rect, p: Vec2, x: bool, y: bool, dt: f32) -> Vec2 {
329    let past = |lo: f32, hi: f32, v: f32| -> f32 {
330        if v < lo {
331            -(lo - v).min(AUTOSCROLL_REACH)
332        } else if v > hi {
333            (v - hi).min(AUTOSCROLL_REACH)
334        } else {
335            0.0
336        }
337    };
338    let k = AUTOSCROLL_RATE * dt;
339    Vec2::new(
340        if x {
341            past(rect.x, rect.x + rect.w, p.x) * k
342        } else {
343            0.0
344        },
345        if y {
346            past(rect.y, rect.y + rect.h, p.y) * k
347        } else {
348            0.0
349        },
350    )
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn a_pointer_inside_steps_nothing() {
359        let r = Rect::new(10.0, 10.0, 100.0, 50.0);
360        assert_eq!(
361            edge_step(r, Vec2::new(50.0, 30.0), true, true, 1.0 / 60.0),
362            Vec2::ZERO
363        );
364    }
365
366    #[test]
367    fn the_step_is_proportional_capped_and_per_frame() {
368        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
369        // 60 px below at a clockless frame: 600 px/s, 10 px a frame.
370        let s = edge_step(r, Vec2::new(50.0, 160.0), false, true, 1.0 / 60.0);
371        assert!((s.y - 10.0).abs() < 1e-4, "{s:?}");
372        assert_eq!(s.x, 0.0);
373        // 300 px above: capped at the reach, and toward earlier content.
374        let s = edge_step(r, Vec2::new(50.0, -300.0), false, true, 1.0 / 60.0);
375        assert!((s.y + AUTOSCROLL_REACH / 6.0).abs() < 1e-3, "{s:?}");
376        // An axis the scroller does not have steps nothing.
377        let s = edge_step(r, Vec2::new(160.0, 50.0), false, true, 1.0 / 60.0);
378        assert_eq!(s, Vec2::ZERO);
379        let s = edge_step(r, Vec2::new(160.0, 50.0), true, true, 1.0 / 60.0);
380        assert!((s.x - 10.0).abs() < 1e-4, "{s:?}");
381    }
382}