Skip to main content

dioxus_dnd/
multiselect.rs

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