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> {
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
46pub 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
77pub 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: Option<Bounds<Pixels>>,
138 since: Instant,
139}
140
141struct Carried<I> {
142 item: I,
143 gesture: Rc<crate::drag::Gesture>,
144 pointer: Point<Pixels>,
145}
146
147impl<I> Carried<I> {
148 fn detached(&self) -> bool {
150 !self.gesture.claimed.get()
151 }
152
153 fn hosted(&self) -> bool {
155 self.detached() && !self.gesture.ghosted.get()
156 }
157
158 fn ghost_bounds(&self) -> Bounds<Pixels> {
159 self.gesture.ghost_bounds(self.pointer)
160 }
161}
162
163struct State<P, I> {
164 painter: Painter,
165 ghost: Rc<Ghost<I>>,
166 dropped: Option<Rc<OnDrop<P, I>>>,
167 targets: Vec<Target<P>>,
168 carried: Option<Carried<I>>,
169 landing: Option<(P, Zone)>,
170 preview: Option<Tween>,
171 settling: Option<Settling<P, I>>,
172}
173
174impl<P: Clone + PartialEq + 'static, I: Clone + PartialEq + 'static> Dock<P, I> {
175 pub fn new(
177 painter: Painter,
178 ghost: impl Fn(&I, &mut Window, &mut App) -> AnyElement + 'static,
179 ) -> Self {
180 Self(Rc::new(RefCell::new(State {
181 painter,
182 ghost: Rc::new(ghost),
183 dropped: None,
184 targets: Vec::new(),
185 carried: None,
186 landing: None,
187 preview: None,
188 settling: None,
189 })))
190 }
191
192 pub fn surface(&self, id: impl Into<ElementId>, child: impl IntoElement) -> Surface<P, I> {
194 element::Surface {
195 dock: self.clone(),
196 id: id.into(),
197 child: child.into_any_element(),
198 dropped: None,
199 }
200 }
201
202 pub fn pane(&self, id: P, bar_height: Pixels, child: impl IntoElement) -> Pane<P, I> {
204 element::Pane {
205 dock: self.clone(),
206 id,
207 bar_height,
208 child: child.into_any_element(),
209 }
210 }
211}
212
213impl<P: Clone + PartialEq, I: Clone + PartialEq> State<P, I> {
214 fn aim(&mut self, cx: &mut App) {
215 let target = self
216 .carried
217 .as_ref()
218 .filter(|carried| carried.detached())
219 .and_then(|carried| {
220 self.targets.iter().rev().find_map(|target| {
221 if !target.visible.contains(&carried.pointer) {
222 return None;
223 }
224 zone(target.bounds, target.bar_height, carried.pointer)
225 .map(|zone| (target.id.clone(), zone, preview_bounds(target.bounds, zone)))
226 })
227 });
228 let Some((pane, zone, bounds)) = target else {
229 self.landing = None;
230 self.preview = None;
231 return;
232 };
233 if self
234 .preview
235 .as_ref()
236 .is_none_or(|preview| preview.to != bounds)
237 {
238 let from = self
239 .preview
240 .as_ref()
241 .map(|preview| preview.bounds(cx))
242 .unwrap_or(bounds);
243 self.preview = Some(Tween {
244 from,
245 to: bounds,
246 since: cx.background_executor().now(),
247 });
248 }
249 self.landing = Some((pane, zone));
250 }
251}
252
253impl<P: Clone + PartialEq + 'static, I: Clone + PartialEq + 'static> Dock<P, I> {
254 fn update(
255 &self,
256 item: I,
257 gesture: Rc<crate::drag::Gesture>,
258 pointer: Point<Pixels>,
259 cx: &mut App,
260 ) {
261 let mut state = self.0.borrow_mut();
262 state.carried = Some(Carried {
263 item,
264 gesture,
265 pointer,
266 });
267 state.settling = None;
268 state.aim(cx);
269 state.painter.notify(cx);
270 }
271
272 fn clear(&self, cx: &mut App) {
273 let mut state = self.0.borrow_mut();
274 if state.carried.take().is_some() {
275 state.landing = None;
276 state.preview = None;
277 state.painter.notify(cx);
278 }
279 }
280
281 fn release(
282 &self,
283 carry: &crate::drag::Carry<I>,
284 pointer: Point<Pixels>,
285 window: &mut Window,
286 cx: &mut App,
287 ) {
288 self.update(carry.item().clone(), carry.gesture.clone(), pointer, cx);
289 let proposal = {
290 let state = self.0.borrow();
291 let carried = state.carried.as_ref().filter(|carried| carried.detached());
292 carried
293 .zip(state.landing.as_ref())
294 .zip(state.dropped.as_ref())
295 .map(|((carried, (pane, zone)), callback)| {
296 (
297 Drop {
298 item: carried.item.clone(),
299 pane: pane.clone(),
300 zone: *zone,
301 },
302 callback.clone(),
303 state.preview.as_ref().unwrap().bounds(cx),
304 carried.hosted().then(|| carried.ghost_bounds()),
305 )
306 })
307 };
308 self.clear(cx);
309 if let Some((event, callback, preview, ghost)) = proposal
310 && let Some(pane) = callback(&event, window, cx)
311 {
312 let mut state = self.0.borrow_mut();
313 state.settling = Some(Settling {
314 pane,
315 item: event.item,
316 preview,
317 ghost,
318 since: cx.background_executor().now(),
319 });
320 state.painter.notify(cx);
321 }
322 }
323}