1use std::rc::Rc;
6
7use dioxus::prelude::*;
8
9use crate::core::{
10 use_dnd, use_zone_id, use_zone_registry, DragMode, DropOutcome, ParentZone, Point, Rect,
11 ZoneId, ZoneRecord,
12};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct CanvasDrop<T> {
17 pub payload: T,
18 pub position: Point,
21 pub pointer: Point,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct SnapGrid(pub f64);
28
29impl SnapGrid {
30 pub fn snap(&self, p: Point) -> Point {
31 if self.0 <= 0.0 {
32 return p;
33 }
34 Point::new(
35 (p.x / self.0).round() * self.0,
36 (p.y / self.0).round() * self.0,
37 )
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Default)]
46pub enum CanvasKeyboardPlacement {
47 #[default]
49 Center,
50 Origin,
52 Fixed(Point),
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct Bounds {
63 pub width: f64,
64 pub height: f64,
65}
66
67impl Bounds {
68 pub fn clamp(&self, p: Point) -> Point {
69 Point::new(
70 clamp_axis(p.x, 0.0, self.width),
71 clamp_axis(p.y, 0.0, self.height),
72 )
73 }
74
75 pub fn clamp_item(&self, p: Point, width: f64, height: f64) -> Point {
79 Point::new(
80 clamp_axis(p.x, 0.0, self.width - width),
81 clamp_axis(p.y, 0.0, self.height - height),
82 )
83 }
84
85 pub fn clamp_rect(&self, rect: Rect) -> Point {
89 self.clamp_item(Point::new(rect.x, rect.y), rect.width, rect.height)
90 }
91}
92
93pub fn client_to_canvas(client: Point, canvas_rect: Rect) -> Point {
95 client - canvas_rect.origin()
96}
97
98pub fn canvas_to_client(point: Point, canvas_rect: Rect) -> Point {
100 point + canvas_rect.origin()
101}
102
103pub fn canvas_position(
106 pointer: Point,
107 grab: Point,
108 snap: Option<SnapGrid>,
109 bounds: Option<Bounds>,
110) -> Point {
111 let mut position = pointer - grab;
112 if let Some(g) = snap {
113 position = g.snap(position);
114 }
115 if let Some(b) = bounds {
116 position = b.clamp(position);
117 }
118 position
119}
120
121pub fn canvas_keyboard_pointer(policy: CanvasKeyboardPlacement, element: Point) -> Point {
123 match policy {
124 CanvasKeyboardPlacement::Center => element,
125 CanvasKeyboardPlacement::Origin => Point::default(),
126 CanvasKeyboardPlacement::Fixed(point) => point,
127 }
128}
129
130fn clamp_axis(v: f64, min: f64, max: f64) -> f64 {
131 let lo = if min.is_nan() { f64::NEG_INFINITY } else { min };
136 let hi = if max.is_nan() { f64::INFINITY } else { max };
137 if lo > hi {
138 return if min.is_nan() { v } else { min };
139 }
140 v.clamp(lo, hi)
141}
142
143#[component]
153pub fn CanvasDropZone<T: Clone + PartialEq + 'static>(
154 #[props(default)]
156 id: Option<ZoneId>,
157 #[props(default)]
159 snap: Option<SnapGrid>,
160 #[props(default)]
162 bounds: Option<Bounds>,
163 #[props(default)]
165 keyboard: CanvasKeyboardPlacement,
166 #[props(default)]
168 label: Option<String>,
169 on_drop: EventHandler<CanvasDrop<T>>,
170 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
171 children: Element,
172) -> Element {
173 let dnd = use_dnd::<T>();
174 let mut registry = use_zone_registry::<T>();
175 let auto_id = use_zone_id();
176 let zone_id = id.unwrap_or(auto_id);
177
178 let mut snap_now = use_signal(|| snap);
183 let mut bounds_now = use_signal(|| bounds);
184 let mut keyboard_now = use_signal(|| keyboard);
185 if *snap_now.peek() != snap {
186 snap_now.set(snap);
187 }
188 if *bounds_now.peek() != bounds {
189 bounds_now.set(bounds);
190 }
191 if *keyboard_now.peek() != keyboard {
192 keyboard_now.set(keyboard);
193 }
194
195 let place = move |payload: T, pointer: Point, grab: Point| {
197 let position = canvas_position(pointer, grab, *snap_now.peek(), *bounds_now.peek());
198 on_drop.call(CanvasDrop {
199 payload,
200 position,
201 pointer,
202 });
203 };
204
205 let parent = try_use_context::<ParentZone>().map(|p| p.0);
209 let mounted = use_signal(|| None::<Rc<MountedData>>);
210 let rect = use_signal(|| None::<Rect>);
211 let registered_drop = Callback::new(move |o: DropOutcome<T>| {
212 let pointer = if o.mode == DragMode::Keyboard {
213 canvas_keyboard_pointer(*keyboard_now.peek(), o.element)
214 } else {
215 o.element
216 };
217 place(o.payload, pointer, o.grab);
218 });
219 use_hook(|| {
220 registry.register(ZoneRecord {
221 id: zone_id,
222 parent,
223 label: label.clone(),
224 on_drop: registered_drop,
225 accepts: None,
226 mounted,
227 rect,
228 });
229 });
230 use_drop(move || {
231 registry.unregister(zone_id);
232 });
233 registry.sync_label(zone_id, label.clone());
236
237 rsx! {
238 div {
239 "data-active": if dnd.dragging() { "true" },
240 onmounted: move |evt: Event<MountedData>| {
241 let m: Rc<MountedData> = evt.data();
242 let mut mounted = mounted;
243 let mut rect = rect;
244 mounted.set(Some(m.clone()));
245 spawn(async move {
246 if let Ok(r) = m.get_client_rect().await {
247 rect.set(Some(Rect::new(
248 r.origin.x,
249 r.origin.y,
250 r.size.width,
251 r.size.height,
252 )));
253 }
254 });
255 },
256 ..attributes,
257 {children}
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn snap_and_clamp() {
268 let g = SnapGrid(10.0);
269 let p = g.snap(Point::new(14.9, 15.1));
270 assert_eq!((p.x, p.y), (10.0, 20.0));
271 assert_eq!(
272 SnapGrid(0.0).snap(Point::new(3.3, 4.4)),
273 Point::new(3.3, 4.4)
274 );
275
276 let b = Bounds {
277 width: 100.0,
278 height: 50.0,
279 };
280 let p = b.clamp(Point::new(-5.0, 999.0));
281 assert_eq!((p.x, p.y), (0.0, 50.0));
282
283 let corrected = Point::new(107.0, 46.0) - Point::new(9.0, 8.0);
284 let positioned = b.clamp(g.snap(corrected));
285 assert_eq!(positioned, Point::new(100.0, 40.0));
286 }
287
288 #[test]
289 fn clamp_does_not_panic_on_non_finite_bounds() {
290 let b = Bounds {
294 width: f64::NAN,
295 height: f64::INFINITY,
296 };
297 let p = b.clamp(Point::new(25.0, 25.0));
298 assert_eq!((p.x, p.y), (25.0, 25.0), "unconstrained, unchanged");
299 let p = b.clamp(Point::new(-10.0, -10.0));
300 assert_eq!((p.x, p.y), (0.0, 0.0), "still floored at the origin");
301 let q = b.clamp_item(Point::new(-3.0, 10.0), 5.0, 5.0);
303 assert_eq!(q.x, 0.0);
304 }
305
306 #[test]
307 fn bounds_can_clamp_whole_items() {
308 let b = Bounds {
309 width: 100.0,
310 height: 50.0,
311 };
312
313 assert_eq!(
314 b.clamp_item(Point::new(90.0, 45.0), 20.0, 12.0),
315 Point::new(80.0, 38.0)
316 );
317 assert_eq!(
318 b.clamp_rect(Rect::new(-5.0, 60.0, 20.0, 10.0)),
319 Point::new(0.0, 40.0)
320 );
321 assert_eq!(
322 b.clamp_item(Point::new(20.0, 20.0), 150.0, 80.0),
323 Point::new(0.0, 0.0)
324 );
325 }
326
327 #[test]
328 fn coordinate_helpers_convert_between_client_and_canvas() {
329 let rect = Rect::new(40.0, 80.0, 320.0, 200.0);
330 let client = Point::new(64.0, 128.0);
331 let canvas = client_to_canvas(client, rect);
332
333 assert_eq!(canvas, Point::new(24.0, 48.0));
334 assert_eq!(canvas_to_client(canvas, rect), client);
335 }
336
337 #[test]
338 fn canvas_position_applies_grab_snap_then_bounds() {
339 let p = canvas_position(
340 Point::new(107.0, 46.0),
341 Point::new(9.0, 8.0),
342 Some(SnapGrid(10.0)),
343 Some(Bounds {
344 width: 100.0,
345 height: 50.0,
346 }),
347 );
348
349 assert_eq!(p, Point::new(100.0, 40.0));
350 }
351
352 #[test]
353 fn canvas_keyboard_pointer_uses_center_element_by_default() {
354 assert_eq!(
355 canvas_keyboard_pointer(CanvasKeyboardPlacement::default(), Point::new(40.0, 20.0)),
356 Point::new(40.0, 20.0)
357 );
358 }
359
360 #[test]
361 fn canvas_keyboard_pointer_can_use_origin() {
362 assert_eq!(
363 canvas_keyboard_pointer(CanvasKeyboardPlacement::Origin, Point::new(40.0, 20.0)),
364 Point::default()
365 );
366 }
367
368 #[test]
369 fn canvas_keyboard_pointer_can_use_fixed_point() {
370 assert_eq!(
371 canvas_keyboard_pointer(
372 CanvasKeyboardPlacement::Fixed(Point::new(12.0, 18.0)),
373 Point::new(40.0, 20.0),
374 ),
375 Point::new(12.0, 18.0)
376 );
377 }
378
379 #[test]
380 fn item_clamp_composes_after_canvas_position() {
381 let top_left = canvas_position(Point::new(156.0, 86.0), Point::new(4.0, 5.0), None, None);
382 let constrained = Bounds {
383 width: 160.0,
384 height: 90.0,
385 }
386 .clamp_item(top_left, 48.0, 32.0);
387
388 assert_eq!(top_left, Point::new(152.0, 81.0));
389 assert_eq!(constrained, Point::new(112.0, 58.0));
390 }
391}