Skip to main content

dioxus_dnd_kit/
context.rs

1use std::rc::Rc;
2
3use dioxus::{
4    html::geometry::{ClientPoint, PixelsRect},
5    prelude::*,
6};
7use dioxus_floating::{OffsetOptions, ScrollableView};
8
9use crate::{
10    DndItem,
11    hooks::{use_body_on_mouse_up, use_window_on_resize},
12    prelude::DraggableOverlay,
13};
14
15/// State management for a Drag-and-Drop session.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct DndContext<T: DndItem> {
18    /// How to render the item in the list and overlay.
19    pub render: Callback<T, Element>,
20    /// Optional placeholder renderer used when an item is lifted.
21    pub placeholder_render: Option<Callback<(f64, f64), Element>>,
22    /// Current active (dragged) item data.
23    pub active: Signal<Option<T>>,
24    /// Rect of the active item before it was lifted.
25    pub active_rect: Signal<Option<PixelsRect>>,
26    /// Internal: Reference to the mounted data of the active element.
27    pub active_mounted: Signal<Option<Rc<MountedData>>>,
28    /// Flag indicating that the floating overlay is ready for display.
29    pub is_floating_ready: Signal<bool>,
30    /// Current mouse coordinates relative to the viewport.
31    pub mouse_pos: Signal<ClientPoint>,
32    /// Mouse coordinates at the start of the drag operation.
33    pub start_pos: Signal<ClientPoint>,
34    /// Distance between the grab point and the element's top-left corner.
35    pub grab_offset: Signal<OffsetOptions>,
36    /// Key generator for DOM stability.
37    pub key_gen: Callback<T, String>,
38    /// Global trigger to force recalculation of element rectangles.
39    pub recalculate_rects: Signal<u64>,
40}
41
42impl<T: DndItem> DndContext<T> {
43    fn new(
44        key_gen: Callback<T, String>,
45        render: Callback<T, Element>,
46        placeholder_render: Option<Callback<(f64, f64), Element>>,
47    ) -> Self {
48        Self {
49            key_gen,
50            render,
51            placeholder_render,
52            active: Signal::new(None),
53            active_mounted: Signal::new(None),
54            active_rect: Signal::new(None),
55            is_floating_ready: Signal::new(false),
56            mouse_pos: Signal::new(ClientPoint::zero()),
57            start_pos: Signal::new(ClientPoint::zero()),
58            grab_offset: Signal::new(OffsetOptions::default()),
59            recalculate_rects: Signal::new(0),
60        }
61    }
62}
63
64/// The root container and manager for the Drag-and-Drop area.
65///
66/// `DraggableView` initializes the [`DndContext`], handles global mouse events (move/up),
67/// and manages the visual lifecycle of the dragged item.
68///
69/// ### Features:
70/// - **Selection Lock**: Automatically toggles `user-select: none` during dragging to prevent text highlighting.
71/// - **Overlay Management**: Renders the [`DraggableOverlay`] automatically when an item is picked up.
72/// - **Smooth Landing**: Implements a short delay before clearing the active state to prevent layout flickering.
73#[component]
74pub fn DraggableView<T: DndItem>(
75    /// CSS classes for the outer container.
76    #[props(default)]
77    class: ReadSignal<String>,
78
79    /// Content containing [`Draggable`] items and [`Droppable`] zones.
80    children: Element,
81
82    /// A callback to generate a stable, unique key for each item.
83    key_gen: Callback<T, String>,
84
85    /// The primary rendering callback used for both the list items and the dragging ghost.
86    render: Callback<T, Element>,
87
88    /// An optional callback to render a placeholder that stays in the list while an item is being dragged.
89    /// Receives `(height, width)` in pixels.
90    placeholder_render: Option<Callback<(f64, f64), Element>>,
91) -> Element {
92    let mut context =
93        use_context_provider(move || DndContext::new(key_gen, render, placeholder_render));
94
95    let mouseup = use_callback(move |_| {
96        context.is_floating_ready.set(false);
97        spawn(async move {
98            gloo_timers::future::TimeoutFuture::new(50).await;
99            context.active.set(None);
100            context.active_mounted.set(None);
101            context.active_rect.set(None);
102            context.start_pos.set(ClientPoint::zero());
103        });
104    });
105    use_body_on_mouse_up(mouseup);
106    use_window_on_resize(use_callback(move |_| {
107        context.recalculate_rects.with_mut(|r| *r += 1);
108    }));
109
110    rsx! {
111        ScrollableView { class,
112            style: if context.active.read().is_some() {
113                "user-select: none; -webkit-user-select: none;"
114            } else {
115                "user-select: auto; -webkit-user-select: auto;"
116            },
117            on_mouse_move: move |e: MouseEvent| {
118                context.mouse_pos.set(e.client_coordinates());
119            },
120            // on_mouse_up: move |_| {
121            //     context.active.set(None);
122            //     context.start_pos.set(ClientPoint::zero());
123            // },
124            {children}
125            if let Some(active_item) = context.active.read().as_ref() {
126                DraggableOverlay::<T> { item: active_item.clone() }
127            }
128        }
129    }
130}