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