Skip to main content

gpui_motion_kit/
reorder_native.rs

1//! A native single-axis reorder group with measured item geometry.
2use 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/// Retain in the view. Owns order, stable-key geometry and pointer gesture lifetime.
44/// Supports one-dimensional lists, variable item sizes, keyboard Alt+Arrow reorder,
45/// Escape/deactivation cancellation, and release outside item bounds. Supply native
46/// roles/labels/click handlers on each card. Host owns persistence, removal focus
47/// restoration and scrolling/autoscroll policy; multi-column sorting is not implied.
48#[derive(Clone)]
49pub struct ReorderGroup<K>(Rc<RefCell<Data<K>>>);
50
51impl<K: Clone + Eq + Hash + 'static> ReorderGroup<K> {
52    /// Creates a group, rejecting duplicate keys transactionally.
53    ///
54    /// ```
55    /// use gpui_kit::{div, prelude::*};
56    /// use gpui_motion_kit::{Axis, ReorderGroup};
57    /// let group = ReorderGroup::new(vec![1usize, 2, 3], Axis::Y)?; // persistent view field
58    /// let mut list = div().flex().flex_col();
59    /// for &key in group.order().iter() {
60    ///     list = list.child(group.item(("row", key), key, div().id(("card", key)).child(key.to_string())));
61    /// }
62    /// # Ok::<(), gpui_motion_kit::core::AnimationError>(())
63    /// ```
64    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    /// Current logical order. Drop this borrow before processing input or updating.
84    pub fn order(&self) -> Ref<'_, [K]> {
85        Ref::map(self.0.borrow(), |v| v.order.as_slice())
86    }
87
88    /// Registers a callback after pointer commit or keyboard reorder. The callback
89    /// runs without borrowing group state and may update application entities.
90    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    /// Updates membership/order and cancels an active gesture. Duplicate keys fail
97    /// without mutation. Invoke on model changes, not every render. Notify the view.
98    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    /// Wraps a styled item in measured position interpolation and native handlers.
117    /// The key must belong to this group. Preserve the same Kit ID through reorder.
118    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    /// Whether a pointer currently owns a reorder gesture.
136    pub fn is_dragging(&self) -> bool {
137        self.0.borrow().grab.is_some()
138    }
139}
140
141/// Native measured reorder item.
142pub 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    /// Selects sibling/release position timing.
152    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        // Register key listeners on this wrapper before child paint selects a descendant node.
520        child.paint(w, cx);
521    }
522}