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}