Skip to main content

dioxus_dnd/core/
model.rs

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