1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Zone {
17 Join,
18 Left,
19 Right,
20 Top,
21 Bottom,
22}
23
24pub 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
48pub 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
79pub 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 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 fn detached(&self) -> bool {
152 !self.gesture.claimed.get()
153 }
154
155 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 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 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 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}