1#![doc = include_str!("../docs/api/canvas.md")]
2
3use std::rc::Rc;
4
5use dioxus::prelude::*;
6
7use crate::core::{
8 use_dnd, use_parent_zone, use_zone_id, use_zone_registry, DragMode, DropEffect, DropOutcome,
9 Point, Rect, ZoneId, ZoneRecord,
10};
11
12#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub struct CanvasDrop<T> {
19 pub payload: T,
20 pub position: Point,
23 pub pointer: Point,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct SnapGrid(pub f64);
30
31impl SnapGrid {
32 pub fn snap(&self, p: Point) -> Point {
33 if self.0 <= 0.0 {
34 return p;
35 }
36 Point::new(
37 (p.x / self.0).round() * self.0,
38 (p.y / self.0).round() * self.0,
39 )
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Default)]
48pub enum CanvasKeyboardPlacement {
49 #[default]
51 Center,
52 Origin,
54 Fixed(Point),
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct Bounds {
65 pub width: f64,
66 pub height: f64,
67}
68
69impl Bounds {
70 pub fn clamp(&self, p: Point) -> Point {
71 Point::new(
72 clamp_axis(p.x, 0.0, self.width),
73 clamp_axis(p.y, 0.0, self.height),
74 )
75 }
76
77 pub fn clamp_item(&self, p: Point, width: f64, height: f64) -> Point {
81 Point::new(
82 clamp_axis(p.x, 0.0, self.width - width),
83 clamp_axis(p.y, 0.0, self.height - height),
84 )
85 }
86
87 pub fn clamp_rect(&self, rect: Rect) -> Point {
91 self.clamp_item(Point::new(rect.x, rect.y), rect.width, rect.height)
92 }
93}
94
95pub fn client_to_canvas(client: Point, canvas_rect: Rect) -> Point {
97 client - canvas_rect.origin()
98}
99
100pub fn canvas_to_client(point: Point, canvas_rect: Rect) -> Point {
102 point + canvas_rect.origin()
103}
104
105pub fn canvas_position(
108 pointer: Point,
109 grab: Point,
110 snap: Option<SnapGrid>,
111 bounds: Option<Bounds>,
112) -> Point {
113 let mut position = pointer - grab;
114 if let Some(g) = snap {
115 position = g.snap(position);
116 }
117 if let Some(b) = bounds {
118 position = b.clamp(position);
119 }
120 position
121}
122
123pub fn canvas_keyboard_pointer(policy: CanvasKeyboardPlacement, element: Point) -> Point {
125 match policy {
126 CanvasKeyboardPlacement::Center => element,
127 CanvasKeyboardPlacement::Origin => Point::default(),
128 CanvasKeyboardPlacement::Fixed(point) => point,
129 }
130}
131
132fn clamp_axis(v: f64, min: f64, max: f64) -> f64 {
133 let lo = if min.is_nan() { f64::NEG_INFINITY } else { min };
138 let hi = if max.is_nan() { f64::INFINITY } else { max };
139 if lo > hi {
140 return if min.is_nan() { v } else { min };
141 }
142 v.clamp(lo, hi)
143}
144
145#[component]
155pub fn CanvasDropZone<T: Clone + PartialEq + 'static>(
156 #[props(default)]
158 id: Option<ZoneId>,
159 #[props(default)]
161 snap: Option<SnapGrid>,
162 #[props(default)]
164 bounds: Option<Bounds>,
165 #[props(default)]
167 keyboard: CanvasKeyboardPlacement,
168 #[props(default)]
170 label: Option<String>,
171 on_drop: EventHandler<CanvasDrop<T>>,
172 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
173 children: Element,
174) -> Element {
175 let auto_id = use_zone_id();
176 let zone_id = id.unwrap_or(auto_id);
177 rsx! {
178 for keyed_zone_id in [zone_id] {
179 CanvasDropZoneInstance::<T> {
180 key: "{keyed_zone_id.0}",
181 zone_id: keyed_zone_id,
182 snap,
183 bounds,
184 keyboard,
185 label: label.clone(),
186 on_drop,
187 attributes: attributes.clone(),
188 {children.clone()}
189 }
190 }
191 }
192}
193
194#[component]
195fn CanvasDropZoneInstance<T: Clone + PartialEq + 'static>(
196 zone_id: ZoneId,
197 snap: Option<SnapGrid>,
198 bounds: Option<Bounds>,
199 keyboard: CanvasKeyboardPlacement,
200 label: Option<String>,
201 on_drop: EventHandler<CanvasDrop<T>>,
202 attributes: Vec<Attribute>,
203 children: Element,
204) -> Element {
205 let dnd = use_dnd::<T>();
206 let mut registry = use_zone_registry::<T>();
207
208 let parent = use_parent_zone();
212 let registered_drop = use_callback(move |o: DropOutcome<T>| {
213 let pointer = if o.mode == DragMode::Keyboard {
214 canvas_keyboard_pointer(keyboard, o.element)
215 } else {
216 o.element
217 };
218 let position = canvas_position(pointer, o.grab, snap, bounds);
219 on_drop.call(CanvasDrop {
220 payload: o.payload,
221 position,
222 pointer,
223 });
224 });
225 let registered_label = label.clone();
226 let registration = use_hook(|| {
227 registry.register(ZoneRecord {
228 id: zone_id,
229 parent,
230 label: registered_label.clone(),
231 on_drop: registered_drop,
232 accepts: None,
233 mounted: None,
234 rect: None,
235 })
236 });
237 use_drop(move || {
238 registry.unregister_registration(registration);
239 });
240 let label_for_sync = label.clone();
241 use_effect(use_reactive!(|(label_for_sync)| {
242 registry.sync_label(zone_id, label_for_sync);
243 }));
244 use_effect(use_reactive!(|(parent)| {
245 registry.sync_parent(registration, parent);
246 }));
247 let mut attributes = attributes;
248 crate::core::components::protect_attributes(&mut attributes, &["data-active", "onmounted"]);
249
250 rsx! {
251 div {
252 "data-active": if dnd.dragging() && dnd.proposed_effect() != DropEffect::None { "true" },
253 onmounted: move |evt: Event<MountedData>| {
254 let m: Rc<MountedData> = evt.data();
255 let mut registry = registry;
256 registry.set_mounted(registration, m.clone());
257 spawn(async move {
258 if let Ok(r) = m.get_client_rect().await {
259 registry.set_rect_if_present(registration, Rect::new(
260 r.origin.x,
261 r.origin.y,
262 r.size.width,
263 r.size.height,
264 ));
265 }
266 });
267 },
268 ..attributes,
269 {children}
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn snap_and_clamp() {
280 let g = SnapGrid(10.0);
281 let p = g.snap(Point::new(14.9, 15.1));
282 assert_eq!((p.x, p.y), (10.0, 20.0));
283 assert_eq!(
284 SnapGrid(0.0).snap(Point::new(3.3, 4.4)),
285 Point::new(3.3, 4.4)
286 );
287
288 let b = Bounds {
289 width: 100.0,
290 height: 50.0,
291 };
292 let p = b.clamp(Point::new(-5.0, 999.0));
293 assert_eq!((p.x, p.y), (0.0, 50.0));
294
295 let corrected = Point::new(107.0, 46.0) - Point::new(9.0, 8.0);
296 let positioned = b.clamp(g.snap(corrected));
297 assert_eq!(positioned, Point::new(100.0, 40.0));
298 }
299
300 #[test]
301 fn clamp_does_not_panic_on_non_finite_bounds() {
302 let b = Bounds {
306 width: f64::NAN,
307 height: f64::INFINITY,
308 };
309 let p = b.clamp(Point::new(25.0, 25.0));
310 assert_eq!((p.x, p.y), (25.0, 25.0), "unconstrained, unchanged");
311 let p = b.clamp(Point::new(-10.0, -10.0));
312 assert_eq!((p.x, p.y), (0.0, 0.0), "still floored at the origin");
313 let q = b.clamp_item(Point::new(-3.0, 10.0), 5.0, 5.0);
315 assert_eq!(q.x, 0.0);
316 }
317
318 #[test]
319 fn bounds_can_clamp_whole_items() {
320 let b = Bounds {
321 width: 100.0,
322 height: 50.0,
323 };
324
325 assert_eq!(
326 b.clamp_item(Point::new(90.0, 45.0), 20.0, 12.0),
327 Point::new(80.0, 38.0)
328 );
329 assert_eq!(
330 b.clamp_rect(Rect::new(-5.0, 60.0, 20.0, 10.0)),
331 Point::new(0.0, 40.0)
332 );
333 assert_eq!(
334 b.clamp_item(Point::new(20.0, 20.0), 150.0, 80.0),
335 Point::new(0.0, 0.0)
336 );
337 }
338
339 #[test]
340 fn coordinate_helpers_convert_between_client_and_canvas() {
341 let rect = Rect::new(40.0, 80.0, 320.0, 200.0);
342 let client = Point::new(64.0, 128.0);
343 let canvas = client_to_canvas(client, rect);
344
345 assert_eq!(canvas, Point::new(24.0, 48.0));
346 assert_eq!(canvas_to_client(canvas, rect), client);
347 }
348
349 #[test]
350 fn canvas_position_applies_grab_snap_then_bounds() {
351 let p = canvas_position(
352 Point::new(107.0, 46.0),
353 Point::new(9.0, 8.0),
354 Some(SnapGrid(10.0)),
355 Some(Bounds {
356 width: 100.0,
357 height: 50.0,
358 }),
359 );
360
361 assert_eq!(p, Point::new(100.0, 40.0));
362 }
363
364 #[test]
365 fn canvas_keyboard_pointer_uses_center_element_by_default() {
366 assert_eq!(
367 canvas_keyboard_pointer(CanvasKeyboardPlacement::default(), Point::new(40.0, 20.0)),
368 Point::new(40.0, 20.0)
369 );
370 }
371
372 #[test]
373 fn canvas_keyboard_pointer_can_use_origin() {
374 assert_eq!(
375 canvas_keyboard_pointer(CanvasKeyboardPlacement::Origin, Point::new(40.0, 20.0)),
376 Point::default()
377 );
378 }
379
380 #[test]
381 fn canvas_keyboard_pointer_can_use_fixed_point() {
382 assert_eq!(
383 canvas_keyboard_pointer(
384 CanvasKeyboardPlacement::Fixed(Point::new(12.0, 18.0)),
385 Point::new(40.0, 20.0),
386 ),
387 Point::new(12.0, 18.0)
388 );
389 }
390
391 #[test]
392 fn item_clamp_composes_after_canvas_position() {
393 let top_left = canvas_position(Point::new(156.0, 86.0), Point::new(4.0, 5.0), None, None);
394 let constrained = Bounds {
395 width: 160.0,
396 height: 90.0,
397 }
398 .clamp_item(top_left, 48.0, 32.0);
399
400 assert_eq!(top_left, Point::new(152.0, 81.0));
401 assert_eq!(constrained, Point::new(112.0, 58.0));
402 }
403}