Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
eede7929
Unverified
Commit
eede7929
authored
Oct 10, 2019
by
Jonah Williams
Committed by
GitHub
Oct 10, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Revert "Rewrite MouseTracker's tracking and notifying algorithm (#42031)" (#42478)
parent
2b635816
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
345 additions
and
783 deletions
+345
-783
diagnostics.dart
packages/flutter/lib/src/foundation/diagnostics.dart
+1
-1
mouse_tracking.dart
packages/flutter/lib/src/gestures/mouse_tracking.dart
+210
-280
mouse_tracking_test.dart
packages/flutter/test/gestures/mouse_tracking_test.dart
+134
-502
No files found.
packages/flutter/lib/src/foundation/diagnostics.dart
View file @
eede7929
...
@@ -2435,7 +2435,7 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
...
@@ -2435,7 +2435,7 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
///
///
/// See also:
/// See also:
///
///
/// * [ObjectFlag
Propert
y], which provides similar functionality but accepts
/// * [ObjectFlag
Summar
y], which provides similar functionality but accepts
/// only one flag, and is preferred if there is only one entry.
/// only one flag, and is preferred if there is only one entry.
/// * [IterableProperty], which provides similar functionality describing
/// * [IterableProperty], which provides similar functionality describing
/// the values a collection of objects.
/// the values a collection of objects.
...
...
packages/flutter/lib/src/gestures/mouse_tracking.dart
View file @
eede7929
...
@@ -2,10 +2,9 @@
...
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:collection'
show
LinkedHashSet
;
import
'dart:ui'
;
import
'dart:ui'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
show
ChangeNotifier
,
visibleForTesting
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'events.dart'
;
import
'events.dart'
;
...
@@ -49,82 +48,49 @@ class MouseTrackerAnnotation {
...
@@ -49,82 +48,49 @@ class MouseTrackerAnnotation {
@override
@override
String
toString
()
{
String
toString
()
{
final
List
<
String
>
callbacks
=
<
String
>[];
final
String
none
=
(
onEnter
==
null
&&
onExit
==
null
&&
onHover
==
null
)
?
' <none>'
:
''
;
if
(
onEnter
!=
null
)
return
'[
$runtimeType${hashCode.toRadixString(16)}$none
'
callbacks
.
add
(
'enter'
);
'
${onEnter == null ? '' : ' onEnter'}
'
if
(
onHover
!=
null
)
'
${onHover == null ? '' : ' onHover'}
'
callbacks
.
add
(
'hover'
);
'
${onExit == null ? '' : ' onExit'}
]'
;
if
(
onExit
!=
null
)
callbacks
.
add
(
'exit'
);
final
String
describeCallbacks
=
callbacks
.
isEmpty
?
'<none>'
:
callbacks
.
join
(
' '
);
return
'
${describeIdentity(this)}
(callbacks:
$describeCallbacks
)'
;
}
}
}
}
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
// Used internally by the MouseTracker for accounting for which annotation is
// active on which devices inside of the MouseTracker.
class
_TrackedAnnotation
{
_TrackedAnnotation
(
this
.
annotation
);
final
MouseTrackerAnnotation
annotation
;
/// Tracks devices that are currently active for this annotation.
///
/// If the mouse pointer corresponding to the integer device ID is
/// present in the Set, then it is currently inside of the annotated layer.
///
/// This is used to detect layers that used to have the mouse pointer inside
/// them, but now no longer do (to facilitate exit notification).
Set
<
int
>
activeDevices
=
<
int
>{};
}
/// Describes a function that finds an annotation given an offset in logical
/// coordinates.
///
///
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// It is used by the [MouseTracker] to fetch annotations for the mouse
/// position.
/// position.
typedef
MouseDetectorAnnotationFinder
=
Iterable
<
MouseTrackerAnnotation
>
Function
(
Offset
offset
);
typedef
MouseDetectorAnnotationFinder
=
Iterable
<
MouseTrackerAnnotation
>
Function
(
Offset
offset
);
// Various states of each connected mouse device.
/// Keeps state about which objects are interested in tracking mouse positions
//
/// and notifies them when a mouse pointer enters, moves, or leaves an annotated
// It is used by [MouseTracker] to compute which callbacks should be triggered
/// region that they are interested in.
// by each event.
class
_MouseState
{
_MouseState
({
@required
PointerEvent
mostRecentEvent
,
})
:
assert
(
mostRecentEvent
!=
null
),
_mostRecentEvent
=
mostRecentEvent
;
// The list of annotations that contains this device during the last frame.
//
// It uses [LinkedHashSet] to keep the insertion order.
LinkedHashSet
<
MouseTrackerAnnotation
>
lastAnnotations
=
LinkedHashSet
<
MouseTrackerAnnotation
>();
// The most recent mouse event observed from this device.
//
// The [mostRecentEvent] is never null.
PointerEvent
get
mostRecentEvent
=>
_mostRecentEvent
;
PointerEvent
_mostRecentEvent
;
set
mostRecentEvent
(
PointerEvent
value
)
{
assert
(
value
!=
null
);
assert
(
value
.
device
==
_mostRecentEvent
.
device
);
_mostRecentEvent
=
value
;
}
int
get
device
=>
_mostRecentEvent
.
device
;
@override
String
toString
()
{
final
String
describeEvent
=
'
${_mostRecentEvent.runtimeType}
(device:
${_mostRecentEvent.device}
)'
;
final
String
describeAnnotations
=
'[list of
${lastAnnotations.length}
]'
;
return
'
${describeIdentity(this)}
(event:
$describeEvent
, annotations:
$describeAnnotations
)'
;
}
}
/// Maintains the relationship between mouse devices and
/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes
/// thereof.
///
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
/// [mouseIsConnected] changes.
///
///
/// An instance of [MouseTracker] is owned by the global singleton of
/// Owned by the [RendererBinding] class.
/// [RendererBinding].
class
MouseTracker
extends
ChangeNotifier
{
class
MouseTracker
extends
ChangeNotifier
{
/// Creates a mouse tracker to keep track of mouse locations.
/// Creates a mouse tracker to keep track of mouse locations.
///
///
/// The first parameter is a [PointerRouter], which [MouseTracker] will
/// subscribe to and receive events from. Usually it is the global singleton
/// instance [GestureBinding.pointerRouter].
///
/// The second parameter is a function with which the [MouseTracker] can
/// search for [MouseTrackerAnnotation]s at a given position.
/// Usually it is [Layer.findAll] of the root layer.
///
/// All of the parameters must not be null.
/// All of the parameters must not be null.
MouseTracker
(
this
.
_router
,
this
.
annotationFinder
)
MouseTracker
(
this
.
_router
,
this
.
annotationFinder
)
:
assert
(
_router
!=
null
),
:
assert
(
_router
!=
null
),
...
@@ -138,67 +104,60 @@ class MouseTracker extends ChangeNotifier {
...
@@ -138,67 +104,60 @@ class MouseTracker extends ChangeNotifier {
_router
.
removeGlobalRoute
(
_handleEvent
);
_router
.
removeGlobalRoute
(
_handleEvent
);
}
}
/// Find annotations at a given offset in global logical coordinate space
// The pointer router that the mouse tracker listens to for events.
/// in visual order from front to back.
///
/// [MouseTracker] uses this callback to know which annotations are affected
/// by each device.
///
/// The annotations should be returned in visual order from front to
/// back, so that the callbacks are called in an correct order.
final
MouseDetectorAnnotationFinder
annotationFinder
;
// The pointer router that the mouse tracker listens to, and receives new
// mouse events from.
final
PointerRouter
_router
;
final
PointerRouter
_router
;
// Tracks the state of connected mouse devices.
/// Used to find annotations at a given logical coordinate.
//
final
MouseDetectorAnnotationFinder
annotationFinder
;
// It is the source of truth for the list of connected mouse devices.
final
Map
<
int
,
_MouseState
>
_mouseStates
=
<
int
,
_MouseState
>{};
// Returns the mouse state of a device. If it doesn't exist, create one using
// The collection of annotations that are currently being tracked. They may or
// `mostRecentEvent`.
// may not be active, depending on the value of _TrackedAnnotation.active.
//
final
Map
<
MouseTrackerAnnotation
,
_TrackedAnnotation
>
_trackedAnnotations
=
<
MouseTrackerAnnotation
,
_TrackedAnnotation
>{};
// The returned value is never null.
_MouseState
_guaranteeMouseState
(
int
device
,
PointerEvent
mostRecentEvent
)
{
/// Track an annotation so that if the mouse enters it, we send it events.
final
_MouseState
currentState
=
_mouseStates
[
device
];
///
if
(
currentState
==
null
)
{
/// This is typically called when the [AnnotatedRegion] containing this
_addMouseDevice
(
device
,
mostRecentEvent
);
/// annotation has been added to the layer tree.
void
attachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
_trackedAnnotations
[
annotation
]
=
_TrackedAnnotation
(
annotation
);
// Schedule a check so that we test this new annotation to see if any mouse
// is currently inside its region. It has to happen after the frame is
// complete so that the annotation layer has been added before the check.
if
(
mouseIsConnected
)
{
_scheduleMousePositionCheck
();
}
}
final
_MouseState
result
=
currentState
??
_mouseStates
[
device
];
assert
(
result
!=
null
);
return
result
;
}
}
//
The collection of annotations that are currently being tracked.
//
/ Stops tracking an annotation, indicating that it has been removed from the
//
It is operated on by [attachAnnotation] and [detachAnnotation]
.
//
/ layer tree
.
final
Set
<
MouseTrackerAnnotation
>
_trackedAnnotations
=
<
MouseTrackerAnnotation
>{};
///
bool
get
_hasAttachedAnnotations
=>
_trackedAnnotations
.
isNotEmpty
;
/// An assertion error will be thrown if the associated layer is not removed
/// and receives another mouse hit.
void
_addMouseDevice
(
int
device
,
PointerEvent
event
)
{
void
detachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
final
bool
wasConnected
=
mouseIsConnected
;
final
_TrackedAnnotation
trackedAnnotation
=
_findAnnotation
(
annotation
)
;
assert
(!
_mouseStates
.
containsKey
(
device
));
for
(
int
deviceId
in
trackedAnnotation
.
activeDevices
)
{
_mouseStates
[
device
]
=
_MouseState
(
mostRecentEvent:
event
);
if
(
annotation
.
onExit
!=
null
)
{
// Schedule a check to enter annotations that might contain this pointer.
final
PointerEvent
event
=
_lastMouseEvent
[
deviceId
]
??
_pendingRemovals
[
deviceId
];
_checkDeviceUpdates
(
device:
device
);
assert
(
event
!=
null
);
if
(
mouseIsConnected
!=
wasConnected
)
{
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
notifyListeners
();
}
}
}
_trackedAnnotations
.
remove
(
annotation
);
}
}
void
_removeMouseDevice
(
int
device
,
PointerEvent
event
)
{
bool
_scheduledPostFramePositionCheck
=
false
;
final
bool
wasConnected
=
mouseIsConnected
;
// Schedules a position check at the end of this frame for those annotations
assert
(
_mouseStates
.
containsKey
(
device
));
// that have been added.
final
_MouseState
disconnectedMouseState
=
_mouseStates
.
remove
(
device
);
void
_scheduleMousePositionCheck
()
{
disconnectedMouseState
.
mostRecentEvent
=
event
;
// If we're not tracking anything, then there is no point in registering a
// Schedule a check to exit annotations that used to contain this pointer.
// frame callback or scheduling a frame. By definition there are no active
_checkDeviceUpdates
(
// annotations that need exiting, either.
device:
device
,
if
(
_trackedAnnotations
.
isNotEmpty
&&
!
_scheduledPostFramePositionCheck
)
{
disconnectedMouseState:
disconnectedMouseState
,
_scheduledPostFramePositionCheck
=
true
;
);
SchedulerBinding
.
instance
.
addPostFrameCallback
((
Duration
duration
)
{
if
(
mouseIsConnected
!=
wasConnected
)
{
_sendMouseNotifications
(
_lastMouseEvent
.
keys
);
notifyListeners
();
_scheduledPostFramePositionCheck
=
false
;
});
}
}
}
}
...
@@ -207,199 +166,170 @@ class MouseTracker extends ChangeNotifier {
...
@@ -207,199 +166,170 @@ class MouseTracker extends ChangeNotifier {
if
(
event
.
kind
!=
PointerDeviceKind
.
mouse
)
{
if
(
event
.
kind
!=
PointerDeviceKind
.
mouse
)
{
return
;
return
;
}
}
final
int
device
=
event
.
device
;
final
int
device
Id
=
event
.
device
;
if
(
event
is
PointerAddedEvent
)
{
if
(
event
is
PointerAddedEvent
)
{
_addMouseDevice
(
device
,
event
);
// If we are adding the device again, then we're not removing it anymore.
}
else
if
(
event
is
PointerRemovedEvent
)
{
_pendingRemovals
.
remove
(
deviceId
);
_removeMouseDevice
(
device
,
event
);
_addMouseEvent
(
deviceId
,
event
);
}
else
if
(
event
is
PointerHoverEvent
)
{
_sendMouseNotifications
(<
int
>{
deviceId
});
final
_MouseState
mouseState
=
_guaranteeMouseState
(
device
,
event
);
return
;
final
PointerEvent
previousEvent
=
mouseState
.
mostRecentEvent
;
}
mouseState
.
mostRecentEvent
=
event
;
if
(
event
is
PointerRemovedEvent
)
{
if
(
previousEvent
is
PointerAddedEvent
||
previousEvent
.
position
!=
event
.
position
)
{
_removeMouseEvent
(
deviceId
,
event
);
// Only send notifications if we have our first event, or if the
// If the mouse was removed, then we need to schedule one more check to
// location of the mouse has changed
// exit any annotations that were active.
_checkDeviceUpdates
(
device:
device
);
_sendMouseNotifications
(<
int
>{
deviceId
});
}
else
{
if
(
event
is
PointerMoveEvent
||
event
is
PointerHoverEvent
||
event
is
PointerDownEvent
)
{
final
PointerEvent
lastEvent
=
_lastMouseEvent
[
deviceId
];
_addMouseEvent
(
deviceId
,
event
);
if
(
lastEvent
==
null
||
lastEvent
is
PointerAddedEvent
||
lastEvent
.
position
!=
event
.
position
)
{
// Only schedule a frame if we have our first event, or if the
// location of the mouse has changed, and only if there are tracked annotations.
_sendMouseNotifications
(<
int
>{
deviceId
});
}
}
}
}
}
}
}
bool
_scheduledPostFramePositionCheck
=
false
;
_TrackedAnnotation
_findAnnotation
(
MouseTrackerAnnotation
annotation
)
{
// Schedules a position check at the end of this frame.
final
_TrackedAnnotation
trackedAnnotation
=
_trackedAnnotations
[
annotation
];
// It is only called during a frame during which annotations have been added.
assert
(
void
_scheduleMousePositionCheck
()
{
trackedAnnotation
!=
null
,
// If we're not tracking anything, then there is no point in registering a
'Unable to find annotation
$annotation
in tracked annotations. '
// frame callback or scheduling a frame. By definition there are no active
'Check that attachAnnotation has been called for all annotated layers.'
);
// annotations that need exiting, either.
return
trackedAnnotation
;
if
(!
_scheduledPostFramePositionCheck
)
{
_scheduledPostFramePositionCheck
=
true
;
SchedulerBinding
.
instance
.
addPostFrameCallback
((
Duration
duration
)
{
_checkAllDevicesUpdates
();
_scheduledPostFramePositionCheck
=
false
;
});
}
}
}
// Collect the latest states of the given mouse device `device`, and call
/// Checks if the given [MouseTrackerAnnotation] is attached to this
// interested callbacks.
/// [MouseTracker].
//
///
// The enter or exit events are called for annotations that the pointer
/// This function is only public to allow for proper testing of the
// enters or leaves, while hover events are always called for each
/// MouseTracker. Do not call in other contexts.
// annotations that the pointer stays in, even if the pointer has not moved
@visibleForTesting
// since the last call. Therefore it's caller's responsibility to check if
bool
isAnnotationAttached
(
MouseTrackerAnnotation
annotation
)
{
// the pointer has moved.
return
_trackedAnnotations
.
containsKey
(
annotation
);
//
// If `disconnectedMouseState` is provided, this state will be used instead,
// but this mouse will be hovering no annotations.
void
_checkDeviceUpdates
({
int
device
,
_MouseState
disconnectedMouseState
,
})
{
final
_MouseState
mouseState
=
disconnectedMouseState
??
_mouseStates
[
device
];
final
bool
thisDeviceIsConnected
=
mouseState
!=
disconnectedMouseState
;
assert
(
mouseState
!=
null
);
final
LinkedHashSet
<
MouseTrackerAnnotation
>
nextAnnotations
=
(
_hasAttachedAnnotations
&&
thisDeviceIsConnected
)
?
LinkedHashSet
<
MouseTrackerAnnotation
>.
from
(
annotationFinder
(
mouseState
.
mostRecentEvent
.
position
)
)
:
<
MouseTrackerAnnotation
>{};
_dispatchDeviceCallbacks
(
currentState:
mouseState
,
nextAnnotations:
nextAnnotations
,
);
mouseState
.
lastAnnotations
=
nextAnnotations
;
}
}
// Collect the latest states of all mouse devices, and call interested
// Tells interested objects that a mouse has entered, exited, or moved, given
// callbacks.
// a callback to fetch the [MouseTrackerAnnotation] associated with a global
// offset.
//
//
// For detailed behaviors, see [_checkDeviceUpdates].
// This is called from a post-frame callback when the layer tree has been
void
_checkAllDevicesUpdates
()
{
// updated, right after rendering the frame.
for
(
final
int
device
in
_mouseStates
.
keys
)
{
void
_sendMouseNotifications
(
Iterable
<
int
>
deviceIds
)
{
_checkDeviceUpdates
(
device:
device
);
if
(
_trackedAnnotations
.
isEmpty
)
{
return
;
}
}
}
// Dispatch callbacks related to a device after all necessary information
void
exitAnnotation
(
_TrackedAnnotation
trackedAnnotation
,
int
deviceId
)
{
// has been collected.
if
(
trackedAnnotation
.
annotation
?.
onExit
!=
null
&&
trackedAnnotation
.
activeDevices
.
contains
(
deviceId
))
{
//
final
PointerEvent
event
=
_lastMouseEvent
[
deviceId
]
??
_pendingRemovals
[
deviceId
];
// This function should not change the provided states, and should not access
assert
(
event
!=
null
);
// information that is not provided in parameters (hence being static).
trackedAnnotation
.
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
static
void
_dispatchDeviceCallbacks
({
@required
LinkedHashSet
<
MouseTrackerAnnotation
>
nextAnnotations
,
@required
_MouseState
currentState
,
})
{
// Order is important for mouse event callbacks. The `findAnnotations`
// returns annotations in the visual order from front to back. We call
// it the "visual order", and the opposite one "reverse visual order".
// The algorithm here is explained in
// https://github.com/flutter/flutter/issues/41420
// The `nextAnnotations` is annotations that contains this device in the
// coming frame in visual order.
// Order is preserved with the help of [LinkedHashSet].
final
PointerEvent
mostRecentEvent
=
currentState
.
mostRecentEvent
;
// The `lastAnnotations` is annotations that contains this device in the
// previous frame in visual order.
final
LinkedHashSet
<
MouseTrackerAnnotation
>
lastAnnotations
=
currentState
.
lastAnnotations
;
// Send exit events in visual order.
final
Iterable
<
MouseTrackerAnnotation
>
exitingAnnotations
=
lastAnnotations
.
difference
(
nextAnnotations
);
for
(
final
MouseTrackerAnnotation
annotation
in
exitingAnnotations
)
{
if
(
annotation
.
onExit
!=
null
)
{
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
mostRecentEvent
));
}
}
trackedAnnotation
.
activeDevices
.
remove
(
deviceId
);
}
}
// Send enter events in reverse visual order.
void
exitAllDevices
(
_TrackedAnnotation
trackedAnnotation
)
{
final
Iterable
<
MouseTrackerAnnotation
>
enteringAnnotations
=
if
(
trackedAnnotation
.
activeDevices
.
isNotEmpty
)
{
nextAnnotations
.
difference
(
lastAnnotations
).
toList
().
reversed
;
final
Set
<
int
>
deviceIds
=
trackedAnnotation
.
activeDevices
.
toSet
()
;
for
(
final
MouseTrackerAnnotation
annotation
in
enteringAnnotation
s
)
{
for
(
int
deviceId
in
deviceId
s
)
{
if
(
annotation
.
onEnter
!=
null
)
{
exitAnnotation
(
trackedAnnotation
,
deviceId
);
annotation
.
onEnter
(
PointerEnterEvent
.
fromMouseEvent
(
mostRecentEvent
));
}
}
}
}
}
// Send hover events in reverse visual order.
try
{
// For now the order between the hover events is designed this way for no
// This indicates that all mouse pointers were removed, or none have been
// solid reasons but to keep it aligned with enter events for simplicity.
// connected yet. If no mouse is connected, then we want to make sure that
if
(
mostRecentEvent
is
PointerHoverEvent
)
{
// all active annotations are exited.
final
Iterable
<
MouseTrackerAnnotation
>
hoveringAnnotations
=
if
(!
mouseIsConnected
)
{
nextAnnotations
.
toList
().
reversed
;
_trackedAnnotations
.
values
.
forEach
(
exitAllDevices
);
for
(
final
MouseTrackerAnnotation
annotation
in
hoveringAnnotations
)
{
return
;
if
(
annotation
.
onHover
!=
null
)
{
}
annotation
.
onHover
(
mostRecentEvent
);
for
(
int
deviceId
in
deviceIds
)
{
final
PointerEvent
lastEvent
=
_lastMouseEvent
[
deviceId
];
assert
(
lastEvent
!=
null
);
final
Iterable
<
MouseTrackerAnnotation
>
hits
=
annotationFinder
(
lastEvent
.
position
);
// No annotations were found at this position for this deviceId, so send an
// exit to all active tracked annotations, since none of them were hit.
if
(
hits
.
isEmpty
)
{
// Send an exit to all tracked animations tracking this deviceId.
for
(
_TrackedAnnotation
trackedAnnotation
in
_trackedAnnotations
.
values
)
{
exitAnnotation
(
trackedAnnotation
,
deviceId
);
}
continue
;
}
final
Set
<
_TrackedAnnotation
>
hitAnnotations
=
hits
.
map
<
_TrackedAnnotation
>((
MouseTrackerAnnotation
hit
)
=>
_findAnnotation
(
hit
)).
toSet
();
for
(
_TrackedAnnotation
hitAnnotation
in
hitAnnotations
)
{
if
(!
hitAnnotation
.
activeDevices
.
contains
(
deviceId
))
{
// A tracked annotation that just became active and needs to have an enter
// event sent to it.
hitAnnotation
.
activeDevices
.
add
(
deviceId
);
if
(
hitAnnotation
.
annotation
?.
onEnter
!=
null
)
{
hitAnnotation
.
annotation
.
onEnter
(
PointerEnterEvent
.
fromMouseEvent
(
lastEvent
));
}
}
if
(
hitAnnotation
.
annotation
?.
onHover
!=
null
&&
lastEvent
is
PointerHoverEvent
)
{
hitAnnotation
.
annotation
.
onHover
(
lastEvent
);
}
// Tell any tracked annotations that weren't hit that they are no longer
// active.
for
(
_TrackedAnnotation
trackedAnnotation
in
_trackedAnnotations
.
values
)
{
if
(
hitAnnotations
.
contains
(
trackedAnnotation
))
{
continue
;
}
if
(
trackedAnnotation
.
activeDevices
.
contains
(
deviceId
))
{
if
(
trackedAnnotation
.
annotation
?.
onExit
!=
null
)
{
trackedAnnotation
.
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
lastEvent
));
}
trackedAnnotation
.
activeDevices
.
remove
(
deviceId
);
}
}
}
}
}
}
}
finally
{
_pendingRemovals
.
clear
();
}
}
}
}
/// Checks if the given [MouseTrackerAnnotation] is attached to this
void
_addMouseEvent
(
int
deviceId
,
PointerEvent
event
)
{
/// [MouseTracker].
final
bool
wasConnected
=
mouseIsConnected
;
///
if
(
event
is
PointerAddedEvent
)
{
/// This function is only public to allow for proper testing of the
// If we are adding the device again, then we're not removing it anymore.
/// MouseTracker. Do not call in other contexts.
_pendingRemovals
.
remove
(
deviceId
);
@visibleForTesting
}
bool
isAnnotationAttached
(
MouseTrackerAnnotation
annotation
)
{
_lastMouseEvent
[
deviceId
]
=
event
;
return
_trackedAnnotations
.
contains
(
annotation
);
if
(
mouseIsConnected
!=
wasConnected
)
{
notifyListeners
();
}
}
}
/// Whether or not a mouse is connected and has produced events.
void
_removeMouseEvent
(
int
deviceId
,
PointerEvent
event
)
{
bool
get
mouseIsConnected
=>
_mouseStates
.
isNotEmpty
;
final
bool
wasConnected
=
mouseIsConnected
;
assert
(
event
is
PointerRemovedEvent
);
/// Notify [MouseTracker] that a new mouse tracker annotation has started to
_pendingRemovals
[
deviceId
]
=
event
;
/// take effect.
_lastMouseEvent
.
remove
(
deviceId
);
///
if
(
mouseIsConnected
!=
wasConnected
)
{
/// This should be called as soon as the layer that owns this annotation is
notifyListeners
();
/// added to the layer tree.
///
/// This triggers [MouseTracker] to schedule a mouse position check during the
/// post frame to see if this new annotation might trigger enter events.
///
/// The [MouseTracker] also uses this to track the number of attached
/// annotations, and will skip mouse position checks if there is no
/// annotations attached.
void
attachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
// Schedule a check so that we test this new annotation to see if any mouse
// is currently inside its region. It has to happen after the frame is
// complete so that the annotation layer has been added before the check.
_trackedAnnotations
.
add
(
annotation
);
if
(
mouseIsConnected
)
{
_scheduleMousePositionCheck
();
}
}
}
}
// A list of device IDs that should be removed and notified when scheduling a
// mouse position check.
final
Map
<
int
,
PointerRemovedEvent
>
_pendingRemovals
=
<
int
,
PointerRemovedEvent
>{};
/// Notify [MouseTracker] that a mouse tracker annotation that was previously
/// The most recent mouse event observed for each mouse device ID observed.
/// attached has stopped taking effect.
///
/// This should be called as soon as the layer that owns this annotation is
/// removed from the layer tree. An assertion error will be thrown if the
/// associated layer is not removed and receives another mouse hit.
///
/// This triggers [MouseTracker] to perform a mouse position check immediately
/// to see if this annotation removal triggers any exit events.
///
///
/// The [MouseTracker] also uses this to track the number of attached
/// May be null if no mouse is connected, or hasn't produced an event yet.
/// annotations, and will skip mouse position checks if there is no
final
Map
<
int
,
PointerEvent
>
_lastMouseEvent
=
<
int
,
PointerEvent
>{};
/// annotations attached.
void
detachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
/// Whether or not a mouse is connected and has produced events.
_mouseStates
.
forEach
((
int
device
,
_MouseState
mouseState
)
{
bool
get
mouseIsConnected
=>
_lastMouseEvent
.
isNotEmpty
;
if
(
mouseState
.
lastAnnotations
.
contains
(
annotation
))
{
if
(
annotation
.
onExit
!=
null
)
{
final
PointerEvent
event
=
mouseState
.
mostRecentEvent
;
assert
(
event
!=
null
);
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
}
mouseState
.
lastAnnotations
.
remove
(
annotation
);
}
});
_trackedAnnotations
.
remove
(
annotation
);
}
}
}
packages/flutter/test/gestures/mouse_tracking_test.dart
View file @
eede7929
...
@@ -16,579 +16,211 @@ import '../flutter_test_alternative.dart';
...
@@ -16,579 +16,211 @@ import '../flutter_test_alternative.dart';
typedef
HandleEventCallback
=
void
Function
(
PointerEvent
event
);
typedef
HandleEventCallback
=
void
Function
(
PointerEvent
event
);
class
_TestGestureFlutterBinding
extends
BindingBase
class
TestGestureFlutterBinding
extends
BindingBase
with
ServicesBinding
,
SchedulerBinding
,
GestureBinding
,
SemanticsBinding
,
RendererBinding
{
with
ServicesBinding
,
SchedulerBinding
,
GestureBinding
,
SemanticsBinding
,
RendererBinding
{
HandleEventCallback
callback
;
@override
void
initInstances
()
{
super
.
initInstances
();
postFrameCallbacks
=
<
void
Function
(
Duration
)>[];
}
List
<
void
Function
(
Duration
)>
postFrameCallbacks
;
// Proxy post-frame callbacks
@override
@override
void
addPostFrameCallback
(
void
Function
(
Duration
)
callback
)
{
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
postFrameCallbacks
.
add
(
callback
);
super
.
handleEvent
(
event
,
entry
);
}
if
(
callback
!=
null
)
{
callback
(
event
);
void
flushPostFrameCallbacks
(
Duration
duration
)
{
for
(
final
void
Function
(
Duration
)
callback
in
postFrameCallbacks
)
{
callback
(
duration
);
}
}
postFrameCallbacks
.
clear
();
}
}
}
}
_TestGestureFlutterBinding
_binding
=
_TestGestureFlutterBinding
();
TestGestureFlutterBinding
_binding
=
TestGestureFlutterBinding
();
MouseTracker
get
_mouseTracker
=>
RendererBinding
.
instance
.
mouseTracker
;
void
_
ensureTestGestureBinding
(
)
{
void
ensureTestGestureBinding
(
)
{
_binding
??=
_
TestGestureFlutterBinding
();
_binding
??=
TestGestureFlutterBinding
();
assert
(
GestureBinding
.
instance
!=
null
);
assert
(
GestureBinding
.
instance
!=
null
);
}
}
void
main
(
)
{
void
main
(
)
{
void
_setUpMouseAnnotationFinder
(
MouseDetectorAnnotationFinder
annotationFinder
)
{
setUp
(
ensureTestGestureBinding
);
final
MouseTracker
mouseTracker
=
MouseTracker
(
GestureBinding
.
instance
.
pointerRouter
,
annotationFinder
,
);
RendererBinding
.
instance
.
initMouseTracker
(
mouseTracker
);
}
// Set up a trivial test environment that includes one annotation, which adds
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
// the enter, hover, and exit events it received to [logEvents].
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
MouseTrackerAnnotation
_setUpWithOneAnnotation
({
List
<
PointerEvent
>
logEvents
})
{
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
),
onEnter:
(
PointerEnterEvent
event
)
=>
logEvents
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
events
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
logEvents
.
add
(
event
),
);
onExit:
(
PointerExitEvent
event
)
=>
logEvents
.
add
(
event
),
// Only respond to some mouse events.
);
final
MouseTrackerAnnotation
partialAnnotation
=
MouseTrackerAnnotation
(
_setUpMouseAnnotationFinder
(
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
(
Offset
position
)
sync
*
{
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
),
yield
annotation
;
);
},
bool
isInHitRegionOne
;
);
bool
isInHitRegionTwo
;
_mouseTracker
.
attachAnnotation
(
annotation
);
return
annotation
;
void
clear
()
{
events
.
clear
();
}
}
setUp
(()
{
setUp
(()
{
_ensureTestGestureBinding
();
clear
();
_binding
.
postFrameCallbacks
.
clear
();
isInHitRegionOne
=
true
;
PointerEventConverter
.
clearPointers
();
isInHitRegionTwo
=
false
;
});
RendererBinding
.
instance
.
initMouseTracker
(
MouseTracker
(
test
(
'MouseTrackerAnnotation has correct toString'
,
()
{
GestureBinding
.
instance
.
pointerRouter
,
final
MouseTrackerAnnotation
annotation1
=
MouseTrackerAnnotation
(
(
Offset
position
)
sync
*
{
onEnter:
(
_
)
{},
if
(
isInHitRegionOne
)
onExit:
(
_
)
{},
yield
annotation
;
onHover:
(
_
)
{},
else
if
(
isInHitRegionTwo
)
{
);
yield
partialAnnotation
;
expect
(
}
annotation1
.
toString
(),
},
equals
(
'MouseTrackerAnnotation#
${shortHash(annotation1)}
(callbacks: enter hover exit)'
),
),
);
const
MouseTrackerAnnotation
annotation2
=
MouseTrackerAnnotation
();
expect
(
annotation2
.
toString
(),
equals
(
'MouseTrackerAnnotation#
${shortHash(annotation2)}
(callbacks: <none>)'
),
);
);
PointerEventConverter
.
clearPointers
();
});
});
test
(
'should detect enter, hover, and exit from Added, Hover, and Removed events'
,
()
{
test
(
'receives and processes mouse hover events'
,
()
{
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_setUpWithOneAnnotation
(
logEvents:
events
);
// Will implicitly also add a PointerAdded event.
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
final
List
<
bool
>
listenerLogs
=
<
bool
>[];
]);
_mouseTracker
.
addListener
(()
{
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
listenerLogs
.
add
(
_mouseTracker
.
mouseIsConnected
);
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
});
]);
final
ui
.
PointerDataPacket
packet3
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
expect
(
_mouseTracker
.
mouseIsConnected
,
isFalse
);
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
201.0
)),
]);
// Enter
final
ui
.
PointerDataPacket
packet4
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
301.0
)),
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
0.0
)),
]);
]));
final
ui
.
PointerDataPacket
packet5
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
401.0
),
device:
1
),
]);
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
isInHitRegionOne
=
true
;
ui
.
window
.
onPointerDataPacket
(
packet1
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
1
.0
,
0.0
)),
const
PointerEnterEvent
(
position:
Offset
(
0
.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1
.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
0
.0
,
0.0
)),
]));
]));
expect
(
listenerLogs
,
<
bool
>[
true
]);
clear
();
events
.
clear
();
listenerLogs
.
clear
();
// Hover
ui
.
window
.
onPointerDataPacket
(
packet2
);
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
101.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
101.0
)),
]));
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
clear
();
expect
(
listenerLogs
,
<
bool
>[]);
events
.
clear
();
// Remove
ui
.
window
.
onPointerDataPacket
(
packet3
);
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
201.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
201.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
201.0
)),
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
]));
]));
expect
(
listenerLogs
,
<
bool
>[
false
]);
events
.
clear
();
listenerLogs
.
clear
();
// Add again
clear
();
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
ui
.
window
.
onPointerDataPacket
(
packet4
);
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
301.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
1.0
,
301.0
)),
const
PointerEnterEvent
(
position:
Offset
(
1.0
,
301.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
301.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
301.0
)),
]));
]));
expect
(
listenerLogs
,
<
bool
>[
true
]);
events
.
clear
();
listenerLogs
.
clear
();
});
test
(
'should correctly handle multiple devices'
,
()
{
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
_setUpWithOneAnnotation
(
logEvents:
events
);
expect
(
_mouseTracker
.
mouseIsConnected
,
isFalse
);
// First mouse
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
1.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
1.0
)),
const
PointerHoverEvent
(
position:
Offset
(
0.0
,
1.0
)),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Second mouse
// add in a second mouse simultaneously.
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
clear
();
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
401.0
),
device:
1
),
ui
.
window
.
onPointerDataPacket
(
packet5
);
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
1.0
,
401.0
),
device:
1
),
const
PointerEnterEvent
(
position:
Offset
(
1.0
,
401.0
),
device:
1
),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
401.0
),
device:
1
),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
401.0
),
device:
1
),
]));
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// First mouse hover
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
101.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
0.0
,
101.0
)),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Second mouse hover
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
501.0
),
device:
1
),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
501.0
),
device:
1
),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// First mouse remove
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
0.0
,
101.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerExitEvent
(
position:
Offset
(
0.0
,
101.0
)),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Second mouse hover
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
601.0
),
device:
1
),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
601.0
),
device:
1
),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Second mouse remove
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
601.0
),
device:
1
),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerExitEvent
(
position:
Offset
(
1.0
,
601.0
),
device:
1
),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isFalse
);
events
.
clear
();
});
});
test
(
'should handle detaching during the callback of exiting'
,
()
{
test
(
'detects exit when annotated layer no longer hit'
,
()
{
bool
isInHitRegion
;
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
]);
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
),
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
onExit:
(
PointerExitEvent
event
)
=>
events
.
add
(
event
),
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
201.0
)),
);
]);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
isInHitRegionOne
=
true
;
if
(
isInHitRegion
)
{
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
yield
annotation
;
}
});
isInHitRegion
=
true
;
_mouseTracker
.
attachAnnotation
(
annotation
);
// Enter
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
0.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
1.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
0.0
)),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Remove
_mouseTracker
.
addListener
(()
{
if
(!
_mouseTracker
.
mouseIsConnected
)
{
_mouseTracker
.
detachAnnotation
(
annotation
);
isInHitRegion
=
false
;
}
});
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
0.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerExitEvent
(
position:
Offset
(
1.0
,
0.0
)),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isFalse
);
events
.
clear
();
});
test
(
'should not handle non-hover events'
,
()
{
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
_setUpWithOneAnnotation
(
logEvents:
events
);
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
down
,
const
Offset
(
0.0
,
101.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
// This Enter event is triggered by the [PointerAddedEvent] that was
// synthesized during the event normalization of pointer event converter.
// The [PointerDownEvent] is ignored by [MouseTracker].
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
101.0
)),
]));
events
.
clear
();
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
move
,
const
Offset
(
0.0
,
201.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
events
.
clear
();
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
up
,
const
Offset
(
0.0
,
301.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
events
.
clear
();
});
test
(
'should detect enter or exit when annotations are attached or detached on the pointer'
,
()
{
bool
isInHitRegion
;
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
events
.
add
(
event
),
);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
if
(
isInHitRegion
)
{
yield
annotation
;
}
});
isInHitRegion
=
false
;
// Connect a mouse when there is no annotation
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
add
,
const
Offset
(
0.0
,
100.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Attach an annotation
isInHitRegion
=
true
;
_mouseTracker
.
attachAnnotation
(
annotation
);
// No callbacks are triggered immediately
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
expect
(
_binding
.
postFrameCallbacks
,
hasLength
(
1
));
_binding
.
flushPostFrameCallbacks
(
Duration
.
zero
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
100.0
)),
]));
events
.
clear
();
// Detach the annotation
isInHitRegion
=
false
;
_mouseTracker
.
detachAnnotation
(
annotation
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerExitEvent
(
position:
Offset
(
0.0
,
100.0
)),
]));
expect
(
_binding
.
postFrameCallbacks
,
hasLength
(
0
));
});
test
(
'should correctly stay quiet when annotations are attached or detached not on the pointer'
,
()
{
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
events
.
add
(
event
),
);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
// This annotation is never in the region
});
// Connect a mouse when there is no annotation
ui
.
window
.
onPointerDataPacket
(
packet1
);
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
add
,
const
Offset
(
0.0
,
100.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
events
.
clear
();
// Attach an annotation out of region
_mouseTracker
.
attachAnnotation
(
annotation
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
0.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
101.0
)),
]));
]));
expect
(
_binding
.
postFrameCallbacks
,
hasLength
(
1
));
// Simulate layer going away by detaching it.
clear
();
isInHitRegionOne
=
false
;
_binding
.
flushPostFrameCallbacks
(
Duration
.
zero
);
ui
.
window
.
onPointerDataPacket
(
packet2
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
]));
]));
events
.
clear
();
// Detach the annotation
// Actually detach annotation. Shouldn't receive hit.
_mouseTracker
.
detachAnnotation
(
annotation
);
RendererBinding
.
instance
.
mouseTracker
.
detachAnnotation
(
annotation
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
clear
();
]));
isInHitRegionOne
=
false
;
expect
(
_binding
.
postFrameCallbacks
,
hasLength
(
0
));
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
ui
.
window
.
onPointerDataPacket
(
packet2
);
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
0.0
,
100.0
)),
]));
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
]));
});
});
test
(
'should not flip out if not all mouse events are listened to'
,
()
{
test
(
"don't flip out if not all mouse events are listened to"
,
()
{
bool
isInHitRegionOne
=
true
;
bool
isInHitRegionTwo
=
false
;
final
MouseTrackerAnnotation
annotation1
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
{}
);
final
MouseTrackerAnnotation
annotation2
=
MouseTrackerAnnotation
(
onExit:
(
PointerExitEvent
event
)
{}
);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
if
(
isInHitRegionOne
)
yield
annotation1
;
else
if
(
isInHitRegionTwo
)
yield
annotation2
;
});
final
ui
.
PointerDataPacket
packet
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
final
ui
.
PointerDataPacket
packet
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]);
]);
isInHitRegionOne
=
false
;
isInHitRegionOne
=
false
;
isInHitRegionTwo
=
true
;
isInHitRegionTwo
=
true
;
_mouseTracker
.
attachAnnotation
(
annotation2
);
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
partialAnnotation
);
ui
.
window
.
onPointerDataPacket
(
packet
);
ui
.
window
.
onPointerDataPacket
(
packet
);
_mouseTracker
.
detachAnnotation
(
annotation2
);
RendererBinding
.
instance
.
mouseTracker
.
detachAnnotation
(
partialAnnotation
);
isInHitRegionTwo
=
false
;
isInHitRegionTwo
=
false
;
// Passes if no errors are thrown
// Passes if no errors are thrown
});
});
test
(
'should not call annotationFinder when no annotations are attached'
,
()
{
test
(
'detects exit when mouse goes away'
,
()
{
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
onEnter:
(
PointerEnterEvent
event
)
{},
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
);
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
int
finderCalled
=
0
;
]);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
finderCalled
++;
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
201.0
)),
// This annotation is never in the region
]);
});
isInHitRegionOne
=
true
;
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
// When no annotations are attached, hovering should not call finder.
ui
.
window
.
onPointerDataPacket
(
packet1
);
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
ui
.
window
.
onPointerDataPacket
(
packet2
);
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
101.0
)),
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
0.0
)),
expect
(
finderCalled
,
0
);
const
PointerHoverEvent
(
position:
Offset
(
0.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
101.0
)),
// Attaching should call finder during the post frame.
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
201.0
)),
_mouseTracker
.
attachAnnotation
(
annotation
);
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
expect
(
finderCalled
,
0
);
_binding
.
flushPostFrameCallbacks
(
Duration
.
zero
);
expect
(
finderCalled
,
1
);
finderCalled
=
0
;
// When annotations are attached, hovering should call finder.
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
201.0
)),
]));
expect
(
finderCalled
,
1
);
finderCalled
=
0
;
// Detaching an annotation should not call finder (because only history
// records are needed).
_mouseTracker
.
detachAnnotation
(
annotation
);
expect
(
finderCalled
,
0
);
// When all annotations are detached, hovering should not call finder.
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
201.0
)),
]));
expect
(
finderCalled
,
0
);
});
test
(
'should trigger callbacks between parents and children in correct order'
,
()
{
// This test simulates the scenario of a layer being the child of another.
//
// ———————————
// |A |
// | —————— |
// | |B | |
// | —————— |
// ———————————
bool
isInB
;
final
List
<
String
>
logs
=
<
String
>[];
final
MouseTrackerAnnotation
annotationA
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
logs
.
add
(
'enterA'
),
onExit:
(
PointerExitEvent
event
)
=>
logs
.
add
(
'exitA'
),
onHover:
(
PointerHoverEvent
event
)
=>
logs
.
add
(
'hoverA'
),
);
final
MouseTrackerAnnotation
annotationB
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
logs
.
add
(
'enterB'
),
onExit:
(
PointerExitEvent
event
)
=>
logs
.
add
(
'exitB'
),
onHover:
(
PointerHoverEvent
event
)
=>
logs
.
add
(
'hoverB'
),
);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
// Children's annotations come before parents'
if
(
isInB
)
{
yield
annotationB
;
yield
annotationA
;
}
});
_mouseTracker
.
attachAnnotation
(
annotationA
);
_mouseTracker
.
attachAnnotation
(
annotationB
);
// Starts out of A
isInB
=
false
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
1.0
)),
]));
expect
(
logs
,
<
String
>[]);
// Moves into B within one frame
isInB
=
true
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
10.0
)),
]));
expect
(
logs
,
<
String
>[
'enterA'
,
'enterB'
,
'hoverA'
,
'hoverB'
]);
logs
.
clear
();
// Moves out of A within one frame
isInB
=
false
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
20.0
)),
]));
]));
expect
(
logs
,
<
String
>[
'exitB'
,
'exitA'
]);
});
});
test
(
'should trigger callbacks between disjoint siblings in correctly order'
,
()
{
test
(
'handles mouse down and move'
,
()
{
// This test simulates the scenario of 2 sibling layers that do not overlap
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
// with each other.
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
//
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
// ———————— ————————
]);
// |A | |B |
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
// | | | |
_pointerData
(
PointerChange
.
down
,
const
Offset
(
1.0
,
101.0
)),
// ———————— ————————
_pointerData
(
PointerChange
.
move
,
const
Offset
(
1.0
,
201.0
)),
]);
bool
isInA
;
isInHitRegionOne
=
true
;
bool
isInB
;
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
final
List
<
String
>
logs
=
<
String
>[];
ui
.
window
.
onPointerDataPacket
(
packet1
);
final
MouseTrackerAnnotation
annotationA
=
MouseTrackerAnnotation
(
ui
.
window
.
onPointerDataPacket
(
packet2
);
onEnter:
(
PointerEnterEvent
event
)
=>
logs
.
add
(
'enterA'
),
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
onExit:
(
PointerExitEvent
event
)
=>
logs
.
add
(
'exitA'
),
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
0.0
),
delta:
Offset
(
0.0
,
0.0
)),
onHover:
(
PointerHoverEvent
event
)
=>
logs
.
add
(
'hoverA'
),
const
PointerHoverEvent
(
position:
Offset
(
0.0
,
0.0
),
delta:
Offset
(
0.0
,
0.0
)),
);
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
101.0
),
delta:
Offset
(
1.0
,
101.0
)),
final
MouseTrackerAnnotation
annotationB
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
logs
.
add
(
'enterB'
),
onExit:
(
PointerExitEvent
event
)
=>
logs
.
add
(
'exitB'
),
onHover:
(
PointerHoverEvent
event
)
=>
logs
.
add
(
'hoverB'
),
);
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
if
(
isInA
)
{
yield
annotationA
;
}
else
if
(
isInB
)
{
yield
annotationB
;
}
});
_mouseTracker
.
attachAnnotation
(
annotationA
);
_mouseTracker
.
attachAnnotation
(
annotationB
);
// Starts within A
isInA
=
true
;
isInB
=
false
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
1.0
)),
]));
expect
(
logs
,
<
String
>[
'enterA'
,
'hoverA'
]);
logs
.
clear
();
// Moves into B within one frame
isInA
=
false
;
isInB
=
true
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
10.0
)),
]));
expect
(
logs
,
<
String
>[
'exitA'
,
'enterB'
,
'hoverB'
]);
logs
.
clear
();
// Moves into A within one frame
isInA
=
true
;
isInB
=
false
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
1.0
)),
]));
]));
expect
(
logs
,
<
String
>[
'exitB'
,
'enterA'
,
'hoverA'
]);
});
});
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment