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 }
123 }
124
125 #[test]
126 fn move_removes_from_source_and_appends_to_target() {
127 let a = ZoneId(1);
128 let b = ZoneId(2);
129 let mut zones = HashMap::from([
130 (
131 a,
132 vec![
133 Card {
134 id: 1,
135 title: "one",
136 },
137 Card {
138 id: 2,
139 title: "two",
140 },
141 ],
142 ),
143 (
144 b,
145 vec![Card {
146 id: 3,
147 title: "three",
148 }],
149 ),
150 ]);
151
152 apply_clone_or_move(
153 &mut zones,
154 outcome(
155 Card {
156 id: 2,
157 title: "two",
158 },
159 Some(a),
160 b,
161 DropEffect::Move,
162 ),
163 |card| card.id,
164 |card| card,
165 );
166
167 assert_eq!(
168 zones[&a],
169 vec![Card {
170 id: 1,
171 title: "one"
172 }]
173 );
174 assert_eq!(
175 zones[&b],
176 vec![
177 Card {
178 id: 3,
179 title: "three"
180 },
181 Card {
182 id: 2,
183 title: "two"
184 }
185 ]
186 );
187 }
188
189 #[test]
190 fn copy_leaves_source_and_allows_new_identity() {
191 let a = ZoneId(1);
192 let b = ZoneId(2);
193 let mut zones = HashMap::from([
194 (
195 a,
196 vec![Card {
197 id: 1,
198 title: "one",
199 }],
200 ),
201 (b, Vec::new()),
202 ]);
203
204 apply_clone_or_move(
205 &mut zones,
206 outcome(
207 Card {
208 id: 1,
209 title: "one",
210 },
211 Some(a),
212 b,
213 DropEffect::Copy,
214 ),
215 |card| card.id,
216 |mut card| {
217 card.id = 10;
218 card
219 },
220 );
221
222 assert_eq!(
223 zones[&a],
224 vec![Card {
225 id: 1,
226 title: "one"
227 }]
228 );
229 assert_eq!(
230 zones[&b],
231 vec![Card {
232 id: 10,
233 title: "one"
234 }]
235 );
236 }
237
238 /// Pins the self-drop semantics documented on `apply_clone_or_move`: a
239 /// `Move` back onto the source zone reorders the item to the end.
240 #[test]
241 fn move_onto_own_zone_reorders_to_end() {
242 let a = ZoneId(1);
243 let mut zones = HashMap::from([(
244 a,
245 vec![
246 Card {
247 id: 1,
248 title: "one",
249 },
250 Card {
251 id: 2,
252 title: "two",
253 },
254 ],
255 )]);
256
257 apply_clone_or_move(
258 &mut zones,
259 outcome(
260 Card {
261 id: 1,
262 title: "one",
263 },
264 Some(a),
265 a,
266 DropEffect::Move,
267 ),
268 |card| card.id,
269 |card| card,
270 );
271
272 assert_eq!(
273 zones[&a],
274 vec![
275 Card {
276 id: 2,
277 title: "two"
278 },
279 Card {
280 id: 1,
281 title: "one"
282 }
283 ]
284 );
285 }
286
287 /// A payload from outside any zone (palette, external drop) has no
288 /// source to prune; `Move` just appends.
289 #[test]
290 fn move_without_source_zone_just_appends() {
291 let b = ZoneId(2);
292 let mut zones = HashMap::from([(b, Vec::new())]);
293
294 apply_clone_or_move(
295 &mut zones,
296 outcome(
297 Card {
298 id: 7,
299 title: "seven",
300 },
301 None,
302 b,
303 DropEffect::Move,
304 ),
305 |card| card.id,
306 |card| card,
307 );
308
309 assert_eq!(
310 zones[&b],
311 vec![Card {
312 id: 7,
313 title: "seven"
314 }]
315 );
316 }
317
318 /// An unknown target zone is created rather than losing the item.
319 #[test]
320 fn unknown_target_zone_is_created() {
321 let a = ZoneId(1);
322 let ghost = ZoneId(99);
323 let mut zones = HashMap::from([(
324 a,
325 vec![Card {
326 id: 1,
327 title: "one",
328 }],
329 )]);
330
331 apply_clone_or_move(
332 &mut zones,
333 outcome(
334 Card {
335 id: 1,
336 title: "one",
337 },
338 Some(a),
339 ghost,
340 DropEffect::Move,
341 ),
342 |card| card.id,
343 |card| card,
344 );
345
346 assert!(zones[&a].is_empty());
347 assert_eq!(
348 zones[&ghost],
349 vec![Card {
350 id: 1,
351 title: "one"
352 }]
353 );
354 }
355
356 #[test]
357 fn list_move_removes_from_source_and_appends_to_target() {
358 let mut source = vec![
359 Card {
360 id: 1,
361 title: "one",
362 },
363 Card {
364 id: 2,
365 title: "two",
366 },
367 ];
368 let mut target = vec![Card {
369 id: 3,
370 title: "three",
371 }];
372
373 apply_list_clone_or_move(
374 Some(&mut source),
375 &mut target,
376 outcome(
377 Card {
378 id: 2,
379 title: "two",
380 },
381 Some(ZoneId(1)),
382 ZoneId(2),
383 DropEffect::Move,
384 ),
385 |card| card.id,
386 |card| card,
387 );
388
389 assert_eq!(
390 source,
391 vec![Card {
392 id: 1,
393 title: "one"
394 }]
395 );
396 assert_eq!(
397 target,
398 vec![
399 Card {
400 id: 3,
401 title: "three"
402 },
403 Card {
404 id: 2,
405 title: "two"
406 }
407 ]
408 );
409 }
410
411 #[test]
412 fn list_copy_leaves_source_and_allows_new_identity() {
413 let mut source = vec![Card {
414 id: 1,
415 title: "one",
416 }];
417 let mut target = Vec::new();
418
419 apply_list_clone_or_move(
420 Some(&mut source),
421 &mut target,
422 outcome(
423 Card {
424 id: 1,
425 title: "one",
426 },
427 Some(ZoneId(1)),
428 ZoneId(2),
429 DropEffect::Copy,
430 ),
431 |card| card.id,
432 |mut card| {
433 card.id = 10;
434 card
435 },
436 );
437
438 assert_eq!(
439 source,
440 vec![Card {
441 id: 1,
442 title: "one"
443 }]
444 );
445 assert_eq!(
446 target,
447 vec![Card {
448 id: 10,
449 title: "one"
450 }]
451 );
452 }
453
454 /// `Move` into a list without a source (`None`) skips removal.
455 #[test]
456 fn list_move_without_source_just_appends() {
457 let mut target = Vec::new();
458
459 apply_list_clone_or_move(
460 None,
461 &mut target,
462 outcome(
463 Card {
464 id: 7,
465 title: "seven",
466 },
467 None,
468 ZoneId(2),
469 DropEffect::Move,
470 ),
471 |card| card.id,
472 |card| card,
473 );
474
475 assert_eq!(
476 target,
477 vec![Card {
478 id: 7,
479 title: "seven"
480 }]
481 );
482 }
483}