Skip to main content

guise/dnd/
sortable.rs

1//! `SortableList` — drag rows to reorder.
2//!
3//! Stateless: the parent owns the items; the list reports `(from, to)` and
4//! the parent applies it (usually with [`apply_reorder`](super::apply_reorder)).
5
6use std::rc::Rc;
7
8use gpui::prelude::*;
9use gpui::{div, px, AnyElement, App, ElementId, IntoElement, SharedString, Window};
10
11use super::chip::DragChip;
12use crate::devtools::Probed;
13use crate::theme::theme;
14
15type ItemBuilder = Rc<dyn Fn(usize, &mut Window, &mut App) -> AnyElement + 'static>;
16type ReorderHandler = Rc<dyn Fn(usize, usize, &mut Window, &mut App) + 'static>;
17type Labeler = Rc<dyn Fn(usize) -> SharedString + 'static>;
18
19/// The payload a sortable row drags: its list group + index.
20#[derive(Clone)]
21struct SortDrag {
22  group: SharedString,
23  index: usize,
24}
25
26/// A vertical list whose rows drag to reorder. Dropping row `from` on row
27/// `to` calls `on_reorder(from, to)` — "place it where I dropped it".
28///
29/// ```ignore
30/// let view = cx.entity().downgrade();
31/// SortableList::new("queue", self.tracks.len(), {
32///     let tracks = self.tracks.clone();
33///     move |i, _w, _cx| Text::new(tracks[i].clone()).into_any_element()
34/// })
35/// .on_reorder(move |from, to, _window, cx| {
36///     view.update(cx, |this, cx| {
37///         guise::dnd::apply_reorder(&mut this.tracks, from, to);
38///         cx.notify();
39///     })
40///     .ok();
41/// })
42/// ```
43#[derive(IntoElement)]
44pub struct SortableList {
45  id: ElementId,
46  group: SharedString,
47  count: usize,
48  item: ItemBuilder,
49  labeler: Option<Labeler>,
50  gap: f32,
51  on_reorder: Option<ReorderHandler>,
52}
53
54impl SortableList {
55  /// `group` (from `id`) guards drops: rows only accept drags from the
56  /// same list. `item` builds each row's content, re-invoked every frame.
57  pub fn new<E>(
58    id: impl Into<SharedString>,
59    count: usize,
60    item: impl Fn(usize, &mut Window, &mut App) -> E + 'static,
61  ) -> Self
62  where
63    E: IntoElement,
64  {
65    let group: SharedString = id.into();
66    SortableList {
67      id: ElementId::Name(group.clone()),
68      group,
69      count,
70      item: Rc::new(move |i, window, cx| item(i, window, cx).into_any_element()),
71      labeler: None,
72      gap: 4.0,
73      on_reorder: None,
74    }
75  }
76
77  /// Chip label for the dragged row (default "Item N").
78  pub fn label_of(mut self, labeler: impl Fn(usize) -> SharedString + 'static) -> Self {
79    self.labeler = Some(Rc::new(labeler));
80    self
81  }
82
83  /// Vertical gap between rows in px (default 4).
84  pub fn gap(mut self, gap: f32) -> Self {
85    self.gap = gap.max(0.0);
86    self
87  }
88
89  /// Called with `(from, to)` when a row is dropped on another.
90  pub fn on_reorder(
91    mut self,
92    handler: impl Fn(usize, usize, &mut Window, &mut App) + 'static,
93  ) -> Self {
94    self.on_reorder = Some(Rc::new(handler));
95    self
96  }
97}
98
99impl RenderOnce for SortableList {
100  fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
101    let t = theme(cx);
102    let accent = t.primary().hsla();
103
104    let mut root = div().id(self.id).flex().flex_col().gap(px(self.gap));
105    for i in 0..self.count {
106      let content = (self.item)(i, window, cx);
107      let label = match &self.labeler {
108        Some(labeler) => labeler(i),
109        None => SharedString::from(format!("Item {}", i + 1)),
110      };
111      let chip = DragChip {
112        value: SortDrag {
113          group: self.group.clone(),
114          index: i,
115        },
116        label,
117      };
118
119      let mut row = div()
120        .id(("guise-sortable-row", i))
121        .cursor_grab()
122        // A constant transparent top border keeps layout stable while
123        // the drag-over highlight recolors it as the insert marker.
124        .border_t_2()
125        .border_color(gpui::transparent_black())
126        .on_drag(chip, |dragged: &DragChip<SortDrag>, _off, _w, cx| {
127          cx.new(|_| dragged.clone())
128        })
129        .drag_over::<DragChip<SortDrag>>(move |style, _drag, _window, _cx| {
130          style.border_color(accent)
131        })
132        .child(content);
133
134      if let Some(handler) = self.on_reorder.clone() {
135        let group = self.group.clone();
136        row = row.on_drop(move |dragged: &DragChip<SortDrag>, window, cx| {
137          if dragged.value.group == group && dragged.value.index != i {
138            handler(dragged.value.index, i, window, cx);
139          }
140        });
141      }
142      root = root.child(row);
143    }
144    root.probe("SortableList")
145  }
146}