ui/scroll/claim.rs
1//! Nesting: which pane a wheel belongs to.
2
3use super::*;
4
5/// Where a [`claim_wheel`] pane was before the wheel that is being dispatched.
6///
7/// Shaped like [`ScrollbarState`] and [`FollowState`], and owned by the view
8/// for the same reason: an element rebuilt every render cannot remember
9/// anything, and this has to outlive the frame it was written in.
10#[derive(Clone)]
11pub struct ClaimState(Rc<Cell<Point<Pixels>>>);
12
13impl Default for ClaimState {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl ClaimState {
20 pub fn new() -> Self {
21 Self(Rc::new(Cell::new(point(px(0.0), px(0.0)))))
22 }
23}
24
25/// Let a pane keep the wheel it can act on, instead of passing it to the pane
26/// behind as well.
27///
28/// gpui's scroll listener neither stops propagation nor asks whether an
29/// ancestor scrolls too, so a wheel over a nested pane moves *both* — an output
30/// box inside a transcript scrolls itself and drags the transcript with it
31/// (user report). Every scrolling ancestor under the pointer does this, so the
32/// deeper the nesting the further the page jumps.
33///
34/// The question it asks is whether the pane *moved*, not whether it had room
35/// to. This listener runs after the element's own — gpui registers that one
36/// later and the bubble phase runs the list backwards — so `handle` already
37/// holds the post-scroll offset, and `state` is where it stood before. "Had
38/// room" is the same answer one notch too late, and gets the notch that lands
39/// exactly on the end wrong: the pane finishes its travel *and* the page jumps
40/// a full notch behind it.
41///
42/// Chained at the ends, not sealed: a pane with nowhere left to go hands the
43/// wheel to the page, so reaching the end of a short inner list does not strand
44/// it there and make the pointer move. A pane whose content fits never claims
45/// anything, for the same reason. For `overscroll-behavior: contain` — a pane
46/// that never lets a wheel past — stop unconditionally instead.
47///
48/// ```ignore
49/// scroll::claim_wheel(
50/// scroll::pane("output", Axes::Vertical).track_scroll(&self.scroll),
51/// &self.scroll,
52/// Axes::Vertical,
53/// &self.claim,
54/// )
55/// ```
56///
57/// `axes` is what the pane scrolls, and must be what [`pane`] was given: an
58/// axis left out here is one whose movement goes unnoticed, so the wheel is
59/// handed on and the ancestor moves too.
60pub fn claim_wheel<E: gpui::StatefulInteractiveElement>(
61 el: E,
62 handle: &ScrollHandle,
63 axes: Axes,
64 state: &ClaimState,
65) -> E {
66 let handle = handle.clone();
67 let state = state.0.clone();
68 // Where the pane stands as the frame is built, which is where it stands
69 // before anything this frame's listeners are handed. The listener writes it
70 // too: a wheel the pane could not act on produces no `notify` and so no
71 // render, and the reading below has to stay true across that gap.
72 state.set(travel(&handle, axes));
73 el.on_scroll_wheel(move |_, _, cx| {
74 let now = travel(&handle, axes);
75 if now != state.get() {
76 state.set(now);
77 cx.stop_propagation();
78 }
79 })
80}
81
82/// How far `handle` has visibly travelled along each of `axes`. Negative, as
83/// gpui counts it; an axis the pane does not scroll reads zero forever, so it
84/// can never be mistaken for movement.
85///
86/// Clamped here because gpui's scroll listener is not: it adds the raw delta
87/// and leaves the clamp to the next `paint`, so a pane held at its end keeps
88/// accumulating offset it will never show. Compared raw, every notch past the
89/// end reads as movement and the pane never lets go of the wheel.
90pub(super) fn travel(handle: &ScrollHandle, axes: Axes) -> Point<Pixels> {
91 let (offset, max) = (handle.offset(), handle.max_offset());
92 let seen = |offset: Pixels, max: Pixels| offset.clamp(-max.max(px(0.0)), px(0.0));
93 point(
94 match axes.horizontal() {
95 true => seen(offset.x, max.x),
96 false => px(0.0),
97 },
98 match axes.vertical() {
99 true => seen(offset.y, max.y),
100 false => px(0.0),
101 },
102 )
103}