1use std::collections::HashMap;
29use std::rc::Rc;
30
31use dioxus::html::MountedData;
32use dioxus::prelude::*;
33
34use crate::core::{transition, GestureEffect, GestureEvent, GesturePhase, Point, Rect};
35use crate::pointer::pointer_client;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct SortEvent {
40 pub from: usize,
41 pub to: usize,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum ReorderMode {
47 #[default]
49 Insert,
50 Swap,
52}
53
54pub fn apply_swap<T>(list: &mut [T], ev: SortEvent) {
56 if ev.from != ev.to && ev.from < list.len() && ev.to < list.len() {
57 list.swap(ev.from, ev.to);
58 }
59}
60
61pub fn displacement(ix: usize, from: usize, over: usize, step: f64) -> f64 {
72 if ix == from {
73 (over as f64 - from as f64) * step
74 } else if from < over && ix > from && ix <= over {
75 -step
76 } else if over < from && ix >= over && ix < from {
77 step
78 } else {
79 0.0
80 }
81}
82
83pub fn pointer_target(
91 rects: &HashMap<usize, Rect>,
92 from: usize,
93 current: Option<usize>,
94 at: Point,
95 axis: Axis,
96) -> Option<usize> {
97 let Some((&ix, rect)) = rects.iter().find(|(_, r)| r.contains(at)) else {
98 return current;
99 };
100 if ix == from || Some(ix) == current {
101 return current;
102 }
103 let (pos, size) = match axis {
104 Axis::Vertical => (at.y - rect.y, rect.height),
105 Axis::Horizontal => (at.x - rect.x, rect.width),
106 };
107 let crossed = if from < ix {
108 pos > size * 0.5
109 } else {
110 pos < size * 0.5
111 };
112 if crossed {
113 Some(ix)
114 } else {
115 current
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum Axis {
123 #[default]
124 Vertical,
125 Horizontal,
126}
127
128pub fn apply_sort<T>(list: &mut Vec<T>, ev: SortEvent) {
130 if ev.from == ev.to || ev.from >= list.len() || ev.to >= list.len() {
131 return;
132 }
133 let item = list.remove(ev.from);
134 list.insert(ev.to, item);
135}
136
137#[component]
145pub fn SortableList(
146 len: usize,
148 render: Callback<usize, Element>,
150 on_sort: EventHandler<SortEvent>,
152 #[props(default)]
154 axis: Axis,
155 #[props(default = true)]
158 live_preview: bool,
159 #[props(default = false)]
164 touch_handle: bool,
165 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
166) -> Element {
167 let mut drag_from = use_signal(|| None::<usize>);
168 let mut over = use_signal(|| None::<usize>);
169 let rects = use_signal(HashMap::<usize, Rect>::new);
172 let mounteds = use_signal(HashMap::<usize, Rc<MountedData>>::new);
173 let size_of = move |ix: usize| {
174 rects
175 .peek()
176 .get(&ix)
177 .map(|r| match axis {
178 Axis::Vertical => r.height,
179 Axis::Horizontal => r.width,
180 })
181 .unwrap_or(40.0)
182 };
183
184 let mut gesture = use_signal(|| GesturePhase::Idle);
187 let mut step = move |event: GestureEvent| -> GestureEffect {
188 let (next, fx) = transition(*gesture.peek(), event, 8.0);
189 gesture.set(next);
190 fx
191 };
192 let mut feed = move |ix: usize, event: GestureEvent| {
194 match step(event) {
195 GestureEffect::Begin { .. } => {
196 drag_from.set(Some(ix));
197 over.set(None);
198 for (i, m) in mounteds.peek().clone() {
202 let mut rects = rects;
203 spawn(async move {
204 if let Ok(r) = m.get_client_rect().await {
205 rects.write().insert(
206 i,
207 Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
208 );
209 }
210 });
211 }
212 }
213 GestureEffect::Track { at } => {
214 if let Some(from) = *drag_from.peek() {
215 let next = pointer_target(&rects.peek(), from, *over.peek(), at, axis);
216 if next != *over.peek() {
217 over.set(next);
218 }
219 }
220 }
221 GestureEffect::Drop { .. } => {
222 if let (Some(from), Some(to)) = (*drag_from.peek(), *over.peek()) {
223 if from != to {
224 on_sort.call(SortEvent { from, to });
225 }
226 }
227 drag_from.set(None);
228 over.set(None);
229 }
230 GestureEffect::Abort => {
231 drag_from.set(None);
232 over.set(None);
233 }
234 GestureEffect::Tap | GestureEffect::None => {}
235 }
236 };
237 let touch_pointer = |evt: &PointerEvent| evt.pointer_type() != "mouse" && evt.is_primary();
238
239 rsx! {
240 div {
241 ..attributes,
242 for ix in 0..len {
243 div {
244 key: "{ix}",
245 draggable: true,
246 "data-dragging": drag_from() == Some(ix),
247 "data-drop-target": over() == Some(ix) && drag_from() != Some(ix),
248 style: {
249 let base = match (live_preview, drag_from(), over()) {
250 (true, Some(from), Some(o)) => {
251 let d = displacement(ix, from, o, size_of(from));
252 let (x, y) = match axis {
253 Axis::Vertical => (0.0, d),
254 Axis::Horizontal => (d, 0.0),
255 };
256 format!("transform: translate({x}px, {y}px); transition: transform 160ms ease;")
257 }
258 (true, Some(_), None) => {
259 "transform: none; transition: transform 160ms ease;".to_string()
260 }
261 _ => String::new(),
262 };
263 if touch_handle {
264 format!("display: flex; align-items: stretch; width: 100%; {base}")
265 } else {
266 format!("touch-action: none; {base}")
267 }
268 },
269 onpointerdown: move |evt: PointerEvent| {
272 if touch_handle || !touch_pointer(&evt) { return; }
273 feed(ix, GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
274 },
275 onpointermove: move |evt: PointerEvent| {
276 if touch_handle { return; }
277 feed(ix, GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
278 },
279 onpointerup: move |evt: PointerEvent| {
280 if touch_handle { return; }
281 feed(ix, GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
282 },
283 onpointercancel: move |_| {
284 if touch_handle { return; }
285 feed(ix, GestureEvent::Cancel);
286 },
287 onlostpointercapture: move |_| {
288 if touch_handle { return; }
292 feed(ix, GestureEvent::Cancel);
293 },
294 onmounted: move |evt: Event<MountedData>| {
295 let m: Rc<MountedData> = evt.data();
296 let mut mounteds = mounteds;
297 let mut rects = rects;
298 mounteds.write().insert(ix, m.clone());
299 spawn(async move {
300 if let Ok(r) = m.get_client_rect().await {
301 rects.write().insert(
302 ix,
303 Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
304 );
305 }
306 });
307 },
308 ondragstart: move |evt: DragEvent| {
309 evt.stop_propagation();
313 let _ = evt.data_transfer().set_data("text/plain", "dioxus-dnd-sort");
314 drag_from.set(Some(ix));
315 },
316 ondragover: move |evt: DragEvent| {
317 let Some(from) = drag_from() else { return };
318 evt.prevent_default();
319 if from == ix || over() == Some(ix) {
320 return;
321 }
322 let pos = match axis {
328 Axis::Vertical => evt.element_coordinates().y,
329 Axis::Horizontal => evt.element_coordinates().x,
330 };
331 let mid = size_of(ix) * 0.5;
332 let crossed = if from < ix { pos > mid } else { pos < mid };
333 if crossed {
334 over.set(Some(ix));
335 }
336 },
337 ondrop: move |evt: DragEvent| {
338 evt.prevent_default();
339 evt.stop_propagation();
340 if let Some(from) = drag_from() {
341 if from != ix {
342 on_sort.call(SortEvent { from, to: ix });
343 }
344 }
345 drag_from.set(None);
346 over.set(None);
347 },
348 ondragend: move |_| {
349 drag_from.set(None);
350 over.set(None);
351 },
352 if touch_handle {
353 span {
354 "data-sort-handle": true,
355 aria_hidden: true,
356 style: "touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none; flex: 0 0 1.35rem; display: grid; place-items: center;",
357 onpointerdown: move |evt: PointerEvent| {
358 if !touch_pointer(&evt) { return; }
359 feed(ix, GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
360 },
361 onpointermove: move |evt: PointerEvent| {
362 feed(ix, GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
363 },
364 onpointerup: move |evt: PointerEvent| {
365 feed(ix, GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
366 },
367 onpointercancel: move |_| feed(ix, GestureEvent::Cancel),
368 onlostpointercapture: move |_| feed(ix, GestureEvent::Cancel),
369 "⠿"
370 }
371 div {
372 "data-sort-content": true,
373 style: "flex: 1 1 auto; min-width: 0;",
374 {render.call(ix)}
375 }
376 } else {
377 {render.call(ix)}
378 }
379 }
380 }
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn sort_moves_forward_and_back() {
391 let mut v = vec!["a", "b", "c", "d"];
392 apply_sort(&mut v, SortEvent { from: 0, to: 2 });
393 assert_eq!(v, vec!["b", "c", "a", "d"]);
394 apply_sort(&mut v, SortEvent { from: 3, to: 0 });
395 assert_eq!(v, vec!["d", "b", "c", "a"]);
396 }
397
398 #[test]
399 fn sort_ignores_out_of_bounds_and_noops() {
400 let mut v = vec![1, 2, 3];
401 apply_sort(&mut v, SortEvent { from: 1, to: 1 });
402 apply_sort(&mut v, SortEvent { from: 9, to: 0 });
403 apply_sort(&mut v, SortEvent { from: 0, to: 9 });
404 assert_eq!(v, vec![1, 2, 3]);
405 }
406}
407
408#[cfg(test)]
409mod pointer_target_tests {
410 use super::*;
411
412 fn rows() -> HashMap<usize, Rect> {
414 (0..3)
415 .map(|i| (i, Rect::new(0.0, i as f64 * 40.0, 200.0, 40.0)))
416 .collect()
417 }
418
419 #[test]
420 fn adopts_a_row_only_past_its_midpoint() {
421 let r = rows();
422 let t = pointer_target(&r, 0, None, Point::new(50.0, 45.0), Axis::Vertical);
424 assert_eq!(t, None);
425 let t = pointer_target(&r, 0, None, Point::new(50.0, 65.0), Axis::Vertical);
427 assert_eq!(t, Some(1));
428 let t = pointer_target(&r, 2, None, Point::new(50.0, 75.0), Axis::Vertical);
430 assert_eq!(t, None);
431 let t = pointer_target(&r, 2, None, Point::new(50.0, 55.0), Axis::Vertical);
432 assert_eq!(t, Some(1));
433 }
434
435 #[test]
436 fn keeps_current_over_source_row_and_outside_all_rects() {
437 let r = rows();
438 let t = pointer_target(&r, 0, Some(2), Point::new(50.0, 10.0), Axis::Vertical);
440 assert_eq!(t, Some(2));
441 let t = pointer_target(&r, 0, Some(2), Point::new(500.0, 500.0), Axis::Vertical);
443 assert_eq!(t, Some(2));
444 }
445
446 #[test]
447 fn horizontal_axis_uses_x() {
448 let r: HashMap<usize, Rect> = (0..3)
449 .map(|i| (i, Rect::new(i as f64 * 60.0, 0.0, 60.0, 40.0)))
450 .collect();
451 let t = pointer_target(&r, 0, None, Point::new(100.0, 20.0), Axis::Horizontal);
452 assert_eq!(t, Some(1)); }
454}
455
456#[cfg(test)]
457mod swap_tests {
458 use super::*;
459
460 #[test]
461 fn swap_exchanges_and_guards_bounds() {
462 let mut v = vec![1, 2, 3, 4];
463 apply_swap(&mut v, SortEvent { from: 0, to: 3 });
464 assert_eq!(v, vec![4, 2, 3, 1]);
465 apply_swap(&mut v, SortEvent { from: 9, to: 0 });
466 assert_eq!(v, vec![4, 2, 3, 1]);
467 }
468}
469
470#[cfg(test)]
471mod displacement_tests {
472 use super::*;
473
474 #[test]
475 fn displacement_moves_source_to_target_and_neighbors_aside() {
476 let d: Vec<f64> = (0..5).map(|ix| displacement(ix, 1, 3, 40.0)).collect();
479 assert_eq!(d, vec![0.0, 80.0, -40.0, -40.0, 0.0]);
480 let d: Vec<f64> = (0..5).map(|ix| displacement(ix, 3, 1, 40.0)).collect();
482 assert_eq!(d, vec![0.0, 40.0, 40.0, -80.0, 0.0]);
483 assert!((0..5).all(|ix| displacement(ix, 2, 2, 40.0) == 0.0));
485 let sum: f64 = (0..5).map(|ix| displacement(ix, 1, 3, 40.0)).sum();
487 assert_eq!(sum, 0.0);
488 }
489}