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.device(),
325                details.triggering_event.as_ref(),
326                cursors,
327            );
328    }
329
330    /// Whether or not at least one mouse is connected and has produced events.
331    pub fn mouse_is_connected(self: Handle<Self>, app: &App) -> bool {
332        !app.get(self).mouse_states.is_empty()
333    }
334
335    /// Perform a device update for one device according to the given new event.
336    ///
337    /// The [`update_with_event`] is typically called by the `RendererBinding` during
338    /// the handler of a pointer event. All pointer events should call this
339    /// method, and let [`MouseTracker`] filter which to react to.
340    ///
341    /// The `hit_test_result` serves as an optional optimization, and is the hit
342    /// test result already performed by the `RendererBinding` for other gestures.
343    /// It can be `None`, but when it's not `None`, it should be identical to the
344    /// result from directly calling `hit_test_in_view` given in the constructor
345    /// (which means that it must not use the cached result for
346    /// [`PointerEvent::Move`]).
347    ///
348    /// The `update_with_event` is one of the two ways of updating mouse
349    /// states, the other one being [`update_all_devices`].
350    ///
351    /// [`update_with_event`]: MouseTracker::update_with_event
352    /// [`update_all_devices`]: MouseTracker::update_all_devices
353    pub fn update_with_event(
354        self: Handle<Self>,
355        app: &mut App,
356        event: &PointerEvent,
357        hit_test_result: Option<&HitTestResult>,
358    ) {
359        if event.kind() != PointerDeviceKind::Mouse && event.kind() != PointerDeviceKind::Stylus {
360            // Only track mouse and stylus events.
361            return;
362        }
363        if is_signal_event(event) {
364            // Ignore signal events, which are not tracked by the mouse tracker.
365            return;
366        }
367        let is_removed = matches!(event, PointerEvent::Removed(_));
368        let owned_result;
369        let result: Option<&HitTestResult> = if is_removed {
370            None
371        } else if let Some(result) = hit_test_result {
372            Some(result)
373        } else {
374            owned_result = self.run_hit_test(app, event.position(), event.view_id());
375            Some(&owned_result)
376        };
377        let device = event.device();
378        if !MouseTracker::should_mark_state_dirty(app.get(self).mouse_states.get(&device), event) {
379            return;
380        }
381
382        self.monitor_mouse_connection(app, |app| {
383            self.device_update_phase(app, |app| {
384                // Update mouse_states
385                if !app.get(self).mouse_states.contains_key(&device) {
386                    if is_removed {
387                        return;
388                    }
389                    app.get_mut(self)
390                        .mouse_states
391                        .insert(device, MouseState::new(event.clone()));
392                } else {
393                    debug_assert!(!matches!(event, PointerEvent::Added(_)));
394                }
395                let next_annotations = match result {
396                    Some(result) => {
397                        MouseTracker::hit_test_in_view_result_to_annotations(app, result)
398                    }
399                    None => Annotations::new(),
400                };
401                let target_state = app
402                    .get_mut(self)
403                    .mouse_states
404                    .get_mut(&device)
405                    .expect("the state was just found or created");
406                let last_event = target_state.replace_latest_event(event.clone());
407                let retained = MouseTracker::retain_annotations(app, &next_annotations);
408                let target_state = app
409                    .get_mut(self)
410                    .mouse_states
411                    .get_mut(&device)
412                    .expect("the state was just found or created");
413                let (last_annotations, last_retained) =
414                    target_state.replace_annotations(next_annotations.clone(), retained);
415                if is_removed {
416                    app.get_mut(self).mouse_states.shift_remove(&device);
417                }
418                self.handle_device_update(
419                    app,
420                    MouseTrackerUpdateDetails::by_pointer_event(
421                        last_annotations,
422                        next_annotations,
423                        Some(last_event),
424                        event.clone(),
425                    ),
426                );
427                MouseTracker::release_annotations(app, last_retained);
428            });
429        });
430    }
431
432    /// Perform a device update for all devices.
433    ///
434    /// This is called by the `RendererBinding` after a frame, since the render
435    /// tree might have changed under a still mouse.
436    ///
437    /// The `update_all_devices` is one of the two ways of updating mouse
438    /// states, the other one being [`update_with_event`].
439    ///
440    /// [`update_with_event`]: MouseTracker::update_with_event
441    pub fn update_all_devices(self: Handle<Self>, app: &mut App) {
442        self.device_update_phase(app, |app| {
443            let devices: Vec<i64> = app.get(self).mouse_states.keys().copied().collect();
444            for device in devices {
445                let Some(last_event) = app
446                    .get(self)
447                    .mouse_states
448                    .get(&device)
449                    .map(|state| state.latest_event.clone())
450                else {
451                    continue;
452                };
453                let next_annotations = self.find_annotations(app, &last_event);
454                if !app.get(self).mouse_states.contains_key(&device) {
455                    continue;
456                }
457                let retained = MouseTracker::retain_annotations(app, &next_annotations);
458                let Some(dirty_state) = app.get_mut(self).mouse_states.get_mut(&device) else {
459                    continue;
460                };
461                let (last_annotations, last_retained) =
462                    dirty_state.replace_annotations(next_annotations.clone(), retained);
463                self.handle_device_update(
464                    app,
465                    MouseTrackerUpdateDetails::by_new_frame(
466                        last_annotations,
467                        next_annotations,
468                        last_event,
469                    ),
470                );
471                MouseTracker::release_annotations(app, last_retained);
472            }
473        });
474    }
475
476    /// Returns the active mouse cursor for a device.
477    ///
478    /// The return value is the last [`MouseCursorRef`] activated onto this
479    /// device, even if the activation failed.
480    ///
481    /// This function is only active in debug mode: in release it returns `None`.
482    pub fn debug_device_active_cursor(
483        self: Handle<Self>,
484        app: &App,
485        device: i64,
486    ) -> Option<MouseCursorRef> {
487        app.get(self)
488            .mouse_cursor_mixin
489            .debug_device_active_cursor(device)
490    }
491
492    // Used to wrap any procedures that might change `mouse_is_connected`.
493    //
494    // This method records `mouse_is_connected` before and after the `task`, and
495    // notifies listeners if the value changed.
496    fn monitor_mouse_connection(self: Handle<Self>, app: &mut App, task: impl FnOnce(&mut App)) {
497        let mouse_was_connected = self.mouse_is_connected(app);
498        task(app);
499        if mouse_was_connected != self.mouse_is_connected(app) {
500            self.notify_listeners(app);
501        }
502    }
503
504    // Used to wrap any procedures that might change the state of a device.
505    //
506    // A device update, i.e. a change that might affect which annotations a
507    // device hovers, must not happen during another device update.
508    fn device_update_phase(self: Handle<Self>, app: &mut App, task: impl FnOnce(&mut App)) {
509        debug_assert!(!app.get(self).debug_during_device_update);
510        if cfg!(debug_assertions) {
511            app.get_mut(self).debug_during_device_update = true;
512        }
513        task(app);
514        if cfg!(debug_assertions) {
515            app.get_mut(self).debug_during_device_update = false;
516        }
517    }
518
519    // Handles device update and dispatches mouse event callbacks.
520    fn handle_device_update_mouse_events(app: &mut App, details: &MouseTrackerUpdateDetails) {
521        let latest_event = details.latest_event();
522        let last_annotations = &details.last_annotations;
523        let next_annotations = &details.next_annotations;
524
525        // Order is important for mouse event callbacks. The
526        // `hit_test_in_view_result_to_annotations` returns annotations in the visual order
527        // from front to back, called the "hit-test order". The algorithm here is
528        // explained in https://github.com/flutter/flutter/issues/41420
529
530        // Send exit events to annotations that are in last but not in next, in
531        // hit-test order.
532        let base_exit_event = PointerExitEvent::from_mouse_event(latest_event);
533        for (target, transform) in last_annotations {
534            if next_annotations.contains_key(target) {
535                continue;
536            }
537            let Some(annotation) = target.mouse_tracker_annotation(app) else {
538                continue;
539            };
540            if annotation.valid_for_mouse_tracker
541                && let Some(on_exit) = annotation.on_exit
542            {
543                on_exit(app, base_exit_event.transformed(Some(*transform)));
544            }
545        }
546
547        // Send enter events to annotations that are not in last but in next, in
548        // reverse hit-test order.
549        let entering_annotations: Vec<(AnyRenderObject, Matrix4)> = next_annotations
550            .iter()
551            .filter(|(target, _)| !last_annotations.contains_key(*target))
552            .map(|(target, transform)| (*target, *transform))
553            .collect();
554        let base_enter_event = PointerEnterEvent::from_mouse_event(latest_event);
555        for (target, transform) in entering_annotations.into_iter().rev() {
556            let Some(annotation) = target.mouse_tracker_annotation(app) else {
557                continue;
558            };
559            if annotation.valid_for_mouse_tracker
560                && let Some(on_enter) = annotation.on_enter
561            {
562                on_enter(app, base_enter_event.transformed(Some(transform)));
563            }
564        }
565    }
566}
567
568/// Dart's `event is PointerSignalEvent`.
569fn is_signal_event(event: &PointerEvent) -> bool {
570    matches!(
571        event,
572        PointerEvent::Scroll(_) | PointerEvent::ScrollInertiaCancel(_) | PointerEvent::Scale(_)
573    )
574}
575
576/// Dart's `entry.target is MouseTrackerAnnotation`: the render object behind a hit test entry,
577/// when it implements the annotation.
578fn annotation_target(app: &App, entry: &HitTestEntry) -> Option<AnyRenderObject> {
579    let target: &dyn Any = entry.target();
580    let target = target
581        .downcast_ref::<BoxHitTestEntry>()?
582        .target()
583        .as_object();
584    target.mouse_tracker_annotation(app).map(|_| target)
585}
586
587#[cfg(test)]
588mod tests {
589    use inset_foundation::AppCell;
590    use std::cell::{Cell, RefCell};
591
592    use inset_embedder::{
593        InertPlatform, Platform, PlatformRef, Size, SystemMouseCursorKind, TargetPlatform, ViewRef,
594    };
595    use inset_gestures::{
596        PointerAddedEvent, PointerHoverEvent, PointerRemovedEvent, PointerScrollEvent,
597    };
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    struct CursorRecordingPlatform {
611        activated: RefCell<Vec<(i64, SystemMouseCursorKind)>>,
612    }
613
614    impl Platform for CursorRecordingPlatform {
615        fn target_platform(&self) -> TargetPlatform {
616            InertPlatform.target_platform()
617        }
618
619        fn request_frame(&self) {}
620
621        fn now(&self) -> std::time::Instant {
622            InertPlatform.now()
623        }
624
625        fn wake_at(&self, _deadline: std::time::Instant) {}
626
627        fn views(&self) -> Vec<ViewRef> {
628            Vec::new()
629        }
630
631        fn view(&self, _id: ViewId) -> Option<ViewRef> {
632            None
633        }
634
635        fn implicit_view(&self) -> Option<ViewRef> {
636            None
637        }
638
639        fn activate_system_cursor(&self, device: i64, kind: SystemMouseCursorKind) {
640            self.activated.borrow_mut().push((device, kind));
641        }
642    }
643
644    fn app_with_cursor_host() -> (Rc<AppCell>, Rc<CursorRecordingPlatform>) {
645        let platform = Rc::new(CursorRecordingPlatform {
646            activated: RefCell::new(Vec::new()),
647        });
648        let platform_ref: PlatformRef = Rc::clone(&platform) as PlatformRef;
649        (AppCell::with_platform(platform_ref), platform)
650    }
651
652    /// A laid-out tree: a 200×200 root holding a mouse region padded 50 on each side, so the
653    /// region covers (50..150, 50..150). Returns the root and the region.
654    struct RegionTree {
655        owner: Handle<PipelineOwner>,
656        root: crate::RenderHandle<RenderRepaintBoundary>,
657        padding: crate::RenderHandle<RenderPadding>,
658        region: crate::RenderHandle<RenderMouseRegion>,
659    }
660
661    fn tree_with_region(app: &mut App) -> RegionTree {
662        let region = RenderMouseRegion::new(app, true, None);
663        let padding = RenderPadding::new(
664            app,
665            inset_painting::EdgeInsets::all(50.0).into(),
666            None,
667            Some(region.as_box()),
668        );
669        let root = RenderRepaintBoundary::new(app, Some(padding.as_box()));
670        let owner = PipelineOwner::new(app, None);
671        owner.set_root_node(app, Some(root.as_object()));
672        root.schedule_initial_layout(app);
673        root.layout(app, BoxConstraints::tight(Size::new(200.0, 200.0)), false);
674        RegionTree {
675            owner,
676            root,
677            padding,
678            region,
679        }
680    }
681
682    fn tracker_for(
683        app: &mut App,
684        root: crate::RenderHandle<RenderRepaintBoundary>,
685    ) -> Handle<MouseTracker> {
686        MouseTracker::new(
687            app,
688            Rc::new(move |app: &mut App, position: Offset, _view_id: ViewId| {
689                let mut result = HitTestResult::new();
690                root.hit_test(app, &mut BoxHitTestResult::wrap(&mut result), position);
691                result
692            }),
693        )
694    }
695
696    fn mouse_hover(position: Offset) -> PointerEvent {
697        PointerEvent::Hover(PointerHoverEvent {
698            kind: PointerDeviceKind::Mouse,
699            position,
700            ..PointerHoverEvent::default()
701        })
702    }
703
704    fn mouse_added(position: Offset) -> PointerEvent {
705        PointerEvent::Added(PointerAddedEvent {
706            kind: PointerDeviceKind::Mouse,
707            position,
708            ..PointerAddedEvent::default()
709        })
710    }
711
712    fn mouse_removed(position: Offset) -> PointerEvent {
713        PointerEvent::Removed(PointerRemovedEvent {
714            kind: PointerDeviceKind::Mouse,
715            position,
716            ..PointerRemovedEvent::default()
717        })
718    }
719
720    /// Records the events a region's callbacks receive.
721    fn record_events(
722        app: &mut App,
723        region: crate::RenderHandle<RenderMouseRegion>,
724    ) -> Rc<RefCell<Vec<String>>> {
725        let log = Rc::new(RefCell::new(Vec::new()));
726        let enter_log = Rc::clone(&log);
727        region.set_on_enter(
728            app,
729            Some(Rc::new(move |_app: &mut App, event: PointerEnterEvent| {
730                enter_log
731                    .borrow_mut()
732                    .push(format!("enter {:?}", event.local_position()));
733            })),
734        );
735        let hover_log = Rc::clone(&log);
736        region.set_on_hover(
737            app,
738            Some(Rc::new(move |_app: &mut App, event: PointerHoverEvent| {
739                hover_log
740                    .borrow_mut()
741                    .push(format!("hover {:?}", event.local_position()));
742            })),
743        );
744        let exit_log = Rc::clone(&log);
745        region.set_on_exit(
746            app,
747            Some(Rc::new(move |_app: &mut App, event: PointerExitEvent| {
748                exit_log
749                    .borrow_mut()
750                    .push(format!("exit {:?}", event.local_position()));
751            })),
752        );
753        log
754    }
755
756    #[test]
757    fn entering_and_leaving_a_region_sends_enter_then_exit_in_local_coordinates() {
758        let cell = AppCell::new();
759        let mut app = cell.borrow_mut();
760        let RegionTree { root, region, .. } = tree_with_region(&mut app);
761        let tracker = tracker_for(&mut app, root);
762        let log = record_events(&mut app, region);
763
764        tracker.update_with_event(&mut app, &mouse_added(Offset::new(10.0, 10.0)), None);
765        assert!(log.borrow().is_empty(), "outside the region: nothing");
766
767        tracker.update_with_event(&mut app, &mouse_hover(Offset::new(60.0, 70.0)), None);
768        assert_eq!(*log.borrow(), ["enter Offset(10.0, 20.0)"]);
769
770        tracker.update_with_event(&mut app, &mouse_hover(Offset::new(160.0, 70.0)), None);
771        assert_eq!(
772            *log.borrow(),
773            ["enter Offset(10.0, 20.0)", "exit Offset(110.0, 20.0)"]
774        );
775    }
776
777    #[test]
778    fn a_still_mouse_notices_the_tree_moving_under_it_after_a_frame() {
779        let cell = AppCell::new();
780        let mut app = cell.borrow_mut();
781        let RegionTree {
782            owner,
783            root,
784            padding,
785            region,
786        } = tree_with_region(&mut app);
787        let tracker = tracker_for(&mut app, root);
788        let log = record_events(&mut app, region);
789        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
790        assert_eq!(*log.borrow(), ["enter Offset(50.0, 50.0)"]);
791
792        // Move the region to the right of the still mouse, then run the frame's update.
793        padding.set_padding(
794            &mut app,
795            inset_painting::EdgeInsetsGeometry::only(150.0, 0.0, 0.0, 0.0),
796        );
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)", "exit Offset(50.0, 50.0)"],
802            "the exit is placed with the transform recorded when the region was entered"
803        );
804    }
805
806    #[test]
807    fn a_region_removed_from_the_tree_sends_no_exit() {
808        let cell = AppCell::new();
809        let mut app = cell.borrow_mut();
810        let RegionTree {
811            owner,
812            root,
813            padding,
814            region,
815        } = tree_with_region(&mut app);
816        let tracker = tracker_for(&mut app, root);
817        let log = record_events(&mut app, region);
818        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
819        assert_eq!(*log.borrow(), ["enter Offset(50.0, 50.0)"]);
820
821        padding.set_child(&mut app, None);
822        owner.flush_layout(&mut app);
823        tracker.update_all_devices(&mut app);
824        assert_eq!(
825            *log.borrow(),
826            ["enter Offset(50.0, 50.0)"],
827            "Flutter: a detached region is no longer valid for the tracker, so no exit is sent"
828        );
829    }
830
831    #[test]
832    fn a_detached_region_is_invalid_for_the_tracker() {
833        let cell = AppCell::new();
834        let mut app = cell.borrow_mut();
835        let region = RenderMouseRegion::new(&mut app, true, None);
836        assert!(region.valid_for_mouse_tracker(&app));
837        let owner = PipelineOwner::new(&mut app, None);
838        region.as_object().attach(&mut app, owner);
839        assert!(region.valid_for_mouse_tracker(&app));
840        region.as_object().detach(&mut app);
841        assert!(!region.valid_for_mouse_tracker(&app));
842        assert!(
843            !region
844                .mouse_tracker_annotation(&app)
845                .unwrap()
846                .valid_for_mouse_tracker
847        );
848    }
849
850    #[test]
851    fn a_removed_mouse_exits_and_disconnects() {
852        let cell = AppCell::new();
853        let mut app = cell.borrow_mut();
854        let RegionTree { root, region, .. } = tree_with_region(&mut app);
855        let tracker = tracker_for(&mut app, root);
856        let log = record_events(&mut app, region);
857        let notifications = Rc::new(Cell::new(0));
858        let counted = Rc::clone(&notifications);
859        tracker.add_listener(
860            &mut app,
861            Listener::new(move |_app| counted.set(counted.get() + 1)),
862        );
863
864        assert!(!tracker.mouse_is_connected(&app));
865        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
866        assert!(tracker.mouse_is_connected(&app));
867        assert_eq!(notifications.get(), 1);
868
869        tracker.update_with_event(&mut app, &mouse_removed(Offset::new(100.0, 100.0)), None);
870        assert!(!tracker.mouse_is_connected(&app));
871        assert_eq!(notifications.get(), 2);
872        assert_eq!(
873            *log.borrow(),
874            ["enter Offset(50.0, 50.0)", "exit Offset(50.0, 50.0)"]
875        );
876    }
877
878    #[test]
879    fn touch_and_signal_events_are_ignored() {
880        let cell = AppCell::new();
881        let mut app = cell.borrow_mut();
882        let RegionTree { root, region, .. } = tree_with_region(&mut app);
883        let tracker = tracker_for(&mut app, root);
884        let log = record_events(&mut app, region);
885
886        tracker.update_with_event(
887            &mut app,
888            &PointerEvent::Hover(PointerHoverEvent {
889                position: Offset::new(100.0, 100.0),
890                ..PointerHoverEvent::default()
891            }),
892            None,
893        );
894        assert!(!tracker.mouse_is_connected(&app), "touch: not tracked");
895
896        tracker.update_with_event(
897            &mut app,
898            &PointerEvent::Scroll(PointerScrollEvent {
899                kind: PointerDeviceKind::Mouse,
900                position: Offset::new(100.0, 100.0),
901                ..PointerScrollEvent::default()
902            }),
903            None,
904        );
905        assert!(!tracker.mouse_is_connected(&app), "a signal: not tracked");
906        assert!(log.borrow().is_empty());
907    }
908
909    #[test]
910    fn the_front_regions_cursor_wins_and_deferring_regions_let_the_one_behind_decide() {
911        let (cell, platform) = app_with_cursor_host();
912        let mut app = cell.borrow_mut();
913        let RegionTree {
914            owner,
915            root,
916            region,
917            ..
918        } = tree_with_region(&mut app);
919        region.set_cursor(&mut app, SystemMouseCursors::TEXT.into());
920        // A deferring region in front of the text region, sharing its bounds.
921        let front = RenderMouseRegion::new(&mut app, true, None);
922        region.set_child(&mut app, Some(front.as_box()));
923        owner.flush_layout(&mut app);
924        let tracker = tracker_for(&mut app, root);
925
926        tracker.update_with_event(&mut app, &mouse_added(Offset::new(100.0, 100.0)), None);
927        assert_eq!(
928            *platform.activated.borrow(),
929            [(0, SystemMouseCursorKind::Text)]
930        );
931
932        front.set_cursor(&mut app, SystemMouseCursors::CLICK.into());
933        tracker.update_all_devices(&mut app);
934        assert_eq!(
935            *platform.activated.borrow(),
936            [
937                (0, SystemMouseCursorKind::Text),
938                (0, SystemMouseCursorKind::Click)
939            ]
940        );
941        let click: MouseCursorRef = SystemMouseCursors::CLICK.into();
942        assert!(*tracker.debug_device_active_cursor(&app, 0).unwrap() == *click);
943
944        tracker.update_with_event(&mut app, &mouse_hover(Offset::new(10.0, 10.0)), None);
945        assert_eq!(
946            platform.activated.borrow().last().unwrap().1,
947            SystemMouseCursorKind::Basic,
948            "over nothing, the fallback cursor"
949        );
950    }
951
952    #[test]
953    fn an_opaque_region_hides_the_region_behind_it_and_a_transparent_one_does_not() {
954        let cell = AppCell::new();
955        let mut app = cell.borrow_mut();
956        let back = RenderMouseRegion::new(&mut app, true, None);
957        let front = RenderMouseRegion::new(&mut app, true, Some(back.as_box()));
958        let root = RenderRepaintBoundary::new(&mut app, Some(front.as_box()));
959        let owner = PipelineOwner::new(&mut app, None);
960        owner.set_root_node(&mut app, Some(root.as_object()));
961        root.schedule_initial_layout(&mut app);
962        root.layout(
963            &mut app,
964            BoxConstraints::tight(Size::new(100.0, 100.0)),
965            false,
966        );
967        let tracker = tracker_for(&mut app, root);
968        let back_log = record_events(&mut app, back);
969        let front_log = record_events(&mut app, front);
970
971        tracker.update_with_event(&mut app, &mouse_added(Offset::new(10.0, 10.0)), None);
972        assert_eq!(*front_log.borrow(), ["enter Offset(10.0, 10.0)"]);
973        assert_eq!(
974            *back_log.borrow(),
975            ["enter Offset(10.0, 10.0)"],
976            "the front region's hit test still visits its child; opacity only stops what is behind the region in the parent's hit test"
977        );
978    }
979
980    #[test]
981    fn hover_reaches_the_region_through_handle_event() {
982        let cell = AppCell::new();
983        let mut app = cell.borrow_mut();
984        let region = RenderMouseRegion::new(&mut app, true, None);
985        let log = record_events(&mut app, region);
986        let event = mouse_hover(Offset::new(3.0, 4.0));
987        let entry = BoxHitTestEntry::new(region.as_box(), Offset::new(3.0, 4.0));
988        region.handle_event(&mut app, &event, &entry);
989        assert_eq!(*log.borrow(), ["hover Offset(3.0, 4.0)"]);
990    }
991
992    #[test]
993    fn an_opaque_region_absorbs_the_hit_test_for_what_is_behind_it() {
994        let cell = AppCell::new();
995        let mut app = cell.borrow_mut();
996        let region = RenderMouseRegion::new(&mut app, true, None);
997        let root = RenderRepaintBoundary::new(&mut app, Some(region.as_box()));
998        let owner = PipelineOwner::new(&mut app, None);
999        owner.set_root_node(&mut app, Some(root.as_object()));
1000        root.schedule_initial_layout(&mut app);
1001        root.layout(
1002            &mut app,
1003            BoxConstraints::tight(Size::new(100.0, 100.0)),
1004            false,
1005        );
1006
1007        let mut result = HitTestResult::new();
1008        assert!(region.hit_test(
1009            &mut app,
1010            &mut BoxHitTestResult::wrap(&mut result),
1011            Offset::new(5.0, 5.0)
1012        ));
1013        region.set_opaque(&mut app, false);
1014        let mut result = HitTestResult::new();
1015        assert!(!region.hit_test(
1016            &mut app,
1017            &mut BoxHitTestResult::wrap(&mut result),
1018            Offset::new(5.0, 5.0)
1019        ));
1020        assert_eq!(
1021            result.path().len(),
1022            1,
1023            "the region is still on the path; it just does not claim the hit"
1024        );
1025    }
1026
1027    #[test]
1028    fn a_region_with_no_annotation_neighbours_still_uses_the_fallback_cursor() {
1029        let (cell, platform) = app_with_cursor_host();
1030        let mut app = cell.borrow_mut();
1031        let child =
1032            RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(10.0, 10.0)), None);
1033        let root = RenderRepaintBoundary::new(&mut app, Some(child.as_box()));
1034        let owner = PipelineOwner::new(&mut app, None);
1035        owner.set_root_node(&mut app, Some(root.as_object()));
1036        root.schedule_initial_layout(&mut app);
1037        root.layout(
1038            &mut app,
1039            BoxConstraints::tight(Size::new(100.0, 100.0)),
1040            false,
1041        );
1042        let tracker = tracker_for(&mut app, root);
1043        tracker.update_with_event(&mut app, &mouse_added(Offset::new(5.0, 5.0)), None);
1044        assert_eq!(
1045            *platform.activated.borrow(),
1046            [(0, SystemMouseCursorKind::Basic)]
1047        );
1048    }
1049}