Skip to main content

armas_basic/animation/
velocity_drag.rs

1//! Velocity-based drag control for fine parameter adjustment
2//!
3//! Provides a drag mode where faster mouse movement creates larger value changes,
4//! allowing for both quick coarse adjustments and precise fine-tuning.
5
6use egui::Modifiers;
7
8/// Configuration for velocity-based dragging
9#[derive(Debug, Clone)]
10pub struct VelocityDragConfig {
11    /// Sensitivity multiplier (higher = more responsive). Default: 1.0
12    pub sensitivity: f64,
13    /// Minimum pixel movement before registering as drag. Default: 1
14    pub threshold: i32,
15    /// Offset added to velocity calculation (higher = faster minimum speed). Default: 0.0
16    pub offset: f64,
17    /// Whether user can toggle velocity mode with modifier key. Default: true
18    pub allow_modifier_toggle: bool,
19    /// Modifier keys that toggle velocity mode. Default: Ctrl/Cmd
20    pub toggle_modifiers: Modifiers,
21}
22
23impl Default for VelocityDragConfig {
24    fn default() -> Self {
25        Self {
26            sensitivity: 1.0,
27            threshold: 1,
28            offset: 0.0,
29            allow_modifier_toggle: true,
30            toggle_modifiers: Modifiers::COMMAND | Modifiers::CTRL,
31        }
32    }
33}
34
35impl VelocityDragConfig {
36    /// Create a new velocity drag configuration
37    #[must_use]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Set sensitivity (higher = more responsive to mouse speed)
43    #[must_use]
44    pub const fn sensitivity(mut self, sensitivity: f64) -> Self {
45        self.sensitivity = sensitivity.max(0.1);
46        self
47    }
48
49    /// Set the minimum pixel threshold for drag detection
50    #[must_use]
51    pub fn threshold(mut self, threshold: i32) -> Self {
52        self.threshold = threshold.max(1);
53        self
54    }
55
56    /// Set the velocity offset (minimum speed)
57    #[must_use]
58    pub const fn offset(mut self, offset: f64) -> Self {
59        self.offset = offset.max(0.0);
60        self
61    }
62
63    /// Enable/disable modifier key toggle for velocity mode
64    #[must_use]
65    pub const fn allow_modifier_toggle(mut self, allow: bool) -> Self {
66        self.allow_modifier_toggle = allow;
67        self
68    }
69
70    /// Set which modifier keys toggle velocity mode
71    #[must_use]
72    pub const fn toggle_modifiers(mut self, modifiers: Modifiers) -> Self {
73        self.toggle_modifiers = modifiers;
74        self
75    }
76}
77
78/// Drag mode for parameter adjustment
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum DragMode {
81    /// No drag in progress
82    #[default]
83    None,
84    /// Absolute drag - value jumps to mouse position
85    Absolute,
86    /// Velocity drag - value changes based on mouse speed
87    Velocity,
88}
89
90/// State for velocity-based dragging
91///
92/// This helper manages drag state for sliders, knobs, or any parameter control
93/// that benefits from velocity-sensitive adjustment.
94///
95/// # Example
96/// ```ignore
97/// use armas_basic::animation::{VelocityDrag, VelocityDragConfig, DragMode};
98///
99/// let mut drag = VelocityDrag::new(VelocityDragConfig::new().sensitivity(1.5));
100///
101/// // On mouse down:
102/// drag.begin(current_value, mouse_y, use_velocity_mode);
103///
104/// // On mouse drag:
105/// let delta = drag.update(mouse_y, value_range);
106/// value += delta;
107///
108/// // On mouse up:
109/// drag.end();
110/// ```
111#[derive(Debug, Clone)]
112pub struct VelocityDrag {
113    config: VelocityDragConfig,
114    mode: DragMode,
115    start_value: f64,
116    start_pos: f64,
117    last_pos: f64,
118    accumulated_delta: f64,
119}
120
121impl VelocityDrag {
122    /// Create a new velocity drag helper with the given configuration
123    #[must_use]
124    pub const fn new(config: VelocityDragConfig) -> Self {
125        Self {
126            config,
127            mode: DragMode::None,
128            start_value: 0.0,
129            start_pos: 0.0,
130            last_pos: 0.0,
131            accumulated_delta: 0.0,
132        }
133    }
134
135    /// Create with default configuration
136    #[must_use]
137    pub fn with_defaults() -> Self {
138        Self::new(VelocityDragConfig::default())
139    }
140
141    /// Get the current drag mode
142    #[must_use]
143    pub const fn mode(&self) -> DragMode {
144        self.mode
145    }
146
147    /// Check if a drag is in progress
148    #[must_use]
149    pub fn is_dragging(&self) -> bool {
150        self.mode != DragMode::None
151    }
152
153    /// Check if velocity mode is active
154    #[must_use]
155    pub fn is_velocity_mode(&self) -> bool {
156        self.mode == DragMode::Velocity
157    }
158
159    /// Check if absolute mode is active
160    #[must_use]
161    pub fn is_absolute_mode(&self) -> bool {
162        self.mode == DragMode::Absolute
163    }
164
165    /// Begin a drag operation
166    ///
167    /// - `current_value`: The current parameter value
168    /// - `mouse_pos`: Current mouse position (typically Y for vertical, X for horizontal)
169    /// - `use_velocity_mode`: Whether to use velocity mode (can be toggled by modifier)
170    pub const fn begin(&mut self, current_value: f64, mouse_pos: f64, use_velocity_mode: bool) {
171        self.start_value = current_value;
172        self.start_pos = mouse_pos;
173        self.last_pos = mouse_pos;
174        self.accumulated_delta = 0.0;
175        self.mode = if use_velocity_mode {
176            DragMode::Velocity
177        } else {
178            DragMode::Absolute
179        };
180    }
181
182    /// Begin drag, automatically choosing mode based on modifier keys
183    ///
184    /// If `default_velocity_mode` is true, velocity mode is the default and
185    /// modifier keys switch to absolute. Otherwise, absolute is default.
186    pub fn begin_auto(
187        &mut self,
188        current_value: f64,
189        mouse_pos: f64,
190        modifiers: &Modifiers,
191        default_velocity_mode: bool,
192    ) {
193        let modifier_pressed = self.config.allow_modifier_toggle
194            && modifiers.matches_logically(self.config.toggle_modifiers);
195
196        let use_velocity = if default_velocity_mode {
197            !modifier_pressed
198        } else {
199            modifier_pressed
200        };
201
202        self.begin(current_value, mouse_pos, use_velocity);
203    }
204
205    /// Update during drag and return the value delta
206    ///
207    /// - `mouse_pos`: Current mouse position
208    /// - `value_range`: Total range of the parameter (max - min)
209    /// - `drag_pixels`: Number of pixels for full range in absolute mode
210    ///
211    /// Returns the delta to add to the current value
212    pub fn update(&mut self, mouse_pos: f64, value_range: f64, drag_pixels: f64) -> f64 {
213        if self.mode == DragMode::None {
214            return 0.0;
215        }
216
217        let pixel_delta = mouse_pos - self.last_pos;
218        self.last_pos = mouse_pos;
219
220        // Check threshold
221        if pixel_delta.abs() < f64::from(self.config.threshold) {
222            return 0.0;
223        }
224
225        match self.mode {
226            DragMode::Absolute => {
227                // Linear mapping: pixels -> value
228                let total_delta = mouse_pos - self.start_pos;
229                let value_per_pixel = value_range / drag_pixels;
230                let target_value = self.start_value + total_delta * value_per_pixel;
231                target_value - (self.start_value + self.accumulated_delta)
232            }
233            DragMode::Velocity => {
234                // Velocity-based: faster movement = larger change
235                let speed = pixel_delta.abs();
236                let sign = pixel_delta.signum();
237
238                // Apply sensitivity and offset
239                let velocity = (speed * self.config.sensitivity + self.config.offset) * sign;
240
241                // Scale to value range (assume ~200px for full range as baseline)
242                let value_per_unit = value_range / 200.0;
243                velocity * value_per_unit
244            }
245            DragMode::None => 0.0,
246        }
247    }
248
249    /// Update and track accumulated delta
250    ///
251    /// Same as `update()` but also tracks the total change for absolute mode
252    pub fn update_tracked(&mut self, mouse_pos: f64, value_range: f64, drag_pixels: f64) -> f64 {
253        let delta = self.update(mouse_pos, value_range, drag_pixels);
254        self.accumulated_delta += delta;
255        delta
256    }
257
258    /// End the drag operation
259    pub const fn end(&mut self) {
260        self.mode = DragMode::None;
261    }
262
263    /// Get the start value when drag began
264    #[must_use]
265    pub const fn start_value(&self) -> f64 {
266        self.start_value
267    }
268
269    /// Get the total accumulated delta since drag began
270    #[must_use]
271    pub const fn accumulated_delta(&self) -> f64 {
272        self.accumulated_delta
273    }
274}
275
276/// Double-click to reset functionality
277#[derive(Debug, Clone)]
278pub struct DoubleClickReset {
279    /// Whether double-click reset is enabled
280    pub enabled: bool,
281    /// The default value to reset to
282    pub default_value: f64,
283    /// Time window for double-click detection (seconds)
284    pub double_click_time: f64,
285    /// Last click time
286    last_click: f64,
287}
288
289impl DoubleClickReset {
290    /// Create a new double-click reset helper
291    #[must_use]
292    pub const fn new(default_value: f64) -> Self {
293        Self {
294            enabled: true,
295            default_value,
296            double_click_time: 0.3,
297            last_click: 0.0,
298        }
299    }
300
301    /// Enable or disable the feature
302    #[must_use]
303    pub const fn enabled(mut self, enabled: bool) -> Self {
304        self.enabled = enabled;
305        self
306    }
307
308    /// Set the default value to reset to
309    #[must_use]
310    pub const fn default_value(mut self, value: f64) -> Self {
311        self.default_value = value;
312        self
313    }
314
315    /// Set the double-click time window
316    #[must_use]
317    pub const fn double_click_time(mut self, seconds: f64) -> Self {
318        self.double_click_time = seconds.max(0.1);
319        self
320    }
321
322    /// Handle a click and return true if it was a double-click
323    ///
324    /// `current_time` should be the current time in seconds
325    pub fn on_click(&mut self, current_time: f64) -> bool {
326        if !self.enabled {
327            return false;
328        }
329
330        let is_double_click = (current_time - self.last_click) < self.double_click_time;
331        self.last_click = current_time;
332        is_double_click
333    }
334
335    /// Get the value to reset to
336    #[must_use]
337    pub const fn reset_value(&self) -> f64 {
338        self.default_value
339    }
340}