use std::collections::HashMap;
use std::rc::Rc;
use dioxus::html::MountedData;
use dioxus::prelude::*;
use crate::core::components::merge_style;
use crate::core::{platform, transition, GestureEffect, GestureEvent, GesturePhase, Point, Rect};
use crate::sortable::{list_bounds, 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);
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);
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 } => {
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| evt.is_primary();
rsx! {
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() {
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;
}
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,
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),
);
}
});
},
onpointerdown: move |evt: PointerEvent| {
if !primary_pointer(&evt) { return; }
evt.stop_propagation();
press_from.set(Some(ix));
if let Some(n) = mounteds.peek().get(&ix).cloned() {
platform::capture_pointer(&n, evt.pointer_id());
}
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)); }
}