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