Skip to main content

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}