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
Show 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,206 +104,102 @@ class MouseTracker extends ChangeNotifier {
...
@@ -138,206 +104,102 @@ 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
// `mostRecentEvent`.
//
// The returned value is never null.
_MouseState
_guaranteeMouseState
(
int
device
,
PointerEvent
mostRecentEvent
)
{
final
_MouseState
currentState
=
_mouseStates
[
device
];
if
(
currentState
==
null
)
{
_addMouseDevice
(
device
,
mostRecentEvent
);
}
final
_MouseState
result
=
currentState
??
_mouseStates
[
device
];
assert
(
result
!=
null
);
return
result
;
}
// The collection of annotations that are currently being tracked.
// The collection of annotations that are currently being tracked. They may or
// It is operated on by [attachAnnotation] and [detachAnnotation].
// may not be active, depending on the value of _TrackedAnnotation.active.
final
Set
<
MouseTrackerAnnotation
>
_trackedAnnotations
=
<
MouseTrackerAnnotation
>{};
final
Map
<
MouseTrackerAnnotation
,
_TrackedAnnotation
>
_trackedAnnotations
=
<
MouseTrackerAnnotation
,
_TrackedAnnotation
>{};
bool
get
_hasAttachedAnnotations
=>
_trackedAnnotations
.
isNotEmpty
;
void
_addMouseDevice
(
int
device
,
PointerEvent
event
)
{
/// Track an annotation so that if the mouse enters it, we send it events.
final
bool
wasConnected
=
mouseIsConnected
;
///
assert
(!
_mouseStates
.
containsKey
(
device
));
/// This is typically called when the [AnnotatedRegion] containing this
_mouseStates
[
device
]
=
_MouseState
(
mostRecentEvent:
event
);
/// annotation has been added to the layer tree.
// Schedule a check to enter annotations that might contain this pointer.
void
attachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
_checkDeviceUpdates
(
device:
device
);
_trackedAnnotations
[
annotation
]
=
_TrackedAnnotation
(
annotation
);
if
(
mouseIsConnected
!=
wasConnected
)
{
// Schedule a check so that we test this new annotation to see if any mouse
notifyListeners
();
// 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
();
void
_removeMouseDevice
(
int
device
,
PointerEvent
event
)
{
final
bool
wasConnected
=
mouseIsConnected
;
assert
(
_mouseStates
.
containsKey
(
device
));
final
_MouseState
disconnectedMouseState
=
_mouseStates
.
remove
(
device
);
disconnectedMouseState
.
mostRecentEvent
=
event
;
// Schedule a check to exit annotations that used to contain this pointer.
_checkDeviceUpdates
(
device:
device
,
disconnectedMouseState:
disconnectedMouseState
,
);
if
(
mouseIsConnected
!=
wasConnected
)
{
notifyListeners
();
}
}
}
}
// Handler for events coming from the PointerRouter.
/// Stops tracking an annotation, indicating that it has been removed from the
void
_handleEvent
(
PointerEvent
event
)
{
/// layer tree.
if
(
event
.
kind
!=
PointerDeviceKind
.
mouse
)
{
///
return
;
/// An assertion error will be thrown if the associated layer is not removed
}
/// and receives another mouse hit.
final
int
device
=
event
.
device
;
void
detachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
if
(
event
is
PointerAddedEvent
)
{
final
_TrackedAnnotation
trackedAnnotation
=
_findAnnotation
(
annotation
);
_addMouseDevice
(
device
,
event
);
for
(
int
deviceId
in
trackedAnnotation
.
activeDevices
)
{
}
else
if
(
event
is
PointerRemovedEvent
)
{
if
(
annotation
.
onExit
!=
null
)
{
_removeMouseDevice
(
device
,
event
);
final
PointerEvent
event
=
_lastMouseEvent
[
deviceId
]
??
_pendingRemovals
[
deviceId
];
}
else
if
(
event
is
PointerHoverEvent
)
{
assert
(
event
!=
null
);
final
_MouseState
mouseState
=
_guaranteeMouseState
(
device
,
event
);
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
final
PointerEvent
previousEvent
=
mouseState
.
mostRecentEvent
;
mouseState
.
mostRecentEvent
=
event
;
if
(
previousEvent
is
PointerAddedEvent
||
previousEvent
.
position
!=
event
.
position
)
{
// Only send notifications if we have our first event, or if the
// location of the mouse has changed
_checkDeviceUpdates
(
device:
device
);
}
}
}
}
_trackedAnnotations
.
remove
(
annotation
);
}
}
bool
_scheduledPostFramePositionCheck
=
false
;
bool
_scheduledPostFramePositionCheck
=
false
;
// Schedules a position check at the end of this frame
.
// Schedules a position check at the end of this frame
for those annotations
//
It is only called during a frame during which annotations
have been added.
//
that
have been added.
void
_scheduleMousePositionCheck
()
{
void
_scheduleMousePositionCheck
()
{
// If we're not tracking anything, then there is no point in registering a
// If we're not tracking anything, then there is no point in registering a
// frame callback or scheduling a frame. By definition there are no active
// frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either.
// annotations that need exiting, either.
if
(!
_scheduledPostFramePositionCheck
)
{
if
(
_trackedAnnotations
.
isNotEmpty
&&
!
_scheduledPostFramePositionCheck
)
{
_scheduledPostFramePositionCheck
=
true
;
_scheduledPostFramePositionCheck
=
true
;
SchedulerBinding
.
instance
.
addPostFrameCallback
((
Duration
duration
)
{
SchedulerBinding
.
instance
.
addPostFrameCallback
((
Duration
duration
)
{
_
checkAllDevicesUpdates
(
);
_
sendMouseNotifications
(
_lastMouseEvent
.
keys
);
_scheduledPostFramePositionCheck
=
false
;
_scheduledPostFramePositionCheck
=
false
;
});
});
}
}
}
}
// Collect the latest states of the given mouse device `device`, and call
// Handler for events coming from the PointerRouter.
// interested callbacks.
void
_handleEvent
(
PointerEvent
event
)
{
//
if
(
event
.
kind
!=
PointerDeviceKind
.
mouse
)
{
// The enter or exit events are called for annotations that the pointer
return
;
// enters or leaves, while hover events are always called for each
// annotations that the pointer stays in, even if the pointer has not moved
// since the last call. Therefore it's caller's responsibility to check if
// the pointer has moved.
//
// 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
// callbacks.
//
// For detailed behaviors, see [_checkDeviceUpdates].
void
_checkAllDevicesUpdates
()
{
for
(
final
int
device
in
_mouseStates
.
keys
)
{
_checkDeviceUpdates
(
device:
device
);
}
}
final
int
deviceId
=
event
.
device
;
if
(
event
is
PointerAddedEvent
)
{
// If we are adding the device again, then we're not removing it anymore.
_pendingRemovals
.
remove
(
deviceId
);
_addMouseEvent
(
deviceId
,
event
);
_sendMouseNotifications
(<
int
>{
deviceId
});
return
;
}
}
if
(
event
is
PointerRemovedEvent
)
{
// Dispatch callbacks related to a device after all necessary information
_removeMouseEvent
(
deviceId
,
event
);
// has been collected.
// If the mouse was removed, then we need to schedule one more check to
//
// exit any annotations that were active.
// This function should not change the provided states, and should not access
_sendMouseNotifications
(<
int
>{
deviceId
});
// information that is not provided in parameters (hence being static).
}
else
{
static
void
_dispatchDeviceCallbacks
({
if
(
event
is
PointerMoveEvent
||
event
is
PointerHoverEvent
||
event
is
PointerDownEvent
)
{
@required
LinkedHashSet
<
MouseTrackerAnnotation
>
nextAnnotations
,
final
PointerEvent
lastEvent
=
_lastMouseEvent
[
deviceId
];
@required
_MouseState
currentState
,
_addMouseEvent
(
deviceId
,
event
);
})
{
if
(
lastEvent
==
null
||
// Order is important for mouse event callbacks. The `findAnnotations`
lastEvent
is
PointerAddedEvent
||
lastEvent
.
position
!=
event
.
position
)
{
// returns annotations in the visual order from front to back. We call
// Only schedule a frame if we have our first event, or if the
// it the "visual order", and the opposite one "reverse visual order".
// location of the mouse has changed, and only if there are tracked annotations.
// The algorithm here is explained in
_sendMouseNotifications
(<
int
>{
deviceId
});
// 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
));
}
}
}
}
// Send enter events in reverse visual order.
final
Iterable
<
MouseTrackerAnnotation
>
enteringAnnotations
=
nextAnnotations
.
difference
(
lastAnnotations
).
toList
().
reversed
;
for
(
final
MouseTrackerAnnotation
annotation
in
enteringAnnotations
)
{
if
(
annotation
.
onEnter
!=
null
)
{
annotation
.
onEnter
(
PointerEnterEvent
.
fromMouseEvent
(
mostRecentEvent
));
}
}
}
}
// Send hover events in reverse visual order.
_TrackedAnnotation
_findAnnotation
(
MouseTrackerAnnotation
annotation
)
{
// For now the order between the hover events is designed this way for no
final
_TrackedAnnotation
trackedAnnotation
=
_trackedAnnotations
[
annotation
];
// solid reasons but to keep it aligned with enter events for simplicity.
assert
(
if
(
mostRecentEvent
is
PointerHoverEvent
)
{
trackedAnnotation
!=
null
,
final
Iterable
<
MouseTrackerAnnotation
>
hoveringAnnotations
=
'Unable to find annotation
$annotation
in tracked annotations. '
nextAnnotations
.
toList
().
reversed
;
'Check that attachAnnotation has been called for all annotated layers.'
);
for
(
final
MouseTrackerAnnotation
annotation
in
hoveringAnnotations
)
{
return
trackedAnnotation
;
if
(
annotation
.
onHover
!=
null
)
{
annotation
.
onHover
(
mostRecentEvent
);
}
}
}
}
}
/// Checks if the given [MouseTrackerAnnotation] is attached to this
/// Checks if the given [MouseTrackerAnnotation] is attached to this
...
@@ -347,59 +209,127 @@ class MouseTracker extends ChangeNotifier {
...
@@ -347,59 +209,127 @@ class MouseTracker extends ChangeNotifier {
/// MouseTracker. Do not call in other contexts.
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
@visibleForTesting
bool
isAnnotationAttached
(
MouseTrackerAnnotation
annotation
)
{
bool
isAnnotationAttached
(
MouseTrackerAnnotation
annotation
)
{
return
_trackedAnnotations
.
contains
(
annotation
);
return
_trackedAnnotations
.
contains
Key
(
annotation
);
}
}
/// Whether or not a mouse is connected and has produced events.
// Tells interested objects that a mouse has entered, exited, or moved, given
bool
get
mouseIsConnected
=>
_mouseStates
.
isNotEmpty
;
// a callback to fetch the [MouseTrackerAnnotation] associated with a global
// offset.
//
// This is called from a post-frame callback when the layer tree has been
// updated, right after rendering the frame.
void
_sendMouseNotifications
(
Iterable
<
int
>
deviceIds
)
{
if
(
_trackedAnnotations
.
isEmpty
)
{
return
;
}
/// Notify [MouseTracker] that a new mouse tracker annotation has started to
void
exitAnnotation
(
_TrackedAnnotation
trackedAnnotation
,
int
deviceId
)
{
/// take effect.
if
(
trackedAnnotation
.
annotation
?.
onExit
!=
null
&&
trackedAnnotation
.
activeDevices
.
contains
(
deviceId
))
{
///
final
PointerEvent
event
=
_lastMouseEvent
[
deviceId
]
??
_pendingRemovals
[
deviceId
];
/// This should be called as soon as the layer that owns this annotation is
assert
(
event
!=
null
);
/// added to the layer tree.
trackedAnnotation
.
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
///
/// 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
();
}
}
trackedAnnotation
.
activeDevices
.
remove
(
deviceId
);
}
}
void
exitAllDevices
(
_TrackedAnnotation
trackedAnnotation
)
{
if
(
trackedAnnotation
.
activeDevices
.
isNotEmpty
)
{
final
Set
<
int
>
deviceIds
=
trackedAnnotation
.
activeDevices
.
toSet
();
for
(
int
deviceId
in
deviceIds
)
{
exitAnnotation
(
trackedAnnotation
,
deviceId
);
}
}
}
/// Notify [MouseTracker] that a mouse tracker annotation that was previously
try
{
/// attached has stopped taking effect.
// This indicates that all mouse pointers were removed, or none have been
///
// connected yet. If no mouse is connected, then we want to make sure that
/// This should be called as soon as the layer that owns this annotation is
// all active annotations are exited.
/// removed from the layer tree. An assertion error will be thrown if the
if
(!
mouseIsConnected
)
{
/// associated layer is not removed and receives another mouse hit.
_trackedAnnotations
.
values
.
forEach
(
exitAllDevices
);
///
return
;
/// 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
/// annotations, and will skip mouse position checks if there is no
/// annotations attached.
void
detachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
_mouseStates
.
forEach
((
int
device
,
_MouseState
mouseState
)
{
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
);
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
;
_trackedAnnotations
.
remove
(
annotation
);
}
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
();
}
}
void
_addMouseEvent
(
int
deviceId
,
PointerEvent
event
)
{
final
bool
wasConnected
=
mouseIsConnected
;
if
(
event
is
PointerAddedEvent
)
{
// If we are adding the device again, then we're not removing it anymore.
_pendingRemovals
.
remove
(
deviceId
);
}
_lastMouseEvent
[
deviceId
]
=
event
;
if
(
mouseIsConnected
!=
wasConnected
)
{
notifyListeners
();
}
}
void
_removeMouseEvent
(
int
deviceId
,
PointerEvent
event
)
{
final
bool
wasConnected
=
mouseIsConnected
;
assert
(
event
is
PointerRemovedEvent
);
_pendingRemovals
[
deviceId
]
=
event
;
_lastMouseEvent
.
remove
(
deviceId
);
if
(
mouseIsConnected
!=
wasConnected
)
{
notifyListeners
();
}
}
// A list of device IDs that should be removed and notified when scheduling a
// mouse position check.
final
Map
<
int
,
PointerRemovedEvent
>
_pendingRemovals
=
<
int
,
PointerRemovedEvent
>{};
/// The most recent mouse event observed for each mouse device ID observed.
///
/// May be null if no mouse is connected, or hasn't produced an event yet.
final
Map
<
int
,
PointerEvent
>
_lastMouseEvent
=
<
int
,
PointerEvent
>{};
/// Whether or not a mouse is connected and has produced events.
bool
get
mouseIsConnected
=>
_lastMouseEvent
.
isNotEmpty
;
}
}
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].
MouseTrackerAnnotation
_setUpWithOneAnnotation
({
List
<
PointerEvent
>
logEvents
})
{
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
logE
vents
.
add
(
event
),
onEnter:
(
PointerEnterEvent
event
)
=>
e
vents
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
logE
vents
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
e
vents
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
logE
vents
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
e
vents
.
add
(
event
),
);
);
_setUpMouseAnnotationFinder
(
// Only respond to some mouse events.
(
Offset
position
)
sync
*
{
final
MouseTrackerAnnotation
partialAnnotation
=
MouseTrackerAnnotation
(
yield
annotation
;
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
}
,
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
)
,
);
);
_mouseTracker
.
attachAnnotation
(
annotation
);
bool
isInHitRegionOne
;
return
annotation
;
bool
isInHitRegionTwo
;
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'
,
()
{
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
=
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
;
test
(
'detects exit when annotated layer no longer hit'
,
()
{
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
// Connect a mouse when there is no annotation
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
_pointerData
(
PointerChange
.
add
,
const
Offset
(
0.0
,
100.0
)),
]);
]));
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
201.0
)),
]));
]);
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
isInHitRegionOne
=
true
;
events
.
clear
();
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
// 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