Skip to main content

dioxus_dnd/core/
activation.rs

1//! Reusable activation policy for pointer and keyboard drag sources.
2
3/// Which element is allowed to activate a drag.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5#[non_exhaustive]
6pub enum Activator {
7    /// The whole draggable surface activates the drag.
8    #[default]
9    Surface,
10    /// Only a nested [`crate::core::DragHandle`] activates the drag.
11    Handle,
12    /// No built-in pointer or keyboard input activates the drag.
13    Manual,
14}
15
16/// A condition that promotes a pointer press into a drag.
17#[derive(Debug, Clone, PartialEq)]
18#[non_exhaustive]
19pub enum ActivationConstraint {
20    /// Promote after travelling this many CSS pixels.
21    Distance(f64),
22    /// Promote after a delay, provided movement stays inside `tolerance`.
23    Delay { duration_ms: u32, tolerance: f64 },
24    /// Promote when any contained constraint succeeds.
25    Either(Vec<ActivationConstraint>),
26    /// Never promote automatically. Custom code may start the drag through
27    /// [`crate::core::DndContext`].
28    Manual,
29}
30
31impl Default for ActivationConstraint {
32    fn default() -> Self {
33        Self::Distance(8.0)
34    }
35}
36
37impl ActivationConstraint {
38    fn collect_delays(&self, delays: &mut Vec<(u32, f64)>) {
39        match self {
40            Self::Delay {
41                duration_ms,
42                tolerance,
43            } => delays.push((*duration_ms, tolerance.max(0.0))),
44            Self::Either(items) => {
45                for item in items {
46                    item.collect_delays(delays);
47                }
48            }
49            Self::Distance(_) | Self::Manual => {}
50        }
51    }
52
53    pub(crate) fn delays(&self) -> Vec<(u32, f64)> {
54        let mut delays = Vec::new();
55        self.collect_delays(&mut delays);
56        delays
57    }
58
59    /// Smallest configured distance threshold, when one exists.
60    pub fn distance(&self) -> Option<f64> {
61        match self {
62            Self::Distance(distance) => Some(distance.max(0.0)),
63            Self::Either(items) => items
64                .iter()
65                .filter_map(Self::distance)
66                .min_by(f64::total_cmp),
67            Self::Delay { .. } | Self::Manual => None,
68        }
69    }
70
71    /// Shortest configured delay and its movement tolerance.
72    pub fn delay(&self) -> Option<(u32, f64)> {
73        self.delays()
74            .into_iter()
75            .min_by_key(|(duration, _)| *duration)
76    }
77
78    /// Whether no built-in event may promote this policy.
79    pub fn is_manual(&self) -> bool {
80        match self {
81            Self::Manual => true,
82            // An empty disjunction can never promote, just like `Manual`.
83            Self::Either(items) => items.iter().all(Self::is_manual),
84            Self::Distance(_) | Self::Delay { .. } => false,
85        }
86    }
87
88    /// Whether a movement delta exceeded a delay constraint's tolerance.
89    pub fn exceeded_delay_tolerance(&self, dx: f64, dy: f64) -> bool {
90        let distance = dx.hypot(dy);
91        let delays = self.delays();
92        !delays.is_empty() && delays.iter().all(|(_, tolerance)| distance > *tolerance)
93    }
94}
95
96/// Complete activation policy for a draggable source.
97#[derive(Debug, Clone, PartialEq)]
98#[non_exhaustive]
99pub struct ActivationPolicy {
100    pub activator: Activator,
101    pub constraint: ActivationConstraint,
102}
103
104impl Default for ActivationPolicy {
105    fn default() -> Self {
106        Self {
107            activator: Activator::Surface,
108            constraint: ActivationConstraint::default(),
109        }
110    }
111}
112
113impl ActivationPolicy {
114    pub fn surface(constraint: ActivationConstraint) -> Self {
115        Self {
116            activator: Activator::Surface,
117            constraint,
118        }
119    }
120
121    pub fn handle(constraint: ActivationConstraint) -> Self {
122        Self {
123            activator: Activator::Handle,
124            constraint,
125        }
126    }
127
128    pub fn manual() -> Self {
129        Self {
130            activator: Activator::Manual,
131            constraint: ActivationConstraint::Manual,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn either_exposes_earliest_distance_and_delay() {
142        let policy = ActivationConstraint::Either(vec![
143            ActivationConstraint::Distance(12.0),
144            ActivationConstraint::Delay {
145                duration_ms: 300,
146                tolerance: 5.0,
147            },
148            ActivationConstraint::Distance(7.0),
149            ActivationConstraint::Delay {
150                duration_ms: 180,
151                tolerance: 3.0,
152            },
153        ]);
154
155        assert_eq!(policy.distance(), Some(7.0));
156        assert_eq!(policy.delay(), Some((180, 3.0)));
157        assert!(!policy.exceeded_delay_tolerance(4.0, 0.0));
158        assert!(policy.exceeded_delay_tolerance(6.0, 0.0));
159        assert_eq!(
160            policy.delays(),
161            vec![(300, 5.0), (180, 3.0)],
162            "nested delay alternatives retain their independent clocks and tolerances"
163        );
164    }
165
166    #[test]
167    fn empty_or_manual_only_disjunctions_are_manual() {
168        assert!(ActivationConstraint::Either(Vec::new()).is_manual());
169        assert!(ActivationConstraint::Either(vec![
170            ActivationConstraint::Manual,
171            ActivationConstraint::Either(Vec::new()),
172        ])
173        .is_manual());
174    }
175}