Skip to main content

gpui_ce_motion/
interaction.rs

1//! Explicit variants, gesture precedence, drag, scroll mapping, and viewport checks.
2use crate::{AnimationError, Frame, Inertia, Motion, MotionValue, Target, Transition};
3use std::time::Duration;
4
5/// Named declarations. Keys can be application enums; propagation is explicit in Rust.
6#[derive(Debug)]
7pub struct Variants<K> {
8    entries: Vec<(K, Motion)>,
9}
10
11impl<K: PartialEq> Variants<K> {
12    /// Creates named declarations, rejecting duplicate names transactionally.
13    pub fn new(entries: impl IntoIterator<Item = (K, Motion)>) -> Result<Self, AnimationError> {
14        let entries: Vec<_> = entries.into_iter().collect();
15
16        if entries
17            .iter()
18            .enumerate()
19            .any(|(ix, (key, _))| entries[..ix].iter().any(|(old, _)| old == key))
20        {
21            return Err(AnimationError::Label);
22        }
23
24        Ok(Self { entries })
25    }
26
27    /// Looks up a declaration. Pass the same key to children for variant propagation.
28    pub fn get(&self, key: &K) -> Option<Motion> {
29        self.entries
30            .iter()
31            .find(|(k, _)| k == key)
32            .map(|(_, motion)| *motion)
33    }
34}
35
36/// Current input state supplied by normal native input/focus handlers.
37#[derive(Clone, Copy, Debug, Default)]
38#[non_exhaustive]
39pub struct Interaction {
40    /// Pointer is inside the native hit region.
41    pub hovered: bool,
42    /// A pointer or keyboard press is active.
43    pub pressed: bool,
44    /// The native control has keyboard-visible focus.
45    pub focused: bool,
46    /// A drag is active.
47    pub dragging: bool,
48    /// The control meets the configured viewport visibility threshold.
49    pub in_view: bool,
50}
51
52/// Partial interaction targets. Priority is in-view, hover, focus, press, then drag.
53#[derive(Clone, Copy, Debug, Default)]
54pub struct GestureTargets {
55    hover: Option<Target>,
56    press: Option<Target>,
57    focus: Option<Target>,
58    drag: Option<Target>,
59    view: Option<Target>,
60}
61
62impl GestureTargets {
63    /// Declares the hover override.
64    pub fn while_hover(mut self, target: Target) -> Self {
65        self.hover = Some(target);
66
67        self
68    }
69
70    /// Declares the pointer/keyboard press override.
71    pub fn while_press(mut self, target: Target) -> Self {
72        self.press = Some(target);
73
74        self
75    }
76
77    /// Declares the keyboard-visible focus override.
78    pub fn while_focus(mut self, target: Target) -> Self {
79        self.focus = Some(target);
80
81        self
82    }
83
84    /// Declares the dragging override.
85    pub fn while_drag(mut self, target: Target) -> Self {
86        self.drag = Some(target);
87
88        self
89    }
90
91    /// Declares the viewport visibility override.
92    pub fn while_in_view(mut self, target: Target) -> Self {
93        self.view = Some(target);
94
95        self
96    }
97
98    /// Combines active overrides without replacing unrelated base properties.
99    pub fn resolve(self, mut base: Target, state: Interaction) -> Target {
100        for (active, target) in [
101            (state.in_view, self.view),
102            (state.hovered, self.hover),
103            (state.focused, self.focus),
104            (state.pressed, self.press),
105            (state.dragging, self.drag),
106        ] {
107            if active && let Some(target) = target {
108                base = base.overlay(target);
109            }
110        }
111
112        base
113    }
114}
115
116/// Bounded two-dimensional drag with explicit ownership of pointer capture/cancellation.
117#[derive(Debug)]
118pub struct Drag {
119    values: [MotionValue; 2],
120    min: [f64; 2],
121    max: [f64; 2],
122    origin: [f64; 2],
123    pointer: [f64; 2],
124    dragging: bool,
125    last_move: Duration,
126    elasticity: f64,
127    snap: Option<f64>,
128    momentum: bool,
129    direction_lock: Option<f64>,
130    locked_axis: Option<usize>,
131}
132
133impl Drag {
134    /// Validates finite ordered bounds and an initial position inside them.
135    pub fn new(initial: [f64; 2], min: [f64; 2], max: [f64; 2]) -> Self {
136        assert!(
137            (0..2).all(|i| initial[i].is_finite()
138                && min[i].is_finite()
139                && max[i].is_finite()
140                && min[i] <= initial[i]
141                && initial[i] <= max[i]),
142            "invalid drag bounds"
143        );
144
145        Self {
146            values: initial.map(MotionValue::new),
147            min,
148            max,
149            origin: initial,
150            pointer: [0.0; 2],
151            dragging: false,
152            last_move: Duration::ZERO,
153            elasticity: 0.15,
154            snap: None,
155            momentum: true,
156            direction_lock: None,
157            locked_axis: None,
158        }
159    }
160
161    /// Creates a drag without user constraints, bounded only by drawable f32
162    /// offsets. Add measured container constraints in a native adapter if needed.
163    pub fn unconstrained(initial: [f64; 2]) -> Self {
164        Self::new(initial, [f64::from(f32::MIN); 2], [f64::from(f32::MAX); 2])
165    }
166
167    /// Sets drag resistance outside bounds. Panics unless in zero through one.
168    pub fn elasticity(mut self, value: f64) -> Self {
169        assert!((0.0..=1.0).contains(&value), "invalid elasticity");
170        self.elasticity = value;
171
172        self
173    }
174
175    /// Snaps release destinations to this positive finite grid spacing.
176    pub fn snap_grid(mut self, spacing: f64) -> Self {
177        assert!(spacing.is_finite() && spacing > 0.0, "invalid snap grid");
178        self.snap = Some(spacing);
179
180        self
181    }
182
183    /// Enables/disables velocity projection on release.
184    pub fn momentum(mut self, enabled: bool) -> Self {
185        self.momentum = enabled;
186
187        self
188    }
189
190    /// Locks to the first dominant axis after this positive pixel threshold.
191    /// The threshold suppresses jitter before ownership is chosen.
192    pub fn direction_lock(mut self, threshold: f64) -> Self {
193        assert!(
194            threshold.is_finite() && threshold > 0.0,
195            "invalid direction threshold"
196        );
197        self.direction_lock = Some(threshold);
198
199        self
200    }
201
202    /// Current dynamically chosen axis: zero horizontal, one vertical.
203    pub fn locked_axis(&self) -> Option<usize> {
204        self.locked_axis
205    }
206
207    /// Updates measured constraints without jumping the presentation. During a
208    /// pointer gesture resistance uses the new bounds; released values spring back.
209    pub fn set_bounds(&mut self, min: [f64; 2], max: [f64; 2], now: Duration) {
210        assert!(
211            (0..2).all(|i| min[i].is_finite() && max[i].is_finite() && min[i] <= max[i]),
212            "invalid drag bounds"
213        );
214
215        if self.min == min && self.max == max {
216            return;
217        }
218
219        self.min = min;
220        self.max = max;
221
222        if !self.dragging {
223            for i in 0..2 {
224                let target = self.values[i].target().clamp(min[i], max[i]);
225
226                self.values[i].animate(target, Transition::default(), now);
227            }
228        }
229    }
230
231    /// Last presented position and velocity, without advancing time.
232    pub fn presentation(&self) -> ([f64; 2], [f64; 2]) {
233        (
234            std::array::from_fn(|i| self.values[i].get()),
235            std::array::from_fn(|i| self.values[i].velocity()),
236        )
237    }
238
239    /// Grabs the currently presented position, interrupting any previous release motion.
240    pub fn begin(&mut self, pointer: [f64; 2], now: Duration) {
241        assert!(pointer.iter().all(|v| v.is_finite()), "invalid pointer");
242
243        for ix in 0..2 {
244            let value = self.values[ix].sample(now, false).value();
245
246            self.values[ix].jump(value, now);
247            self.origin[ix] = value;
248        }
249
250        self.pointer = pointer;
251        self.locked_axis = None;
252        self.last_move = now;
253        self.dragging = true;
254    }
255
256    /// Follows the pointer one-to-one inside bounds, with configured resistance outside.
257    pub fn move_to(&mut self, pointer: [f64; 2], now: Duration) {
258        assert!(pointer.iter().all(|v| v.is_finite()), "invalid pointer");
259
260        if !self.dragging {
261            return;
262        }
263
264        self.last_move = self.last_move.max(now);
265
266        if let Some(threshold) = self.direction_lock
267            && self.locked_axis.is_none()
268        {
269            let delta = std::array::from_fn::<_, 2, _>(|i| (pointer[i] - self.pointer[i]).abs());
270
271            if delta[0].max(delta[1]) < threshold {
272                return;
273            }
274
275            self.locked_axis = Some(usize::from(delta[1] > delta[0]));
276        }
277
278        for (ix, point) in pointer.into_iter().enumerate() {
279            if self.locked_axis.is_some_and(|axis| axis != ix) {
280                continue;
281            }
282
283            let raw = crate::value::finite(
284                self.origin[ix] + crate::value::finite(point - self.pointer[ix]),
285            );
286
287            let clamped = raw.clamp(self.min[ix], self.max[ix]);
288
289            self.values[ix].set(clamped + (raw - clamped) * self.elasticity, now);
290        }
291    }
292
293    /// Releases into bounded inertia, or a velocity-preserving spring to a snap point.
294    /// Holding still for over 100 ms clears stale pointer momentum before release.
295    pub fn end(&mut self, now: Duration) {
296        if !self.dragging {
297            return;
298        }
299
300        self.dragging = false;
301
302        if now.saturating_sub(self.last_move) > Duration::from_millis(100) {
303            for value in &mut self.values {
304                value.jump(value.get(), now);
305            }
306        }
307
308        for ix in 0..2 {
309            let inertia = Inertia::default().bounds(self.min[ix], self.max[ix]);
310
311            if let Some(grid) = self.snap {
312                let projected = if self.momentum {
313                    inertia.project(self.values[ix].get(), self.values[ix].velocity())
314                } else {
315                    self.values[ix].get()
316                };
317
318                let target = (projected / grid)
319                    .round()
320                    .mul_add(grid, 0.0)
321                    .clamp(self.min[ix], self.max[ix]);
322
323                self.values[ix].animate(target, Transition::default(), now);
324            } else if self.momentum {
325                self.values[ix].coast(inertia, now);
326            } else {
327                let target = self.values[ix].get().clamp(self.min[ix], self.max[ix]);
328
329                self.values[ix].animate(target, Transition::default(), now);
330            }
331        }
332    }
333
334    /// Cancels an active drag and returns to the grabbed position. Use on Escape or lost capture.
335    /// Does nothing after release, so later focus loss cannot undo a completed gesture.
336    pub fn cancel(&mut self, now: Duration) {
337        if !self.dragging {
338            return;
339        }
340
341        self.dragging = false;
342
343        for ix in 0..2 {
344            self.values[ix].animate(
345                self.origin[ix].clamp(self.min[ix], self.max[ix]),
346                Transition::default(),
347                now,
348            );
349        }
350    }
351
352    /// A keyboard equivalent of dragging: retargets a bounded position using a spring.
353    pub fn move_by(&mut self, delta: [f64; 2], now: Duration) {
354        assert!(delta.iter().all(|v| v.is_finite()), "invalid drag delta");
355        self.dragging = false;
356
357        for (ix, delta) in delta.into_iter().enumerate() {
358            let target = (self.values[ix].target() + delta).clamp(self.min[ix], self.max[ix]);
359
360            self.values[ix].animate(target, Transition::default(), now);
361        }
362    }
363
364    /// Samples a drawable x/y presentation. The owner requests frames while it is active.
365    pub fn sample(&mut self, now: Duration, reduced_motion: bool) -> Frame {
366        let values = self
367            .values
368            .each_mut()
369            .map(|value| value.sample(now, reduced_motion));
370
371        Frame::animated(
372            Target::default()
373                .x(values[0].value().clamp(-(f32::MAX as f64), f32::MAX as f64) as f32)
374                .y(values[1].value().clamp(-(f32::MAX as f64), f32::MAX as f64) as f32),
375            [
376                values[0].velocity(),
377                values[1].velocity(),
378                0.0,
379                0.0,
380                0.0,
381                0.0,
382            ],
383            values.iter().any(|v| v.is_animating()),
384            values.iter().any(|v| v.just_completed()) && values.iter().all(|v| !v.is_animating()),
385        )
386    }
387
388    /// Whether a pointer currently owns this gesture.
389    pub fn is_dragging(&self) -> bool {
390        self.dragging
391    }
392}
393
394/// Maps a finite scalar between ranges, optionally clamping progress.
395/// Supports reversed input/output ranges. Panics for non-finite or zero input span.
396pub fn transform(value: f64, input: [f64; 2], output: [f64; 2], clamp: bool) -> f64 {
397    assert!(
398        [value, input[0], input[1], output[0], output[1]]
399            .iter()
400            .all(|v| v.is_finite())
401            && input[0] != input[1],
402        "invalid transform range"
403    );
404    let progress = (value - input[0]) / (input[1] - input[0]);
405
406    let progress = if clamp {
407        progress.clamp(0.0, 1.0)
408    } else {
409        progress
410    };
411
412    output[0] + (output[1] - output[0]) * progress
413}
414
415/// Normalizes a positive scroll offset using measured content and viewport extents.
416/// An unscrollable viewport reports zero. Panics for invalid metrics.
417pub fn scroll_progress(offset: f64, content: f64, viewport: f64) -> f64 {
418    assert!(
419        [offset, content, viewport].iter().all(|v| v.is_finite())
420            && content >= 0.0
421            && viewport >= 0.0,
422        "invalid scroll metrics"
423    );
424
425    if content <= viewport || offset <= 0.0 {
426        0.0
427    } else {
428        (offset / (content - viewport)).clamp(0.0, 1.0)
429    }
430}
431
432/// Retained visibility threshold with optional once-only triggering.
433#[derive(Clone, Copy, Debug)]
434pub struct InView {
435    amount: f64,
436    once: bool,
437    entered: bool,
438    visible: bool,
439}
440
441impl InView {
442    /// Creates a threshold. Zero means any positive intersection; one means fully visible.
443    pub fn new(amount: f64, once: bool) -> Self {
444        assert!(
445            (0.0..=1.0).contains(&amount),
446            "invalid visibility threshold"
447        );
448
449        Self {
450            amount,
451            once,
452            entered: false,
453            visible: false,
454        }
455    }
456
457    /// Updates from viewport/element `[x,y,width,height]` bounds in the same coordinate space.
458    /// Returns whether visibility changed. Zero-area elements are invisible.
459    pub fn update(&mut self, viewport: [f64; 4], element: [f64; 4]) -> bool {
460        assert!(
461            viewport.iter().chain(element.iter()).all(|v| v.is_finite())
462                && viewport[2..]
463                    .iter()
464                    .chain(element[2..].iter())
465                    .all(|v| *v >= 0.0),
466            "invalid viewport bounds"
467        );
468        let width =
469            (viewport[0] + viewport[2]).min(element[0] + element[2]) - viewport[0].max(element[0]);
470
471        let height =
472            (viewport[1] + viewport[3]).min(element[1] + element[3]) - viewport[1].max(element[1]);
473
474        let area = element[2] * element[3];
475
476        let fraction = if area > 0.0 {
477            width.max(0.0) * height.max(0.0) / area
478        } else {
479            0.0
480        };
481
482        let visible = (self.once && self.entered) || (fraction > 0.0 && fraction >= self.amount);
483        let changed = self.visible != visible;
484
485        self.visible = visible;
486        self.entered |= visible;
487
488        changed
489    }
490
491    /// Last resolved visibility, including a once-only latch.
492    pub fn is_visible(&self) -> bool {
493        self.visible
494    }
495}
496
497/// Finds the nearest destination in a sorted list of item centers, for native reorder UIs.
498pub fn reorder_index(centers: &[f64], position: f64) -> Result<usize, AnimationError> {
499    if centers.is_empty()
500        || !position.is_finite()
501        || centers.iter().any(|v| !v.is_finite())
502        || centers.windows(2).any(|p| p[0] >= p[1])
503    {
504        return Err(AnimationError::Index);
505    }
506
507    Ok(centers
508        .iter()
509        .enumerate()
510        .min_by(|(_, a), (_, b)| (*a - position).abs().total_cmp(&(*b - position).abs()))
511        .unwrap()
512        .0)
513}
514
515/// Moves an item without reallocating or changing its identity. Invalid indices return an error.
516pub fn move_item<T>(items: &mut [T], from: usize, to: usize) -> Result<(), AnimationError> {
517    if from >= items.len() || to >= items.len() {
518        return Err(AnimationError::Index);
519    }
520
521    if from < to {
522        items[from..=to].rotate_left(1);
523    } else {
524        items[to..=from].rotate_right(1);
525    }
526
527    Ok(())
528}