use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use dioxus::html::MountedData;
use dioxus::prelude::*;
use crate::a11y::use_reduced_motion_css;
use crate::core::components::merge_style_user_last;
use crate::core::hooks::use_rect_refresh_thunk;
use crate::core::{platform, transition, GestureEffect, GestureEvent, GesturePhase, Point, Rect};
use crate::sortable::{
build_render_keys, current_rects, list_bounds, measure_rect, mounted_at, refresh_rects,
RenderKey, ReorderMode, SortEvent,
};
fn pointer_client(evt: &PointerEvent) -> Point {
let c = evt.client_coordinates();
Point::new(c.x, c.y)
}
pub fn cell_of(index: usize, cols: usize) -> (usize, usize) {
let cols = cols.max(1);
(index / cols, index % cols)
}
pub fn index_of(row: usize, col: usize, cols: usize, len: usize) -> Option<usize> {
let cols = cols.max(1);
if col >= cols {
return None;
}
let ix = row * cols + col;
(ix < len).then_some(ix)
}
#[component]
pub fn SortableGrid(
len: usize,
cols: usize,
render: Callback<usize, Element>,
on_sort: EventHandler<SortEvent>,
#[props(default)]
mode: ReorderMode,
#[props(default)]
item_class: Option<String>,
#[props(default)]
item_key: Option<Callback<usize, String>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let render_keys = build_render_keys(len, item_key);
let initial_render_keys = render_keys.clone();
let mut index_keys = use_signal(move || initial_render_keys);
let mode_str = match mode {
ReorderMode::Insert => "insert",
ReorderMode::Swap => "swap",
};
let mut drag_from = use_signal(|| None::<usize>);
let mut over = use_signal(|| None::<usize>);
let mut press_from = use_signal(|| None::<usize>);
let mut attributes = attributes;
crate::core::components::protect_attributes(
&mut attributes,
&[
"data-mode",
"onpointermove",
"onpointerup",
"onpointercancel",
"onlostpointercapture",
],
);
let style = merge_style_user_last(
&mut attributes,
&format!("display: grid; grid-template-columns: repeat({cols}, 1fr);"),
);
let rects = use_signal(HashMap::<RenderKey, Rect>::new);
let mounteds = use_signal(HashMap::<RenderKey, Rc<MountedData>>::new);
let generations = use_signal(HashMap::<RenderKey, u64>::new);
let mut rects_for_keys = rects;
let mut mounteds_for_keys = mounteds;
let mut generations_for_keys = generations;
use_effect(use_reactive!(|(render_keys)| {
let active: HashSet<_> = render_keys.iter().cloned().collect();
index_keys.set(render_keys);
rects_for_keys.write().retain(|key, _| active.contains(key));
mounteds_for_keys
.write()
.retain(|key, _| active.contains(key));
generations_for_keys
.write()
.retain(|key, _| active.contains(key));
}));
use_rect_refresh_thunk(move |_| {
if drag_from.peek().is_some() {
refresh_rects(mounteds, rects, generations);
}
});
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 |event: GestureEvent, fallback_ix: Option<usize>| match step(event) {
GestureEffect::Begin { at, .. } => {
let Some(ix) = *press_from.peek() else {
return;
};
drag_from.set(Some(ix));
let next = current_rects(index_keys, rects)
.iter()
.find(|(_, r)| r.contains(at))
.map(|(&i, _)| i)
.or(fallback_ix)
.filter(|&i| i != ix);
over.set(next);
refresh_rects(mounteds, rects, generations);
}
GestureEffect::Track { at } => {
let next = current_rects(index_keys, rects)
.iter()
.find(|(_, r)| r.contains(at))
.map(|(&i, _)| i)
.or(fallback_ix)
.filter(|&i| Some(i) != *drag_from.peek())
.or(*over.peek());
if next != *over.peek() {
over.set(next);
}
}
GestureEffect::Drop { at } => {
let inside = list_bounds(¤t_rects(index_keys, rects))
.map(|b| b.contains(at))
.unwrap_or(false);
let pair = (*drag_from.peek(), *over.peek());
press_from.set(None);
drag_from.set(None);
over.set(None);
if inside {
if let (Some(from), Some(to)) = pair {
if from != to && from < len && to < len {
on_sort.call(SortEvent { from, to });
}
}
}
}
GestureEffect::Abort => {
press_from.set(None);
drag_from.set(None);
over.set(None);
}
GestureEffect::Tap => {
press_from.set(None);
}
GestureEffect::None => {}
};
let primary_pointer = move |evt: &PointerEvent| crate::core::components::primary_press(evt);
let mut empty_held_moves = use_signal(|| 0u8);
let mut captured = use_signal(|| false);
let reduced_motion_css = use_reduced_motion_css();
rsx! {
{reduced_motion_css}
div {
style: style,
"data-mode": mode_str,
onpointermove: move |evt: PointerEvent| {
let at = pointer_client(&evt);
if drag_from.peek().is_some() && evt.held_buttons().is_empty() {
let streak = empty_held_moves.peek().saturating_add(1);
empty_held_moves.set(streak);
if streak >= crate::core::components::RELEASE_RECOVERY_MOVES {
if let Some(from) = *drag_from.peek() {
if let Some(n) = mounted_at(from, index_keys, mounteds) {
platform::release_pointer(&n, evt.pointer_id());
}
}
feed(GestureEvent::Up { at, pointer_id: evt.pointer_id() }, None);
return;
}
} else if *empty_held_moves.peek() != 0 {
empty_held_moves.set(0);
}
feed(GestureEvent::Move { at, pointer_id: evt.pointer_id() }, None);
},
onpointerup: move |evt: PointerEvent| {
if let Some(from) = *drag_from.peek() {
if let Some(n) = mounted_at(from, index_keys, mounteds) {
platform::release_pointer(&n, evt.pointer_id());
}
}
feed(
GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
None,
);
},
onpointercancel: move |evt: PointerEvent| {
if let Some(from) = *drag_from.peek() {
if let Some(n) = mounted_at(from, index_keys, mounteds) {
platform::release_pointer(&n, evt.pointer_id());
}
}
feed(GestureEvent::Cancel, None);
},
onlostpointercapture: move |_| feed(GestureEvent::Cancel, None),
..attributes,
if drag_from().is_some() && !captured() {
div {
style: "position: fixed; inset: 0; z-index: 9998; touch-action: none;",
aria_hidden: true,
}
}
for (ix, render_key) in (0..len).zip(render_keys) {
div {
key: "{render_key}",
class: item_class.clone(),
style: "touch-action: none;",
"data-dragging": if drag_from() == Some(ix) { "true" },
"data-drop-target": if over() == Some(ix) && drag_from() != Some(ix) { "true" },
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
let mut mounteds = mounteds;
mounteds.write().insert(render_key.clone(), m.clone());
measure_rect(render_key.clone(), m, mounteds, rects, generations);
},
oncontextmenu: move |evt: Event<MouseData>| {
if !matches!(*gesture.peek(), GesturePhase::Idle) {
evt.prevent_default();
}
},
onpointerdown: move |evt: PointerEvent| {
if !primary_pointer(&evt) { return; }
evt.prevent_default();
evt.stop_propagation();
press_from.set(Some(ix));
captured.set(match mounted_at(ix, index_keys, mounteds) {
Some(n) => platform::capture_pointer(&n, evt.pointer_id()),
None => false,
});
feed(
GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
None,
);
},
onpointermove: move |evt: PointerEvent| {
feed(
GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
Some(ix),
);
},
{render.call(ix)}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grid_coordinates_round_trip() {
assert_eq!(cell_of(0, 4), (0, 0));
assert_eq!(cell_of(5, 4), (1, 1));
assert_eq!(index_of(1, 1, 4, 12), Some(5));
assert_eq!(index_of(0, 4, 4, 12), None); assert_eq!(index_of(3, 0, 4, 12), None); assert_eq!(cell_of(7, 0), (7, 0)); }
}