use std::collections::HashMap;
use std::rc::Rc;
use dioxus::html::MountedData;
use dioxus::prelude::*;
use crate::a11y::use_reduced_motion_css;
use crate::core::components::merge_style;
use crate::core::hooks::use_rect_refresh_thunk;
use crate::core::{platform, transition, GestureEffect, GestureEvent, GesturePhase, Point, Rect};
use crate::sortable::{list_bounds, refresh_rects, 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(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
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;
let style = merge_style(
&mut attributes,
&format!("display: grid; grid-template-columns: repeat({cols}, 1fr);"),
);
let rects = use_signal(HashMap::<usize, Rect>::new);
let mounteds = use_signal(HashMap::<usize, Rc<MountedData>>::new);
use_rect_refresh_thunk(move |_| {
if drag_from.peek().is_some() {
refresh_rects(mounteds, rects);
}
});
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 = rects
.peek()
.iter()
.find(|(_, r)| r.contains(at))
.map(|(&i, _)| i)
.or(fallback_ix)
.filter(|&i| i != ix);
over.set(next);
refresh_rects(mounteds, rects);
}
GestureEffect::Track { at } => {
let next = rects
.peek()
.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(&rects.peek())
.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 {
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) = mounteds.peek().get(&from).cloned() {
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) = mounteds.peek().get(&from).cloned() {
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) = mounteds.peek().get(&from).cloned() {
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 in 0..len {
div {
key: "{ix}",
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;
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),
);
}
});
},
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 mounteds.peek().get(&ix).cloned() {
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)); }
}