1use std::collections::HashMap;
28use std::rc::Rc;
29
30use dioxus::html::MountedData;
31use dioxus::prelude::*;
32
33use crate::core::{
34 use_dnd, use_zone_id, use_zone_registry, Draggable, DropOutcome, DropZone, ParentZone, ZoneId,
35 ZoneRecord,
36};
37
38pub type ContainerId = ZoneId;
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct BoardPayload<T> {
44 pub item: T,
45 pub from: ContainerId,
47 pub index: usize,
49}
50
51struct ColumnAccepts<T: Clone + 'static>(Option<Callback<BoardPayload<T>, bool>>);
55
56impl<T: Clone + 'static> Clone for ColumnAccepts<T> {
59 fn clone(&self) -> Self {
60 *self
61 }
62}
63impl<T: Clone + 'static> Copy for ColumnAccepts<T> {}
64
65#[derive(Debug, Clone, PartialEq)]
67pub struct MoveEvent<T> {
68 pub item: T,
69 pub from: (ContainerId, usize),
71 pub to: (ContainerId, Option<usize>),
73}
74
75pub fn apply_move<T>(board: &mut HashMap<ContainerId, Vec<T>>, mv: MoveEvent<T>) {
79 let (from_col, from_ix) = mv.from;
80 let mut removed = false;
81 if let Some(src) = board.get_mut(&from_col) {
82 if from_ix < src.len() {
83 src.remove(from_ix);
84 removed = true;
85 }
86 }
87 let (to_col, to_ix) = mv.to;
88 let adjusted_to_ix = match to_ix {
89 Some(ix) if removed && from_col == to_col && from_ix < ix => Some(ix - 1),
90 other => other,
91 };
92 let dst = board.entry(to_col).or_default();
93 match adjusted_to_ix {
94 Some(ix) if ix <= dst.len() => dst.insert(ix, mv.item),
95 _ => dst.push(mv.item),
96 }
97}
98
99#[component]
102pub fn BoardItem<T: Clone + PartialEq + 'static>(
103 item: T,
104 column: ContainerId,
106 index: usize,
108 #[props(default)]
110 label: Option<String>,
111 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
112 children: Element,
113) -> Element {
114 rsx! {
115 Draggable::<BoardPayload<T>> {
116 payload: BoardPayload { item, from: column, index },
117 zone: column,
118 label,
119 attributes,
120 {children}
121 }
122 }
123}
124
125#[component]
129pub fn BoardColumn<T: Clone + PartialEq + 'static>(
130 id: ContainerId,
131 #[props(default)]
133 label: Option<String>,
134 on_move: EventHandler<MoveEvent<T>>,
135 #[props(default)]
137 accepts: Option<Callback<BoardPayload<T>, bool>>,
138 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
139 children: Element,
140) -> Element {
141 use_context_provider(|| ColumnAccepts(accepts));
144 rsx! {
145 DropZone::<BoardPayload<T>> {
146 id,
147 label,
148 accepts,
149 on_drop: move |outcome: DropOutcome<BoardPayload<T>>| {
150 let p = outcome.payload;
151 on_move.call(MoveEvent {
152 item: p.item,
153 from: (p.from, p.index),
154 to: (id, None),
155 });
156 },
157 attributes,
158 {children}
159 }
160 }
161}
162
163#[component]
171pub fn BoardSlot<T: Clone + PartialEq + 'static>(
172 column: ContainerId,
174 index: usize,
176 #[props(default)]
178 label: Option<String>,
179 on_move: EventHandler<MoveEvent<T>>,
180 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
181 children: Element,
182) -> Element {
183 let dnd = use_dnd::<BoardPayload<T>>();
184 let mut registry = use_zone_registry::<BoardPayload<T>>();
185 let zone_id = use_zone_id();
186 let parent = try_use_context::<ParentZone>().map(|p| p.0);
187 let mounted = use_signal(|| None::<Rc<MountedData>>);
188 let rect = use_signal(|| None);
189 let column_accepts = try_use_context::<ColumnAccepts<T>>().and_then(|c| c.0);
194 let accepts = move |p: BoardPayload<T>| column_accepts.map(|cb| cb.call(p)).unwrap_or(true);
195
196 let mut column_now = use_signal(|| column);
200 let mut index_now = use_signal(|| index);
201 let mut on_move_now = use_signal(|| on_move);
202 if *column_now.peek() != column {
203 column_now.set(column);
204 }
205 if *index_now.peek() != index {
206 index_now.set(index);
207 }
208 if *on_move_now.peek() != on_move {
209 on_move_now.set(on_move);
210 }
211
212 let slot_label = label
213 .clone()
214 .or_else(|| Some(format!("Insert at position {index}")));
215
216 let registered_accepts = Callback::new(move |p: BoardPayload<T>| accepts(p));
217 let registered_drop = Callback::new(move |outcome: DropOutcome<BoardPayload<T>>| {
218 let p = outcome.payload;
219 if !accepts(p.clone()) {
220 return;
221 }
222 on_move_now.peek().call(MoveEvent {
223 item: p.item,
224 from: (p.from, p.index),
225 to: (*column_now.peek(), Some(*index_now.peek())),
226 });
227 });
228 let registered_label = slot_label.clone();
229 use_hook(move || {
230 registry.register(ZoneRecord {
231 id: zone_id,
232 parent,
233 label: registered_label.clone(),
234 on_drop: registered_drop,
235 accepts: Some(registered_accepts),
236 mounted,
237 rect,
238 });
239 });
240 use_drop(move || {
241 registry.unregister(zone_id);
242 });
243 registry.sync_label(zone_id, slot_label);
244
245 let acceptable = move || dnd.payload().map(accepts).unwrap_or(false);
247
248 rsx! {
249 div {
250 "data-active": if acceptable() { "true" },
251 "data-over": if dnd.over() == Some(zone_id) && acceptable() { "true" },
252 onmounted: move |evt: Event<MountedData>| {
253 let mut mounted = mounted;
254 mounted.set(Some(evt.data()));
255 },
256 ..attributes,
257 {children}
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn move_between_columns() {
268 let a = crate::core::ZoneId(1);
269 let b = crate::core::ZoneId(2);
270 let mut board: HashMap<ContainerId, Vec<&str>> = HashMap::new();
271 board.insert(a, vec!["x", "y"]);
272 board.insert(b, vec!["z"]);
273
274 apply_move(
276 &mut board,
277 MoveEvent {
278 item: "y",
279 from: (a, 1),
280 to: (b, Some(0)),
281 },
282 );
283 assert_eq!(board[&a], vec!["x"]);
284 assert_eq!(board[&b], vec!["y", "z"]);
285
286 let c = crate::core::ZoneId(3);
288 apply_move(
289 &mut board,
290 MoveEvent {
291 item: "x",
292 from: (a, 0),
293 to: (c, None),
294 },
295 );
296 assert!(board[&a].is_empty());
297 assert_eq!(board[&c], vec!["x"]);
298 }
299
300 #[test]
301 fn move_within_column_adjusts_forward_insert_after_removal() {
302 let a = crate::core::ZoneId(1);
303 let mut board: HashMap<ContainerId, Vec<&str>> = HashMap::new();
304 board.insert(a, vec!["a", "b", "c", "d"]);
305
306 apply_move(
307 &mut board,
308 MoveEvent {
309 item: "a",
310 from: (a, 0),
311 to: (a, Some(3)),
312 },
313 );
314
315 assert_eq!(board[&a], vec!["b", "c", "a", "d"]);
316 }
317
318 #[test]
319 fn move_within_column_keeps_backward_insert_index() {
320 let a = crate::core::ZoneId(1);
321 let mut board: HashMap<ContainerId, Vec<&str>> = HashMap::new();
322 board.insert(a, vec!["a", "b", "c", "d"]);
323
324 apply_move(
325 &mut board,
326 MoveEvent {
327 item: "d",
328 from: (a, 3),
329 to: (a, Some(1)),
330 },
331 );
332
333 assert_eq!(board[&a], vec!["a", "d", "b", "c"]);
334 }
335
336 #[test]
337 fn move_within_column_appends_after_removal() {
338 let a = crate::core::ZoneId(1);
339 let mut board: HashMap<ContainerId, Vec<&str>> = HashMap::new();
340 board.insert(a, vec!["a", "b", "c"]);
341
342 apply_move(
343 &mut board,
344 MoveEvent {
345 item: "a",
346 from: (a, 0),
347 to: (a, None),
348 },
349 );
350
351 assert_eq!(board[&a], vec!["b", "c", "a"]);
352 }
353}