Skip to main content

ui/
docking.rs

1//! Pane docking previews. Hosts own the layout and apply one accepted drop.
2
3use std::{cell::RefCell, rc::Rc};
4
5use gpui::{
6    AnyElement, App, Bounds, ElementId, IntoElement, Pixels, Point, Window, point, px, size,
7};
8use motion::{Painter, TAB_SLIDE};
9use web_time::Instant;
10
11mod element;
12pub use element::{Pane, Surface};
13
14/// The destination within a pane. The bar and centre both join its tab strip.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Zone {
17    Join,
18    Left,
19    Right,
20    Top,
21    Bottom,
22}
23
24/// Hit-test a pane in window coordinates. Corners choose the nearest normalized edge.
25pub fn zone(bounds: Bounds<Pixels>, bar_height: Pixels, pointer: Point<Pixels>) -> Option<Zone> {
26    if bounds.size.width <= px(0.) || bounds.size.height <= px(0.) || !bounds.contains(&pointer) {
27        return None;
28    }
29    if pointer.y < bounds.top() + bar_height.max(px(0.)) {
30        return Some(Zone::Join);
31    }
32    let x = (pointer.x - bounds.left()) / bounds.size.width;
33    let y = (pointer.y - bounds.top()) / bounds.size.height;
34    let (distance, edge) = [
35        (x, Zone::Left),
36        (1. - x, Zone::Right),
37        (y, Zone::Top),
38        (1. - y, Zone::Bottom),
39    ]
40    .into_iter()
41    .min_by(|a, b| a.0.total_cmp(&b.0))
42    .unwrap();
43    Some(if distance < 0.25 { edge } else { Zone::Join })
44}
45
46/// The inset preview rectangle for a destination.
47pub fn preview_bounds(mut bounds: Bounds<Pixels>, zone: Zone) -> Bounds<Pixels> {
48    match zone {
49        Zone::Join => {}
50        Zone::Left => bounds.size.width /= 2.,
51        Zone::Right => {
52            bounds.size.width /= 2.;
53            bounds.origin.x += bounds.size.width;
54        }
55        Zone::Top => bounds.size.height /= 2.,
56        Zone::Bottom => {
57            bounds.size.height /= 2.;
58            bounds.origin.y += bounds.size.height;
59        }
60    }
61    let inset = px(4.)
62        .min(bounds.size.width / 2.)
63        .min(bounds.size.height / 2.);
64    bounds.origin += point(inset, inset);
65    bounds.size.width -= 2. * inset;
66    bounds.size.height -= 2. * inset;
67    bounds
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct Drop<PaneId, ItemId> {
72    pub item: ItemId,
73    pub pane: PaneId,
74    pub zone: Zone,
75}
76
77/// Keep one controller on the view that renders the whole docking surface.
78pub struct Dock<P, I>(Rc<RefCell<State<P, I>>>);
79
80impl<P, I> Clone for Dock<P, I> {
81    fn clone(&self) -> Self {
82        Self(self.0.clone())
83    }
84}
85
86type Ghost<I> = dyn Fn(&I, &mut Window, &mut App) -> AnyElement;
87type OnDrop<P, I> = dyn Fn(&Drop<P, I>, &mut Window, &mut App) -> Option<P>;
88
89struct Target<P> {
90    id: P,
91    bounds: Bounds<Pixels>,
92    visible: Bounds<Pixels>,
93    bar_height: Pixels,
94}
95
96struct Tween {
97    from: Bounds<Pixels>,
98    to: Bounds<Pixels>,
99    since: Instant,
100}
101
102impl Tween {
103    fn progress(&self, cx: &App) -> f32 {
104        if cx.reduce_motion() {
105            return 1.;
106        }
107        let elapsed = cx
108            .background_executor()
109            .now()
110            .saturating_duration_since(self.since);
111        TAB_SLIDE.progress(
112            elapsed.as_secs_f32()
113                / TAB_SLIDE
114                    .total()
115                    .mul_f32(motion::speed_scale())
116                    .as_secs_f32(),
117        )
118    }
119    fn bounds(&self, cx: &App) -> Bounds<Pixels> {
120        let t = self.progress(cx);
121        Bounds::new(
122            self.from.origin + (self.to.origin - self.from.origin) * t,
123            size(
124                self.from.size.width + (self.to.size.width - self.from.size.width) * t,
125                self.from.size.height + (self.to.size.height - self.from.size.height) * t,
126            ),
127        )
128    }
129}
130
131struct Settling<P, I> {
132    pane: P,
133    item: I,
134    preview: Bounds<Pixels>,
135    ghost: Bounds<Pixels>,
136    since: Instant,
137}
138
139struct Carried<I> {
140    item: I,
141    gesture: Rc<crate::drag::Gesture>,
142    pointer: Point<Pixels>,
143}
144
145impl<I> Carried<I> {
146    /// Off every region of its domain: this surface's to take.
147    fn detached(&self) -> bool {
148        !self.gesture.claimed.get()
149    }
150
151    fn ghost_bounds(&self) -> Bounds<Pixels> {
152        self.gesture.ghost_bounds(self.pointer)
153    }
154}
155
156struct State<P, I> {
157    painter: Painter,
158    ghost: Rc<Ghost<I>>,
159    dropped: Option<Rc<OnDrop<P, I>>>,
160    targets: Vec<Target<P>>,
161    carried: Option<Carried<I>>,
162    landing: Option<(P, Zone)>,
163    preview: Option<Tween>,
164    settling: Option<Settling<P, I>>,
165}
166
167impl<P: Clone + PartialEq + 'static, I: Clone + PartialEq + 'static> Dock<P, I> {
168    /// `ghost` renders the detached item and its brief settle after a drop.
169    pub fn new(
170        painter: Painter,
171        ghost: impl Fn(&I, &mut Window, &mut App) -> AnyElement + 'static,
172    ) -> Self {
173        Self(Rc::new(RefCell::new(State {
174            painter,
175            ghost: Rc::new(ghost),
176            dropped: None,
177            targets: Vec::new(),
178            carried: None,
179            landing: None,
180            preview: None,
181            settling: None,
182        })))
183    }
184
185    /// Mount every target inside this surface, in the controller's owning view.
186    pub fn surface(&self, id: impl Into<ElementId>, child: impl IntoElement) -> Surface<P, I> {
187        element::Surface {
188            dock: self.clone(),
189            id: id.into(),
190            child: child.into_any_element(),
191            dropped: None,
192        }
193    }
194
195    /// Measure a pane and animate its entry after an accepted drop.
196    pub fn pane(&self, id: P, bar_height: Pixels, child: impl IntoElement) -> Pane<P, I> {
197        element::Pane {
198            dock: self.clone(),
199            id,
200            bar_height,
201            child: child.into_any_element(),
202        }
203    }
204}
205
206impl<P: Clone + PartialEq, I: Clone + PartialEq> State<P, I> {
207    fn aim(&mut self, cx: &mut App) {
208        let target = self
209            .carried
210            .as_ref()
211            .filter(|carried| carried.detached())
212            .and_then(|carried| {
213                self.targets.iter().rev().find_map(|target| {
214                    if !target.visible.contains(&carried.pointer) {
215                        return None;
216                    }
217                    zone(target.bounds, target.bar_height, carried.pointer)
218                        .map(|zone| (target.id.clone(), zone, preview_bounds(target.bounds, zone)))
219                })
220            });
221        let Some((pane, zone, bounds)) = target else {
222            self.landing = None;
223            self.preview = None;
224            return;
225        };
226        if self
227            .preview
228            .as_ref()
229            .is_none_or(|preview| preview.to != bounds)
230        {
231            let from = self
232                .preview
233                .as_ref()
234                .map(|preview| preview.bounds(cx))
235                .unwrap_or(bounds);
236            self.preview = Some(Tween {
237                from,
238                to: bounds,
239                since: cx.background_executor().now(),
240            });
241        }
242        self.landing = Some((pane, zone));
243    }
244}
245
246impl<P: Clone + PartialEq + 'static, I: Clone + PartialEq + 'static> Dock<P, I> {
247    fn update(
248        &self,
249        item: I,
250        gesture: Rc<crate::drag::Gesture>,
251        pointer: Point<Pixels>,
252        cx: &mut App,
253    ) {
254        let mut state = self.0.borrow_mut();
255        state.carried = Some(Carried {
256            item,
257            gesture,
258            pointer,
259        });
260        state.settling = None;
261        state.aim(cx);
262        state.painter.notify(cx);
263    }
264
265    fn clear(&self, cx: &mut App) {
266        let mut state = self.0.borrow_mut();
267        if state.carried.take().is_some() {
268            state.landing = None;
269            state.preview = None;
270            state.painter.notify(cx);
271        }
272    }
273
274    fn release(
275        &self,
276        carry: &crate::drag::Carry<I>,
277        pointer: Point<Pixels>,
278        window: &mut Window,
279        cx: &mut App,
280    ) {
281        self.update(carry.item().clone(), carry.gesture.clone(), pointer, cx);
282        let proposal = {
283            let state = self.0.borrow();
284            let carried = state.carried.as_ref().filter(|carried| carried.detached());
285            carried
286                .zip(state.landing.as_ref())
287                .zip(state.dropped.as_ref())
288                .map(|((carried, (pane, zone)), callback)| {
289                    (
290                        Drop {
291                            item: carried.item.clone(),
292                            pane: pane.clone(),
293                            zone: *zone,
294                        },
295                        callback.clone(),
296                        state.preview.as_ref().unwrap().bounds(cx),
297                        carried.ghost_bounds(),
298                    )
299                })
300        };
301        self.clear(cx);
302        if let Some((event, callback, preview, ghost)) = proposal
303            && let Some(pane) = callback(&event, window, cx)
304        {
305            let mut state = self.0.borrow_mut();
306            state.settling = Some(Settling {
307                pane,
308                item: event.item,
309                preview,
310                ghost,
311                since: cx.background_executor().now(),
312            });
313            state.painter.notify(cx);
314        }
315    }
316}