Skip to main content

inset_rendering/
mouse_tracker.rs

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