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: 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 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 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 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 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}