dioxus_dnd/multiselect.rs
1//! Multi-select drag: select several items, drag them as one payload.
2//!
3//! The design leans on the core being generic: the payload type flowing
4//! through the provider is simply `Vec<K>` — so wrap your app in
5//! `DndProvider::<Vec<K>>` and your `DropZone`s receive the whole selection
6//! in one `DropOutcome<Vec<K>>`.
7//!
8//! What this module adds is the interaction layer:
9//! - [`use_selection`] — selection state with the usual click semantics
10//! (click selects one, Ctrl/Cmd+click toggles).
11//! - [`SelectableDraggable`] — a `Draggable` that resolves its payload from
12//! the selection: dragging a *selected* item carries the whole selection;
13//! dragging an *unselected* item carries just that item.
14//! - [`SelectionCount`] — a badge for your `DragOverlay` ghost ("3 items").
15//!
16//! ```rust,ignore
17//! let selection = use_selection::<FileId>();
18//! rsx! {
19//! DndProvider::<Vec<FileId>> {
20//! for file in files {
21//! SelectableDraggable::<FileId> {
22//! key: "{file.id.0}",
23//! item: file.id,
24//! selection,
25//! FileRow { file }
26//! }
27//! }
28//! DropZone::<Vec<FileId>> {
29//! on_drop: move |o: DropOutcome<Vec<FileId>>| trash(o.payload),
30//! "Trash"
31//! }
32//! DragOverlay::<Vec<FileId>> { SelectionCount::<FileId> { selection } }
33//! }
34//! }
35//! ```
36
37use dioxus::prelude::*;
38
39use crate::core::{use_dnd, DropEffect, ZoneId};
40use crate::pointer::PointerDraggable;
41
42/// Selection state for keys of type `K`. Cheap to copy.
43pub struct Selection<K: Clone + PartialEq + 'static> {
44 items: Signal<Vec<K>>,
45}
46
47impl<K: Clone + PartialEq + 'static> Copy for Selection<K> {}
48impl<K: Clone + PartialEq + 'static> Clone for Selection<K> {
49 fn clone(&self) -> Self {
50 *self
51 }
52}
53impl<K: Clone + PartialEq + 'static> PartialEq for Selection<K> {
54 fn eq(&self, other: &Self) -> bool {
55 self.items == other.items
56 }
57}
58
59impl<K: Clone + PartialEq + 'static> Selection<K> {
60 /// Wrap an existing signal. Prefer [`use_selection`].
61 pub fn from_signal(items: Signal<Vec<K>>) -> Self {
62 Self { items }
63 }
64
65 /// Is `key` currently selected?
66 pub fn is_selected(&self, key: &K) -> bool {
67 self.items.read().contains(key)
68 }
69
70 /// Replace the selection with just `key`.
71 pub fn select_only(&mut self, key: K) {
72 self.items.set(vec![key]);
73 }
74
75 /// Add or remove `key` (Ctrl/Cmd+click semantics).
76 pub fn toggle(&mut self, key: K) {
77 let mut items = self.items.write();
78 if let Some(ix) = items.iter().position(|k| *k == key) {
79 items.remove(ix);
80 } else {
81 items.push(key);
82 }
83 }
84
85 /// Clear the selection.
86 pub fn clear(&mut self) {
87 self.items.write().clear();
88 }
89
90 /// Snapshot of the selected keys, in selection order.
91 pub fn items(&self) -> Vec<K> {
92 self.items.read().clone()
93 }
94
95 /// Number of selected keys.
96 pub fn len(&self) -> usize {
97 self.items.read().len()
98 }
99
100 /// Is nothing selected?
101 pub fn is_empty(&self) -> bool {
102 self.items.read().is_empty()
103 }
104
105 /// Apply the standard click convention: plain click selects only this
106 /// key; a click with Ctrl or Cmd held toggles it.
107 pub fn click(&mut self, key: K, modifiers: Modifiers) {
108 if modifiers.contains(Modifiers::CONTROL) || modifiers.contains(Modifiers::META) {
109 self.toggle(key);
110 } else {
111 self.select_only(key);
112 }
113 }
114}
115
116/// Create selection state owned by this component.
117pub fn use_selection<K: Clone + PartialEq + 'static>() -> Selection<K> {
118 Selection {
119 items: use_signal(Vec::new),
120 }
121}
122
123/// A draggable list/grid item participating in a selection.
124///
125/// - Click / Ctrl+click manage the selection (via [`Selection::click`]).
126/// - Dragging a selected item picks up **the whole selection**; dragging an
127/// unselected one picks up just that item (and selects it).
128/// - Works with mouse, touch, pen and keyboard (wraps `PointerDraggable`).
129/// - The wrapper exposes `data-selected="true"` for styling.
130///
131/// Requires a `DndProvider::<Vec<K>>` ancestor.
132#[component]
133pub fn SelectableDraggable<K: Clone + PartialEq + 'static>(
134 /// This item's key.
135 item: K,
136 /// Shared selection state from [`use_selection`].
137 selection: Selection<K>,
138 /// The zone this item lives in.
139 #[props(default)]
140 zone: Option<ZoneId>,
141 /// Drop effect. Defaults to `Move`.
142 #[props(default)]
143 effect: DropEffect,
144 /// Label for screen-reader announcements.
145 #[props(default)]
146 label: Option<String>,
147 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
148 children: Element,
149) -> Element {
150 let _ = use_dnd::<Vec<K>>(); // fail fast with a clear panic if unprovided
151 let selected = selection.is_selected(&item);
152 // Payload resolved from *current* selection each render: a selected item
153 // drags the group, an unselected one drags itself.
154 let payload = if selected {
155 selection.items()
156 } else {
157 vec![item.clone()]
158 };
159 let click_key = item.clone();
160 let mut selection = selection;
161
162 rsx! {
163 div {
164 "data-selected": selected,
165 onclick: move |evt: MouseEvent| {
166 selection.click(click_key.clone(), evt.modifiers());
167 },
168 ..attributes,
169 PointerDraggable::<Vec<K>> {
170 payload,
171 zone,
172 effect,
173 label,
174 {children}
175 }
176 }
177 }
178}
179
180/// A "N items" badge for the drag ghost. Render inside
181/// `DragOverlay::<Vec<K>>`; shows the size of the payload being dragged.
182#[component]
183pub fn SelectionCount<K: Clone + PartialEq + 'static>(
184 /// Internal marker; never set this.
185 #[props(default)]
186 phantom: std::marker::PhantomData<K>,
187) -> Element {
188 let _ = phantom;
189 let dnd = use_dnd::<Vec<K>>();
190 let n = dnd.payload().map(|p| p.len()).unwrap_or(0);
191 rsx! {
192 span { "{n} item(s)" }
193 }
194}