#![doc = include_str!("../docs/api/canvas.md")]
use std::rc::Rc;
use dioxus::prelude::*;
use crate::core::{
use_dnd, use_parent_zone, use_zone_id, use_zone_registry, DragMode, DropEffect, DropOutcome,
Point, Rect, ZoneId, ZoneRecord,
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct CanvasDrop<T> {
pub payload: T,
pub position: Point,
pub pointer: Point,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SnapGrid(pub f64);
impl SnapGrid {
pub fn snap(&self, p: Point) -> Point {
if self.0 <= 0.0 {
return p;
}
Point::new(
(p.x / self.0).round() * self.0,
(p.y / self.0).round() * self.0,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CanvasKeyboardPlacement {
#[default]
Center,
Origin,
Fixed(Point),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bounds {
pub width: f64,
pub height: f64,
}
impl Bounds {
pub fn clamp(&self, p: Point) -> Point {
Point::new(
clamp_axis(p.x, 0.0, self.width),
clamp_axis(p.y, 0.0, self.height),
)
}
pub fn clamp_item(&self, p: Point, width: f64, height: f64) -> Point {
Point::new(
clamp_axis(p.x, 0.0, self.width - width),
clamp_axis(p.y, 0.0, self.height - height),
)
}
pub fn clamp_rect(&self, rect: Rect) -> Point {
self.clamp_item(Point::new(rect.x, rect.y), rect.width, rect.height)
}
}
pub fn client_to_canvas(client: Point, canvas_rect: Rect) -> Point {
client - canvas_rect.origin()
}
pub fn canvas_to_client(point: Point, canvas_rect: Rect) -> Point {
point + canvas_rect.origin()
}
pub fn canvas_position(
pointer: Point,
grab: Point,
snap: Option<SnapGrid>,
bounds: Option<Bounds>,
) -> Point {
let mut position = pointer - grab;
if let Some(g) = snap {
position = g.snap(position);
}
if let Some(b) = bounds {
position = b.clamp(position);
}
position
}
pub fn canvas_keyboard_pointer(policy: CanvasKeyboardPlacement, element: Point) -> Point {
match policy {
CanvasKeyboardPlacement::Center => element,
CanvasKeyboardPlacement::Origin => Point::default(),
CanvasKeyboardPlacement::Fixed(point) => point,
}
}
fn clamp_axis(v: f64, min: f64, max: f64) -> f64 {
let lo = if min.is_nan() { f64::NEG_INFINITY } else { min };
let hi = if max.is_nan() { f64::INFINITY } else { max };
if lo > hi {
return if min.is_nan() { v } else { min };
}
v.clamp(lo, hi)
}
#[component]
pub fn CanvasDropZone<T: Clone + PartialEq + 'static>(
#[props(default)]
id: Option<ZoneId>,
#[props(default)]
snap: Option<SnapGrid>,
#[props(default)]
bounds: Option<Bounds>,
#[props(default)]
keyboard: CanvasKeyboardPlacement,
#[props(default)]
label: Option<String>,
on_drop: EventHandler<CanvasDrop<T>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let auto_id = use_zone_id();
let zone_id = id.unwrap_or(auto_id);
rsx! {
for keyed_zone_id in [zone_id] {
CanvasDropZoneInstance::<T> {
key: "{keyed_zone_id.0}",
zone_id: keyed_zone_id,
snap,
bounds,
keyboard,
label: label.clone(),
on_drop,
attributes: attributes.clone(),
{children.clone()}
}
}
}
}
#[component]
fn CanvasDropZoneInstance<T: Clone + PartialEq + 'static>(
zone_id: ZoneId,
snap: Option<SnapGrid>,
bounds: Option<Bounds>,
keyboard: CanvasKeyboardPlacement,
label: Option<String>,
on_drop: EventHandler<CanvasDrop<T>>,
attributes: Vec<Attribute>,
children: Element,
) -> Element {
let dnd = use_dnd::<T>();
let mut registry = use_zone_registry::<T>();
let parent = use_parent_zone();
let registered_drop = use_callback(move |o: DropOutcome<T>| {
let pointer = if o.mode == DragMode::Keyboard {
canvas_keyboard_pointer(keyboard, o.element)
} else {
o.element
};
let position = canvas_position(pointer, o.grab, snap, bounds);
on_drop.call(CanvasDrop {
payload: o.payload,
position,
pointer,
});
});
let registered_label = label.clone();
let registration = use_hook(|| {
registry.register(ZoneRecord {
id: zone_id,
parent,
label: registered_label.clone(),
on_drop: registered_drop,
accepts: None,
mounted: None,
rect: None,
})
});
use_drop(move || {
registry.unregister_registration(registration);
});
let label_for_sync = label.clone();
use_effect(use_reactive!(|(label_for_sync)| {
registry.sync_label(zone_id, label_for_sync);
}));
use_effect(use_reactive!(|(parent)| {
registry.sync_parent(registration, parent);
}));
let mut attributes = attributes;
crate::core::components::protect_attributes(&mut attributes, &["data-active", "onmounted"]);
rsx! {
div {
"data-active": if dnd.dragging() && dnd.proposed_effect() != DropEffect::None { "true" },
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
let mut registry = registry;
registry.set_mounted(registration, m.clone());
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
registry.set_rect_if_present(registration, Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
));
}
});
},
..attributes,
{children}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snap_and_clamp() {
let g = SnapGrid(10.0);
let p = g.snap(Point::new(14.9, 15.1));
assert_eq!((p.x, p.y), (10.0, 20.0));
assert_eq!(
SnapGrid(0.0).snap(Point::new(3.3, 4.4)),
Point::new(3.3, 4.4)
);
let b = Bounds {
width: 100.0,
height: 50.0,
};
let p = b.clamp(Point::new(-5.0, 999.0));
assert_eq!((p.x, p.y), (0.0, 50.0));
let corrected = Point::new(107.0, 46.0) - Point::new(9.0, 8.0);
let positioned = b.clamp(g.snap(corrected));
assert_eq!(positioned, Point::new(100.0, 40.0));
}
#[test]
fn clamp_does_not_panic_on_non_finite_bounds() {
let b = Bounds {
width: f64::NAN,
height: f64::INFINITY,
};
let p = b.clamp(Point::new(25.0, 25.0));
assert_eq!((p.x, p.y), (25.0, 25.0), "unconstrained, unchanged");
let p = b.clamp(Point::new(-10.0, -10.0));
assert_eq!((p.x, p.y), (0.0, 0.0), "still floored at the origin");
let q = b.clamp_item(Point::new(-3.0, 10.0), 5.0, 5.0);
assert_eq!(q.x, 0.0);
}
#[test]
fn bounds_can_clamp_whole_items() {
let b = Bounds {
width: 100.0,
height: 50.0,
};
assert_eq!(
b.clamp_item(Point::new(90.0, 45.0), 20.0, 12.0),
Point::new(80.0, 38.0)
);
assert_eq!(
b.clamp_rect(Rect::new(-5.0, 60.0, 20.0, 10.0)),
Point::new(0.0, 40.0)
);
assert_eq!(
b.clamp_item(Point::new(20.0, 20.0), 150.0, 80.0),
Point::new(0.0, 0.0)
);
}
#[test]
fn coordinate_helpers_convert_between_client_and_canvas() {
let rect = Rect::new(40.0, 80.0, 320.0, 200.0);
let client = Point::new(64.0, 128.0);
let canvas = client_to_canvas(client, rect);
assert_eq!(canvas, Point::new(24.0, 48.0));
assert_eq!(canvas_to_client(canvas, rect), client);
}
#[test]
fn canvas_position_applies_grab_snap_then_bounds() {
let p = canvas_position(
Point::new(107.0, 46.0),
Point::new(9.0, 8.0),
Some(SnapGrid(10.0)),
Some(Bounds {
width: 100.0,
height: 50.0,
}),
);
assert_eq!(p, Point::new(100.0, 40.0));
}
#[test]
fn canvas_keyboard_pointer_uses_center_element_by_default() {
assert_eq!(
canvas_keyboard_pointer(CanvasKeyboardPlacement::default(), Point::new(40.0, 20.0)),
Point::new(40.0, 20.0)
);
}
#[test]
fn canvas_keyboard_pointer_can_use_origin() {
assert_eq!(
canvas_keyboard_pointer(CanvasKeyboardPlacement::Origin, Point::new(40.0, 20.0)),
Point::default()
);
}
#[test]
fn canvas_keyboard_pointer_can_use_fixed_point() {
assert_eq!(
canvas_keyboard_pointer(
CanvasKeyboardPlacement::Fixed(Point::new(12.0, 18.0)),
Point::new(40.0, 20.0),
),
Point::new(12.0, 18.0)
);
}
#[test]
fn item_clamp_composes_after_canvas_position() {
let top_left = canvas_position(Point::new(156.0, 86.0), Point::new(4.0, 5.0), None, None);
let constrained = Bounds {
width: 160.0,
height: 90.0,
}
.clamp_item(top_left, 48.0, 32.0);
assert_eq!(top_left, Point::new(152.0, 81.0));
assert_eq!(constrained, Point::new(112.0, 58.0));
}
}