dioxus_dnd/core/modifiers.rs
1//! Composable drag constraints, applied as a chain to a proposed position.
2//!
3//! Where [`crate::canvas`] bakes snap-and-clamp into one component, this
4//! module generalizes the idea (in the spirit of dnd-kit's modifiers): each
5//! [`DragModifier`] is a pure `Point → Point` transform, and a chain feeds
6//! each output into the next.
7//!
8//! ```rust
9//! use dioxus_dnd::core::{apply_modifiers, DragModifier, ModifierCtx, Point, Rect};
10//!
11//! let ctx = ModifierCtx {
12//! container: Some(Rect::new(0.0, 0.0, 400.0, 300.0)),
13//! element: Some(Rect::new(0.0, 0.0, 40.0, 40.0)),
14//! };
15//! let chain = [
16//! DragModifier::LockAxis { horizontal: false, vertical: true },
17//! DragModifier::Snap { x: 8.0, y: 8.0 },
18//! DragModifier::KeepInside,
19//! ];
20//! let p = apply_modifiers(&chain, Point::new(123.0, 999.0), &ctx);
21//! assert_eq!((p.x, p.y), (0.0, 260.0));
22//! ```
23
24use super::types::{Point, Rect};
25
26/// Geometry a modifier may need. Fields it doesn't need can stay `None`.
27#[derive(Debug, Clone, Copy, PartialEq, Default)]
28pub struct ModifierCtx {
29 /// The container the element should stay inside (for [`DragModifier::KeepInside`]).
30 pub container: Option<Rect>,
31 /// The dragged element's size, positioned at the proposed point.
32 pub element: Option<Rect>,
33}
34
35/// A single constraint on a proposed drag position.
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub enum DragModifier {
38 /// Zero out movement on locked axes. `horizontal: false` freezes X.
39 LockAxis {
40 /// Allow horizontal movement.
41 horizontal: bool,
42 /// Allow vertical movement.
43 vertical: bool,
44 },
45 /// Snap each axis to a grid step; a step `<= 0` leaves that axis alone.
46 Snap { x: f64, y: f64 },
47 /// Clamp so the element (its `ModifierCtx::element` size) stays inside
48 /// `ModifierCtx::container`. No-op when either rect is missing. If the
49 /// element is larger than the container on an axis, it pins to the
50 /// container's origin on that axis.
51 KeepInside,
52}
53
54impl DragModifier {
55 /// Apply this modifier to a proposed top-left position.
56 pub fn apply(self, p: Point, ctx: &ModifierCtx) -> Point {
57 match self {
58 DragModifier::LockAxis {
59 horizontal,
60 vertical,
61 } => Point::new(
62 if horizontal { p.x } else { 0.0 },
63 if vertical { p.y } else { 0.0 },
64 ),
65 DragModifier::Snap { x, y } => Point::new(snap(p.x, x), snap(p.y, y)),
66 DragModifier::KeepInside => {
67 let (Some(c), Some(e)) = (ctx.container, ctx.element) else {
68 return p;
69 };
70 Point::new(
71 clamp_axis(p.x, c.x, c.x + c.width - e.width),
72 clamp_axis(p.y, c.y, c.y + c.height - e.height),
73 )
74 }
75 }
76 }
77}
78
79/// Run a chain of modifiers in order over a proposed position.
80pub fn apply_modifiers(chain: &[DragModifier], mut p: Point, ctx: &ModifierCtx) -> Point {
81 for m in chain {
82 p = m.apply(p, ctx);
83 }
84 p
85}
86
87fn snap(v: f64, step: f64) -> f64 {
88 if step > 0.0 {
89 (v / step).round() * step
90 } else {
91 v
92 }
93}
94
95fn clamp_axis(v: f64, min: f64, max: f64) -> f64 {
96 if min > max {
97 min // element larger than container: pin to origin
98 } else {
99 v.clamp(min, max)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn modifiers_compose_in_order() {
109 let ctx = ModifierCtx {
110 container: Some(Rect::new(0.0, 0.0, 100.0, 100.0)),
111 element: Some(Rect::new(0.0, 0.0, 30.0, 30.0)),
112 };
113 // lock X, snap Y to 20, keep inside
114 let chain = [
115 DragModifier::LockAxis {
116 horizontal: false,
117 vertical: true,
118 },
119 DragModifier::Snap { x: 0.0, y: 20.0 },
120 DragModifier::KeepInside,
121 ];
122 let p = apply_modifiers(&chain, Point::new(55.0, 91.0), &ctx);
123 assert_eq!((p.x, p.y), (0.0, 70.0)); // x frozen, y 91→100(snap)→70(clamp)
124 }
125
126 #[test]
127 fn keep_inside_pins_oversized_elements() {
128 let ctx = ModifierCtx {
129 container: Some(Rect::new(10.0, 10.0, 50.0, 50.0)),
130 element: Some(Rect::new(0.0, 0.0, 200.0, 20.0)), // wider than container
131 };
132 let p = DragModifier::KeepInside.apply(Point::new(-40.0, 100.0), &ctx);
133 assert_eq!((p.x, p.y), (10.0, 40.0));
134 }
135
136 #[test]
137 fn keep_inside_without_rects_is_noop() {
138 let p = DragModifier::KeepInside.apply(Point::new(7.0, 8.0), &ModifierCtx::default());
139 assert_eq!((p.x, p.y), (7.0, 8.0));
140 }
141}