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