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> {
///
/// 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.
/// * [IterableProperty], which provides similar functionality describing
/// the values a collection of objects.
...
...
packages/flutter/lib/src/gestures/mouse_tracking.dart
View file @
eede7929
...
...
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:collection'
show
LinkedHashSet
;
import
'dart:ui'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
show
ChangeNotifier
,
visibleForTesting
;
import
'package:flutter/scheduler.dart'
;
import
'events.dart'
;
...
...
@@ -49,82 +48,49 @@ class MouseTrackerAnnotation {
@override
String
toString
()
{
final
List
<
String
>
callbacks
=
<
String
>[];
if
(
onEnter
!=
null
)
callbacks
.
add
(
'enter'
);
if
(
onHover
!=
null
)
callbacks
.
add
(
'hover'
);
if
(
onExit
!=
null
)
callbacks
.
add
(
'exit'
);
final
String
describeCallbacks
=
callbacks
.
isEmpty
?
'<none>'
:
callbacks
.
join
(
' '
);
return
'
${describeIdentity(this)}
(callbacks:
$describeCallbacks
)'
;
final
String
none
=
(
onEnter
==
null
&&
onExit
==
null
&&
onHover
==
null
)
?
' <none>'
:
''
;
return
'[
$runtimeType${hashCode.toRadixString(16)}$none
'
'
${onEnter == null ? '' : ' onEnter'}
'
'
${onHover == null ? '' : ' onHover'}
'
'
${onExit == null ? '' : ' onExit'}
]'
;
}
}
/// 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
/// position.
typedef
MouseDetectorAnnotationFinder
=
Iterable
<
MouseTrackerAnnotation
>
Function
(
Offset
offset
);
// Various states of each connected mouse device.
//
// It is used by [MouseTracker] to compute which callbacks should be triggered
// 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.
/// Keeps state about which objects are interested in tracking mouse positions
/// and notifies them when a mouse pointer enters, moves, or leaves an annotated
/// region that they are interested in.
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
/// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
/// Owned by the [RendererBinding] class.
class
MouseTracker
extends
ChangeNotifier
{
/// 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.
MouseTracker
(
this
.
_router
,
this
.
annotationFinder
)
:
assert
(
_router
!=
null
),
...
...
@@ -138,206 +104,102 @@ class MouseTracker extends ChangeNotifier {
_router
.
removeGlobalRoute
(
_handleEvent
);
}
/// Find annotations at a given offset in global logical coordinate space
/// 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.
// The pointer router that the mouse tracker listens to for events.
final
PointerRouter
_router
;
// Tracks the state of connected mouse devices.
//
// 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
;
}
/// Used to find annotations at a given logical coordinate.
final
MouseDetectorAnnotationFinder
annotationFinder
;
// The collection of annotations that are currently being tracked.
// It is operated on by [attachAnnotation] and [detachAnnotation].
final
Set
<
MouseTrackerAnnotation
>
_trackedAnnotations
=
<
MouseTrackerAnnotation
>{};
bool
get
_hasAttachedAnnotations
=>
_trackedAnnotations
.
isNotEmpty
;
// The collection of annotations that are currently being tracked. They may or
// may not be active, depending on the value of _TrackedAnnotation.active.
final
Map
<
MouseTrackerAnnotation
,
_TrackedAnnotation
>
_trackedAnnotations
=
<
MouseTrackerAnnotation
,
_TrackedAnnotation
>{};
void
_addMouseDevice
(
int
device
,
PointerEvent
event
)
{
final
bool
wasConnected
=
mouseIsConnected
;
assert
(!
_mouseStates
.
containsKey
(
device
));
_mouseStates
[
device
]
=
_MouseState
(
mostRecentEvent:
event
);
// Schedule a check to enter annotations that might contain this pointer.
_checkDeviceUpdates
(
device:
device
);
if
(
mouseIsConnected
!=
wasConnected
)
{
notifyListeners
();
}
}
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
();
/// Track an annotation so that if the mouse enters it, we send it events.
///
/// This is typically called when the [AnnotatedRegion] containing this
/// 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
();
}
}
// Handler for events coming from the PointerRouter.
void
_handleEvent
(
PointerEvent
event
)
{
if
(
event
.
kind
!=
PointerDeviceKind
.
mouse
)
{
return
;
}
final
int
device
=
event
.
device
;
if
(
event
is
PointerAddedEvent
)
{
_addMouseDevice
(
device
,
event
);
}
else
if
(
event
is
PointerRemovedEvent
)
{
_removeMouseDevice
(
device
,
event
);
}
else
if
(
event
is
PointerHoverEvent
)
{
final
_MouseState
mouseState
=
_guaranteeMouseState
(
device
,
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
);
/// Stops tracking an annotation, indicating that it has been removed from the
/// layer tree.
///
/// An assertion error will be thrown if the associated layer is not removed
/// and receives another mouse hit.
void
detachAnnotation
(
MouseTrackerAnnotation
annotation
)
{
final
_TrackedAnnotation
trackedAnnotation
=
_findAnnotation
(
annotation
);
for
(
int
deviceId
in
trackedAnnotation
.
activeDevices
)
{
if
(
annotation
.
onExit
!=
null
)
{
final
PointerEvent
event
=
_lastMouseEvent
[
deviceId
]
??
_pendingRemovals
[
deviceId
];
assert
(
event
!=
null
);
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
}
}
_trackedAnnotations
.
remove
(
annotation
);
}
bool
_scheduledPostFramePositionCheck
=
false
;
// Schedules a position check at the end of this frame
.
//
It is only called during a frame during which annotations
have been added.
// Schedules a position check at the end of this frame
for those annotations
//
that
have been added.
void
_scheduleMousePositionCheck
()
{
// 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
// annotations that need exiting, either.
if
(!
_scheduledPostFramePositionCheck
)
{
if
(
_trackedAnnotations
.
isNotEmpty
&&
!
_scheduledPostFramePositionCheck
)
{
_scheduledPostFramePositionCheck
=
true
;
SchedulerBinding
.
instance
.
addPostFrameCallback
((
Duration
duration
)
{
_
checkAllDevicesUpdates
(
);
_
sendMouseNotifications
(
_lastMouseEvent
.
keys
);
_scheduledPostFramePositionCheck
=
false
;
});
}
}
// Collect the latest states of the given mouse device `device`, and call
// interested callbacks.
//
// The enter or exit events are called for annotations that the pointer
// 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
);
// Handler for events coming from the PointerRouter.
void
_handleEvent
(
PointerEvent
event
)
{
if
(
event
.
kind
!=
PointerDeviceKind
.
mouse
)
{
return
;
}
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
;
}
// Dispatch callbacks related to a device after all necessary information
// has been collected.
//
// This function should not change the provided states, and should not access
// information that is not provided in parameters (hence being static).
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
));
if
(
event
is
PointerRemovedEvent
)
{
_removeMouseEvent
(
deviceId
,
event
);
// If the mouse was removed, then we need to schedule one more check to
// exit any annotations that were active.
_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
});
}
}
// 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.
// For now the order between the hover events is designed this way for no
// solid reasons but to keep it aligned with enter events for simplicity.
if
(
mostRecentEvent
is
PointerHoverEvent
)
{
final
Iterable
<
MouseTrackerAnnotation
>
hoveringAnnotations
=
nextAnnotations
.
toList
().
reversed
;
for
(
final
MouseTrackerAnnotation
annotation
in
hoveringAnnotations
)
{
if
(
annotation
.
onHover
!=
null
)
{
annotation
.
onHover
(
mostRecentEvent
);
}
}
}
_TrackedAnnotation
_findAnnotation
(
MouseTrackerAnnotation
annotation
)
{
final
_TrackedAnnotation
trackedAnnotation
=
_trackedAnnotations
[
annotation
];
assert
(
trackedAnnotation
!=
null
,
'Unable to find annotation
$annotation
in tracked annotations. '
'Check that attachAnnotation has been called for all annotated layers.'
);
return
trackedAnnotation
;
}
/// Checks if the given [MouseTrackerAnnotation] is attached to this
...
...
@@ -347,59 +209,127 @@ class MouseTracker extends ChangeNotifier {
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
bool
isAnnotationAttached
(
MouseTrackerAnnotation
annotation
)
{
return
_trackedAnnotations
.
contains
(
annotation
);
return
_trackedAnnotations
.
contains
Key
(
annotation
);
}
/// Whether or not a mouse is connected and has produced events.
bool
get
mouseIsConnected
=>
_mouseStates
.
isNotEmpty
;
// Tells interested objects that a mouse has entered, exited, or moved, given
// 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
/// take effect.
///
/// This should be called as soon as the layer that owns this annotation is
/// 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
();
void
exitAnnotation
(
_TrackedAnnotation
trackedAnnotation
,
int
deviceId
)
{
if
(
trackedAnnotation
.
annotation
?.
onExit
!=
null
&&
trackedAnnotation
.
activeDevices
.
contains
(
deviceId
))
{
final
PointerEvent
event
=
_lastMouseEvent
[
deviceId
]
??
_pendingRemovals
[
deviceId
];
assert
(
event
!=
null
);
trackedAnnotation
.
annotation
.
onExit
(
PointerExitEvent
.
fromMouseEvent
(
event
));
}
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
/// 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
/// 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
));
try
{
// 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
// all active annotations are exited.
if
(!
mouseIsConnected
)
{
_trackedAnnotations
.
values
.
forEach
(
exitAllDevices
);
return
;
}
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
);
}
});
_trackedAnnotations
.
remove
(
annotation
);
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
();
}
}
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';
typedef
HandleEventCallback
=
void
Function
(
PointerEvent
event
);
class
_TestGestureFlutterBinding
extends
BindingBase
with
ServicesBinding
,
SchedulerBinding
,
GestureBinding
,
SemanticsBinding
,
RendererBinding
{
@override
void
initInstances
()
{
super
.
initInstances
();
postFrameCallbacks
=
<
void
Function
(
Duration
)>[];
}
class
TestGestureFlutterBinding
extends
BindingBase
with
ServicesBinding
,
SchedulerBinding
,
GestureBinding
,
SemanticsBinding
,
RendererBinding
{
HandleEventCallback
callback
;
List
<
void
Function
(
Duration
)>
postFrameCallbacks
;
// Proxy post-frame callbacks
@override
void
addPostFrameCallback
(
void
Function
(
Duration
)
callback
)
{
postFrameCallbacks
.
add
(
callback
);
}
void
flushPostFrameCallbacks
(
Duration
duration
)
{
for
(
final
void
Function
(
Duration
)
callback
in
postFrameCallbacks
)
{
callback
(
duration
);
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
super
.
handleEvent
(
event
,
entry
);
if
(
callback
!=
null
)
{
callback
(
event
);
}
postFrameCallbacks
.
clear
();
}
}
_TestGestureFlutterBinding
_binding
=
_TestGestureFlutterBinding
();
MouseTracker
get
_mouseTracker
=>
RendererBinding
.
instance
.
mouseTracker
;
TestGestureFlutterBinding
_binding
=
TestGestureFlutterBinding
();
void
_
ensureTestGestureBinding
(
)
{
_binding
??=
_
TestGestureFlutterBinding
();
void
ensureTestGestureBinding
(
)
{
_binding
??=
TestGestureFlutterBinding
();
assert
(
GestureBinding
.
instance
!=
null
);
}
void
main
(
)
{
void
_setUpMouseAnnotationFinder
(
MouseDetectorAnnotationFinder
annotationFinder
)
{
final
MouseTracker
mouseTracker
=
MouseTracker
(
GestureBinding
.
instance
.
pointerRouter
,
annotationFinder
,
);
RendererBinding
.
instance
.
initMouseTracker
(
mouseTracker
);
}
setUp
(
ensureTestGestureBinding
);
// Set up a trivial test environment that includes one annotation, which adds
// the enter, hover, and exit events it received to [logEvents].
MouseTrackerAnnotation
_setUpWithOneAnnotation
({
List
<
PointerEvent
>
logEvents
})
{
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
logE
vents
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
logE
vents
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
logE
vents
.
add
(
event
),
onEnter:
(
PointerEnterEvent
event
)
=>
e
vents
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
e
vents
.
add
(
event
),
onExit:
(
PointerExitEvent
event
)
=>
e
vents
.
add
(
event
),
);
_setUpMouseAnnotationFinder
(
(
Offset
position
)
sync
*
{
yield
annotation
;
}
,
// Only respond to some mouse events.
final
MouseTrackerAnnotation
partialAnnotation
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
=>
events
.
add
(
event
),
onHover:
(
PointerHoverEvent
event
)
=>
events
.
add
(
event
)
,
);
_mouseTracker
.
attachAnnotation
(
annotation
);
return
annotation
;
bool
isInHitRegionOne
;
bool
isInHitRegionTwo
;
void
clear
()
{
events
.
clear
();
}
setUp
(()
{
_ensureTestGestureBinding
();
_binding
.
postFrameCallbacks
.
clear
();
PointerEventConverter
.
clearPointers
();
});
test
(
'MouseTrackerAnnotation has correct toString'
,
()
{
final
MouseTrackerAnnotation
annotation1
=
MouseTrackerAnnotation
(
onEnter:
(
_
)
{},
onExit:
(
_
)
{},
onHover:
(
_
)
{},
);
expect
(
annotation1
.
toString
(),
equals
(
'MouseTrackerAnnotation#
${shortHash(annotation1)}
(callbacks: enter hover exit)'
),
);
const
MouseTrackerAnnotation
annotation2
=
MouseTrackerAnnotation
();
expect
(
annotation2
.
toString
(),
equals
(
'MouseTrackerAnnotation#
${shortHash(annotation2)}
(callbacks: <none>)'
),
clear
();
isInHitRegionOne
=
true
;
isInHitRegionTwo
=
false
;
RendererBinding
.
instance
.
initMouseTracker
(
MouseTracker
(
GestureBinding
.
instance
.
pointerRouter
,
(
Offset
position
)
sync
*
{
if
(
isInHitRegionOne
)
yield
annotation
;
else
if
(
isInHitRegionTwo
)
{
yield
partialAnnotation
;
}
},
),
);
PointerEventConverter
.
clearPointers
();
});
test
(
'should detect enter, hover, and exit from Added, Hover, and Removed events'
,
()
{
final
List
<
PointerEvent
>
events
=
<
PointerEvent
>[];
_setUpWithOneAnnotation
(
logEvents:
events
);
final
List
<
bool
>
listenerLogs
=
<
bool
>[];
_mouseTracker
.
addListener
(()
{
listenerLogs
.
add
(
_mouseTracker
.
mouseIsConnected
);
});
expect
(
_mouseTracker
.
mouseIsConnected
,
isFalse
);
// Enter
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
0.0
)),
]));
test
(
'receives and processes mouse hover events'
,
()
{
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
// Will implicitly also add a PointerAdded event.
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
]);
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]);
final
ui
.
PointerDataPacket
packet3
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
201.0
)),
]);
final
ui
.
PointerDataPacket
packet4
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
301.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
>[
const
PointerEnterEvent
(
position:
Offset
(
1
.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
1
.0
,
0.0
)),
const
PointerEnterEvent
(
position:
Offset
(
0
.0
,
0.0
)),
const
PointerHoverEvent
(
position:
Offset
(
0
.0
,
0.0
)),
]));
expect
(
listenerLogs
,
<
bool
>[
true
]);
events
.
clear
();
listenerLogs
.
clear
();
clear
();
// Hover
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]));
ui
.
window
.
onPointerDataPacket
(
packet2
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
101.0
)),
]));
expect
(
_mouseTracker
.
mouseIsConnected
,
isTrue
);
expect
(
listenerLogs
,
<
bool
>[]);
events
.
clear
();
clear
();
// Remove
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
201.0
)),
]));
ui
.
window
.
onPointerDataPacket
(
packet3
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
201.0
)),
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
]));
expect
(
listenerLogs
,
<
bool
>[
false
]);
events
.
clear
();
listenerLogs
.
clear
();
// Add again
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
301.0
)),
]));
clear
();
ui
.
window
.
onPointerDataPacket
(
packet4
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
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
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
401.0
),
device:
1
),
]));
// add in a second mouse simultaneously.
clear
();
ui
.
window
.
onPointerDataPacket
(
packet5
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
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
;
// 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
});
test
(
'detects exit when annotated layer no longer hit'
,
()
{
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]);
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
201.0
)),
]);
isInHitRegionOne
=
true
;
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
// 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
();
ui
.
window
.
onPointerDataPacket
(
packet1
);
// Attach an annotation out of region
_mouseTracker
.
attachAnnotation
(
annotation
);
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
>[
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
]));
events
.
clear
();
// Detach the annotation
_mouseTracker
.
detachAnnotation
(
annotation
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
expect
(
_binding
.
postFrameCallbacks
,
hasLength
(
0
));
// Actually detach annotation. Shouldn't receive hit.
RendererBinding
.
instance
.
mouseTracker
.
detachAnnotation
(
annotation
);
clear
();
isInHitRegionOne
=
false
;
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
0.0
,
100.0
)),
]));
ui
.
window
.
onPointerDataPacket
(
packet2
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
]));
});
test
(
'should not 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
;
});
test
(
"don't flip out if not all mouse events are listened to"
,
()
{
final
ui
.
PointerDataPacket
packet
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]);
isInHitRegionOne
=
false
;
isInHitRegionTwo
=
true
;
_mouseTracker
.
attachAnnotation
(
annotation2
);
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
partialAnnotation
);
ui
.
window
.
onPointerDataPacket
(
packet
);
_mouseTracker
.
detachAnnotation
(
annotation2
);
RendererBinding
.
instance
.
mouseTracker
.
detachAnnotation
(
partialAnnotation
);
isInHitRegionTwo
=
false
;
// Passes if no errors are thrown
});
test
(
'should not call annotationFinder when no annotations are attached'
,
()
{
final
MouseTrackerAnnotation
annotation
=
MouseTrackerAnnotation
(
onEnter:
(
PointerEnterEvent
event
)
{},
);
int
finderCalled
=
0
;
_setUpMouseAnnotationFinder
((
Offset
position
)
sync
*
{
finderCalled
++;
// This annotation is never in the region
});
// When no annotations are attached, hovering should not call finder.
ui
.
window
.
onPointerDataPacket
(
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
101.0
)),
]));
expect
(
finderCalled
,
0
);
// Attaching should call finder during the post frame.
_mouseTracker
.
attachAnnotation
(
annotation
);
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
)),
test
(
'detects exit when mouse goes away'
,
()
{
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]);
final
ui
.
PointerDataPacket
packet2
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
remove
,
const
Offset
(
1.0
,
201.0
)),
]);
isInHitRegionOne
=
true
;
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
ui
.
window
.
onPointerDataPacket
(
packet1
);
ui
.
window
.
onPointerDataPacket
(
packet2
);
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
)),
const
PointerHoverEvent
(
position:
Offset
(
1.0
,
201.0
)),
const
PointerExitEvent
(
position:
Offset
(
1.0
,
201.0
)),
]));
expect
(
logs
,
<
String
>[
'exitB'
,
'exitA'
]);
});
test
(
'should trigger callbacks between disjoint siblings in correctly order'
,
()
{
// This test simulates the scenario of 2 sibling layers that do not overlap
// with each other.
//
// ———————— ————————
// |A | |B |
// | | | |
// ———————— ————————
bool
isInA
;
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
*
{
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
)),
test
(
'handles mouse down and move'
,
()
{
final
ui
.
PointerDataPacket
packet1
=
ui
.
PointerDataPacket
(
data:
<
ui
.
PointerData
>[
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
0.0
,
0.0
)),
_pointerData
(
PointerChange
.
hover
,
const
Offset
(
1.0
,
101.0
)),
]);
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
)),
]);
isInHitRegionOne
=
true
;
RendererBinding
.
instance
.
mouseTracker
.
attachAnnotation
(
annotation
);
ui
.
window
.
onPointerDataPacket
(
packet1
);
ui
.
window
.
onPointerDataPacket
(
packet2
);
expect
(
events
,
_equalToEventsOnCriticalFields
(<
PointerEvent
>[
const
PointerEnterEvent
(
position:
Offset
(
0.0
,
0.0
),
delta:
Offset
(
0.0
,
0.0
)),
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
)),
]));
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