dioxus_dnd/core/model.rs
1//! Small model helpers for applying completed drops to app-owned state.
2//!
3//! The crate never touches your data: drops arrive as [`DropOutcome`]s and
4//! you decide what they mean. These helpers cover the most common meaning,
5//! the remove-from-source, append-to-target dance, without imposing bounds
6//! on your item type: no `Clone` (the payload arrives owned), no
7//! `PartialEq` (matching is by the key you extract).
8
9use std::collections::HashMap;
10
11use super::{DropEffect, DropOutcome, ZoneId};
12
13/// Apply a drop to a `HashMap<ZoneId, Vec<T>>` model.
14///
15/// `Move` removes the matching item from `outcome.from` before appending it
16/// to `outcome.to`. `Copy` leaves the source alone and passes the payload
17/// through `clone_item` first, which is where you should assign a fresh id.
18///
19/// Semantics worth knowing:
20///
21/// - Removal matches **every** item in the source whose key equals the
22/// payload's key. Keys are expected to be unique within a zone; if they
23/// are not, a single `Move` prunes all of them.
24/// - A `Move` where `from == Some(to)` removes and re-appends, so dropping
25/// an item back onto its own zone sends it to the **end of that list**.
26/// - A `Move` with `from: None` (payload from outside any zone, e.g. a
27/// palette) skips removal and just appends.
28/// - An unknown `to` zone is created on the fly rather than dropping the
29/// item on the floor.
30pub fn apply_clone_or_move<T, K>(
31 zones: &mut HashMap<ZoneId, Vec<T>>,
32 outcome: DropOutcome<T>,
33 key: impl Fn(&T) -> K,
34 mut clone_item: impl FnMut(T) -> T,
35) where
36 K: PartialEq,
37{
38 let DropOutcome {
39 payload,
40 from,
41 to,
42 effect,
43 ..
44 } = outcome;
45 let item = if effect == DropEffect::Copy {
46 clone_item(payload)
47 } else {
48 if let Some(from) = from {
49 let payload_key = key(&payload);
50 if let Some(source) = zones.get_mut(&from) {
51 source.retain(|item| key(item) != payload_key);
52 }
53 }
54 payload
55 };
56
57 zones.entry(to).or_default().push(item);
58}
59
60/// Apply a drop between two plain `Vec<T>` lists.
61///
62/// `Move` removes the matching item from `source` before appending it to
63/// `target`. `Copy` leaves the source alone and passes the payload through
64/// `clone_item` first, which is where you should assign a fresh id.
65///
66/// You choose which lists to pass, so the outcome's `from` and `to` fields
67/// are **ignored** here; only `payload` and `effect` are consulted. Pass
68/// `None` for `source` when the payload came from outside any list. As with
69/// [`apply_clone_or_move`], removal matches every item whose key equals the
70/// payload's key.
71pub fn apply_list_clone_or_move<T, K>(
72 source: Option<&mut Vec<T>>,
73 target: &mut Vec<T>,
74 outcome: DropOutcome<T>,
75 key: impl Fn(&T) -> K,
76 mut clone_item: impl FnMut(T) -> T,
77) where
78 K: PartialEq,
79{
80 let DropOutcome {
81 payload, effect, ..
82 } = outcome;
83 let item = if effect == DropEffect::Copy {
84 clone_item(payload)
85 } else {
86 if let Some(source) = source {
87 let payload_key = key(&payload);
88 source.retain(|item| key(item) != payload_key);
89 }
90 payload
91 };
92
93 target.push(item);
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::core::{DragMode, Point};
100
101 #[derive(Debug, Clone, PartialEq)]
102 struct Card {
103 id: u32,
104 title: &'static str,
105 }
106
107 fn outcome(
108 payload: Card,
109 from: Option<ZoneId>,
110 to: ZoneId,
111 effect: DropEffect,
112 ) -> DropOutcome<Card> {
113 DropOutcome {
114 payload,
115 from,
116 to,
117 effect,
118 mode: DragMode::Pointer,
119 client: Point::default(),
120 element: Point::default(),
121 grab: Point::default(),
122 edge: None,
123 }
124 }
125
126 #[test]
127 fn move_removes_from_source_and_appends_to_target() {
128 let a = ZoneId(1);
129 let b = ZoneId(2);
130 let mut zones = HashMap::from([
131 (
132 a,
133 vec![
134 Card {
135 id: 1,
136 title: "one",
137 },
138 Card {
139 id: 2,
140 title: "two",
141 },
142 ],
143 ),
144 (
145 b,
146 vec![Card {
147 id: 3,
148 title: "three",
149 }],
150 ),
151 ]);
152
153 apply_clone_or_move(
154 &mut zones,
155 outcome(
156 Card {
157 id: 2,
158 title: "two",
159 },
160 Some(a),
161 b,
162 DropEffect::Move,
163 ),
164 |card| card.id,
165 |card| card,
166 );
167
168 assert_eq!(
169 zones[&a],
170 vec![Card {
171 id: 1,
172 title: "one"
173 }]
174 );
175 assert_eq!(
176 zones[&b],
177 vec![
178 Card {
179 id: 3,
180 title: "three"
181 },
182 Card {
183 id: 2,
184 title: "two"
185 }
186 ]
187 );
188 }
189
190 #[test]
191 fn copy_leaves_source_and_allows_new_identity() {
192 let a = ZoneId(1);
193 let b = ZoneId(2);
194 let mut zones = HashMap::from([
195 (
196 a,
197 vec![Card {
198 id: 1,
199 title: "one",
200 }],
201 ),
202 (b, Vec::new()),
203 ]);
204
205 apply_clone_or_move(
206 &mut zones,
207 outcome(
208 Card {
209 id: 1,
210 title: "one",
211 },
212 Some(a),
213 b,
214 DropEffect::Copy,
215 ),
216 |card| card.id,
217 |mut card| {
218 card.id = 10;
219 card
220 },
221 );
222
223 assert_eq!(
224 zones[&a],
225 vec![Card {
226 id: 1,
227 title: "one"
228 }]
229 );
230 assert_eq!(
231 zones[&b],
232 vec![Card {
233 id: 10,
234 title: "one"
235 }]
236 );
237 }
238
239 /// Pins the self-drop semantics documented on `apply_clone_or_move`: a
240 /// `Move` back onto the source zone reorders the item to the end.
241 #[test]
242 fn move_onto_own_zone_reorders_to_end() {
243 let a = ZoneId(1);
244 let mut zones = HashMap::from([(
245 a,
246 vec![
247 Card {
248 id: 1,
249 title: "one",
250 },
251 Card {
252 id: 2,
253 title: "two",
254 },
255 ],
256 )]);
257
258 apply_clone_or_move(
259 &mut zones,
260 outcome(
261 Card {
262 id: 1,
263 title: "one",
264 },
265 Some(a),
266 a,
267 DropEffect::Move,
268 ),
269 |card| card.id,
270 |card| card,
271 );
272
273 assert_eq!(
274 zones[&a],
275 vec![
276 Card {
277 id: 2,
278 title: "two"
279 },
280 Card {
281 id: 1,
282 title: "one"
283 }
284 ]
285 );
286 }
287
288 /// A payload from outside any zone (palette, external drop) has no
289 /// source to prune; `Move` just appends.
290 #[test]
291 fn move_without_source_zone_just_appends() {
292 let b = ZoneId(2);
293 let mut zones = HashMap::from([(b, Vec::new())]);
294
295 apply_clone_or_move(
296 &mut zones,
297 outcome(
298 Card {
299 id: 7,
300 title: "seven",
301 },
302 None,
303 b,
304 DropEffect::Move,
305 ),
306 |card| card.id,
307 |card| card,
308 );
309
310 assert_eq!(
311 zones[&b],
312 vec![Card {
313 id: 7,
314 title: "seven"
315 }]
316 );
317 }
318
319 /// An unknown target zone is created rather than losing the item.
320 #[test]
321 fn unknown_target_zone_is_created() {
322 let a = ZoneId(1);
323 let ghost = ZoneId(99);
324 let mut zones = HashMap::from([(
325 a,
326 vec![Card {
327 id: 1,
328 title: "one",
329 }],
330 )]);
331
332 apply_clone_or_move(
333 &mut zones,
334 outcome(
335 Card {
336 id: 1,
337 title: "one",
338 },
339 Some(a),
340 ghost,
341 DropEffect::Move,
342 ),
343 |card| card.id,
344 |card| card,
345 );
346
347 assert!(zones[&a].is_empty());
348 assert_eq!(
349 zones[&ghost],
350 vec![Card {
351 id: 1,
352 title: "one"
353 }]
354 );
355 }
356
357 #[test]
358 fn list_move_removes_from_source_and_appends_to_target() {
359 let mut source = vec![
360 Card {
361 id: 1,
362 title: "one",
363 },
364 Card {
365 id: 2,
366 title: "two",
367 },
368 ];
369 let mut target = vec![Card {
370 id: 3,
371 title: "three",
372 }];
373
374 apply_list_clone_or_move(
375 Some(&mut source),
376 &mut target,
377 outcome(
378 Card {
379 id: 2,
380 title: "two",
381 },
382 Some(ZoneId(1)),
383 ZoneId(2),
384 DropEffect::Move,
385 ),
386 |card| card.id,
387 |card| card,
388 );
389
390 assert_eq!(
391 source,
392 vec![Card {
393 id: 1,
394 title: "one"
395 }]
396 );
397 assert_eq!(
398 target,
399 vec![
400 Card {
401 id: 3,
402 title: "three"
403 },
404 Card {
405 id: 2,
406 title: "two"
407 }
408 ]
409 );
410 }
411
412 #[test]
413 fn list_copy_leaves_source_and_allows_new_identity() {
414 let mut source = vec![Card {
415 id: 1,
416 title: "one",
417 }];
418 let mut target = Vec::new();
419
420 apply_list_clone_or_move(
421 Some(&mut source),
422 &mut target,
423 outcome(
424 Card {
425 id: 1,
426 title: "one",
427 },
428 Some(ZoneId(1)),
429 ZoneId(2),
430 DropEffect::Copy,
431 ),
432 |card| card.id,
433 |mut card| {
434 card.id = 10;
435 card
436 },
437 );
438
439 assert_eq!(
440 source,
441 vec![Card {
442 id: 1,
443 title: "one"
444 }]
445 );
446 assert_eq!(
447 target,
448 vec![Card {
449 id: 10,
450 title: "one"
451 }]
452 );
453 }
454
455 /// `Move` into a list without a source (`None`) skips removal.
456 #[test]
457 fn list_move_without_source_just_appends() {
458 let mut target = Vec::new();
459
460 apply_list_clone_or_move(
461 None,
462 &mut target,
463 outcome(
464 Card {
465 id: 7,
466 title: "seven",
467 },
468 None,
469 ZoneId(2),
470 DropEffect::Move,
471 ),
472 |card| card.id,
473 |card| card,
474 );
475
476 assert_eq!(
477 target,
478 vec![Card {
479 id: 7,
480 title: "seven"
481 }]
482 );
483 }
484}