use std::collections::HashMap;
use std::rc::Rc;
use dioxus::html::MountedData;
use dioxus::prelude::*;
use crate::core::{transition, GestureEffect, GestureEvent, GesturePhase, Point, Rect};
use crate::pointer::pointer_client;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SortEvent {
pub from: usize,
pub to: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReorderMode {
#[default]
Insert,
Swap,
}
pub fn apply_swap<T>(list: &mut [T], ev: SortEvent) {
if ev.from != ev.to && ev.from < list.len() && ev.to < list.len() {
list.swap(ev.from, ev.to);
}
}
pub fn displacement(ix: usize, from: usize, over: usize, step: f64) -> f64 {
if ix == from {
(over as f64 - from as f64) * step
} else if from < over && ix > from && ix <= over {
-step
} else if over < from && ix >= over && ix < from {
step
} else {
0.0
}
}
pub fn pointer_target(
rects: &HashMap<usize, Rect>,
from: usize,
current: Option<usize>,
at: Point,
axis: Axis,
) -> Option<usize> {
let Some((&ix, rect)) = rects.iter().find(|(_, r)| r.contains(at)) else {
return current;
};
if ix == from || Some(ix) == current {
return current;
}
let (pos, size) = match axis {
Axis::Vertical => (at.y - rect.y, rect.height),
Axis::Horizontal => (at.x - rect.x, rect.width),
};
let crossed = if from < ix {
pos > size * 0.5
} else {
pos < size * 0.5
};
if crossed {
Some(ix)
} else {
current
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Axis {
#[default]
Vertical,
Horizontal,
}
pub fn apply_sort<T>(list: &mut Vec<T>, ev: SortEvent) {
if ev.from == ev.to || ev.from >= list.len() || ev.to >= list.len() {
return;
}
let item = list.remove(ev.from);
list.insert(ev.to, item);
}
#[component]
pub fn SortableList(
len: usize,
render: Callback<usize, Element>,
on_sort: EventHandler<SortEvent>,
#[props(default)]
axis: Axis,
#[props(default = true)]
live_preview: bool,
#[props(default = false)]
touch_handle: bool,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let mut drag_from = use_signal(|| None::<usize>);
let mut over = use_signal(|| None::<usize>);
let rects = use_signal(HashMap::<usize, Rect>::new);
let mounteds = use_signal(HashMap::<usize, Rc<MountedData>>::new);
let size_of = move |ix: usize| {
rects
.peek()
.get(&ix)
.map(|r| match axis {
Axis::Vertical => r.height,
Axis::Horizontal => r.width,
})
.unwrap_or(40.0)
};
let mut gesture = use_signal(|| GesturePhase::Idle);
let mut step = move |event: GestureEvent| -> GestureEffect {
let (next, fx) = transition(*gesture.peek(), event, 8.0);
gesture.set(next);
fx
};
let mut feed = move |ix: usize, event: GestureEvent| {
match step(event) {
GestureEffect::Begin { .. } => {
drag_from.set(Some(ix));
over.set(None);
for (i, m) in mounteds.peek().clone() {
let mut rects = rects;
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
rects.write().insert(
i,
Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
);
}
});
}
}
GestureEffect::Track { at } => {
if let Some(from) = *drag_from.peek() {
let next = pointer_target(&rects.peek(), from, *over.peek(), at, axis);
if next != *over.peek() {
over.set(next);
}
}
}
GestureEffect::Drop { .. } => {
if let (Some(from), Some(to)) = (*drag_from.peek(), *over.peek()) {
if from != to {
on_sort.call(SortEvent { from, to });
}
}
drag_from.set(None);
over.set(None);
}
GestureEffect::Abort => {
drag_from.set(None);
over.set(None);
}
GestureEffect::Tap | GestureEffect::None => {}
}
};
let touch_pointer = |evt: &PointerEvent| evt.pointer_type() != "mouse" && evt.is_primary();
rsx! {
div {
..attributes,
for ix in 0..len {
div {
key: "{ix}",
draggable: true,
"data-dragging": drag_from() == Some(ix),
"data-drop-target": over() == Some(ix) && drag_from() != Some(ix),
style: {
let base = match (live_preview, drag_from(), over()) {
(true, Some(from), Some(o)) => {
let d = displacement(ix, from, o, size_of(from));
let (x, y) = match axis {
Axis::Vertical => (0.0, d),
Axis::Horizontal => (d, 0.0),
};
format!("transform: translate({x}px, {y}px); transition: transform 160ms ease;")
}
(true, Some(_), None) => {
"transform: none; transition: transform 160ms ease;".to_string()
}
_ => String::new(),
};
if touch_handle {
format!("display: flex; align-items: stretch; width: 100%; {base}")
} else {
format!("touch-action: none; {base}")
}
},
onpointerdown: move |evt: PointerEvent| {
if touch_handle || !touch_pointer(&evt) { return; }
feed(ix, GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
},
onpointermove: move |evt: PointerEvent| {
if touch_handle { return; }
feed(ix, GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
},
onpointerup: move |evt: PointerEvent| {
if touch_handle { return; }
feed(ix, GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
},
onpointercancel: move |_| {
if touch_handle { return; }
feed(ix, GestureEvent::Cancel);
},
onlostpointercapture: move |_| {
if touch_handle { return; }
feed(ix, GestureEvent::Cancel);
},
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
let mut mounteds = mounteds;
let mut rects = rects;
mounteds.write().insert(ix, m.clone());
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
rects.write().insert(
ix,
Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
);
}
});
},
ondragstart: move |evt: DragEvent| {
evt.stop_propagation();
let _ = evt.data_transfer().set_data("text/plain", "dioxus-dnd-sort");
drag_from.set(Some(ix));
},
ondragover: move |evt: DragEvent| {
let Some(from) = drag_from() else { return };
evt.prevent_default();
if from == ix || over() == Some(ix) {
return;
}
let pos = match axis {
Axis::Vertical => evt.element_coordinates().y,
Axis::Horizontal => evt.element_coordinates().x,
};
let mid = size_of(ix) * 0.5;
let crossed = if from < ix { pos > mid } else { pos < mid };
if crossed {
over.set(Some(ix));
}
},
ondrop: move |evt: DragEvent| {
evt.prevent_default();
evt.stop_propagation();
if let Some(from) = drag_from() {
if from != ix {
on_sort.call(SortEvent { from, to: ix });
}
}
drag_from.set(None);
over.set(None);
},
ondragend: move |_| {
drag_from.set(None);
over.set(None);
},
if touch_handle {
span {
"data-sort-handle": true,
aria_hidden: true,
style: "touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none; flex: 0 0 1.35rem; display: grid; place-items: center;",
onpointerdown: move |evt: PointerEvent| {
if !touch_pointer(&evt) { return; }
feed(ix, GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
},
onpointermove: move |evt: PointerEvent| {
feed(ix, GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
},
onpointerup: move |evt: PointerEvent| {
feed(ix, GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
},
onpointercancel: move |_| feed(ix, GestureEvent::Cancel),
onlostpointercapture: move |_| feed(ix, GestureEvent::Cancel),
"⠿"
}
div {
"data-sort-content": true,
style: "flex: 1 1 auto; min-width: 0;",
{render.call(ix)}
}
} else {
{render.call(ix)}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_moves_forward_and_back() {
let mut v = vec!["a", "b", "c", "d"];
apply_sort(&mut v, SortEvent { from: 0, to: 2 });
assert_eq!(v, vec!["b", "c", "a", "d"]);
apply_sort(&mut v, SortEvent { from: 3, to: 0 });
assert_eq!(v, vec!["d", "b", "c", "a"]);
}
#[test]
fn sort_ignores_out_of_bounds_and_noops() {
let mut v = vec![1, 2, 3];
apply_sort(&mut v, SortEvent { from: 1, to: 1 });
apply_sort(&mut v, SortEvent { from: 9, to: 0 });
apply_sort(&mut v, SortEvent { from: 0, to: 9 });
assert_eq!(v, vec![1, 2, 3]);
}
}
#[cfg(test)]
mod pointer_target_tests {
use super::*;
fn rows() -> HashMap<usize, Rect> {
(0..3)
.map(|i| (i, Rect::new(0.0, i as f64 * 40.0, 200.0, 40.0)))
.collect()
}
#[test]
fn adopts_a_row_only_past_its_midpoint() {
let r = rows();
let t = pointer_target(&r, 0, None, Point::new(50.0, 45.0), Axis::Vertical);
assert_eq!(t, None);
let t = pointer_target(&r, 0, None, Point::new(50.0, 65.0), Axis::Vertical);
assert_eq!(t, Some(1));
let t = pointer_target(&r, 2, None, Point::new(50.0, 75.0), Axis::Vertical);
assert_eq!(t, None);
let t = pointer_target(&r, 2, None, Point::new(50.0, 55.0), Axis::Vertical);
assert_eq!(t, Some(1));
}
#[test]
fn keeps_current_over_source_row_and_outside_all_rects() {
let r = rows();
let t = pointer_target(&r, 0, Some(2), Point::new(50.0, 10.0), Axis::Vertical);
assert_eq!(t, Some(2));
let t = pointer_target(&r, 0, Some(2), Point::new(500.0, 500.0), Axis::Vertical);
assert_eq!(t, Some(2));
}
#[test]
fn horizontal_axis_uses_x() {
let r: HashMap<usize, Rect> = (0..3)
.map(|i| (i, Rect::new(i as f64 * 60.0, 0.0, 60.0, 40.0)))
.collect();
let t = pointer_target(&r, 0, None, Point::new(100.0, 20.0), Axis::Horizontal);
assert_eq!(t, Some(1)); }
}
#[cfg(test)]
mod swap_tests {
use super::*;
#[test]
fn swap_exchanges_and_guards_bounds() {
let mut v = vec![1, 2, 3, 4];
apply_swap(&mut v, SortEvent { from: 0, to: 3 });
assert_eq!(v, vec![4, 2, 3, 1]);
apply_swap(&mut v, SortEvent { from: 9, to: 0 });
assert_eq!(v, vec![4, 2, 3, 1]);
}
}
#[cfg(test)]
mod displacement_tests {
use super::*;
#[test]
fn displacement_moves_source_to_target_and_neighbors_aside() {
let d: Vec<f64> = (0..5).map(|ix| displacement(ix, 1, 3, 40.0)).collect();
assert_eq!(d, vec![0.0, 80.0, -40.0, -40.0, 0.0]);
let d: Vec<f64> = (0..5).map(|ix| displacement(ix, 3, 1, 40.0)).collect();
assert_eq!(d, vec![0.0, 40.0, 40.0, -80.0, 0.0]);
assert!((0..5).all(|ix| displacement(ix, 2, 2, 40.0) == 0.0));
let sum: f64 = (0..5).map(|ix| displacement(ix, 1, 3, 40.0)).sum();
assert_eq!(sum, 0.0);
}
}