1use crate::Axis;
3use crate::core::{AnimationError, MotionValue, Transition, move_item};
4
5use gpui_kit::{
6 AnyElement, App, Bounds, DispatchPhase, Div, Element, ElementId, FocusHandle, GlobalElementId,
7 Hitbox, HitboxBehavior, InspectorElementId, IntoElement, KeyDownEvent, LayoutId, MouseButton,
8 MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Stateful, Window, point, prelude::*, px,
9};
10
11use std::{
12 cell::{Ref, RefCell},
13 collections::HashMap,
14 hash::Hash,
15 rc::Rc,
16 time::Instant,
17};
18
19struct Metrics {
20 bounds: Bounds<Pixels>,
21 values: [MotionValue; 2],
22 focus: FocusHandle,
23}
24
25struct Grab<K> {
26 key: K,
27 offset: f64,
28 pointer: f64,
29 original: Vec<K>,
30}
31
32type Change<K> = Rc<dyn Fn(&[K], &mut Window, &mut App)>;
33
34struct Data<K> {
35 order: Vec<K>,
36 metrics: HashMap<K, Metrics>,
37 grab: Option<Grab<K>>,
38 axis: Axis,
39 epoch: Option<Instant>,
40 change: Option<Change<K>>,
41}
42
43#[derive(Clone)]
49pub struct ReorderGroup<K>(Rc<RefCell<Data<K>>>);
50
51impl<K: Clone + Eq + Hash + 'static> ReorderGroup<K> {
52 pub fn new(order: Vec<K>, axis: Axis) -> Result<Self, AnimationError> {
65 if order
66 .iter()
67 .enumerate()
68 .any(|(i, k)| order[..i].contains(k))
69 {
70 return Err(AnimationError::Index);
71 }
72
73 Ok(Self(Rc::new(RefCell::new(Data {
74 order,
75 metrics: HashMap::new(),
76 grab: None,
77 axis,
78 epoch: None,
79 change: None,
80 }))))
81 }
82
83 pub fn order(&self) -> Ref<'_, [K]> {
85 Ref::map(self.0.borrow(), |v| v.order.as_slice())
86 }
87
88 pub fn on_change(self, callback: impl Fn(&[K], &mut Window, &mut App) + 'static) -> Self {
91 self.0.borrow_mut().change = Some(Rc::new(callback));
92
93 self
94 }
95
96 pub fn set_order(&self, order: Vec<K>) -> Result<(), AnimationError> {
99 if order
100 .iter()
101 .enumerate()
102 .any(|(i, k)| order[..i].contains(k))
103 {
104 return Err(AnimationError::Index);
105 }
106
107 let mut data = self.0.borrow_mut();
108
109 data.grab = None;
110 data.metrics.retain(|k, _| order.contains(k));
111 data.order = order;
112
113 Ok(())
114 }
115
116 pub fn item<E: IntoElement + StatefulInteractiveElement + 'static>(
119 &self,
120 id: impl Into<ElementId>,
121 key: K,
122 child: E,
123 ) -> ReorderElement<K, E> {
124 assert!(self.0.borrow().order.contains(&key), "unknown reorder key");
125
126 ReorderElement {
127 group: self.clone(),
128 id: id.into(),
129 key,
130 child: Some(child),
131 transition: Transition::default(),
132 }
133 }
134
135 pub fn is_dragging(&self) -> bool {
137 self.0.borrow().grab.is_some()
138 }
139}
140
141pub struct ReorderElement<K, E = Stateful<Div>> {
143 group: ReorderGroup<K>,
144 id: ElementId,
145 key: K,
146 child: Option<E>,
147 transition: Transition,
148}
149
150impl<K, E> ReorderElement<K, E> {
151 pub fn transition(mut self, transition: Transition) -> Self {
153 self.transition = transition;
154
155 self
156 }
157}
158
159fn coordinate(bounds: Bounds<Pixels>, axis: Axis) -> (f64, f64) {
160 match axis {
161 Axis::X => (f64::from(bounds.origin.x), f64::from(bounds.size.width)),
162 Axis::Y => (f64::from(bounds.origin.y), f64::from(bounds.size.height)),
163 }
164}
165
166fn pointer(position: gpui_kit::Point<Pixels>, axis: Axis) -> f64 {
167 match axis {
168 Axis::X => f64::from(position.x),
169 Axis::Y => f64::from(position.y),
170 }
171}
172
173impl<K: Clone + Eq + Hash + 'static, E: IntoElement + StatefulInteractiveElement + 'static>
174 IntoElement for ReorderElement<K, E>
175{
176 type Element = Self;
177
178 fn into_element(self) -> Self {
179 self
180 }
181}
182
183impl<K: Clone + Eq + Hash + 'static, E: IntoElement + StatefulInteractiveElement + 'static> Element
184 for ReorderElement<K, E>
185{
186 type RequestLayoutState = AnyElement;
187 type PrepaintState = Hitbox;
188
189 fn id(&self) -> Option<ElementId> {
190 Some(self.id.clone())
191 }
192
193 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
194 None
195 }
196
197 fn request_layout(
198 &mut self,
199 _: Option<&GlobalElementId>,
200 _: Option<&InspectorElementId>,
201 w: &mut Window,
202 cx: &mut App,
203 ) -> (LayoutId, AnyElement) {
204 let mut data = self.group.0.borrow_mut();
205
206 data.epoch
207 .get_or_insert_with(|| cx.background_executor().now());
208
209 if (!w.is_window_active()
210 || data.grab.as_ref().is_some_and(|g| {
211 data.metrics
212 .get(&g.key)
213 .is_some_and(|m| !m.focus.is_focused(w))
214 }))
215 && let Some(grab) = data.grab.take()
216 {
217 data.order = grab.original;
218 w.request_animation_frame();
219 }
220
221 let metric = data
222 .metrics
223 .entry(self.key.clone())
224 .or_insert_with(|| Metrics {
225 bounds: Bounds::default(),
226 values: [MotionValue::new(0.), MotionValue::new(0.)],
227 focus: cx.focus_handle().tab_stop(true),
228 });
229
230 let focus = metric.focus.clone();
231
232 drop(data);
233 let mut child = self
234 .child
235 .take()
236 .expect("one layout")
237 .track_focus(&focus)
238 .tab_index(0)
239 .into_any_element();
240
241 let layout = child.request_layout(w, cx);
242
243 (layout, child)
244 }
245
246 fn prepaint(
247 &mut self,
248 _: Option<&GlobalElementId>,
249 _: Option<&InspectorElementId>,
250 bounds: Bounds<Pixels>,
251 child: &mut AnyElement,
252 w: &mut Window,
253 cx: &mut App,
254 ) -> Hitbox {
255 let mut data = self.group.0.borrow_mut();
256
257 let now = cx
258 .background_executor()
259 .now()
260 .saturating_duration_since(data.epoch.expect("epoch"));
261
262 let raw = bounds.origin - w.pixel_snap_point(w.element_offset());
263 let target = [f64::from(raw.x), f64::from(raw.y)];
264 let axis = data.axis;
265
266 let drag = data
267 .grab
268 .as_ref()
269 .filter(|g| g.key == self.key)
270 .map(|g| g.pointer - g.offset);
271
272 let metric = data.metrics.get_mut(&self.key).expect("layout first");
273 let first = metric.bounds.size.width == px(0.) && metric.bounds.size.height == px(0.);
274
275 metric.bounds = bounds;
276 let mut presented = target;
277 let mut active = false;
278
279 for i in 0..2 {
280 if first {
281 metric.values[i].jump(target[i], now);
282 }
283
284 if let Some(position) = drag
285 && i == match axis {
286 Axis::X => 0,
287 Axis::Y => 1,
288 }
289 {
290 let offset = if i == 0 {
291 f64::from(w.element_offset().x)
292 } else {
293 f64::from(w.element_offset().y)
294 };
295
296 metric.values[i].set(position - offset, now);
297 } else {
298 metric.values[i].animate(target[i], self.transition, now);
299 }
300
301 let frame = metric.values[i].sample(now, cx.reduce_motion());
302
303 presented[i] = frame.value();
304 active |= frame.is_animating();
305 }
306
307 if active {
308 w.request_animation_frame();
309 }
310
311 drop(data);
312 let offset = point(
313 px((presented[0] - target[0]) as f32),
314 px((presented[1] - target[1]) as f32),
315 );
316
317 let displayed = Bounds {
318 origin: bounds.origin + offset,
319 size: bounds.size,
320 };
321
322 let hitbox = w.insert_hitbox(displayed, HitboxBehavior::Normal);
323
324 w.with_element_offset(offset, |w| {
325 child.prepaint(w, cx);
326 });
327
328 hitbox
329 }
330
331 fn paint(
332 &mut self,
333 _: Option<&GlobalElementId>,
334 _: Option<&InspectorElementId>,
335 _: Bounds<Pixels>,
336 child: &mut AnyElement,
337 hitbox: &mut Hitbox,
338 w: &mut Window,
339 cx: &mut App,
340 ) {
341 let owner = w.current_view();
342 let group = self.group.clone();
343 let key = self.key.clone();
344 let region = hitbox.clone();
345
346 w.on_mouse_event(move |event: &MouseDownEvent, phase, w, cx| {
347 if phase == DispatchPhase::Bubble
348 && event.button == MouseButton::Left
349 && region.is_hovered(w)
350 {
351 let mut data = group.0.borrow_mut();
352
353 if data.grab.is_some() {
354 return;
355 }
356
357 let p = pointer(event.position, data.axis);
358 let original = data.order.clone();
359
360 data.grab = Some(Grab {
361 key: key.clone(),
362 offset: p - coordinate(region.bounds, data.axis).0,
363 pointer: p,
364 original,
365 });
366
367 if let Some(metric) = data.metrics.get(&key) {
368 metric.focus.focus(w, cx);
369 }
370
371 cx.notify(owner);
372 cx.stop_propagation();
373 }
374 });
375 let group = self.group.clone();
376 let key = self.key.clone();
377
378 w.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
379 if phase != DispatchPhase::Capture {
380 return;
381 }
382
383 let mut data = group.0.borrow_mut();
384
385 if !data.grab.as_ref().is_some_and(|g| g.key == key) {
386 return;
387 }
388
389 if event.pressed_button != Some(MouseButton::Left) {
390 let original = data.grab.take().expect("grab").original;
391
392 data.order = original;
393 cx.notify(owner);
394
395 return;
396 }
397
398 let p = pointer(event.position, data.axis);
399 let axis = data.axis;
400 let grab = data.grab.as_mut().expect("grab");
401
402 grab.pointer = p;
403 let start = p - grab.offset;
404
405 let length = data
406 .metrics
407 .get(&key)
408 .map_or(0., |m| coordinate(m.bounds, axis).1);
409
410 let center = start + length * 0.5;
411
412 let destination = data
413 .order
414 .iter()
415 .enumerate()
416 .filter_map(|(i, k)| {
417 data.metrics.get(k).map(|m| {
418 let (start, length) = coordinate(m.bounds, axis);
419
420 (i, (start + length * 0.5 - center).abs())
421 })
422 })
423 .min_by(|a, b| a.1.total_cmp(&b.1))
424 .map(|v| v.0);
425
426 if let Some(to) = destination {
427 let from = data.order.iter().position(|k| *k == key).expect("key");
428
429 move_item(&mut data.order, from, to).expect("indices");
430 }
431
432 cx.notify(owner);
433 });
434 let group = self.group.clone();
435 let key = self.key.clone();
436
437 w.on_mouse_event(move |event: &MouseUpEvent, phase, w, cx| {
438 if phase == DispatchPhase::Capture && event.button == MouseButton::Left {
439 let mut data = group.0.borrow_mut();
440
441 if data.grab.as_ref().is_some_and(|g| g.key == key) {
442 let changed = data.grab.take().expect("grab").original != data.order;
443
444 let notification = if changed {
445 data.change
446 .clone()
447 .map(|change| (change, data.order.clone()))
448 } else {
449 None
450 };
451
452 drop(data);
453
454 if let Some((change, order)) = notification {
455 change(&order, w, cx);
456 }
457
458 cx.notify(owner);
459 }
460 }
461 });
462 let group = self.group.clone();
463 let key = self.key.clone();
464
465 w.on_key_event(move |event: &KeyDownEvent, phase, w, cx| {
466 if phase != DispatchPhase::Capture {
467 return;
468 }
469
470 let mut data = group.0.borrow_mut();
471
472 if !data
473 .metrics
474 .get(&key)
475 .is_some_and(|m| m.focus.is_focused(w))
476 {
477 return;
478 }
479
480 if event.keystroke.key == "escape" {
481 if let Some(grab) = data.grab.take() {
482 data.order = grab.original;
483 cx.notify(owner);
484 }
485
486 return;
487 }
488
489 if !event.keystroke.modifiers.alt {
490 return;
491 }
492
493 let delta = match (data.axis, event.keystroke.key.as_str()) {
494 (Axis::Y, "up") | (Axis::X, "left") => -1,
495 (Axis::Y, "down") | (Axis::X, "right") => 1,
496 _ => return,
497 };
498
499 let from = data.order.iter().position(|k| *k == key).expect("key");
500 let to = from.saturating_add_signed(delta).min(data.order.len() - 1);
501
502 if from != to {
503 move_item(&mut data.order, from, to).expect("indices");
504 let notification = data
505 .change
506 .clone()
507 .map(|change| (change, data.order.clone()));
508
509 drop(data);
510
511 if let Some((change, order)) = notification {
512 change(&order, w, cx);
513 }
514
515 cx.notify(owner);
516 cx.stop_propagation();
517 }
518 });
519 child.paint(w, cx);
521 }
522}