Skip to main content

ui/
floating.rs

1//! [`panel`] — content that floats over a page and is dragged around it: a
2//! meter, an inspector, a detached preview.
3//!
4//! The panel lays a full-size layer over its container and places the box
5//! inside it, because the drag has to be heard somewhere larger than the thing
6//! being dragged: a pointer that outruns a frame lands outside a box-sized
7//! hitbox, and the gesture stalls with the box stranded behind the cursor.
8//! [`crate::scroll`] hangs its thumb drag off the track for the same reason.
9//!
10//! ```ignore
11//! // The state is a field of the view that mounts it; the panel is one line.
12//! div().relative().size_full()
13//!     .child(page)
14//!     .child(floating::panel("meter", &self.meter, home, self.stats.clone()))
15//! ```
16//!
17//! It clamps nothing and remembers nothing across launches. A panel dragged
18//! half off the window stays there, and the point it was grabbed by is under
19//! the pointer, so it can always be dragged back.
20
21use std::{cell::Cell, rc::Rc};
22
23use std::time::Duration;
24
25use gpui::{
26    App, ElementId, InteractiveElement as _, IntoElement, MouseButton, MouseDownEvent,
27    MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, SharedString, Styled as _,
28    Window, div,
29};
30use motion::Painter;
31
32/// Redraw rate while a panel is being dragged, beside [`motion`]'s 30 for
33/// spinners and 60 for hover fades.
34///
35/// The rate is the point of the exercise: gpui's own drag redraws the whole
36/// window once per *pointer sample*, and a pointer reports far faster than a
37/// window can paint.
38const DRAG_FPS: f32 = 60.0;
39
40/// How long a claim outlives the move that took it. A pointer holding still
41/// has nothing to paint, so the clock parks and the next move claims again.
42const DRAG_LEASE: Duration = Duration::from_millis(200);
43
44/// How far the pointer travels before a press becomes a drag — gpui's own
45/// `DRAG_THRESHOLD`, which this gesture no longer goes through.
46const THRESHOLD: f64 = 2.0;
47
48/// Where a panel sits, and where inside it the pointer took hold. A field of
49/// the view that mounts the panel, like [`crate::scroll::ScrollbarState`] —
50/// and, for the same reason, carrying its own [`Painter`]: a drag runs in
51/// event-dispatch context, where the window cannot resolve which view is
52/// asking to be redrawn.
53#[derive(Clone)]
54pub struct Floating {
55    /// `None` until the first drag: the panel opens at the `home` its host
56    /// passes and only holds a position of its own once moved.
57    at: Rc<Cell<Option<Point<Pixels>>>>,
58    /// The last pointer sample this gesture accounted for. Movement is carried
59    /// as a delta from it, and a delta reads the same from the window's corner
60    /// or the container's — so the panel needs no element bounds to place
61    /// itself, and none are on offer in a plain mouse listener.
62    anchor: Rc<Cell<Point<Pixels>>>,
63    /// The pointer is pressed on the panel, travelling or not. The hand closes
64    /// here rather than on the first movement, which is where every other
65    /// grabbable surface closes it.
66    held: Rc<Cell<bool>>,
67    /// The panel is being moved: past the threshold that separates a drag from
68    /// a click.
69    dragging: Rc<Cell<bool>>,
70    painter: Painter,
71}
72
73impl Floating {
74    pub fn new(painter: Painter) -> Self {
75        Self {
76            at: Rc::new(Cell::new(None)),
77            anchor: Rc::new(Cell::new(Point::default())),
78            held: Rc::new(Cell::new(false)),
79            dragging: Rc::new(Cell::new(false)),
80            painter,
81        }
82    }
83
84    /// Where the panel sits, once it has been moved. Read it to persist a
85    /// position; hand it back with [`Self::move_to`].
86    pub fn at(&self) -> Option<Point<Pixels>> {
87        self.at.get()
88    }
89
90    /// Whether the pointer is holding the panel, travelling or not — the
91    /// closed hand.
92    pub fn held(&self) -> bool {
93        self.held.get()
94    }
95
96    /// Whether the panel is being moved, which a press alone is not — the lift,
97    /// the shadow, whatever a host shows for a thing in flight.
98    pub fn dragging(&self) -> bool {
99        self.dragging.get()
100    }
101
102    pub fn move_to(&self, at: Point<Pixels>) {
103        self.at.set(Some(at));
104    }
105
106    /// One pointer sample. Cheap on purpose: it moves the panel and claims a
107    /// frame, and claiming is not redrawing — [`Painter::lease`] schedules,
108    /// leaving the clock to paint at [`DRAG_FPS`] however fast the samples
109    /// arrive.
110    fn sample(&self, home: Point<Pixels>, pointer: Point<Pixels>, cx: &mut App) {
111        if !self.held.get() {
112            return;
113        }
114        let travelled = pointer - self.anchor.get();
115        if !self.dragging.get() {
116            if travelled.magnitude() <= THRESHOLD {
117                return;
118            }
119            // The gesture starts here, so the first step is measured from here
120            // and the panel does not jump by the threshold it just crossed.
121            self.dragging.set(true);
122            self.anchor.set(pointer);
123            self.painter.notify(cx);
124            return;
125        }
126        self.move_to(self.at.get().unwrap_or(home) + travelled);
127        self.anchor.set(pointer);
128        self.painter.lease(DRAG_FPS, DRAG_LEASE, cx);
129    }
130}
131
132/// A layer over a page: placed by the caller, and taking the mouse it covers.
133///
134/// Hitboxes in gpui are paint-order only. Every hitbox containing the pointer
135/// hears a press, and `should_handle_scroll` is looser still — it asks only
136/// whether the hitbox is in the hit list at all. So a band drawn over a page
137/// passes both through: a press inside it also fires whatever sits behind, and
138/// a wheel over a box that scrolls moves the page as well. Only a hitbox that
139/// blocks the mouse (gpui's `occlude`) ends the walk.
140///
141/// That is what this is: `absolute` and occluding, for a control the app floats
142/// over its own content — a composer band, a heading held over a list, the
143/// scrim a dialog is asked over. [`crate::popover`]'s layers do the same for
144/// menus, and [`panel`] is this plus a drag.
145///
146/// ```ignore
147/// floating::layer("composer").bottom(px(16.0)).left_0().right_0().child(composer)
148/// ```
149///
150/// It blocks the whole box it covers, gaps included: a band of cards with air
151/// between them takes the presses landing in the air too. Where that matters,
152/// put the layer around each card rather than around the group.
153pub fn layer(id: impl Into<ElementId>) -> gpui::Stateful<gpui::Div> {
154    div().id(id).absolute().occlude()
155}
156
157/// The panel: `child` floating where `state` left it, or at `home` until it is
158/// dragged. Mount it in a `relative()` container — it lays a layer over that
159/// container's whole box.
160///
161/// `home` is passed every render rather than stored, so a host can read it off
162/// the viewport and a window that grows never strands the panel out of reach.
163pub fn panel(
164    id: impl Into<SharedString>,
165    state: &Floating,
166    home: Point<Pixels>,
167    child: impl IntoElement,
168) -> impl IntoElement {
169    let id = id.into();
170    let at = state.at.get().unwrap_or(home);
171
172    let pressed = {
173        let state = state.clone();
174        move |event: &MouseDownEvent, _: &mut Window, cx: &mut App| {
175            state.held.set(true);
176            state.anchor.set(event.position);
177            state.painter.notify(cx);
178        }
179    };
180    let moved = {
181        let state = state.clone();
182        move |event: &MouseMoveEvent, _: &mut Window, cx: &mut App| {
183            state.sample(home, event.position, cx);
184        }
185    };
186    let released = {
187        let state = state.clone();
188        move |_: &MouseUpEvent, _: &mut Window, cx: &mut App| {
189            // Both, separately: `||` would short-circuit past the second
190            // cell and leave a panel dragging for the rest of its life.
191            let held = state.held.replace(false);
192            let dragging = state.dragging.replace(false);
193            if held || dragging {
194                state.painter.notify(cx);
195            }
196        }
197    };
198
199    let box_ = div()
200        .absolute()
201        .left(at.x)
202        .top(at.y)
203        .id(ElementId::from(id.clone()));
204    let box_ = if state.held() || state.dragging() {
205        box_.cursor_grabbing()
206    } else {
207        box_.cursor_grab()
208    };
209
210    div()
211        .id(ElementId::from(SharedString::from(format!("{id}-layer"))))
212        .absolute()
213        .inset_0()
214        .child(box_.on_mouse_down(MouseButton::Left, pressed).child(child))
215        // The gesture is heard on the layer rather than the box: a pointer
216        // moving faster than the frames that follow it is outside the box for
217        // most of the drag, and a box-mounted listener would go quiet.
218        .on_mouse_move(moved)
219        .on_mouse_up(MouseButton::Left, released.clone())
220        .on_mouse_up_out(MouseButton::Left, released)
221}