1use std::collections::{HashMap, HashSet};
9use std::rc::Rc;
10
11use dioxus::html::MountedData;
12use dioxus::prelude::*;
13
14use crate::a11y::use_reduced_motion_css;
15use crate::core::components::merge_style_user_last;
16use crate::core::hooks::use_rect_refresh_thunk;
17use crate::core::{platform, transition, GestureEffect, GestureEvent, GesturePhase, Point, Rect};
18use crate::sortable::{
19 build_render_keys, current_rects, list_bounds, measure_rect, mounted_at, refresh_rects,
20 RenderKey, ReorderMode, SortEvent,
21};
22
23fn pointer_client(evt: &PointerEvent) -> Point {
24 let c = evt.client_coordinates();
25 Point::new(c.x, c.y)
26}
27
28pub fn cell_of(index: usize, cols: usize) -> (usize, usize) {
30 let cols = cols.max(1);
31 (index / cols, index % cols)
32}
33
34pub fn index_of(row: usize, col: usize, cols: usize, len: usize) -> Option<usize> {
37 let cols = cols.max(1);
38 if col >= cols {
39 return None;
40 }
41 let ix = row * cols + col;
42 (ix < len).then_some(ix)
43}
44
45#[component]
59pub fn SortableGrid(
60 len: usize,
62 cols: usize,
64 render: Callback<usize, Element>,
66 on_sort: EventHandler<SortEvent>,
68 #[props(default)]
70 mode: ReorderMode,
71 #[props(default)]
74 item_class: Option<String>,
75 #[props(default)]
78 item_key: Option<Callback<usize, String>>,
79 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
80) -> Element {
81 let render_keys = build_render_keys(len, item_key);
82 let initial_render_keys = render_keys.clone();
83 let mut index_keys = use_signal(move || initial_render_keys);
84 let mode_str = match mode {
88 ReorderMode::Insert => "insert",
89 ReorderMode::Swap => "swap",
90 };
91 let mut drag_from = use_signal(|| None::<usize>);
92 let mut over = use_signal(|| None::<usize>);
93 let mut press_from = use_signal(|| None::<usize>);
94 let mut attributes = attributes;
95 crate::core::components::protect_attributes(
96 &mut attributes,
97 &[
98 "data-mode",
99 "onpointermove",
100 "onpointerup",
101 "onpointercancel",
102 "onlostpointercapture",
103 ],
104 );
105 let style = merge_style_user_last(
106 &mut attributes,
107 &format!("display: grid; grid-template-columns: repeat({cols}, 1fr);"),
108 );
109
110 let rects = use_signal(HashMap::<RenderKey, Rect>::new);
113 let mounteds = use_signal(HashMap::<RenderKey, Rc<MountedData>>::new);
114 let generations = use_signal(HashMap::<RenderKey, u64>::new);
115 let mut rects_for_keys = rects;
116 let mut mounteds_for_keys = mounteds;
117 let mut generations_for_keys = generations;
118 use_effect(use_reactive!(|(render_keys)| {
119 let active: HashSet<_> = render_keys.iter().cloned().collect();
120 index_keys.set(render_keys);
121 rects_for_keys.write().retain(|key, _| active.contains(key));
122 mounteds_for_keys
123 .write()
124 .retain(|key, _| active.contains(key));
125 generations_for_keys
126 .write()
127 .retain(|key, _| active.contains(key));
128 }));
129 use_rect_refresh_thunk(move |_| {
133 if drag_from.peek().is_some() {
134 refresh_rects(mounteds, rects, generations);
135 }
136 });
137 let mut gesture = use_signal(|| GesturePhase::Idle);
138 let mut step = move |event: GestureEvent| -> GestureEffect {
139 let (next, fx) = transition(*gesture.peek(), event, 8.0);
140 gesture.set(next);
141 fx
142 };
143 let mut feed = move |event: GestureEvent, fallback_ix: Option<usize>| match step(event) {
144 GestureEffect::Begin { at, .. } => {
145 let Some(ix) = *press_from.peek() else {
146 return;
147 };
148 drag_from.set(Some(ix));
149 let next = current_rects(index_keys, rects)
150 .iter()
151 .find(|(_, r)| r.contains(at))
152 .map(|(&i, _)| i)
153 .or(fallback_ix)
154 .filter(|&i| i != ix);
155 over.set(next);
156 refresh_rects(mounteds, rects, generations);
157 }
158 GestureEffect::Track { at } => {
159 let next = current_rects(index_keys, rects)
160 .iter()
161 .find(|(_, r)| r.contains(at))
162 .map(|(&i, _)| i)
163 .or(fallback_ix)
164 .filter(|&i| Some(i) != *drag_from.peek())
165 .or(*over.peek());
166 if next != *over.peek() {
167 over.set(next);
168 }
169 }
170 GestureEffect::Drop { at } => {
171 let inside = list_bounds(¤t_rects(index_keys, rects))
175 .map(|b| b.contains(at))
176 .unwrap_or(false);
177 let pair = (*drag_from.peek(), *over.peek());
178 press_from.set(None);
182 drag_from.set(None);
183 over.set(None);
184 if inside {
185 if let (Some(from), Some(to)) = pair {
186 if from != to && from < len && to < len {
187 on_sort.call(SortEvent { from, to });
188 }
189 }
190 }
191 }
192 GestureEffect::Abort => {
193 press_from.set(None);
194 drag_from.set(None);
195 over.set(None);
196 }
197 GestureEffect::Tap => {
198 press_from.set(None);
199 }
200 GestureEffect::None => {}
201 };
202 let primary_pointer = move |evt: &PointerEvent| crate::core::components::primary_press(evt);
203 let mut empty_held_moves = use_signal(|| 0u8);
205 let mut captured = use_signal(|| false);
208 let reduced_motion_css = use_reduced_motion_css();
212
213 rsx! {
214 {reduced_motion_css}
218 div {
219 style: style,
220 "data-mode": mode_str,
221 onpointermove: move |evt: PointerEvent| {
222 let at = pointer_client(&evt);
223 if drag_from.peek().is_some() && evt.held_buttons().is_empty() {
232 let streak = empty_held_moves.peek().saturating_add(1);
233 empty_held_moves.set(streak);
234 if streak >= crate::core::components::RELEASE_RECOVERY_MOVES {
235 if let Some(from) = *drag_from.peek() {
236 if let Some(n) = mounted_at(from, index_keys, mounteds) {
237 platform::release_pointer(&n, evt.pointer_id());
238 }
239 }
240 feed(GestureEvent::Up { at, pointer_id: evt.pointer_id() }, None);
241 return;
242 }
243 } else if *empty_held_moves.peek() != 0 {
244 empty_held_moves.set(0);
245 }
246 feed(GestureEvent::Move { at, pointer_id: evt.pointer_id() }, None);
247 },
248 onpointerup: move |evt: PointerEvent| {
249 if let Some(from) = *drag_from.peek() {
250 if let Some(n) = mounted_at(from, index_keys, mounteds) {
251 platform::release_pointer(&n, evt.pointer_id());
252 }
253 }
254 feed(
255 GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
256 None,
257 );
258 },
259 onpointercancel: move |evt: PointerEvent| {
260 if let Some(from) = *drag_from.peek() {
261 if let Some(n) = mounted_at(from, index_keys, mounteds) {
262 platform::release_pointer(&n, evt.pointer_id());
263 }
264 }
265 feed(GestureEvent::Cancel, None);
266 },
267 onlostpointercapture: move |_| feed(GestureEvent::Cancel, None),
268 ..attributes,
269 if drag_from().is_some() && !captured() {
273 div {
274 style: "position: fixed; inset: 0; z-index: 9998; touch-action: none;",
275 aria_hidden: true,
276 }
277 }
278 for (ix, render_key) in (0..len).zip(render_keys) {
279 div {
280 key: "{render_key}",
281 class: item_class.clone(),
282 style: "touch-action: none;",
283 "data-dragging": if drag_from() == Some(ix) { "true" },
284 "data-drop-target": if over() == Some(ix) && drag_from() != Some(ix) { "true" },
285 onmounted: move |evt: Event<MountedData>| {
286 let m: Rc<MountedData> = evt.data();
287 let mut mounteds = mounteds;
288 mounteds.write().insert(render_key.clone(), m.clone());
289 measure_rect(render_key.clone(), m, mounteds, rects, generations);
290 },
291 oncontextmenu: move |evt: Event<MouseData>| {
292 if !matches!(*gesture.peek(), GesturePhase::Idle) {
295 evt.prevent_default();
296 }
297 },
298 onpointerdown: move |evt: PointerEvent| {
299 if !primary_pointer(&evt) { return; }
300 evt.prevent_default();
305 evt.stop_propagation();
306 press_from.set(Some(ix));
307 captured.set(match mounted_at(ix, index_keys, mounteds) {
312 Some(n) => platform::capture_pointer(&n, evt.pointer_id()),
313 None => false,
314 });
315 feed(
316 GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
317 None,
318 );
319 },
320 onpointermove: move |evt: PointerEvent| {
321 feed(
322 GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
323 Some(ix),
324 );
325 },
326 {render.call(ix)}
327 }
328 }
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn grid_coordinates_round_trip() {
339 assert_eq!(cell_of(0, 4), (0, 0));
340 assert_eq!(cell_of(5, 4), (1, 1));
341 assert_eq!(index_of(1, 1, 4, 12), Some(5));
342 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)); }
346}