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        }
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}