1use std::collections::HashMap;
33use std::rc::Rc;
34
35use dioxus::html::MountedData;
36use dioxus::prelude::*;
37
38use crate::core::{transition, GestureEffect, GestureEvent, GesturePhase, Rect};
39use crate::pointer::pointer_client;
40use crate::sortable::{ReorderMode, SortEvent};
41
42pub fn cell_of(index: usize, cols: usize) -> (usize, usize) {
44 let cols = cols.max(1);
45 (index / cols, index % cols)
46}
47
48pub fn index_of(row: usize, col: usize, cols: usize, len: usize) -> Option<usize> {
51 let cols = cols.max(1);
52 if col >= cols {
53 return None;
54 }
55 let ix = row * cols + col;
56 (ix < len).then_some(ix)
57}
58
59#[component]
67pub fn SortableGrid(
68 len: usize,
70 cols: usize,
72 render: Callback<usize, Element>,
74 on_sort: EventHandler<SortEvent>,
76 #[props(default)]
78 mode: ReorderMode,
79 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
80) -> Element {
81 let mode_str = match mode {
85 ReorderMode::Insert => "insert",
86 ReorderMode::Swap => "swap",
87 };
88 let mut drag_from = use_signal(|| None::<usize>);
89 let mut over = use_signal(|| None::<usize>);
90
91 let rects = use_signal(HashMap::<usize, Rect>::new);
94 let mounteds = use_signal(HashMap::<usize, Rc<MountedData>>::new);
95 let mut gesture = use_signal(|| GesturePhase::Idle);
96 let mut step = move |event: GestureEvent| -> GestureEffect {
97 let (next, fx) = transition(*gesture.peek(), event, 8.0);
98 gesture.set(next);
99 fx
100 };
101 let mut feed = move |ix: usize, event: GestureEvent| match step(event) {
102 GestureEffect::Begin { .. } => {
103 drag_from.set(Some(ix));
104 over.set(None);
105 for (i, m) in mounteds.peek().clone() {
106 let mut rects = rects;
107 spawn(async move {
108 if let Ok(r) = m.get_client_rect().await {
109 rects.write().insert(
110 i,
111 Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
112 );
113 }
114 });
115 }
116 }
117 GestureEffect::Track { at } => {
118 let next = rects
119 .peek()
120 .iter()
121 .find(|(_, r)| r.contains(at))
122 .map(|(&i, _)| i)
123 .filter(|&i| Some(i) != *drag_from.peek())
124 .or(*over.peek());
125 if next != *over.peek() {
126 over.set(next);
127 }
128 }
129 GestureEffect::Drop { .. } => {
130 if let (Some(from), Some(to)) = (*drag_from.peek(), *over.peek()) {
131 if from != to {
132 on_sort.call(SortEvent { from, to });
133 }
134 }
135 drag_from.set(None);
136 over.set(None);
137 }
138 GestureEffect::Abort => {
139 drag_from.set(None);
140 over.set(None);
141 }
142 GestureEffect::Tap | GestureEffect::None => {}
143 };
144
145 rsx! {
146 div {
147 style: "display: grid; grid-template-columns: repeat({cols}, 1fr);",
148 "data-mode": mode_str,
149 ..attributes,
150 for ix in 0..len {
151 div {
152 key: "{ix}",
153 draggable: true,
154 style: "touch-action: none;",
155 "data-dragging": drag_from() == Some(ix),
156 "data-drop-target": over() == Some(ix) && drag_from() != Some(ix),
157 onmounted: move |evt: Event<MountedData>| {
158 let m: Rc<MountedData> = evt.data();
159 let mut mounteds = mounteds;
160 let mut rects = rects;
161 mounteds.write().insert(ix, m.clone());
162 spawn(async move {
163 if let Ok(r) = m.get_client_rect().await {
164 rects.write().insert(
165 ix,
166 Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
167 );
168 }
169 });
170 },
171 onpointerdown: move |evt: PointerEvent| {
172 if evt.pointer_type() == "mouse" || !evt.is_primary() { return; }
173 feed(ix, GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
174 },
175 onpointermove: move |evt: PointerEvent| {
176 feed(ix, GestureEvent::Move { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
177 },
178 onpointerup: move |evt: PointerEvent| {
179 feed(ix, GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() });
180 },
181 onpointercancel: move |_| feed(ix, GestureEvent::Cancel),
182 onlostpointercapture: move |_| feed(ix, GestureEvent::Cancel),
183 ondragstart: move |evt: DragEvent| {
184 evt.stop_propagation();
185 let _ = evt.data_transfer().set_data("text/plain", "dioxus-dnd-grid");
186 drag_from.set(Some(ix));
187 },
188 ondragover: move |evt: DragEvent| {
189 if drag_from().is_some() {
190 evt.prevent_default();
191 if over() != Some(ix) {
192 over.set(Some(ix));
193 }
194 }
195 },
196 ondrop: move |evt: DragEvent| {
197 evt.prevent_default();
198 evt.stop_propagation();
199 if let Some(from) = drag_from() {
200 if from != ix {
201 on_sort.call(SortEvent { from, to: ix });
202 }
203 }
204 drag_from.set(None);
205 over.set(None);
206 },
207 ondragend: move |_| {
208 drag_from.set(None);
209 over.set(None);
210 },
211 {render.call(ix)}
212 }
213 }
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn grid_coordinates_round_trip() {
224 assert_eq!(cell_of(0, 4), (0, 0));
225 assert_eq!(cell_of(5, 4), (1, 1));
226 assert_eq!(index_of(1, 1, 4, 12), Some(5));
227 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)); }
231}