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/// The panel: `child` floating where `state` left it, or at `home` until it is
133/// dragged. Mount it in a `relative()` container — it lays a layer over that
134/// container's whole box.
135///
136/// `home` is passed every render rather than stored, so a host can read it off
137/// the viewport and a window that grows never strands the panel out of reach.
138pub fn panel(
139    id: impl Into<SharedString>,
140    state: &Floating,
141    home: Point<Pixels>,
142    child: impl IntoElement,
143) -> impl IntoElement {
144    let id = id.into();
145    let at = state.at.get().unwrap_or(home);
146
147    let pressed = {
148        let state = state.clone();
149        move |event: &MouseDownEvent, _: &mut Window, cx: &mut App| {
150            state.held.set(true);
151            state.anchor.set(event.position);
152            state.painter.notify(cx);
153        }
154    };
155    let moved = {
156        let state = state.clone();
157        move |event: &MouseMoveEvent, _: &mut Window, cx: &mut App| {
158            state.sample(home, event.position, cx);
159        }
160    };
161    let released = {
162        let state = state.clone();
163        move |_: &MouseUpEvent, _: &mut Window, cx: &mut App| {
164            // Both, separately: `||` would short-circuit past the second
165            // cell and leave a panel dragging for the rest of its life.
166            let held = state.held.replace(false);
167            let dragging = state.dragging.replace(false);
168            if held || dragging {
169                state.painter.notify(cx);
170            }
171        }
172    };
173
174    let box_ = div()
175        .absolute()
176        .left(at.x)
177        .top(at.y)
178        .id(ElementId::from(id.clone()));
179    let box_ = if state.held() || state.dragging() {
180        box_.cursor_grabbing()
181    } else {
182        box_.cursor_grab()
183    };
184
185    div()
186        .id(ElementId::from(SharedString::from(format!("{id}-layer"))))
187        .absolute()
188        .inset_0()
189        .child(box_.on_mouse_down(MouseButton::Left, pressed).child(child))
190        // The gesture is heard on the layer rather than the box: a pointer
191        // moving faster than the frames that follow it is outside the box for
192        // most of the drag, and a box-mounted listener would go quiet.
193        .on_mouse_move(moved)
194        .on_mouse_up(MouseButton::Left, released.clone())
195        .on_mouse_up_out(MouseButton::Left, released)
196}