1use gpui::{AnyElement, Bounds, Div, Hsla, Pixels, Point, div, point, prelude::*, px};
2use liora_icons::Icon;
3use liora_icons_lucide::IconName;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum DragAxis {
8 Horizontal,
9 Vertical,
10 Free,
11}
12
13#[derive(Clone, Debug, Default)]
20pub struct DragState {
21 origin_index: Option<usize>,
22 active_index: Option<usize>,
23 over_index: Option<usize>,
24 start_position: Option<Point<Pixels>>,
25 current_position: Option<Point<Pixels>>,
26 grab_offset: Option<Point<Pixels>>,
27}
28
29impl DragState {
30 pub fn start(&mut self, index: usize, position: Point<Pixels>) {
31 self.start_at(index, position, None);
32 }
33
34 pub fn start_at(
35 &mut self,
36 index: usize,
37 position: Point<Pixels>,
38 bounds: Option<Bounds<Pixels>>,
39 ) {
40 self.origin_index = Some(index);
41 self.active_index = Some(index);
42 self.over_index = Some(index);
43 self.start_position = Some(position);
44 self.current_position = Some(position);
45 self.grab_offset =
46 bounds.map(|bounds| point(position.x - bounds.origin.x, position.y - bounds.origin.y));
47 }
48
49 pub fn update_position(&mut self, position: Point<Pixels>) {
50 if self.active_index.is_some() {
51 self.current_position = Some(position);
52 }
53 }
54
55 pub fn set_over(&mut self, index: usize) {
56 if self.active_index.is_some() {
57 self.over_index = Some(index);
58 }
59 }
60
61 pub fn move_active_to(&mut self, index: usize) {
62 self.active_index = Some(index);
63 self.over_index = Some(index);
64 self.start_position = self.current_position;
65 }
66
67 pub fn finish(&mut self) -> Option<(usize, usize)> {
68 let origin = self.origin_index.take()?;
69 let active = self.active_index.take()?;
70 let target = self.over_index.take().unwrap_or(active);
71 self.start_position = None;
72 self.current_position = None;
73 self.grab_offset = None;
74 Some((origin, target))
75 }
76
77 pub fn cancel(&mut self) {
78 self.origin_index = None;
79 self.active_index = None;
80 self.over_index = None;
81 self.start_position = None;
82 self.current_position = None;
83 self.grab_offset = None;
84 }
85
86 pub fn active_index(&self) -> Option<usize> {
87 self.active_index
88 }
89
90 pub fn origin_index(&self) -> Option<usize> {
91 self.origin_index
92 }
93
94 pub fn over_index(&self) -> Option<usize> {
95 self.over_index
96 }
97
98 pub fn is_active(&self, index: usize) -> bool {
99 self.active_index == Some(index)
100 }
101
102 pub fn is_over(&self, index: usize) -> bool {
103 self.over_index == Some(index) && self.active_index != Some(index)
104 }
105
106 pub fn offset(&self, axis: DragAxis) -> (Pixels, Pixels) {
107 let Some(start) = self.start_position else {
108 return (px(0.0), px(0.0));
109 };
110 let Some(current) = self.current_position else {
111 return (px(0.0), px(0.0));
112 };
113 let dx = current.x - start.x;
114 let dy = current.y - start.y;
115 match axis {
116 DragAxis::Horizontal => (dx, px(0.0)),
117 DragAxis::Vertical => (px(0.0), dy),
118 DragAxis::Free => (dx, dy),
119 }
120 }
121
122 pub fn offset_from_bounds(
131 &self,
132 axis: DragAxis,
133 bounds: Option<Bounds<Pixels>>,
134 ) -> (Pixels, Pixels) {
135 let Some(bounds) = bounds else {
136 return self.offset(axis);
137 };
138 let Some(current) = self.current_position else {
139 return (px(0.0), px(0.0));
140 };
141 let grab_offset = self.grab_offset.unwrap_or_else(|| point(px(0.0), px(0.0)));
142 let dx = current.x - grab_offset.x - bounds.origin.x;
143 let dy = current.y - grab_offset.y - bounds.origin.y;
144 match axis {
145 DragAxis::Horizontal => (dx, px(0.0)),
146 DragAxis::Vertical => (px(0.0), dy),
147 DragAxis::Free => (dx, dy),
148 }
149 }
150}
151
152pub fn reorder_indices<T>(items: &mut Vec<T>, from: usize, to: usize) -> bool {
154 if from >= items.len() || to >= items.len() || from == to {
155 return false;
156 }
157 let item = items.remove(from);
158 items.insert(to, item);
159 true
160}
161
162pub fn drag_handle(color: Hsla, active: bool, width: Pixels) -> Div {
164 div()
165 .flex()
166 .flex_none()
167 .w(width)
168 .items_start()
169 .items_center()
170 .justify_center()
171 .cursor_pointer()
172 .rounded_md()
173 .when(!active, |s| {
174 s.hover(|s| s.cursor_pointer().bg(gpui::black().opacity(0.018)))
175 })
176 .when(active, |s| s.cursor_pointer())
177 .child(
178 Icon::new(IconName::GripVertical)
179 .size(px(16.0))
180 .color(color),
181 )
182}
183
184pub fn drag_handle_element(color: Hsla, active: bool, width: Pixels) -> AnyElement {
186 drag_handle(color, active, width).into_any_element()
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use gpui::{Bounds, point, px, size};
193
194 #[test]
195 fn reorder_indices_moves_items() {
196 let mut items = vec![0, 1, 2, 3];
197 assert!(reorder_indices(&mut items, 0, 2));
198 assert_eq!(items, vec![1, 2, 0, 3]);
199 assert!(reorder_indices(&mut items, 3, 1));
200 assert_eq!(items, vec![1, 3, 2, 0]);
201 }
202
203 #[test]
204 fn drag_state_tracks_axis_offsets() {
205 let mut state = DragState::default();
206 state.start(2, point(px(10.0), px(20.0)));
207 state.update_position(point(px(42.0), px(12.0)));
208 assert_eq!(state.offset(DragAxis::Horizontal), (px(32.0), px(0.0)));
209 assert_eq!(state.offset(DragAxis::Vertical), (px(0.0), px(-8.0)));
210 assert_eq!(state.offset(DragAxis::Free), (px(32.0), px(-8.0)));
211 }
212
213 #[test]
214 fn drag_state_finishes_with_last_over_index() {
215 let mut state = DragState::default();
216 state.start(1, point(px(0.0), px(0.0)));
217 state.update_position(point(px(20.0), px(0.0)));
218 state.move_active_to(3);
219 assert_eq!(state.finish(), Some((1, 3)));
220 assert_eq!(state.origin_index(), None);
221 assert_eq!(state.active_index(), None);
222 assert_eq!(state.over_index(), None);
223 }
224
225 #[test]
226 fn drag_state_keeps_grab_offset_when_slot_moves_backward() {
227 let mut state = DragState::default();
228 state.start_at(
229 3,
230 point(px(310.0), px(10.0)),
231 Some(Bounds::new(
232 point(px(300.0), px(0.0)),
233 size(px(100.0), px(40.0)),
234 )),
235 );
236 state.update_position(point(px(250.0), px(10.0)));
237 state.move_active_to(2);
238
239 assert_eq!(
240 state.offset_from_bounds(
241 DragAxis::Horizontal,
242 Some(Bounds::new(
243 point(px(200.0), px(0.0)),
244 size(px(100.0), px(40.0)),
245 )),
246 ),
247 (px(40.0), px(0.0))
248 );
249 }
250}