1#![doc = include_str!("../../docs/api/drop-effects.md")]
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::fmt;
6use std::mem::ManuallyDrop;
7use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
8use std::rc::Rc;
9
10use dioxus::prelude::{provide_context, use_hook};
11use dioxus::signals::{AnyStorage, Owner, SyncStorage, UnsyncStorage};
12
13use super::{DropEffect, DropOutcome, ZoneId};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ApplyDropError {
19 UnsupportedEffect(DropEffect),
20}
21
22impl fmt::Display for ApplyDropError {
23 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::UnsupportedEffect(effect) => {
26 write!(formatter, "unsupported drop effect `{}`", effect.as_str())
27 }
28 }
29 }
30}
31
32impl std::error::Error for ApplyDropError {}
33
34thread_local! {
35 static MODEL_OWNERS: RefCell<Vec<ManuallyDrop<DndScope>>> = const { RefCell::new(Vec::new()) };
43}
44
45#[must_use = "keep a DndScope alive while using state created under it"]
76#[derive(Clone)]
77pub struct DndScope {
78 owners: Rc<DndScopeOwners>,
79}
80
81struct DndScopeOwners {
82 unsync: Owner<UnsyncStorage>,
83 sync: Owner<SyncStorage>,
84}
85
86impl DndScope {
87 pub fn new() -> Self {
90 Self {
91 owners: Rc::new(DndScopeOwners {
92 unsync: UnsyncStorage::owner(),
93 sync: SyncStorage::owner(),
94 }),
95 }
96 }
97
98 pub fn with<R>(&self, init: impl FnOnce() -> R) -> R {
105 let result = dioxus::core::with_owner(self.owners.unsync.clone(), || {
106 dioxus::core::with_owner(self.owners.sync.clone(), || {
107 catch_unwind(AssertUnwindSafe(init))
108 })
109 });
110 match result {
111 Ok(value) => value,
112 Err(panic) => resume_unwind(panic),
113 }
114 }
115}
116
117impl Default for DndScope {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123pub fn use_dnd_model<M: Clone + 'static>(init: impl FnOnce() -> M) -> M {
185 use_hook(move || {
186 let scope = DndScope::new();
187 let model = scope.with(init);
188 MODEL_OWNERS.with_borrow_mut(|owners| owners.push(ManuallyDrop::new(scope)));
189 provide_context(model)
190 })
191}
192
193pub fn apply_clone_or_move<T, K>(
214 zones: &mut HashMap<ZoneId, Vec<T>>,
215 outcome: DropOutcome<T>,
216 key: impl Fn(&T) -> K,
217 clone_item: impl FnMut(T) -> T,
218) where
219 K: PartialEq,
220{
221 let result = apply_clone_or_move_impl(zones, outcome, key, clone_item, false);
222 debug_assert!(
223 result.is_ok(),
224 "legacy model helper cannot reject an effect"
225 );
226}
227
228pub fn try_apply_clone_or_move<T, K>(
234 zones: &mut HashMap<ZoneId, Vec<T>>,
235 outcome: DropOutcome<T>,
236 key: impl Fn(&T) -> K,
237 clone_item: impl FnMut(T) -> T,
238) -> Result<(), ApplyDropError>
239where
240 K: PartialEq,
241{
242 apply_clone_or_move_impl(zones, outcome, key, clone_item, true)
243}
244
245fn apply_clone_or_move_impl<T, K>(
246 zones: &mut HashMap<ZoneId, Vec<T>>,
247 outcome: DropOutcome<T>,
248 key: impl Fn(&T) -> K,
249 mut clone_item: impl FnMut(T) -> T,
250 checked: bool,
251) -> Result<(), ApplyDropError>
252where
253 K: PartialEq,
254{
255 let DropOutcome {
256 payload,
257 from,
258 to,
259 effect,
260 ..
261 } = outcome;
262 let item = match effect {
263 DropEffect::Copy => clone_item(payload),
264 DropEffect::Move => {
265 if let Some(from) = from {
266 let payload_key = key(&payload);
267 if let Some(source) = zones.get_mut(&from) {
268 source.retain(|item| key(item) != payload_key);
269 }
270 }
271 payload
272 }
273 unsupported if checked => return Err(ApplyDropError::UnsupportedEffect(unsupported)),
274 _ => {
275 if let Some(from) = from {
276 let payload_key = key(&payload);
277 if let Some(source) = zones.get_mut(&from) {
278 source.retain(|item| key(item) != payload_key);
279 }
280 }
281 payload
282 }
283 };
284
285 zones.entry(to).or_default().push(item);
286 Ok(())
287}
288
289pub fn apply_list_clone_or_move<T, K>(
303 source: Option<&mut Vec<T>>,
304 target: &mut Vec<T>,
305 outcome: DropOutcome<T>,
306 key: impl Fn(&T) -> K,
307 clone_item: impl FnMut(T) -> T,
308) where
309 K: PartialEq,
310{
311 let result = apply_list_clone_or_move_impl(source, target, outcome, key, clone_item, false);
312 debug_assert!(
313 result.is_ok(),
314 "legacy model helper cannot reject an effect"
315 );
316}
317
318pub fn try_apply_list_clone_or_move<T, K>(
324 source: Option<&mut Vec<T>>,
325 target: &mut Vec<T>,
326 outcome: DropOutcome<T>,
327 key: impl Fn(&T) -> K,
328 clone_item: impl FnMut(T) -> T,
329) -> Result<(), ApplyDropError>
330where
331 K: PartialEq,
332{
333 apply_list_clone_or_move_impl(source, target, outcome, key, clone_item, true)
334}
335
336fn apply_list_clone_or_move_impl<T, K>(
337 source: Option<&mut Vec<T>>,
338 target: &mut Vec<T>,
339 outcome: DropOutcome<T>,
340 key: impl Fn(&T) -> K,
341 mut clone_item: impl FnMut(T) -> T,
342 checked: bool,
343) -> Result<(), ApplyDropError>
344where
345 K: PartialEq,
346{
347 let DropOutcome {
348 payload, effect, ..
349 } = outcome;
350 let item = match effect {
351 DropEffect::Copy => clone_item(payload),
352 DropEffect::Move => {
353 if let Some(source) = source {
354 let payload_key = key(&payload);
355 source.retain(|item| key(item) != payload_key);
356 }
357 payload
358 }
359 unsupported if checked => return Err(ApplyDropError::UnsupportedEffect(unsupported)),
360 _ => {
361 if let Some(source) = source {
362 let payload_key = key(&payload);
363 source.retain(|item| key(item) != payload_key);
364 }
365 payload
366 }
367 };
368
369 target.push(item);
370 Ok(())
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::core::{DragMode, Point};
377 use dioxus::prelude::*;
378 use dioxus::signals::SyncSignal;
379 use std::cell::Cell;
380 use std::sync::mpsc::Sender;
381
382 #[derive(Debug, Clone, PartialEq)]
383 struct Card {
384 id: u32,
385 title: &'static str,
386 }
387
388 #[derive(Clone, Copy, PartialEq)]
389 struct SignalModel {
390 value: Signal<i32>,
391 }
392
393 type SignalModelSlot = Rc<RefCell<Option<SignalModel>>>;
394
395 #[derive(Store, Clone, PartialEq)]
396 struct StoreState {
397 value: i32,
398 }
399
400 #[derive(Clone, Copy, PartialEq)]
401 struct StoreModel {
402 state: Store<StoreState>,
403 }
404
405 type StoreModelSlot = Rc<RefCell<Option<StoreModel>>>;
406
407 type ScopeSlot = Rc<RefCell<Option<(DndScope, Signal<i32>, Store<i32>, SyncSignal<i32>)>>>;
408
409 type PanicScopeSlot = Rc<RefCell<Option<(DndScope, DndScope, Signal<i32>)>>>;
410
411 type ScopePairSlot = Rc<RefCell<Option<(DndScope, Signal<i32>, DndScope, Signal<i32>)>>>;
412
413 #[derive(Clone, Copy, PartialEq)]
414 struct ThreadModel {
415 value: SyncSignal<i32>,
416 }
417
418 #[derive(Default)]
419 struct SurvivorProbe {
420 renders: Cell<usize>,
421 value: Cell<i32>,
422 }
423
424 fn signal_model_creator() -> Element {
425 let slot = use_context::<SignalModelSlot>();
426 let model = use_dnd_model(|| SignalModel {
427 value: Signal::new(0),
428 });
429 *slot.borrow_mut() = Some(model);
430 rsx! {}
431 }
432
433 fn signal_model_survivor() -> Element {
434 let model = use_context::<SignalModel>();
435 let probe = use_context::<Rc<SurvivorProbe>>();
436 let value = *model.value.read();
437 probe.value.set(value);
438 probe.renders.set(probe.renders.get() + 1);
439 rsx! { "{value}" }
440 }
441
442 fn store_model_creator() -> Element {
443 let slot = use_context::<StoreModelSlot>();
444 let model = use_dnd_model(|| StoreModel {
445 state: Store::new(StoreState { value: 0 }),
446 });
447 *slot.borrow_mut() = Some(model);
448 rsx! {}
449 }
450
451 fn store_model_survivor() -> Element {
452 let model = use_context::<StoreModel>();
453 let probe = use_context::<Rc<SurvivorProbe>>();
454 let value = *model.state.value().read();
455 probe.value.set(value);
456 probe.renders.set(probe.renders.get() + 1);
457 rsx! { "{value}" }
458 }
459
460 fn scoped_state_creator() -> Element {
461 let slot = use_context::<ScopeSlot>();
462 let state = use_hook(|| {
463 let scope = DndScope::new();
464 let (signal, store, sync_signal) =
465 scope.with(|| (Signal::new(1), Store::new(2), SyncSignal::new_maybe_sync(3)));
466 (scope, signal, store, sync_signal)
467 });
468 *slot.borrow_mut() = Some(state);
469 rsx! {}
470 }
471
472 fn panic_scope_creator() -> Element {
473 let slot = use_context::<PanicScopeSlot>();
474 let state = use_hook(|| {
475 let outer = DndScope::new();
476 let inner = DndScope::new();
477 let signal = outer.with(|| {
478 let panic = catch_unwind(AssertUnwindSafe(|| {
479 inner.with(|| panic!("expected owner-restoration probe"));
480 }));
481 assert!(panic.is_err());
482 Signal::new(7)
483 });
484 (outer, inner, signal)
485 });
486 *slot.borrow_mut() = Some(state);
487 rsx! {}
488 }
489
490 fn scope_pair_creator() -> Element {
491 let slot = use_context::<ScopePairSlot>();
492 let state = use_hook(|| {
493 let first = DndScope::new();
494 let first_signal = first.with(|| Signal::new(1));
495 let second = DndScope::new();
496 let second_signal = second.with(|| Signal::new(2));
497 (first, first_signal, second, second_signal)
498 });
499 *slot.borrow_mut() = Some(state);
500 rsx! {}
501 }
502
503 fn scoped_survivor() -> Element {
504 let value = *use_context::<Signal<i32>>().read();
505 let probe = use_context::<Rc<SurvivorProbe>>();
506 probe.value.set(value);
507 probe.renders.set(probe.renders.get() + 1);
508 rsx! { "{value}" }
509 }
510
511 fn thread_model_creator() -> Element {
512 let sender = use_context::<Sender<ThreadModel>>();
513 let model = use_dnd_model(|| ThreadModel {
514 value: SyncSignal::new_maybe_sync(21),
515 });
516 sender.send(model).expect("receiver remains alive");
517 rsx! {}
518 }
519
520 #[test]
521 fn dnd_scope_reclaims_state_only_after_its_last_clone_drops() {
522 let slot = ScopeSlot::default();
523 let mut creator = VirtualDom::new(scoped_state_creator).with_root_context(slot.clone());
524 creator.rebuild_in_place();
525 let (scope, signal, mut store, sync_signal) = slot
526 .borrow_mut()
527 .take()
528 .expect("creator provided its scoped state");
529
530 drop(creator);
533 store.set(3);
534 assert_eq!(*signal.peek(), 1);
535 assert_eq!(*store.peek(), 3);
536
537 drop(scope);
538 assert!(signal.try_read().is_err());
539 assert!(sync_signal.try_read().is_err());
540 }
541
542 #[test]
543 fn dnd_scope_restores_outer_owners_before_resuming_a_panic() {
544 let slot = PanicScopeSlot::default();
545 let mut creator = VirtualDom::new(panic_scope_creator).with_root_context(slot.clone());
546 creator.rebuild_in_place();
547 let (outer, inner, signal) = slot
548 .borrow_mut()
549 .take()
550 .expect("creator provided its scopes");
551
552 drop(creator);
553 drop(inner);
554 assert_eq!(*signal.peek(), 7, "signal must remain owned by outer");
555 drop(outer);
556 assert!(signal.try_read().is_err());
557 }
558
559 #[test]
560 fn retiring_one_dynamic_scope_does_not_break_a_surviving_sibling() {
561 let slot = ScopePairSlot::default();
562 let mut creator = VirtualDom::new(scope_pair_creator).with_root_context(slot.clone());
563 creator.rebuild_in_place();
564 let (first, first_signal, second, second_signal) = slot
565 .borrow_mut()
566 .take()
567 .expect("creator provided both scopes");
568 let probe = Rc::new(SurvivorProbe::default());
569 let mut survivor = VirtualDom::new(scoped_survivor)
570 .with_root_context(second_signal)
571 .with_root_context(probe.clone());
572 survivor.rebuild_in_place();
573 let renders_before_close = probe.renders.get();
574
575 drop(creator);
576 drop(first);
577 assert!(first_signal.try_read().is_err());
578 survivor.in_runtime(|| {
579 let mut value = second_signal;
580 value.set(9);
581 });
582 survivor.render_immediate(&mut dioxus::core::NoOpMutations);
583 assert_eq!(probe.value.get(), 9);
584 assert!(probe.renders.get() > renders_before_close);
585
586 drop(survivor);
587 drop(second);
588 assert!(second_signal.try_read().is_err());
589 }
590
591 #[test]
592 fn model_sync_storage_survives_its_creator_thread() {
593 let (sender, receiver) = std::sync::mpsc::channel::<ThreadModel>();
594 std::thread::spawn(move || {
595 let mut creator =
596 VirtualDom::new(thread_model_creator).with_root_context(sender.clone());
597 creator.rebuild_in_place();
598 })
599 .join()
600 .expect("creator thread completed");
601
602 let model = receiver.recv().expect("creator published its model");
603 assert_eq!(*model.value.peek(), 21);
604 let mut value = model.value;
605 value.set(22);
606 assert_eq!(*model.value.peek(), 22);
607 }
608
609 #[test]
610 fn model_survives_its_creator_window() {
611 let slot = SignalModelSlot::default();
612 let mut creator = VirtualDom::new(signal_model_creator).with_root_context(slot.clone());
613 creator.rebuild_in_place();
614 let model = slot
615 .borrow_mut()
616 .take()
617 .expect("creator provided its model");
618 let probe = Rc::new(SurvivorProbe::default());
619 let mut survivor = VirtualDom::new(signal_model_survivor)
620 .with_root_context(model)
621 .with_root_context(probe.clone());
622 survivor.rebuild_in_place();
623 let renders_before_close = probe.renders.get();
624
625 drop(creator);
626 survivor.in_runtime(|| {
627 let mut value = model.value;
628 value.set(7);
629 });
630 survivor.render_immediate(&mut dioxus::core::NoOpMutations);
631
632 assert_eq!(probe.value.get(), 7);
633 assert!(probe.renders.get() > renders_before_close);
634 }
635
636 #[test]
637 fn store_model_keeps_its_sync_subscription_storage_after_creator_close() {
638 let slot = StoreModelSlot::default();
639 let mut creator = VirtualDom::new(store_model_creator).with_root_context(slot.clone());
640 creator.rebuild_in_place();
641 let model = slot
642 .borrow_mut()
643 .take()
644 .expect("creator provided its store model");
645 let probe = Rc::new(SurvivorProbe::default());
646 let mut survivor = VirtualDom::new(store_model_survivor)
647 .with_root_context(model)
648 .with_root_context(probe.clone());
649 survivor.rebuild_in_place();
650 let renders_before_close = probe.renders.get();
651
652 drop(creator);
653 survivor.in_runtime(|| model.state.value().set(11));
654 survivor.render_immediate(&mut dioxus::core::NoOpMutations);
655
656 assert_eq!(probe.value.get(), 11);
657 assert!(probe.renders.get() > renders_before_close);
658 }
659
660 fn outcome(
661 payload: Card,
662 from: Option<ZoneId>,
663 to: ZoneId,
664 effect: DropEffect,
665 ) -> DropOutcome<Card> {
666 DropOutcome {
667 payload,
668 from,
669 to,
670 effect,
671 mode: DragMode::Pointer,
672 client: Point::default(),
673 element: Point::default(),
674 grab: Point::default(),
675 edge: None,
676 }
677 }
678
679 #[test]
680 fn move_removes_from_source_and_appends_to_target() {
681 let a = ZoneId(1);
682 let b = ZoneId(2);
683 let mut zones = HashMap::from([
684 (
685 a,
686 vec![
687 Card {
688 id: 1,
689 title: "one",
690 },
691 Card {
692 id: 2,
693 title: "two",
694 },
695 ],
696 ),
697 (
698 b,
699 vec![Card {
700 id: 3,
701 title: "three",
702 }],
703 ),
704 ]);
705
706 apply_clone_or_move(
707 &mut zones,
708 outcome(
709 Card {
710 id: 2,
711 title: "two",
712 },
713 Some(a),
714 b,
715 DropEffect::Move,
716 ),
717 |card| card.id,
718 |card| card,
719 );
720
721 assert_eq!(
722 zones[&a],
723 vec![Card {
724 id: 1,
725 title: "one"
726 }]
727 );
728 assert_eq!(
729 zones[&b],
730 vec![
731 Card {
732 id: 3,
733 title: "three"
734 },
735 Card {
736 id: 2,
737 title: "two"
738 }
739 ]
740 );
741 }
742
743 #[test]
744 fn copy_leaves_source_and_allows_new_identity() {
745 let a = ZoneId(1);
746 let b = ZoneId(2);
747 let mut zones = HashMap::from([
748 (
749 a,
750 vec![Card {
751 id: 1,
752 title: "one",
753 }],
754 ),
755 (b, Vec::new()),
756 ]);
757
758 apply_clone_or_move(
759 &mut zones,
760 outcome(
761 Card {
762 id: 1,
763 title: "one",
764 },
765 Some(a),
766 b,
767 DropEffect::Copy,
768 ),
769 |card| card.id,
770 |mut card| {
771 card.id = 10;
772 card
773 },
774 );
775
776 assert_eq!(
777 zones[&a],
778 vec![Card {
779 id: 1,
780 title: "one"
781 }]
782 );
783 assert_eq!(
784 zones[&b],
785 vec![Card {
786 id: 10,
787 title: "one"
788 }]
789 );
790 }
791
792 #[test]
795 fn move_onto_own_zone_reorders_to_end() {
796 let a = ZoneId(1);
797 let mut zones = HashMap::from([(
798 a,
799 vec![
800 Card {
801 id: 1,
802 title: "one",
803 },
804 Card {
805 id: 2,
806 title: "two",
807 },
808 ],
809 )]);
810
811 apply_clone_or_move(
812 &mut zones,
813 outcome(
814 Card {
815 id: 1,
816 title: "one",
817 },
818 Some(a),
819 a,
820 DropEffect::Move,
821 ),
822 |card| card.id,
823 |card| card,
824 );
825
826 assert_eq!(
827 zones[&a],
828 vec![
829 Card {
830 id: 2,
831 title: "two"
832 },
833 Card {
834 id: 1,
835 title: "one"
836 }
837 ]
838 );
839 }
840
841 #[test]
844 fn move_without_source_zone_just_appends() {
845 let b = ZoneId(2);
846 let mut zones = HashMap::from([(b, Vec::new())]);
847
848 apply_clone_or_move(
849 &mut zones,
850 outcome(
851 Card {
852 id: 7,
853 title: "seven",
854 },
855 None,
856 b,
857 DropEffect::Move,
858 ),
859 |card| card.id,
860 |card| card,
861 );
862
863 assert_eq!(
864 zones[&b],
865 vec![Card {
866 id: 7,
867 title: "seven"
868 }]
869 );
870 }
871
872 #[test]
874 fn unknown_target_zone_is_created() {
875 let a = ZoneId(1);
876 let ghost = ZoneId(99);
877 let mut zones = HashMap::from([(
878 a,
879 vec![Card {
880 id: 1,
881 title: "one",
882 }],
883 )]);
884
885 apply_clone_or_move(
886 &mut zones,
887 outcome(
888 Card {
889 id: 1,
890 title: "one",
891 },
892 Some(a),
893 ghost,
894 DropEffect::Move,
895 ),
896 |card| card.id,
897 |card| card,
898 );
899
900 assert!(zones[&a].is_empty());
901 assert_eq!(
902 zones[&ghost],
903 vec![Card {
904 id: 1,
905 title: "one"
906 }]
907 );
908 }
909
910 #[test]
911 fn list_move_removes_from_source_and_appends_to_target() {
912 let mut source = vec![
913 Card {
914 id: 1,
915 title: "one",
916 },
917 Card {
918 id: 2,
919 title: "two",
920 },
921 ];
922 let mut target = vec![Card {
923 id: 3,
924 title: "three",
925 }];
926
927 apply_list_clone_or_move(
928 Some(&mut source),
929 &mut target,
930 outcome(
931 Card {
932 id: 2,
933 title: "two",
934 },
935 Some(ZoneId(1)),
936 ZoneId(2),
937 DropEffect::Move,
938 ),
939 |card| card.id,
940 |card| card,
941 );
942
943 assert_eq!(
944 source,
945 vec![Card {
946 id: 1,
947 title: "one"
948 }]
949 );
950 assert_eq!(
951 target,
952 vec![
953 Card {
954 id: 3,
955 title: "three"
956 },
957 Card {
958 id: 2,
959 title: "two"
960 }
961 ]
962 );
963 }
964
965 #[test]
966 fn list_copy_leaves_source_and_allows_new_identity() {
967 let mut source = vec![Card {
968 id: 1,
969 title: "one",
970 }];
971 let mut target = Vec::new();
972
973 apply_list_clone_or_move(
974 Some(&mut source),
975 &mut target,
976 outcome(
977 Card {
978 id: 1,
979 title: "one",
980 },
981 Some(ZoneId(1)),
982 ZoneId(2),
983 DropEffect::Copy,
984 ),
985 |card| card.id,
986 |mut card| {
987 card.id = 10;
988 card
989 },
990 );
991
992 assert_eq!(
993 source,
994 vec![Card {
995 id: 1,
996 title: "one"
997 }]
998 );
999 assert_eq!(
1000 target,
1001 vec![Card {
1002 id: 10,
1003 title: "one"
1004 }]
1005 );
1006 }
1007
1008 #[test]
1010 fn list_move_without_source_just_appends() {
1011 let mut target = Vec::new();
1012
1013 apply_list_clone_or_move(
1014 None,
1015 &mut target,
1016 outcome(
1017 Card {
1018 id: 7,
1019 title: "seven",
1020 },
1021 None,
1022 ZoneId(2),
1023 DropEffect::Move,
1024 ),
1025 |card| card.id,
1026 |card| card,
1027 );
1028
1029 assert_eq!(
1030 target,
1031 vec![Card {
1032 id: 7,
1033 title: "seven"
1034 }]
1035 );
1036 }
1037
1038 #[test]
1039 fn unsupported_effect_is_explicit_and_does_not_mutate() {
1040 let zone = ZoneId(1);
1041 let original = vec![Card {
1042 id: 1,
1043 title: "one",
1044 }];
1045 let mut zones = HashMap::from([(zone, original.clone())]);
1046 let result = try_apply_clone_or_move(
1047 &mut zones,
1048 outcome(original[0].clone(), Some(zone), ZoneId(2), DropEffect::Link),
1049 |card| card.id,
1050 |card| card,
1051 );
1052
1053 assert_eq!(
1054 result,
1055 Err(ApplyDropError::UnsupportedEffect(DropEffect::Link))
1056 );
1057 assert_eq!(zones, HashMap::from([(zone, original)]));
1058 }
1059
1060 #[test]
1061 fn legacy_helpers_keep_their_unit_return_contract() {
1062 let zone = ZoneId(1);
1063 let mut zones = HashMap::new();
1064 let _: () = apply_clone_or_move(
1065 &mut zones,
1066 outcome(
1067 Card {
1068 id: 1,
1069 title: "one",
1070 },
1071 None,
1072 zone,
1073 DropEffect::Move,
1074 ),
1075 |card| card.id,
1076 |card| card,
1077 );
1078
1079 let mut target = Vec::new();
1080 let _: () = apply_list_clone_or_move(
1081 None,
1082 &mut target,
1083 outcome(
1084 Card {
1085 id: 2,
1086 title: "two",
1087 },
1088 None,
1089 zone,
1090 DropEffect::Move,
1091 ),
1092 |card| card.id,
1093 |card| card,
1094 );
1095 }
1096}