Skip to main content

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}