Skip to main content

inset_rendering/
mouse_tracker.rs

1//! Flutter counterpart: `rendering/mouse_tracker.dart`.
2
3use std::any::Any;
4use std::rc::Rc;
5
6use indexmap::IndexMap;
7use inset_embedder::{Matrix4, Offset, PointerDeviceKind, ViewId};
8use inset_foundation::{App, ChangeNotifier, ChangeNotifierData, Handle, RetainedHandle};
9use inset_gestures::{
10    HitTestEntry, HitTestResult, PointerEnterEvent, PointerEvent, PointerExitEvent,
11};
12use inset_services::{MouseCursorManager, MouseCursorRef, SystemMouseCursors};
13
14use crate::box_::BoxHitTestEntry;
15use crate::object::AnyRenderObject;
16
17/// Signature for hit testing at the given offset for the specified view.
18///
19/// It is used by the [`MouseTracker`] to fetch annotations for the mouse
20/// position.
21pub type MouseTrackerHitTest = Rc<dyn Fn(&mut App, Offset, ViewId) -> HitTestResult>;
22
23/// The annotations under a mouse, front to back, each with the transform of its hit test
24/// entry.
25///
26/// Dart keys this map by the annotation object. Here the key is the render object that
27/// implements the annotation (see [`crate::RenderObject::mouse_tracker_annotation`]), and the
28/// annotation itself is read from it when needed.
29type Annotations = IndexMap<AnyRenderObject, Matrix4>;
30
31// Various states of a connected mouse device used by [MouseTracker].
32struct MouseState {
33    /// The retained handles that keep the annotations' render objects past a rebuild.
34    retained: Vec<RetainedHandle<AnyRenderObject>>,
35    // The list of annotations that contains this device.
36    //
37    // It uses [IndexMap] to keep the insertion order.
38    annotations: Annotations,
39    // The most recently processed mouse event observed from this device.
40    latest_event: PointerEvent,
41}
42
43impl MouseState {
44    fn new(initial_event: PointerEvent) -> MouseState {
45        MouseState {
46            retained: Vec::new(),
47            annotations: Annotations::new(),
48            latest_event: initial_event,
49        }
50    }
51
52    fn replace_annotations(
53        &mut self,
54        value: Annotations,
55        retained: Vec<RetainedHandle<AnyRenderObject>>,
56    ) -> (Annotations, Vec<RetainedHandle<AnyRenderObject>>) {
57        (
58            std::mem::replace(&mut self.annotations, value),
59            std::mem::replace(&mut self.retained, retained),
60        )
61    }
62
63    fn replace_latest_event(&mut self, value: PointerEvent) -> PointerEvent {
64        debug_assert_eq!(value.device(), self.latest_event.device());
65        std::mem::replace(&mut self.latest_event, value)
66    }
67}
68
69// The details of an update of a mouse device.
70//
71// This class contains the information needed to handle the update that might
72// change the state of a mouse device, or the [MouseTrackerAnnotation]s that
73// the mouse device is hovering.
74struct MouseTrackerUpdateDetails {
75    // The annotations that the device is hovering before the update.
76    //
77    // It is never null.
78    last_annotations: Annotations,
79    // The annotations that the device is hovering after the update.
80    //
81    // It is never null.
82    next_annotations: Annotations,
83    // The last event that the device observed before the update.
84    //
85    // If the update is triggered by a frame, the [previous_event] is never null,
86    // since the pointer must have been added before it can move.
87    //
88    // If the update is triggered by a pointer event, the [previous_event] is not
89    // null except for cases where the event is the first event observed by the
90    // pointer (which is not necessarily a [PointerEvent::Added]).
91    previous_event: Option<PointerEvent>,
92    // The event that triggered this update.
93    //
94    // It is non-null if and only if the update is triggered by a pointer event.
95    triggering_event: Option<PointerEvent>,
96}
97
98impl MouseTrackerUpdateDetails {
99    // When device update is triggered by a new frame.
100    //
101    // All parameters are required.
102    fn by_new_frame(
103        last_annotations: Annotations,
104        next_annotations: Annotations,
105        previous_event: PointerEvent,
106    ) -> MouseTrackerUpdateDetails {
107        MouseTrackerUpdateDetails {
108            last_annotations,
109            next_annotations,
110            previous_event: Some(previous_event),
111            triggering_event: None,
112        }
113    }
114
115    // When device update is triggered by a pointer event.
116    //
117    // The [last_annotations], [next_annotations], and [triggering_event] are
118    // required.
119    fn by_pointer_event(
120        last_annotations: Annotations,
121        next_annotations: Annotations,
122        previous_event: Option<PointerEvent>,
123        triggering_event: PointerEvent,
124    ) -> MouseTrackerUpdateDetails {
125        MouseTrackerUpdateDetails {
126            last_annotations,
127            next_annotations,
128            previous_event,
129            triggering_event: Some(triggering_event),
130        }
131    }
132
133    // The pointing device of this update.
134    fn device(&self) -> i64 {
135        self.previous_event
136            .as_ref()
137            .or(self.triggering_event.as_ref())
138            .expect("an update has an event")
139            .device()
140    }
141
142    // The last event that the device observed after the update.
143    //
144    // The [latest_event] is never null.
145    fn latest_event(&self) -> &PointerEvent {
146        self.triggering_event
147            .as_ref()
148            .or(self.previous_event.as_ref())
149            .expect("an update has an event")
150    }
151}
152
153/// Tracks the relationship between mouse devices and annotations, and
154/// triggers mouse events and cursor changes accordingly.
155///
156/// The [`MouseTracker`] tracks the relationship between mouse devices and
157/// [`MouseTrackerAnnotation`](inset_services::MouseTrackerAnnotation), notified by [`update_with_event`] and
158/// [`update_all_devices`]. At every update, [`MouseTracker`] triggers the
159/// following changes if applicable:
160///
161///  * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
162///  * Changes mouse cursors.
163///  * Notifies when [`mouse_is_connected`] changes.
164///
165/// This class is a `ChangeNotifier` that notifies its listeners if the value of
166/// [`mouse_is_connected`] changes.
167///
168/// An instance of [`MouseTracker`] is owned by the global singleton
169/// `RendererBinding`.
170///
171/// [`update_with_event`]: MouseTracker::update_with_event
172/// [`update_all_devices`]: MouseTracker::update_all_devices
173/// [`mouse_is_connected`]: MouseTracker::mouse_is_connected
174pub struct MouseTracker {
175    change_notifier: ChangeNotifierData,
176    hit_test_in_view: MouseTrackerHitTest,
177    mouse_cursor_mixin: MouseCursorManager,
178    mouse_states: IndexMap<i64, MouseState>,
179    debug_during_device_update: bool,
180}
181
182impl ChangeNotifier for MouseTracker {
183    fn change_notifier_data(&self) -> &ChangeNotifierData {
184        &self.change_notifier
185    }
186
187    fn change_notifier_data_mut(&mut self) -> &mut ChangeNotifierData {
188        &mut self.change_notifier
189    }
190}
191
192impl MouseTracker {
193    /// Create a mouse tracker.
194    ///
195    /// The `hit_test_in_view` is used to find the render objects on a given
196    /// position in the specific view. It is typically provided by the
197    /// `RendererBinding`.
198    pub fn new(app: &mut App, hit_test_in_view: MouseTrackerHitTest) -> Handle<MouseTracker> {
199        app.create(MouseTracker {
200            change_notifier: ChangeNotifierData::new(),
201            hit_test_in_view,
202            mouse_cursor_mixin: MouseCursorManager::new(SystemMouseCursors::BASIC.into()),
203            mouse_states: IndexMap::new(),
204            debug_during_device_update: false,
205        })
206    }
207
208    /// Discards any resources used by the object. After this is called, the
209    /// object is not in a usable state and should be discarded.
210    pub fn dispose(self: Handle<Self>, app: &mut App) {
211        let states = std::mem::take(&mut app.get_mut(self).mouse_states);
212        for state in states.into_values() {
213            MouseTracker::release_annotations(app, state.retained);
214        }
215        app.get_mut(self).change_notifier.dispose();
216    }
217
218    /// The remembered annotations keep their render objects past a rebuild, as Dart's map holds
219    /// the annotation objects: an exit is still delivered to a region that just left the tree.
220    fn retain_annotations(
221        app: &mut App,
222        annotations: &Annotations,
223    ) -> Vec<RetainedHandle<AnyRenderObject>> {
224        annotations
225            .keys()
226            .copied()
227            .map(|target| app.retain(target))
228            .collect()
229    }
230
231    fn release_annotations(app: &mut App, retained: Vec<RetainedHandle<AnyRenderObject>>) {
232        for handle in retained {
233            app.release(handle);
234        }
235    }
236
237    // Whether an observed event might update a device.
238    //
239    // For example, a [PointerEvent::Move] would always update a device, while
240    // a [PointerEvent::Scroll] would not.
241    fn should_mark_state_dirty(state: Option<&MouseState>, event: &PointerEvent) -> bool {
242        let Some(state) = state else {
243            return true;
244        };
245        let last_event = &state.latest_event;
246        debug_assert_eq!(event.device(), last_event.device());
247        // An Added can only follow a Removed, and a Removed can only follow an Added.
248        debug_assert_eq!(
249            matches!(event, PointerEvent::Added(_)),
250            matches!(last_event, PointerEvent::Removed(_))
251        );
252        // Ignore events that are unrelated to mouse tracking.
253        if is_signal_event(event) {
254            return false;
255        }
256        matches!(last_event, PointerEvent::Added(_))
257            || matches!(event, PointerEvent::Removed(_))
258            || last_event.position() != event.position()
259    }
260
261    fn hit_test_in_view_result_to_annotations(app: &App, result: &HitTestResult) -> Annotations {
262        let mut annotations = Annotations::new();
263        for entry in result.path() {
264            if let Some(target) = annotation_target(app, entry) {
265                annotations.insert(target, entry.transform().expect("an added entry is placed"));
266            }
267        }
268        annotations
269    }
270
271    // Find the annotations that is hovered by the device of the `state`.
272    //
273    // If the device is not connected or not a mouse, an empty map is returned
274    // without calling `hit_test_in_view`.
275    fn find_annotations(
276        self: Handle<Self>,
277        app: &mut App,
278        latest_event: &PointerEvent,
279    ) -> Annotations {
280        let global_position = latest_event.position();
281        let device = latest_event.device();
282        let view_id = latest_event.view_id();
283        if !app.get(self).mouse_states.contains_key(&device) {
284            return Annotations::new();
285        }
286        let result = self.run_hit_test(app, global_position, view_id);
287        MouseTracker::hit_test_in_view_result_to_annotations(app, &result)
288    }
289
290    fn run_hit_test(
291        self: Handle<Self>,
292        app: &mut App,
293        position: Offset,
294        view_id: ViewId,
295    ) -> HitTestResult {
296        let hit_test_in_view = Rc::clone(&app.get(self).hit_test_in_view);
297        hit_test_in_view(app, position, view_id)
298    }
299
300    // A callback that is called on the update of a device.
301    //
302    // An event (not necessarily a pointer event) that might change the
303    // relationship between mouse devices and [MouseTrackerAnnotation]s is called
304    // a _device update_. This method should be called at each such update.
305    //
306    // The update can be caused by a pointer event, in which case
307    // `triggering_event` should not be null, or by other changes, such as when a
308    // widget has moved under a still mouse, which is detected after the current
309    // frame is complete.
310    fn handle_device_update(self: Handle<Self>, app: &mut App, details: MouseTrackerUpdateDetails) {
311        debug_assert!(app.get(self).debug_during_device_update);
312        MouseTracker::handle_device_update_mouse_events(app, &details);
313        let cursors: Vec<MouseCursorRef> = details
314            .next_annotations
315            .keys()
316            .filter_map(|target| target.mouse_tracker_annotation(app))
317            .map(|annotation| annotation.cursor)
318            .collect();
319        let platform = app.platform();
320        app.get_mut(self)
321            .mouse_cursor_mixin
322            .handle_device_cursor_update(
323                &*platform,
324                details.latest_event().view_id(),
325                details.device(),
326                details.triggering_event.as_ref(),
327                cursors,
328            );
329    }
330
331    /// Whether or not at least one mouse is connected and has produced events.
332    pub fn mouse_is_connected(self: Handle<Self>, app: &App) -> bool {
333        !app.get(self).mouse_states.is_empty()
334    }
335
336    /// Perform a device update for one device according to the given new event.
337    ///
338    /// The [`update_with_event`] is typically called by the `RendererBinding` during
339    /// the handler of a pointer event. All pointer events should call this
340    /// method, and let [`MouseTracker`] filter which to react to.
341    ///
342    /// The `hit_test_result` serves as an optional optimization, and is the hit
343    /// test result already performed by the `RendererBinding` for other gestures.
344    /// It can be `None`, but when it's not `None`, it should be identical to the
345    /// result from directly calling `hit_test_in_view` given in the constructor
346    /// (which means that it must not use the cached result for
347    /// [`PointerEvent::Move`]).
348    ///
349    /// The `update_with_event` is one of the two ways of updating mouse
350    /// states, the other one being [`update_all_devices`].
351    ///
352    /// [`update_with_event`]: MouseTracker::update_with_event
353    /// [`update_all_devices`]: MouseTracker::update_all_devices
354    pub fn update_with_event(
355        self: Handle<Self>,
356        app: &mut App,
357        event: &PointerEvent,
358        hit_test_result: Option<&HitTestResult>,
359    ) {
360        if event.kind() != PointerDeviceKind::Mouse && event.kind() != PointerDeviceKind::Stylus {
361            // Only track mouse and stylus events.
362            return;
363        }
364        if is_signal_event(event) {
365            // Ignore signal events, which are not tracked by the mouse tracker.
366            return;
367        }
368        let is_removed = matches!(event, PointerEvent::Removed(_));
369        let owned_result;
370        let result: Option<&HitTestResult> = if is_removed {
371            None
372        } else if let Some(result) = hit_test_result {
373            Some(result)
374        } else {
375            owned_result = self.run_hit_test(app, event.position(), event.view_id());
376            Some(&owned_result)
377        };
378        let device = event.device();
379        if !MouseTracker::should_mark_state_dirty(app.get(self).mouse_states.get(&device), event) {
380            return;
381        }
382
383        self.monitor_mouse_connection(app, |app| {
384            self.device_update_phase(app, |app| {
385                // Update mouse_states
386                if !app.get(self).mouse_states.contains_key(&device) {
387                    if is_removed {
388                        return;
389                    }
390                    app.get_mut(self)
391                        .mouse_states
392                        .insert(device, MouseState::new(event.clone()));
393                } else {
394                    debug_assert!(!matches!(event, PointerEvent::Added(_)));
395                }
396                let next_annotations = match result {
397                    Some(result) => {
398                        MouseTracker::hit_test_in_view_result_to_annotations(app, result)
399                    }
400                    None => Annotations::new(),
401                };
402                let target_state = app
403                    .get_mut(self)
404                    .mouse_states
405                    .get_mut(&device)
406                    .expect("the state was just found or created");
407                let last_event = target_state.replace_latest_event(event.clone());
408                let retained = MouseTracker::retain_annotations(app, &next_annotations);
409                let target_state = app
410                    .get_mut(self)
411                    .mouse_states
412                    .get_mut(&device)
413                    .expect("the state was just found or created");
414                let (last_annotations, last_retained) =
415                    target_state.replace_annotations(next_annotations.clone(), retained);
416                if is_removed {
417                    app.get_mut(self).mouse_states.shift_remove(&device);
418                }
419                self.handle_device_update(
420                    app,
421                    MouseTrackerUpdateDetails::by_pointer_event(
422                        last_annotations,
423                        next_annotations,
424                        Some(last_event),
425                        event.clone(),
426                    ),
427                );
428                MouseTracker::release_annotations(app, last_retained);
429            });
430        });
431    }
432
433    /// Perform a device update for all devices.
434    ///
435    /// This is called by the `RendererBinding` after a frame, since the render
436    /// tree might have changed under a still mouse.
437    ///
438    /// The `update_all_devices` is one of the two ways of updating mouse
439    /// states, the other one being [`update_with_event`].
440    ///
441    /// [`update_with_event`]: MouseTracker::update_with_event
442    pub fn update_all_devices(self: Handle<Self>, app: &mut App) {
443        self.device_update_phase(app, |app| {
444            let devices: Vec<i64> = app.get(self).mouse_states.keys().copied().collect();
445            for device in devices {
446                let Some(last_event) = app
447                    .get(self)
448                    .mouse_states
449                    .get(&device)
450                    .map(|state| state.latest_event.clone())
451                else {
452                    continue;
453                };
454                let next_annotations = self.find_annotations(app, &last_event);
455                if !app.get(self).mouse_states.contains_key(&device) {
456                    continue;
457                }
458                let retained = MouseTracker::retain_annotations(app, &next_annotations);
459                let Some(dirty_state) = app.get_mut(self).mouse_states.get_mut(&device) else {
460                    continue;
461                };
462                let (last_annotations, last_retained) =
463                    dirty_state.replace_annotations(next_annotations.clone(), retained);
464                self.handle_device_update(
465                    app,
466                    MouseTrackerUpdateDetails::by_new_frame(
467                        last_annotations,
468                        next_annotations,
469                        last_event,
470                    ),
471                );
472                MouseTracker::release_annotations(app, last_retained);
473            }
474        });
475    }
476
477    /// Returns the active mouse cursor for a device.
478    ///
479    /// The return value is the last [`MouseCursorRef`] activated onto this
480    /// device, even if the activation failed.
481    ///
482    /// This function is only active in debug mode: in release it returns `None`.
483    pub fn debug_device_active_cursor(
484        self: Handle<Self>,
485        app: &App,
486        device: i64,
487    ) -> Option<MouseCursorRef> {
488        app.get(self)
489            .mouse_cursor_mixin
490            .debug_device_active_cursor(device)
491    }
492
493    // Used to wrap any procedures that might change `mouse_is_connected`.
494    //
495    // This method records `mouse_is_connected` before and after the `task`, and
496    // notifies listeners if the value changed.
497    fn monitor_mouse_connection(self: Handle<Self>, app: &mut App, task: impl FnOnce(&mut App)) {
498        let mouse_was_connected = self.mouse_is_connected(app);
499        task(app);
500        if mouse_was_connected != self.mouse_is_connected(app) {
501            self.notify_listeners(app);
502        }
503    }
504
505    // Used to wrap any procedures that might change the state of a device.
506    //
507    // A device update, i.e. a change that might affect which annotations a
508    // device hovers, must not happen during another device update.
509    fn device_update_phase(self: Handle<Self>, app: &mut App, task: impl FnOnce(&mut App)) {
510        debug_assert!(!app.get(self).debug_during_device_update);
511        if cfg!(debug_assertions) {
512            app.get_mut(self).debug_during_device_update = true;
513        }
514        task(app);
515        if cfg!(debug_assertions) {
516            app.get_mut(self).debug_during_device_update = false;
517        }
518    }
519
520    // Handles device update and dispatches mouse event callbacks.
521    fn handle_device_update_mouse_events(app: &mut App, details: &MouseTrackerUpdateDetails) {
522        let latest_event = details.latest_event();
523        let last_annotations = &details.last_annotations;
524        let next_annotations = &details.next_annotations;
525
526        // Order is important for mouse event callbacks. The
527        // `hit_test_in_view_result_to_annotations` returns annotations in the visual order
528        // from front to back, called the "hit-test order". The algorithm here is
529        // explained in https://github.com/flutter/flutter/issues/41420
530
531        // Send exit events to annotations that are in last but not in next, in
532        // hit-test order.
533        let base_exit_event = PointerExitEvent::from_mouse_event(latest_event);
534        for (target, transform) in last_annotations {
535            if next_annotations.contains_key(target) {
536                continue;
537            }
538            let Some(annotation) = target.mouse_tracker_annotation(app) else {
539                continue;
540            };
541            if annotation.valid_for_mouse_tracker
542                && let Some(on_exit) = annotation.on_exit
543            {
544                on_exit(app, base_exit_event.transformed(Some(*transform)));
545            }
546        }
547
548        // Send enter events to annotations that are not in last but in next, in
549        // reverse hit-test order.
550        let entering_annotations: Vec<(AnyRenderObject, Matrix4)> = next_annotations
551            .iter()
552            .filter(|(target, _)| !last_annotations.contains_key(*target))
553            .map(|(target, transform)| (*target, *transform))
554            .collect();
555        let base_enter_event = PointerEnterEvent::from_mouse_event(latest_event);
556        for (target, transform) in entering_annotations.into_iter().rev() {
557            let Some(annotation) = target.mouse_tracker_annotation(app) else {
558                continue;
559            };
560            if annotation.valid_for_mouse_tracker
561                && let Some(on_enter) = annotation.on_enter
562            {
563                on_enter(app, base_enter_event.transformed(Some(transform)));
564            }
565        }
566    }
567}
568
569/// Dart's `event is PointerSignalEvent`.
570fn is_signal_event(event: &PointerEvent) -> bool {
571    matches!(
572        event,
573        PointerEvent::Scroll(_) | PointerEvent::ScrollInertiaCancel(_) | PointerEvent::Scale(_)
574    )
575}
576
577/// Dart's `entry.target is MouseTrackerAnnotation`: the render object behind a hit test entry,
578/// when it implements the annotation.
579fn annotation_target(app: &App, entry: &HitTestEntry) -> Option<AnyRenderObject> {
580    let target: &dyn Any = entry.target();
581    let target = target
582        .downcast_ref::<BoxHitTestEntry>()?
583        .target()
584        .as_object();
585    target.mouse_tracker_annotation(app).map(|_| target)
586}
587
588#[cfg(test)]
589mod tests {
590    use inset_foundation::AppCell;
591    use std::cell::{Cell, RefCell};
592
593    use inset_embedder::{MouseCursor, Size, SystemMouseCursorKind};
594    use inset_gestures::{
595        PointerAddedEvent, PointerHoverEvent, PointerRemovedEvent, PointerScrollEvent,
596    };
597    use inset_test::TestPlatform;
598
599    use inset_foundation::{Listenable, Listener};
600
601    use super::*;
602    use crate::box_::{BoxConstraints, BoxHitTestResult, RenderBox};
603    use crate::object::RenderObject;
604    use crate::object::RenderObjectWithChildMixin;
605    use crate::pipeline_owner::PipelineOwner;
606    use crate::proxy_box::{RenderConstrainedBox, RenderMouseRegion, RenderRepaintBoundary};
607    use crate::shifted_box::RenderPadding;
608
609    /// A host that records the cursor requests it receives.
610    #[derive(Default)]
611    struct RecordingCursor {
612        activated: RefCell<Vec<(ViewId, i64, SystemMouseCursorKind)>>,
613    }
614
615    impl MouseCursor for RecordingCursor {
616        fn activate_system_cursor(&self, view: ViewId, device: i64, kind: SystemMouseCursorKind) {
617            self.activated.borrow_mut().push((view, device, kind));
618        }
619    }
620
621    fn app_with_cursor_host() -> (Rc<AppCell>, Rc<RecordingCursor>) {
622        let cursor = Rc::new(RecordingCursor::default());
623        let platform = TestPlatform::new().with_mouse_cursor(cursor.clone());
624        (AppCell::with_platform(Rc::new(platform)), cursor)
625    }
626
627    /// A laid-out tree: a 200×200 root holding a mouse region padded 50 on each side, so the
628    /// region covers (50..150, 50..150). Returns the root and the region.
629    struct RegionTree {
630        owner: Handle<PipelineOwner>,
631        root: crate::RenderHandle<RenderRepaintBoundary>,
632        padding: crate::RenderHandle<RenderPadding>,
633        region: crate::RenderHandle<RenderMouseRegion>,
634    }
635
636    fn tree_with_region(app: &mut App) -> RegionTree {
637        let region = RenderMouseRegion::new(app, true, None);
638        let padding = RenderPadding::new(
639            app,
640            inset_painting::EdgeInsets::all(50.0).into(),
641            None,
642            Some(region.as_box()),
643        );
644        let root = RenderRepaintBoundary::new(app, Some(padding.as_box()));
645        let owner = PipelineOwner::new(app, None);
646        owner.set_root_node(app, Some(root.as_object()));
647        root.schedule_initial_layout(app);
648        root.layout(app, BoxConstraints::tight(Size::new(200.0, 200.0)), false);
649        RegionTree {
650            owner,
651            root,
652            padding,
653            region,
654        }
655    }
656
657    fn tracker_for(
658        app: &mut App,
659        root: crate::RenderHandle<RenderRepaintBoundary>,
660    ) -> Handle<MouseTracker> {
661        MouseTracker::new(
662            app,
663            Rc::new(move |app: &mut App, position: Offset, _view_id: ViewId| {
664                let mut result = HitTestResult::new();
665                root.hit_test(app, &mut BoxHitTestResult::wrap(&mut result), position);
666                result
667            }),
668        )
669    }
670
671    fn mouse_hover(position: Offset) -> PointerEvent {
672        PointerEvent::Hover(PointerHoverEvent {
673            kind: PointerDeviceKind::Mouse,
674            position,
675            ..PointerHoverEvent::default()
676        })
677    }
678
679    fn mouse_added(position: Offset) -> PointerEvent {
680        PointerEvent::Added(PointerAddedEvent {
681            kind: PointerDeviceKind::Mouse,
682            position,
683            ..PointerAddedEvent::default()
684        })
685    }
686
687    fn mouse_removed(position: Offset) -> PointerEvent {
688        PointerEvent::Removed(PointerRemovedEvent {
689            kind: PointerDeviceKind::Mouse,
690            position,
691            ..PointerRemovedEvent::default()
692        })
693    }
694
695    /// Records the events a region's callbacks receive.
696    fn record_events(
697        app: &mut App,
698        region: crate::RenderHandle<RenderMouseRegion>,
699    ) -> Rc<RefCell<Vec<String>>> {
700        let log = Rc::new(RefCell::new(Vec::new()));
701        let enter_log = Rc::clone(&log);
702        region.set_on_enter(
703            app,
704            Some(Rc::new(move |_app: &mut App, event: PointerEnterEvent| {
705                enter_log
706                    .borrow_mut()
707                    .push(format!("enter {:?}", event.local_position()));
708            })),
709        );
710        let hover_log = Rc::clone(&log);
711        region.set_on_hover(
712            app,
713            Some(Rc::new(move |_app: &mut App, event: PointerHoverEvent| {
714                hover_log
715                    .borrow_mut()
716                    .push(format!("hover {:?}", event.local_position()));
717            })),
718        );
719        let exit_log = Rc::clone(&log);
720        region.set_on_exit(
721            app,
722            Some(Rc::new(move |_app: &mut App, event: PointerExitEvent| {
723                exit_log
724                    .borrow_mut()
725                    .push(format!("exit {:?}", event.local_position()));
726            })),
727        );
728        log
729    }
730
731    #[test]
732    fn entering_and_leaving_a_region_sends_enter_then_exit_in_local_coordinates() {
733        let cell = AppCell::new();
734        let mut app = cell.borrow_mut();
735        let RegionTree { root, region, .. } = tree_with_region(&mut app);
736        let tracker = tracker_for(&mut app, root);
737        let log = record_events(&mut app, region);
738
739        tracker.update_with_event(&mut app, &mouse_added(Offset::new(10.0, 10.0)), None);
740        assert!(log.borrow().is_empty(), "outside the region: nothing");
741
742        tracker.update_with_event(&mut app, &mouse_hover(Offset::new(60.0, 70.0)), None);
743        assert_eq!(*log.borrow(), ["enter Offset(10.0, 20.0)"]);
744
745        tracker.update_with_event(&mut app, &mouse_hover(Offset::new(160.0, 70.0)), None);
746        assert_eq!(
747            *log.borrow(),
748            ["enter Offset(10.0, 20.0)", "exit Offset(110.0, 20.0)"]
749        );
750    }
751
752    #[test]
753    fn a_still_mouse_notices_the_tree_moving_under_it_after_a_frame() {
754        let cell = AppCell::new();
755        let mut app = cell.borrow_mut();
756        let RegionTree {
757            owner,
758            root,
759            padding,
760            region,
761        } = tree_with_region(&mut app);
762        let tracker = tracker_for(&mut app, root);
763        let log = record_events(&mut app, region);
764        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
765        assert_eq!(*log.borrow(), ["enter Offset(50.0, 50.0)"]);
766
767        // Move the region to the right of the still mouse, then run the frame's update.
768        padding.set_padding(
769            &mut app,
770            inset_painting::EdgeInsetsGeometry::only(150.0, 0.0, 0.0, 0.0),
771        );
772        owner.flush_layout(&mut app);
773        tracker.update_all_devices(&mut app);
774        assert_eq!(
775            *log.borrow(),
776            ["enter Offset(50.0, 50.0)", "exit Offset(50.0, 50.0)"],
777            "the exit is placed with the transform recorded when the region was entered"
778        );
779    }
780
781    #[test]
782    fn a_region_removed_from_the_tree_sends_no_exit() {
783        let cell = AppCell::new();
784        let mut app = cell.borrow_mut();
785        let RegionTree {
786            owner,
787            root,
788            padding,
789            region,
790        } = tree_with_region(&mut app);
791        let tracker = tracker_for(&mut app, root);
792        let log = record_events(&mut app, region);
793        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
794        assert_eq!(*log.borrow(), ["enter Offset(50.0, 50.0)"]);
795
796        padding.set_child(&mut app, None);
797        owner.flush_layout(&mut app);
798        tracker.update_all_devices(&mut app);
799        assert_eq!(
800            *log.borrow(),
801            ["enter Offset(50.0, 50.0)"],
802            "Flutter: a detached region is no longer valid for the tracker, so no exit is sent"
803        );
804    }
805
806    #[test]
807    fn a_detached_region_is_invalid_for_the_tracker() {
808        let cell = AppCell::new();
809        let mut app = cell.borrow_mut();
810        let region = RenderMouseRegion::new(&mut app, true, None);
811        assert!(region.valid_for_mouse_tracker(&app));
812        let owner = PipelineOwner::new(&mut app, None);
813        region.as_object().attach(&mut app, owner);
814        assert!(region.valid_for_mouse_tracker(&app));
815        region.as_object().detach(&mut app);
816        assert!(!region.valid_for_mouse_tracker(&app));
817        assert!(
818            !region
819                .mouse_tracker_annotation(&app)
820                .unwrap()
821                .valid_for_mouse_tracker
822        );
823    }
824
825    #[test]
826    fn a_removed_mouse_exits_and_disconnects() {
827        let cell = AppCell::new();
828        let mut app = cell.borrow_mut();
829        let RegionTree { root, region, .. } = tree_with_region(&mut app);
830        let tracker = tracker_for(&mut app, root);
831        let log = record_events(&mut app, region);
832        let notifications = Rc::new(Cell::new(0));
833        let counted = Rc::clone(&notifications);
834        tracker.add_listener(
835            &mut app,
836            Listener::new(move |_app| counted.set(counted.get() + 1)),
837        );
838
839        assert!(!tracker.mouse_is_connected(&app));
840        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
841        assert!(tracker.mouse_is_connected(&app));
842        assert_eq!(notifications.get(), 1);
843
844        tracker.update_with_event(&mut app, &mouse_removed(Offset::new(100.0, 100.0)), None);
845        assert!(!tracker.mouse_is_connected(&app));
846        assert_eq!(notifications.get(), 2);
847        assert_eq!(
848            *log.borrow(),
849            ["enter Offset(50.0, 50.0)", "exit Offset(50.0, 50.0)"]
850        );
851    }
852
853    #[test]
854    fn touch_and_signal_events_are_ignored() {
855        let cell = AppCell::new();
856        let mut app = cell.borrow_mut();
857        let RegionTree { root, region, .. } = tree_with_region(&mut app);
858        let tracker = tracker_for(&mut app, root);
859        let log = record_events(&mut app, region);
860
861        tracker.update_with_event(
862            &mut app,
863            &PointerEvent::Hover(PointerHoverEvent {
864                position: Offset::new(100.0, 100.0),
865                ..PointerHoverEvent::default()
866            }),
867            None,
868        );
869        assert!(!tracker.mouse_is_connected(&app), "touch: not tracked");
870
871        tracker.update_with_event(
872            &mut app,
873            &PointerEvent::Scroll(PointerScrollEvent {
874                kind: PointerDeviceKind::Mouse,
875                position: Offset::new(100.0, 100.0),
876                ..PointerScrollEvent::default()
877            }),
878            None,
879        );
880        assert!(!tracker.mouse_is_connected(&app), "a signal: not tracked");
881        assert!(log.borrow().is_empty());
882    }
883
884    #[test]
885    fn the_front_regions_cursor_wins_and_deferring_regions_let_the_one_behind_decide() {
886        let (cell, cursor) = app_with_cursor_host();
887        let mut app = cell.borrow_mut();
888        let RegionTree {
889            owner,
890            root,
891            region,
892            ..
893        } = tree_with_region(&mut app);
894        region.set_cursor(&mut app, SystemMouseCursors::TEXT.into());
895        // A deferring region in front of the text region, sharing its bounds.
896        let front = RenderMouseRegion::new(&mut app, true, None);
897        region.set_child(&mut app, Some(front.as_box()));
898        owner.flush_layout(&mut app);
899        let tracker = tracker_for(&mut app, root);
900
901        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
902        assert_eq!(
903            *cursor.activated.borrow(),
904            [(ViewId(0), 0, SystemMouseCursorKind::Text)]
905        );
906
907        front.set_cursor(&mut app, SystemMouseCursors::CLICK.into());
908        tracker.update_all_devices(&mut app);
909        assert_eq!(
910            *cursor.activated.borrow(),
911            [
912                (ViewId(0), 0, SystemMouseCursorKind::Text),
913                (ViewId(0), 0, SystemMouseCursorKind::Click)
914            ]
915        );
916        let click: MouseCursorRef = SystemMouseCursors::CLICK.into();
917        assert!(*tracker.debug_device_active_cursor(&app, 0).unwrap() == *click);
918
919        tracker.update_with_event(&mut app, &mouse_hover(Offset::new(10.0, 10.0)), None);
920        assert_eq!(
921            cursor.activated.borrow().last().unwrap().2,
922            SystemMouseCursorKind::Basic,
923            "over nothing, the fallback cursor"
924        );
925    }
926
927    #[test]
928    fn the_cursor_goes_to_the_view_the_pointer_was_last_seen_on() {
929        let (cell, cursor) = app_with_cursor_host();
930        let mut app = cell.borrow_mut();
931        let RegionTree { root, region, .. } = tree_with_region(&mut app);
932        region.set_cursor(&mut app, SystemMouseCursors::TEXT.into());
933        let tracker = tracker_for(&mut app, root);
934
935        let added = PointerEvent::Added(PointerAddedEvent {
936            kind: PointerDeviceKind::Mouse,
937            position: Offset::new(100.0, 100.0),
938            view_id: ViewId(2),
939            ..PointerAddedEvent::default()
940        });
941        tracker.update_with_event(&mut app, &added, None);
942        assert_eq!(
943            *cursor.activated.borrow(),
944            [(ViewId(2), 0, SystemMouseCursorKind::Text)]
945        );
946
947        region.set_cursor(&mut app, SystemMouseCursors::CLICK.into());
948        tracker.update_all_devices(&mut app);
949        assert_eq!(
950            cursor.activated.borrow().last(),
951            Some(&(ViewId(2), 0, SystemMouseCursorKind::Click)),
952            "a frame-triggered update has no event of its own; the device's last one names the view"
953        );
954    }
955
956    #[test]
957    fn an_opaque_region_hides_the_region_behind_it_and_a_transparent_one_does_not() {
958        let cell = AppCell::new();
959        let mut app = cell.borrow_mut();
960        let back = RenderMouseRegion::new(&mut app, true, None);
961        let front = RenderMouseRegion::new(&mut app, true, Some(back.as_box()));
962        let root = RenderRepaintBoundary::new(&mut app, Some(front.as_box()));
963        let owner = PipelineOwner::new(&mut app, None);
964        owner.set_root_node(&mut app, Some(root.as_object()));
965        root.schedule_initial_layout(&mut app);
966        root.layout(
967            &mut app,
968            BoxConstraints::tight(Size::new(100.0, 100.0)),
969            false,
970        );
971        let tracker = tracker_for(&mut app, root);
972        let back_log = record_events(&mut app, back);
973        let front_log = record_events(&mut app, front);
974
975        tracker.update_with_event(&mut app, &mouse_added(Offset::new(10.0, 10.0)), None);
976        assert_eq!(*front_log.borrow(), ["enter Offset(10.0, 10.0)"]);
977        assert_eq!(
978            *back_log.borrow(),
979            ["enter Offset(10.0, 10.0)"],
980            "the front region's hit test still visits its child; opacity only stops what is behind the region in the parent's hit test"
981        );
982    }
983
984    #[test]
985    fn hover_reaches_the_region_through_handle_event() {
986        let cell = AppCell::new();
987        let mut app = cell.borrow_mut();
988        let region = RenderMouseRegion::new(&mut app, true, None);
989        let log = record_events(&mut app, region);
990        let event = mouse_hover(Offset::new(3.0, 4.0));
991        let entry = BoxHitTestEntry::new(region.as_box(), Offset::new(3.0, 4.0));
992        region.handle_event(&mut app, &event, &entry);
993        assert_eq!(*log.borrow(), ["hover Offset(3.0, 4.0)"]);
994    }
995
996    #[test]
997    fn an_opaque_region_absorbs_the_hit_test_for_what_is_behind_it() {
998        let cell = AppCell::new();
999        let mut app = cell.borrow_mut();
1000        let region = RenderMouseRegion::new(&mut app, true, None);
1001        let root = RenderRepaintBoundary::new(&mut app, Some(region.as_box()));
1002        let owner = PipelineOwner::new(&mut app, None);
1003        owner.set_root_node(&mut app, Some(root.as_object()));
1004        root.schedule_initial_layout(&mut app);
1005        root.layout(
1006            &mut app,
1007            BoxConstraints::tight(Size::new(100.0, 100.0)),
1008            false,
1009        );
1010
1011        let mut result = HitTestResult::new();
1012        assert!(region.hit_test(
1013            &mut app,
1014            &mut BoxHitTestResult::wrap(&mut result),
1015            Offset::new(5.0, 5.0)
1016        ));
1017        region.set_opaque(&mut app, false);
1018        let mut result = HitTestResult::new();
1019        assert!(!region.hit_test(
1020            &mut app,
1021            &mut BoxHitTestResult::wrap(&mut result),
1022            Offset::new(5.0, 5.0)
1023        ));
1024        assert_eq!(
1025            result.path().len(),
1026            1,
1027            "the region is still on the path; it just does not claim the hit"
1028        );
1029    }
1030
1031    #[test]
1032    fn a_region_with_no_annotation_neighbours_still_uses_the_fallback_cursor() {
1033        let (cell, cursor) = app_with_cursor_host();
1034        let mut app = cell.borrow_mut();
1035        let child =
1036            RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(10.0, 10.0)), None);
1037        let root = RenderRepaintBoundary::new(&mut app, Some(child.as_box()));
1038        let owner = PipelineOwner::new(&mut app, None);
1039        owner.set_root_node(&mut app, Some(root.as_object()));
1040        root.schedule_initial_layout(&mut app);
1041        root.layout(
1042            &mut app,
1043            BoxConstraints::tight(Size::new(100.0, 100.0)),
1044            false,
1045        );
1046        let tracker = tracker_for(&mut app, root);
1047        tracker.update_with_event(&mut app, &mouse_added(Offset::new(5.0, 5.0)), None);
1048        assert_eq!(
1049            *cursor.activated.borrow(),
1050            [(ViewId(0), 0, SystemMouseCursorKind::Basic)]
1051        );
1052    }
1053}