Skip to main content

ferroui_base/rendering/composition/interactions/
visual_interaction_source.rs

1//! `VisualInteractionSource`: the input of an element, redirected to the
2//! interaction trackers it is added to.
3
4use super::interaction_tracker::InteractionTracker;
5use crate::input::{
6    IPointer, InputElement, KeyModifiers, PointerCaptureLostEventArgs, PointerEventArgs, PointerPoint,
7    PointerPressedEventArgs, PointerReleasedEventArgs, PointerWheelEventArgs,
8};
9use crate::interactivity::Interactive;
10use crate::numerics::Vector3;
11use crate::reactive::IDisposable;
12use crate::rendering::composition::{CompositionDrawListVisual, CompositionVisual, Compositor};
13use crate::{Point, Ref, WeakRef};
14use std::cell::{Cell, RefCell};
15use std::collections::VecDeque;
16use std::rc::{Rc, Weak};
17
18/// The pixels a notch of the mouse wheel scrolls, as the scroll content
19/// presenter scrolls without a scroll unit.
20const WHEEL_PIXELS_PER_NOTCH: f64 = 50.0;
21
22/// The scale factor of a notch of the mouse wheel with Ctrl.
23const WHEEL_SCALE_PER_NOTCH: f32 = 1.1;
24
25/// The distance a drag moves before the rails decide its axis.
26const RAILS_DECISION_DISTANCE: f64 = 4.0;
27
28/// `tan(22.5 degrees)`: a drag within this slope of an axis is railed to it.
29const RAILS_SLOPE: f64 = 0.414_213_57;
30
31/// The window of the last moves the release velocity is measured over, in
32/// milliseconds.
33const VELOCITY_WINDOW_MS: u64 = 100;
34
35/// How the input of a source drives an axis (`InteractionSourceMode`).
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
37pub enum InteractionSourceMode {
38    /// The input does not drive the axis.
39    #[default]
40    Disabled,
41    /// The input drives the axis and the release coasts in inertia.
42    EnabledWithInertia,
43    /// The input drives the axis and the release does not coast.
44    EnabledWithoutInertia,
45}
46
47impl InteractionSourceMode {
48    fn is_enabled(self) -> bool {
49        self != Self::Disabled
50    }
51}
52
53/// How the manipulations of nested sources chain
54/// (`InteractionChainingMode`).
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
56pub enum InteractionChainingMode {
57    #[default]
58    Auto,
59    Always,
60    Never,
61}
62
63/// Which input other than touch is redirected to a source without
64/// `TryRedirectForManipulation` (`VisualInteractionSourceRedirectionMode`).
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
66pub enum VisualInteractionSourceRedirectionMode {
67    #[default]
68    Off,
69    CapableTouchpadOnly,
70    PointerWheelOnly,
71    CapableTouchpadAndPointerWheel,
72}
73
74impl VisualInteractionSourceRedirectionMode {
75    fn includes_pointer_wheel(self) -> bool {
76        matches!(self, Self::PointerWheelOnly | Self::CapableTouchpadAndPointerWheel)
77    }
78}
79
80/// Whether a kind of redirected input drives an axis
81/// (`InteractionSourceRedirectionMode`).
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
83pub enum InteractionSourceRedirectionMode {
84    Disabled,
85    #[default]
86    Enabled,
87}
88
89/// Which axes the redirected mouse wheel drives
90/// (`InteractionSourceConfiguration`, the `PointerWheelConfig` of a
91/// source).
92#[derive(Default)]
93pub struct InteractionSourceConfiguration {
94    position_x_source_mode: Cell<InteractionSourceRedirectionMode>,
95    position_y_source_mode: Cell<InteractionSourceRedirectionMode>,
96    scale_source_mode: Cell<InteractionSourceRedirectionMode>,
97}
98
99impl InteractionSourceConfiguration {
100    pub fn position_x_source_mode(&self) -> InteractionSourceRedirectionMode {
101        self.position_x_source_mode.get()
102    }
103
104    pub fn set_position_x_source_mode(&self, value: InteractionSourceRedirectionMode) {
105        self.position_x_source_mode.set(value)
106    }
107
108    pub fn position_y_source_mode(&self) -> InteractionSourceRedirectionMode {
109        self.position_y_source_mode.get()
110    }
111
112    pub fn set_position_y_source_mode(&self, value: InteractionSourceRedirectionMode) {
113        self.position_y_source_mode.set(value)
114    }
115
116    pub fn scale_source_mode(&self) -> InteractionSourceRedirectionMode {
117        self.scale_source_mode.get()
118    }
119
120    pub fn set_scale_source_mode(&self, value: InteractionSourceRedirectionMode) {
121        self.scale_source_mode.set(value)
122    }
123}
124
125/// The axis a drag is railed to.
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127enum Rail {
128    X,
129    Y,
130}
131
132/// A drag in progress, after `TryRedirectForManipulation`.
133struct Manipulation {
134    pointer: Option<Rc<dyn IPointer>>,
135    subscriptions: Vec<Rc<dyn IDisposable>>,
136    trackers: Vec<Rc<InteractionTracker>>,
137    start: Point,
138    last: Point,
139    rails_decided: bool,
140    rail: Option<Rail>,
141    /// The recent moves (timestamp in milliseconds, position).
142    samples: VecDeque<(u64, Point)>,
143}
144
145/// The input of the element of a visual, redirected to the interaction
146/// trackers the source is added to
147/// (`Microsoft.UI.Composition.Interactions.VisualInteractionSource`).
148///
149/// A drag starts with [`try_redirect_for_manipulation`](Self::try_redirect_for_manipulation),
150/// called from a pointer pressed handler: the source captures the pointer to
151/// the element and follows its moves until it is released. Dragging content
152/// up increases `Position.Y` of the trackers, as scrolling does. The mouse
153/// wheel is redirected without a call when `ManipulationRedirectionMode`
154/// includes it.
155///
156/// Deviation (winui-composition.md): WinUI receives the input in the
157/// compositor once redirected. Here the source listens to the routed pointer
158/// events of the element (handled ones too) and drives the trackers on the
159/// UI thread; the element is the one of the visual (the element whose
160/// composition visual it is, or of its nearest ancestor) or the one given to
161/// [`create_for_element`](Self::create_for_element).
162pub struct VisualInteractionSource {
163    this: Weak<VisualInteractionSource>,
164    source: Rc<CompositionVisual>,
165    element: RefCell<Option<WeakRef<InputElement>>>,
166    position_x_source_mode: Cell<InteractionSourceMode>,
167    position_y_source_mode: Cell<InteractionSourceMode>,
168    scale_source_mode: Cell<InteractionSourceMode>,
169    position_x_chaining_mode: Cell<InteractionChainingMode>,
170    position_y_chaining_mode: Cell<InteractionChainingMode>,
171    scale_chaining_mode: Cell<InteractionChainingMode>,
172    manipulation_redirection_mode: Cell<VisualInteractionSourceRedirectionMode>,
173    is_position_x_rails_enabled: Cell<bool>,
174    is_position_y_rails_enabled: Cell<bool>,
175    pointer_wheel_config: Rc<InteractionSourceConfiguration>,
176    delta_position: Cell<Vector3>,
177    delta_scale: Cell<f32>,
178    position: Cell<Vector3>,
179    position_velocity: Cell<Vector3>,
180    scale: Cell<f32>,
181    scale_velocity: Cell<f32>,
182    trackers: RefCell<Vec<Weak<InteractionTracker>>>,
183    manipulation: RefCell<Option<Manipulation>>,
184    wheel_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
185}
186
187impl VisualInteractionSource {
188    /// `VisualInteractionSource.Create(source)`: a source for the input of
189    /// the element of `source`.
190    pub fn create(source: &Rc<CompositionVisual>) -> Rc<VisualInteractionSource> {
191        let element = Self::find_element(source);
192        Self::create_core(source, element)
193    }
194
195    /// A source for the input of `element`, shown by `source` (not a WinUI
196    /// member): for a visual that is not, or not yet, the composition visual
197    /// of an element or below one.
198    pub fn create_for_element(source: &Rc<CompositionVisual>, element: &Ref<InputElement>) -> Rc<VisualInteractionSource> {
199        Self::create_core(source, Some(element.clone()))
200    }
201
202    fn create_core(source: &Rc<CompositionVisual>, element: Option<Ref<InputElement>>) -> Rc<VisualInteractionSource> {
203        Rc::new_cyclic(|this: &Weak<VisualInteractionSource>| VisualInteractionSource {
204            this: this.clone(),
205            source: source.clone(),
206            element: RefCell::new(element.map(|element| element.downgrade())),
207            position_x_source_mode: Cell::new(InteractionSourceMode::Disabled),
208            position_y_source_mode: Cell::new(InteractionSourceMode::Disabled),
209            scale_source_mode: Cell::new(InteractionSourceMode::Disabled),
210            position_x_chaining_mode: Cell::new(InteractionChainingMode::Auto),
211            position_y_chaining_mode: Cell::new(InteractionChainingMode::Auto),
212            scale_chaining_mode: Cell::new(InteractionChainingMode::Auto),
213            manipulation_redirection_mode: Cell::new(VisualInteractionSourceRedirectionMode::Off),
214            is_position_x_rails_enabled: Cell::new(true),
215            is_position_y_rails_enabled: Cell::new(true),
216            pointer_wheel_config: Rc::new(InteractionSourceConfiguration::default()),
217            delta_position: Cell::new(Vector3::ZERO),
218            delta_scale: Cell::new(1.0),
219            position: Cell::new(Vector3::ZERO),
220            position_velocity: Cell::new(Vector3::ZERO),
221            scale: Cell::new(1.0),
222            scale_velocity: Cell::new(0.0),
223            trackers: RefCell::new(Vec::new()),
224            manipulation: RefCell::new(None),
225            wheel_subscription: RefCell::new(None),
226        })
227    }
228
229    /// The element of a composition visual: the element it is the visual
230    /// of, or the one of its nearest ancestor that has one.
231    fn find_element(visual: &Rc<CompositionVisual>) -> Option<Ref<InputElement>> {
232        let mut current = Some(visual.clone());
233        while let Some(visual) = current {
234            let element = CompositionDrawListVisual::from_visual(&visual)
235                .and_then(|draw_list| draw_list.visual())
236                .and_then(|element| element.downcast::<InputElement>().ok());
237            if element.is_some() {
238                return element;
239            }
240            current = visual.parent();
241        }
242        None
243    }
244
245    /// The element whose input the source redirects, if it is known and
246    /// alive. Looked up again from the visual while it is not known.
247    pub fn element(&self) -> Option<Ref<InputElement>> {
248        if let Some(element) = self.element.borrow().as_ref().and_then(WeakRef::upgrade) {
249            return Some(element);
250        }
251        let element = Self::find_element(&self.source)?;
252        *self.element.borrow_mut() = Some(element.downgrade());
253        Some(element)
254    }
255
256    /// The visual the source was created for (`Source`).
257    pub fn source(&self) -> &Rc<CompositionVisual> {
258        &self.source
259    }
260
261    /// The compositor of the visual.
262    pub fn compositor(&self) -> &Rc<Compositor> {
263        self.source.compositor()
264    }
265
266    // --- configuration ------------------------------------------------------
267
268    /// `PositionXSourceMode`.
269    pub fn position_x_source_mode(&self) -> InteractionSourceMode {
270        self.position_x_source_mode.get()
271    }
272
273    pub fn set_position_x_source_mode(&self, value: InteractionSourceMode) {
274        self.position_x_source_mode.set(value)
275    }
276
277    /// `PositionYSourceMode`.
278    pub fn position_y_source_mode(&self) -> InteractionSourceMode {
279        self.position_y_source_mode.get()
280    }
281
282    pub fn set_position_y_source_mode(&self, value: InteractionSourceMode) {
283        self.position_y_source_mode.set(value)
284    }
285
286    /// `ScaleSourceMode`. Only the Ctrl + mouse wheel scales: pinch input
287    /// is not redirected.
288    pub fn scale_source_mode(&self) -> InteractionSourceMode {
289        self.scale_source_mode.get()
290    }
291
292    pub fn set_scale_source_mode(&self, value: InteractionSourceMode) {
293        self.scale_source_mode.set(value)
294    }
295
296    /// `PositionXChainingMode`.
297    ///
298    /// Deviation (winui-composition.md): stored only; a manipulation does
299    /// not chain to the sources of ancestors.
300    pub fn position_x_chaining_mode(&self) -> InteractionChainingMode {
301        self.position_x_chaining_mode.get()
302    }
303
304    pub fn set_position_x_chaining_mode(&self, value: InteractionChainingMode) {
305        self.position_x_chaining_mode.set(value)
306    }
307
308    /// `PositionYChainingMode` (stored only).
309    pub fn position_y_chaining_mode(&self) -> InteractionChainingMode {
310        self.position_y_chaining_mode.get()
311    }
312
313    pub fn set_position_y_chaining_mode(&self, value: InteractionChainingMode) {
314        self.position_y_chaining_mode.set(value)
315    }
316
317    /// `ScaleChainingMode` (stored only).
318    pub fn scale_chaining_mode(&self) -> InteractionChainingMode {
319        self.scale_chaining_mode.get()
320    }
321
322    pub fn set_scale_chaining_mode(&self, value: InteractionChainingMode) {
323        self.scale_chaining_mode.set(value)
324    }
325
326    /// `ManipulationRedirectionMode`: the input redirected without
327    /// `TryRedirectForManipulation`. The mouse wheel is redirected when it
328    /// includes `PointerWheel`; touchpads have no input of their own in
329    /// FerroUI (their scrolling arrives as the wheel).
330    pub fn manipulation_redirection_mode(&self) -> VisualInteractionSourceRedirectionMode {
331        self.manipulation_redirection_mode.get()
332    }
333
334    pub fn set_manipulation_redirection_mode(&self, value: VisualInteractionSourceRedirectionMode) {
335        self.manipulation_redirection_mode.set(value);
336        self.update_wheel_subscription();
337    }
338
339    /// `IsPositionXRailsEnabled`: a drag that starts close to the X axis
340    /// moves along it only.
341    pub fn is_position_x_rails_enabled(&self) -> bool {
342        self.is_position_x_rails_enabled.get()
343    }
344
345    pub fn set_is_position_x_rails_enabled(&self, value: bool) {
346        self.is_position_x_rails_enabled.set(value)
347    }
348
349    /// `IsPositionYRailsEnabled`.
350    pub fn is_position_y_rails_enabled(&self) -> bool {
351        self.is_position_y_rails_enabled.get()
352    }
353
354    pub fn set_is_position_y_rails_enabled(&self, value: bool) {
355        self.is_position_y_rails_enabled.set(value)
356    }
357
358    /// `PointerWheelConfig`: the axes the redirected wheel drives.
359    pub fn pointer_wheel_config(&self) -> &Rc<InteractionSourceConfiguration> {
360        &self.pointer_wheel_config
361    }
362
363    // --- values of the input ------------------------------------------------
364
365    /// The position change of the last move (`DeltaPosition`), as the
366    /// trackers see it.
367    ///
368    /// Deviation (winui-composition.md): the values of the input are
369    /// properties of the UI-thread object; expressions cannot reference a
370    /// source.
371    pub fn delta_position(&self) -> Vector3 {
372        self.delta_position.get()
373    }
374
375    /// The scale change of the last input (`DeltaScale`), as a factor.
376    pub fn delta_scale(&self) -> f32 {
377        self.delta_scale.get()
378    }
379
380    /// The position of the current drag since it started (`Position`).
381    pub fn position(&self) -> Vector3 {
382        self.position.get()
383    }
384
385    /// The velocity of the current drag (`PositionVelocity`).
386    pub fn position_velocity(&self) -> Vector3 {
387        self.position_velocity.get()
388    }
389
390    /// The scale of the input (`Scale`).
391    pub fn scale(&self) -> f32 {
392        self.scale.get()
393    }
394
395    /// The velocity of the scale of the input (`ScaleVelocity`).
396    pub fn scale_velocity(&self) -> f32 {
397        self.scale_velocity.get()
398    }
399
400    // --- trackers -----------------------------------------------------------
401
402    pub(crate) fn attach_tracker(&self, tracker: &Weak<InteractionTracker>) {
403        let mut trackers = self.trackers.borrow_mut();
404        trackers.retain(|t| t.strong_count() > 0);
405        if !trackers.iter().any(|t| t.ptr_eq(tracker)) {
406            trackers.push(tracker.clone());
407        }
408        drop(trackers);
409        self.update_wheel_subscription();
410    }
411
412    pub(crate) fn detach_tracker(&self, tracker: &Weak<InteractionTracker>) {
413        self.trackers.borrow_mut().retain(|t| t.strong_count() > 0 && !t.ptr_eq(tracker));
414    }
415
416    fn live_trackers(&self) -> Vec<Rc<InteractionTracker>> {
417        self.trackers.borrow().iter().filter_map(Weak::upgrade).collect()
418    }
419
420    // --- redirection --------------------------------------------------------
421
422    /// `TryRedirectForManipulation(pointerPoint)`, from the pressed event:
423    /// the drag of the pressed pointer drives the trackers until it is
424    /// released. Returns whether the drag was redirected (not a WinUI
425    /// result): not when a drag of this source is in progress, when no axis
426    /// is enabled, or when the source has no element.
427    ///
428    /// Deviation (winui-composition.md): any pointer type is redirected,
429    /// mouse included.
430    pub fn try_redirect_for_manipulation(&self, e: &PointerPressedEventArgs) -> bool {
431        let element = self
432            .element()
433            .or_else(|| e.source().and_then(|source| source.downcast::<InputElement>().ok()));
434        let Some(element) = element else { return false };
435        let point = e.get_current_point(None);
436        self.try_redirect_for_manipulation_point(&point, &element)
437    }
438
439    /// `TryRedirectForManipulation(pointerPoint)` with the element that
440    /// captures the pointer. The position of `point` must be in the
441    /// coordinates of the top level (`get_current_point(None)`), as the
442    /// moves are followed there.
443    pub fn try_redirect_for_manipulation_point(&self, point: &PointerPoint, element: &Ref<InputElement>) -> bool {
444        if self.manipulation.borrow().is_some() {
445            return false;
446        }
447        if !self.position_x_source_mode.get().is_enabled() && !self.position_y_source_mode.get().is_enabled() {
448            return false;
449        }
450        let pointer = point.pointer.clone();
451        let pointer_id = pointer.id();
452        pointer.capture(Some(element));
453
454        let mut subscriptions = Vec::new();
455        let weak = self.this.clone();
456        subscriptions.push(element.add_disposable_handler_with(
457            InputElement::pointer_moved_event(),
458            move |_, e: &PointerEventArgs| {
459                if e.pointer().id() != pointer_id {
460                    return;
461                }
462                if let Some(this) = weak.upgrade() {
463                    this.manipulation_moved(e.get_current_point(None).position, e.timestamp());
464                }
465            },
466            Interactive::DEFAULT_ROUTES,
467            true,
468        ));
469        let weak = self.this.clone();
470        subscriptions.push(element.add_disposable_handler_with(
471            InputElement::pointer_released_event(),
472            move |_, e: &PointerReleasedEventArgs| {
473                if e.pointer().id() != pointer_id {
474                    return;
475                }
476                if let Some(this) = weak.upgrade() {
477                    this.manipulation_moved(e.get_current_point(None).position, e.timestamp());
478                    this.manipulation_ended(Some(e.timestamp()));
479                }
480            },
481            Interactive::DEFAULT_ROUTES,
482            true,
483        ));
484        let weak = self.this.clone();
485        subscriptions.push(element.add_disposable_handler_with(
486            InputElement::pointer_capture_lost_event(),
487            move |_, e: &PointerCaptureLostEventArgs| {
488                if e.pointer().id() != pointer_id {
489                    return;
490                }
491                if let Some(this) = weak.upgrade() {
492                    // Released already, or taken by another element: a
493                    // drag that is still in progress is cancelled.
494                    this.manipulation_ended(None);
495                }
496            },
497            Interactive::DEFAULT_ROUTES,
498            true,
499        ));
500
501        self.begin_manipulation(point.position, Some(pointer), subscriptions);
502        true
503    }
504
505    /// Starts a drag at `position` (top-level coordinates): the trackers
506    /// enter the interacting state.
507    pub(crate) fn begin_manipulation(
508        &self,
509        position: Point,
510        pointer: Option<Rc<dyn IPointer>>,
511        subscriptions: Vec<Rc<dyn IDisposable>>,
512    ) {
513        let trackers = self.live_trackers();
514        *self.manipulation.borrow_mut() = Some(Manipulation {
515            pointer,
516            subscriptions,
517            trackers: trackers.clone(),
518            start: position,
519            last: position,
520            rails_decided: false,
521            rail: None,
522            samples: VecDeque::new(),
523        });
524        self.position.set(Vector3::ZERO);
525        self.delta_position.set(Vector3::ZERO);
526        self.position_velocity.set(Vector3::ZERO);
527        for tracker in trackers {
528            tracker.begin_interaction();
529        }
530    }
531
532    /// The drag moved to `position` at `timestamp` (milliseconds).
533    pub(crate) fn manipulation_moved(&self, position: Point, timestamp: u64) {
534        let (delta, trackers) = {
535            let mut manipulation = self.manipulation.borrow_mut();
536            let Some(m) = manipulation.as_mut() else { return };
537            let total = position - m.start;
538            if !m.rails_decided && (total.x * total.x + total.y * total.y).sqrt() >= RAILS_DECISION_DISTANCE {
539                m.rails_decided = true;
540                if self.is_position_x_rails_enabled.get() && total.y.abs() <= total.x.abs() * RAILS_SLOPE {
541                    m.rail = Some(Rail::X);
542                } else if self.is_position_y_rails_enabled.get() && total.x.abs() <= total.y.abs() * RAILS_SLOPE {
543                    m.rail = Some(Rail::Y);
544                }
545            }
546            let mut step = position - m.last;
547            m.last = position;
548            match m.rail {
549                Some(Rail::X) => step = Point::new(step.x, 0.0),
550                Some(Rail::Y) => step = Point::new(0.0, step.y),
551                None => {}
552            }
553            m.samples.push_back((timestamp, position));
554            while m.samples.len() > 2 && m.samples.front().is_some_and(|(t, _)| timestamp.saturating_sub(*t) > VELOCITY_WINDOW_MS) {
555                m.samples.pop_front();
556            }
557            // Dragging content up scrolls it down: the position moves
558            // against the pointer.
559            let delta = self.mask(Vector3::new(-step.x as f32, -step.y as f32, 0.0));
560            self.position.set(self.position.get() + delta);
561            (delta, m.trackers.clone())
562        };
563        self.delta_position.set(delta);
564        let velocity = self.release_velocity(timestamp, false);
565        self.position_velocity.set(velocity);
566        if delta == Vector3::ZERO {
567            return;
568        }
569        for tracker in trackers {
570            tracker.interaction_delta(delta, velocity);
571        }
572    }
573
574    /// The drag ended: released at `timestamp`, or cancelled (`None`). The
575    /// trackers coast with the velocity of the last moves, on the axes
576    /// whose mode has inertia.
577    pub(crate) fn manipulation_ended(&self, timestamp: Option<u64>) {
578        let velocity = match timestamp {
579            Some(timestamp) => self.release_velocity(timestamp, true),
580            None => Vector3::ZERO,
581        };
582        let Some(manipulation) = self.manipulation.borrow_mut().take() else { return };
583        for subscription in &manipulation.subscriptions {
584            subscription.dispose();
585        }
586        drop(manipulation.pointer);
587        self.position_velocity.set(Vector3::ZERO);
588        for tracker in manipulation.trackers {
589            tracker.end_interaction(velocity);
590        }
591    }
592
593    /// Zeroes the axes whose mode is disabled.
594    fn mask(&self, v: Vector3) -> Vector3 {
595        Vector3::new(
596            if self.position_x_source_mode.get().is_enabled() { v.x } else { 0.0 },
597            if self.position_y_source_mode.get().is_enabled() { v.y } else { 0.0 },
598            0.0,
599        )
600    }
601
602    /// The velocity of the drag over the last moves, in the direction of
603    /// the trackers. For a release, only the axes with inertia keep it, and
604    /// a pointer held still for longer than the window releases without
605    /// velocity.
606    fn release_velocity(&self, now: u64, release: bool) -> Vector3 {
607        let manipulation = self.manipulation.borrow();
608        let Some(m) = manipulation.as_ref() else { return Vector3::ZERO };
609        let (Some(&(t0, p0)), Some(&(t1, p1))) = (m.samples.front(), m.samples.back()) else { return Vector3::ZERO };
610        if t1 <= t0 || now.saturating_sub(t1) > VELOCITY_WINDOW_MS {
611            return Vector3::ZERO;
612        }
613        let seconds = (t1 - t0) as f64 / 1000.0;
614        let mut v = (p1 - p0) / seconds;
615        match m.rail {
616            Some(Rail::X) => v = Point::new(v.x, 0.0),
617            Some(Rail::Y) => v = Point::new(0.0, v.y),
618            None => {}
619        }
620        let v = self.mask(Vector3::new(-v.x as f32, -v.y as f32, 0.0));
621        if !release {
622            return v;
623        }
624        let inertia = |mode: InteractionSourceMode| mode == InteractionSourceMode::EnabledWithInertia;
625        Vector3::new(
626            if inertia(self.position_x_source_mode.get()) { v.x } else { 0.0 },
627            if inertia(self.position_y_source_mode.get()) { v.y } else { 0.0 },
628            0.0,
629        )
630    }
631
632    // --- mouse wheel --------------------------------------------------------
633
634    fn update_wheel_subscription(&self) {
635        let wanted = self.manipulation_redirection_mode.get().includes_pointer_wheel();
636        if !wanted {
637            if let Some(subscription) = self.wheel_subscription.borrow_mut().take() {
638                subscription.dispose();
639            }
640            return;
641        }
642        if self.wheel_subscription.borrow().is_some() {
643            return;
644        }
645        let Some(element) = self.element() else { return };
646        let weak = self.this.clone();
647        let subscription = element.add_disposable_handler_with(
648            InputElement::pointer_wheel_changed_event(),
649            move |_, e: &PointerWheelEventArgs| {
650                if e.handled() {
651                    return;
652                }
653                if let Some(this) = weak.upgrade() {
654                    if this.on_wheel(e) {
655                        e.set_handled(true);
656                    }
657                }
658            },
659            Interactive::DEFAULT_ROUTES,
660            false,
661        );
662        *self.wheel_subscription.borrow_mut() = Some(subscription);
663    }
664
665    fn on_wheel(&self, e: &PointerWheelEventArgs) -> bool {
666        if !self.manipulation_redirection_mode.get().includes_pointer_wheel() {
667            return false;
668        }
669        let delta = e.delta();
670        let config = &self.pointer_wheel_config;
671        if e.key_modifiers().contains(KeyModifiers::CONTROL) {
672            if !self.scale_source_mode.get().is_enabled()
673                || config.scale_source_mode() != InteractionSourceRedirectionMode::Enabled
674            {
675                return false;
676            }
677            let center = match self.element() {
678                Some(element) => e.get_position(Some(&element)),
679                None => Point::default(),
680            };
681            return self.wheel_scale(delta.y as f32, Vector3::new(center.x as f32, center.y as f32, 0.0));
682        }
683        let x = if self.position_x_source_mode.get().is_enabled()
684            && config.position_x_source_mode() == InteractionSourceRedirectionMode::Enabled
685        {
686            -delta.x * WHEEL_PIXELS_PER_NOTCH
687        } else {
688            0.0
689        };
690        let y = if self.position_y_source_mode.get().is_enabled()
691            && config.position_y_source_mode() == InteractionSourceRedirectionMode::Enabled
692        {
693            -delta.y * WHEEL_PIXELS_PER_NOTCH
694        } else {
695            0.0
696        };
697        self.wheel_position(Vector3::new(x as f32, y as f32, 0.0))
698    }
699
700    /// The redirected wheel scrolled the trackers by `delta` (pixels, in
701    /// the direction of the trackers). Returns whether it drove a tracker.
702    pub(crate) fn wheel_position(&self, delta: Vector3) -> bool {
703        if delta == Vector3::ZERO {
704            return false;
705        }
706        let trackers = self.live_trackers();
707        self.delta_position.set(delta);
708        for tracker in &trackers {
709            tracker.wheel_delta(delta);
710        }
711        !trackers.is_empty()
712    }
713
714    /// The redirected wheel with Ctrl scaled by `notches` around `center`.
715    fn wheel_scale(&self, notches: f32, center: Vector3) -> bool {
716        if notches == 0.0 {
717            return false;
718        }
719        let factor = WHEEL_SCALE_PER_NOTCH.powf(notches);
720        let trackers = self.live_trackers();
721        self.delta_scale.set(factor);
722        self.scale.set(self.scale.get() * factor);
723        for tracker in &trackers {
724            tracker.wheel_scale(factor, center);
725        }
726        !trackers.is_empty()
727    }
728}
729
730impl Drop for VisualInteractionSource {
731    fn drop(&mut self) {
732        if let Some(subscription) = self.wheel_subscription.get_mut().take() {
733            subscription.dispose();
734        }
735        if let Some(manipulation) = self.manipulation.get_mut().take() {
736            for subscription in &manipulation.subscriptions {
737                subscription.dispose();
738            }
739        }
740    }
741}