Skip to main content

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}