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