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}