Skip to main content

dioxus_dnd/
canvas.rs

1//! Free-position drops — node editors, whiteboards, floor planners. The drop
2//! answers not just "what landed" but "*where* exactly", corrected for grab
3//! offset, optionally snapped to a grid and clamped to bounds.
4
5use dioxus::prelude::*;
6
7use crate::core::{element_point, use_dnd, use_zone_id, Point, ZoneId};
8
9/// A payload dropped at a position on the canvas.
10#[derive(Debug, Clone, PartialEq)]
11pub struct CanvasDrop<T> {
12    pub payload: T,
13    /// Top-left position for the dropped element, relative to the canvas —
14    /// already corrected for grab offset, snapping, and bounds.
15    pub position: Point,
16    /// The raw pointer position relative to the canvas, untouched.
17    pub pointer: Point,
18}
19
20/// Snap positions to a square grid.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct SnapGrid(pub f64);
23
24impl SnapGrid {
25    pub fn snap(&self, p: Point) -> Point {
26        if self.0 <= 0.0 {
27            return p;
28        }
29        Point::new(
30            (p.x / self.0).round() * self.0,
31            (p.y / self.0).round() * self.0,
32        )
33    }
34}
35
36/// Clamp positions into `0..=width` × `0..=height`.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct Bounds {
39    pub width: f64,
40    pub height: f64,
41}
42
43impl Bounds {
44    pub fn clamp(&self, p: Point) -> Point {
45        Point::new(p.x.clamp(0.0, self.width), p.y.clamp(0.0, self.height))
46    }
47}
48
49/// A canvas that reports drop positions.
50///
51/// Uses the shared `DndContext<T>`; start drags with the core `Draggable`
52/// (its recorded grab offset is what makes the drop position feel exact —
53/// the element lands where its ghost was, not where the pointer tip was).
54#[component]
55pub fn CanvasDropZone<T: Clone + PartialEq + 'static>(
56    /// Stable identity; auto-generated if omitted.
57    #[props(default)]
58    id: Option<ZoneId>,
59    /// Snap the corrected position to a grid.
60    #[props(default)]
61    snap: Option<SnapGrid>,
62    /// Clamp the corrected position into these bounds.
63    #[props(default)]
64    bounds: Option<Bounds>,
65    on_drop: EventHandler<CanvasDrop<T>>,
66    #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
67    children: Element,
68) -> Element {
69    let mut dnd = use_dnd::<T>();
70    let auto_id = use_zone_id();
71    let _zone_id = id.unwrap_or(auto_id);
72
73    rsx! {
74        div {
75            ondragover: move |evt: DragEvent| {
76                if dnd.dragging() {
77                    evt.prevent_default();
78                }
79            },
80            ondrop: move |evt: DragEvent| {
81                evt.prevent_default();
82                let pointer = element_point(&evt);
83                let grab = dnd.grab();
84                if let Some((payload, _)) = dnd.take() {
85                    let mut position = pointer - grab;
86                    if let Some(g) = snap {
87                        position = g.snap(position);
88                    }
89                    if let Some(b) = bounds {
90                        position = b.clamp(position);
91                    }
92                    on_drop.call(CanvasDrop { payload, position, pointer });
93                }
94            },
95            ..attributes,
96            {children}
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn snap_and_clamp() {
107        let g = SnapGrid(10.0);
108        let p = g.snap(Point::new(14.9, 15.1));
109        assert_eq!((p.x, p.y), (10.0, 20.0));
110        assert_eq!(
111            SnapGrid(0.0).snap(Point::new(3.3, 4.4)),
112            Point::new(3.3, 4.4)
113        );
114
115        let b = Bounds {
116            width: 100.0,
117            height: 50.0,
118        };
119        let p = b.clamp(Point::new(-5.0, 999.0));
120        assert_eq!((p.x, p.y), (0.0, 50.0));
121    }
122}