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//! ```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 n = dnd.payload().map(|p| p.len()).unwrap_or(0);
192    rsx! {
193        span { "{n} item(s)" }
194    }
195}