1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.input.pointer
18 
19 import androidx.collection.LongSparseArray
20 import androidx.collection.MutableLongObjectMap
21 import androidx.collection.MutableObjectList
22 import androidx.collection.mutableObjectListOf
23 import androidx.compose.runtime.collection.MutableVector
24 import androidx.compose.runtime.collection.mutableVectorOf
25 import androidx.compose.ui.ComposeUiFlags
26 import androidx.compose.ui.ExperimentalComposeUiApi
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.input.pointer.util.PointerIdArray
29 import androidx.compose.ui.layout.LayoutCoordinates
30 import androidx.compose.ui.node.InternalCoreApi
31 import androidx.compose.ui.node.Nodes
32 import androidx.compose.ui.node.dispatchForKind
33 import androidx.compose.ui.node.layoutCoordinates
34 import androidx.compose.ui.util.fastFirstOrNull
35 import androidx.compose.ui.util.fastForEach
36 
37 /**
38  * Organizes pointers and the [PointerInputFilter]s that they hit into a hierarchy such that
39  * [PointerInputChange]s can be dispatched to the [PointerInputFilter]s in a hierarchical fashion.
40  *
41  * @property rootCoordinates the root [LayoutCoordinates] that [PointerInputChange]s will be
42  *   relative to.
43  */
44 @OptIn(ExperimentalComposeUiApi::class)
45 internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {
46 
47     private var dispatchingEvent = false
48     private var dispatchCancelAfterDispatchedEvent = false
49     private var clearNodeCacheAfterDispatchedEvent = false
50     private var removeSpecificNodesAfterDispatchedEvent = false
51 
52     private val nodesToRemove = MutableObjectList<Modifier.Node>()
53 
54     /*@VisibleForTesting*/
55     internal val root: NodeParent = NodeParent()
56 
57     private val hitPointerIdsAndNodes = MutableLongObjectMap<MutableObjectList<Node>>(10)
58 
59     init {
60         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
61             println("POINTER_INPUT_DEBUG_LOG_TAG HitPathTracker.init()")
62         }
63     }
64 
65     /**
66      * Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them.
67      *
68      * This enables future calls to [dispatchChanges] to dispatch the correct [PointerInputChange]s
69      * to the right [PointerInputFilter]s at the right time.
70      *
71      * If [pointerInputNodes] is empty, nothing will be added.
72      *
73      * @param pointerId The id of the pointer that was hit tested against [PointerInputFilter]s
74      * @param pointerInputNodes The [PointerInputFilter]s that were hit by [pointerId]. Must be
75      *   ordered from ancestor to descendant.
76      * @param prunePointerIdsAndChangesNotInNodesList Prune [PointerId]s (and associated changes)
77      *   that are NOT in the pointerInputNodes parameter from the cached tree of ParentNode/Node.
78      */
79     fun addHitPath(
80         pointerId: PointerId,
81         pointerInputNodes: List<Modifier.Node>,
82         prunePointerIdsAndChangesNotInNodesList: Boolean = false
83     ) {
84         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
85             println("POINTER_INPUT_DEBUG_LOG_TAG HitPathTracker.addHitPath()")
86         }
87 
88         var parent: NodeParent = root
89         hitPointerIdsAndNodes.clear()
90         var merging = true
91 
92         eachPin@ for (i in pointerInputNodes.indices) {
93             val pointerInputNode = pointerInputNodes[i]
94 
95             // Doesn't add nodes that aren't attached
96             if (pointerInputNode.isAttached) {
97                 pointerInputNode.detachedListener = {
98                     removePointerInputModifierNode(pointerInputNode)
99                 }
100 
101                 if (merging) {
102                     val node = parent.children.firstOrNull { it.modifierNode == pointerInputNode }
103 
104                     if (node != null) {
105                         node.markIsIn()
106                         node.pointerIds.add(pointerId)
107 
108                         val mutableObjectList =
109                             hitPointerIdsAndNodes.getOrPut(pointerId.value) {
110                                 mutableObjectListOf()
111                             }
112 
113                         mutableObjectList.add(node)
114                         parent = node
115                         continue@eachPin
116                     } else {
117                         merging = false
118                     }
119                 }
120                 // TODO(lmr): i wonder if Node here and PointerInputNode ought to be the same thing?
121                 val node = Node(pointerInputNode).apply { pointerIds.add(pointerId) }
122 
123                 val mutableObjectList =
124                     hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
125 
126                 mutableObjectList.add(node)
127 
128                 parent.children.add(node)
129                 parent = node
130             }
131         }
132 
133         if (prunePointerIdsAndChangesNotInNodesList) {
134             hitPointerIdsAndNodes.forEach { key, value ->
135                 removeInvalidPointerIdsAndChanges(key, value)
136             }
137         }
138     }
139 
140     private fun removePointerInputModifierNode(pointerInputNode: Modifier.Node) {
141         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
142             println("POINTER_INPUT_DEBUG_LOG_TAG removePointerInputModifierNode()")
143             println("POINTER_INPUT_DEBUG_LOG_TAG \t\tpointerInputNode: $pointerInputNode")
144             println("POINTER_INPUT_DEBUG_LOG_TAG \t\t$dispatchingEvent: $dispatchingEvent")
145         }
146 
147         if (dispatchingEvent) {
148             removeSpecificNodesAfterDispatchedEvent = true
149             nodesToRemove.add(pointerInputNode)
150             return
151         }
152         root.removePointerInputModifierNode(pointerInputNode)
153     }
154 
155     // Removes pointers/changes that are not in the latest hit test
156     private fun removeInvalidPointerIdsAndChanges(
157         pointerId: Long,
158         hitNodes: MutableObjectList<Node>
159     ) {
160         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
161             println("POINTER_INPUT_DEBUG_LOG_TAG removeInvalidPointerIdsAndChanges()")
162         }
163         root.removeInvalidPointerIdsAndChanges(pointerId, hitNodes)
164     }
165 
166     /**
167      * Dispatches [internalPointerEvent] through the hierarchy.
168      *
169      * @param internalPointerEvent The change to dispatch.
170      * @return whether this event was dispatched to a [PointerInputFilter]
171      */
172     fun dispatchChanges(
173         internalPointerEvent: InternalPointerEvent,
174         isInBounds: Boolean = true
175     ): Boolean {
176         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
177             println("POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges()")
178         }
179 
180         val changed =
181             root.buildCache(
182                 internalPointerEvent.changes,
183                 rootCoordinates,
184                 internalPointerEvent,
185                 isInBounds
186             )
187         if (!changed) {
188             return false
189         }
190 
191         // In some rare cases, a cancel or a request to remove a pointer input node might come in
192         // during an event. To avoid problems, we use `dispatchingEvent` to guard against that and
193         // if a cancel or request to remove nodes comes in, we delay it until after the event has
194         // been dispatched.
195         dispatchingEvent = true
196         var dispatchHit =
197             root.dispatchMainEventPass(
198                 internalPointerEvent.changes,
199                 rootCoordinates,
200                 internalPointerEvent,
201                 isInBounds
202             )
203         dispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit
204         dispatchingEvent = false
205 
206         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
207             println("POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() done, starting after calls...")
208         }
209 
210         if (removeSpecificNodesAfterDispatchedEvent) {
211             removeSpecificNodesAfterDispatchedEvent = false
212 
213             for (i in 0 until nodesToRemove.size) {
214                 removePointerInputModifierNode(nodesToRemove[i])
215             }
216             nodesToRemove.clear()
217 
218             if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
219                 println(
220                     "POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() finished, " +
221                         "removeSpecificNodesAfterDispatchedEvent, " +
222                         "removePointerInputModifierNode() finished"
223                 )
224             }
225         }
226 
227         if (dispatchCancelAfterDispatchedEvent) {
228             dispatchCancelAfterDispatchedEvent = false
229             processCancel()
230 
231             if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
232                 println(
233                     "POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() finished, " +
234                         "dispatchCancelAfterDispatchedEvent, processCancel() finished"
235                 )
236             }
237         }
238 
239         if (clearNodeCacheAfterDispatchedEvent) {
240             clearNodeCacheAfterDispatchedEvent = false
241             clearPreviouslyHitModifierNodeCache()
242 
243             if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
244                 println(
245                     "POINTER_INPUT_DEBUG_LOG_TAG dispatchChanges() finished, " +
246                         "clearNodeCacheAfterDispatchedEvent, " +
247                         "clearPreviouslyHitModifierNodeCache() finished"
248                 )
249             }
250         }
251 
252         return dispatchHit
253     }
254 
255     fun clearPreviouslyHitModifierNodeCache() {
256         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
257             println("POINTER_INPUT_DEBUG_LOG_TAG clearPreviouslyHitModifierNodeCache()")
258             println("POINTER_INPUT_DEBUG_LOG_TAG \t\t$dispatchingEvent: $dispatchingEvent")
259         }
260 
261         if (clearNodeCacheAfterDispatchedEvent) {
262             clearNodeCacheAfterDispatchedEvent = true
263             return
264         }
265         root.clear()
266     }
267 
268     /**
269      * Dispatches cancel events to all tracked [PointerInputFilter]s to notify them that
270      * [PointerInputFilter.onPointerEvent] will not be called again until all pointers have been
271      * removed from the application and then at least one is added again, and removes all tracked
272      * data.
273      */
274     fun processCancel() {
275         if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
276             println("POINTER_INPUT_DEBUG_LOG_TAG processCancel()")
277             println("POINTER_INPUT_DEBUG_LOG_TAG \t\t$dispatchingEvent: $dispatchingEvent")
278         }
279         if (dispatchingEvent) {
280             dispatchCancelAfterDispatchedEvent = true
281             return
282         }
283         root.dispatchCancel()
284         clearPreviouslyHitModifierNodeCache()
285     }
286 }
287 
288 /**
289  * Represents a parent node in the [HitPathTracker]'s tree. This primarily exists because the tree
290  * necessarily has a root that is very similar to all other nodes, except that it does not track any
291  * pointer or [PointerInputFilter] information.
292  */
293 /*@VisibleForTesting*/
294 @OptIn(InternalCoreApi::class)
295 internal open class NodeParent {
296     val children: MutableVector<Node> = mutableVectorOf()
297 
298     // Supports removePointerInputModifierNode() function
299     private val removeMatchingPointerInputModifierNodeList = MutableObjectList<NodeParent>(10)
300 
buildCachenull301     open fun buildCache(
302         changes: LongSparseArray<PointerInputChange>,
303         parentCoordinates: LayoutCoordinates,
304         internalPointerEvent: InternalPointerEvent,
305         isInBounds: Boolean
306     ): Boolean {
307         var changed = false
308         children.forEach {
309             changed =
310                 it.buildCache(changes, parentCoordinates, internalPointerEvent, isInBounds) ||
311                     changed
312         }
313         return changed
314     }
315 
316     /**
317      * Dispatches [changes] down the tree, for the initial and main pass.
318      *
319      * [changes] and other properties needed in all passes should be cached inside this method so
320      * they can be reused in [dispatchFinalEventPass], since the passes happen consecutively.
321      *
322      * @param changes the map containing [PointerInputChange]s that will be dispatched to relevant
323      *   [PointerInputFilter]s
324      * @param parentCoordinates the [LayoutCoordinates] the positional information in [changes] is
325      *   relative to
326      * @param internalPointerEvent the [InternalPointerEvent] needed to construct [PointerEvent]s
327      */
dispatchMainEventPassnull328     open fun dispatchMainEventPass(
329         changes: LongSparseArray<PointerInputChange>,
330         parentCoordinates: LayoutCoordinates,
331         internalPointerEvent: InternalPointerEvent,
332         isInBounds: Boolean
333     ): Boolean {
334         var dispatched = false
335         children.forEach {
336             dispatched =
337                 it.dispatchMainEventPass(
338                     changes,
339                     parentCoordinates,
340                     internalPointerEvent,
341                     isInBounds
342                 ) || dispatched
343         }
344         return dispatched
345     }
346 
347     /**
348      * Dispatches the final event pass down the tree.
349      *
350      * Properties cached in [dispatchMainEventPass] should be reset after this method, to ensure
351      * clean state for a future pass where pointer IDs / positions might be different.
352      */
dispatchFinalEventPassnull353     open fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
354         var dispatched = false
355         children.forEach {
356             dispatched = it.dispatchFinalEventPass(internalPointerEvent) || dispatched
357         }
358         cleanUpHits(internalPointerEvent)
359         return dispatched
360     }
361 
362     /** Dispatches the cancel event to all child [Node]s. */
dispatchCancelnull363     open fun dispatchCancel() {
364         children.forEach { it.dispatchCancel() }
365     }
366 
removePointerInputModifierNodenull367     open fun removePointerInputModifierNode(pointerInputModifierNode: Modifier.Node) {
368         removeMatchingPointerInputModifierNodeList.clear()
369 
370         // adds root first
371         removeMatchingPointerInputModifierNodeList.add(this)
372 
373         while (removeMatchingPointerInputModifierNodeList.isNotEmpty()) {
374             val parent =
375                 removeMatchingPointerInputModifierNodeList.removeAt(
376                     removeMatchingPointerInputModifierNodeList.size - 1
377                 )
378 
379             var index = 0
380             while (index < parent.children.size) {
381                 val child = parent.children[index]
382 
383                 if (child.modifierNode == pointerInputModifierNode) {
384                     parent.children.remove(child)
385                     child.dispatchCancel()
386                     // TODO(JJW): Break here if we change tree structure so same node can't be in
387                     //  multiple locations (they can be now).
388                 } else {
389                     removeMatchingPointerInputModifierNodeList.add(child)
390                     index++
391                 }
392             }
393         }
394     }
395 
396     /** Removes all child nodes. */
clearnull397     fun clear() {
398         children.clear()
399     }
400 
removeInvalidPointerIdsAndChangesnull401     open fun removeInvalidPointerIdsAndChanges(
402         pointerIdValue: Long,
403         hitNodes: MutableObjectList<Node>
404     ) {
405         children.forEach { it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes) }
406     }
407 
cleanUpHitsnull408     open fun cleanUpHits(internalPointerEvent: InternalPointerEvent) {
409         for (i in children.lastIndex downTo 0) {
410             val child = children[i]
411             if (child.pointerIds.isEmpty()) {
412                 children.removeAt(i)
413             }
414         }
415     }
416 }
417 
418 /**
419  * Represents a single Node in the tree that also tracks a [PointerInputFilter] and which pointers
420  * hit it (tracked as [PointerId]s).
421  */
422 /*@VisibleForTesting*/
423 @OptIn(InternalCoreApi::class)
424 internal class Node(val modifierNode: Modifier.Node) : NodeParent() {
425 
426     // Note: pointerIds are stored in a structure specific to their value type (PointerId).
427     // This structure uses a LongArray internally, which avoids auto-boxing caused by
428     // a more generic collection such as HashMap or MutableVector.
429     val pointerIds = PointerIdArray()
430 
431     /**
432      * Cached properties that will be set before the main event pass, and reset after the final
433      * pass. Since we know that these won't change within the entire pass, we don't need to
434      * calculate / create these for each pass / multiple times during a pass.
435      *
436      * @see buildCache
437      * @see clearCache
438      */
439     private val relevantChanges: LongSparseArray<PointerInputChange> = LongSparseArray(2)
440     private var coordinates: LayoutCoordinates? = null
441     private var pointerEvent: PointerEvent? = null
442     private var wasIn = false
443     private var isIn = true
444     private var hasExited = true
445 
removeInvalidPointerIdsAndChangesnull446     override fun removeInvalidPointerIdsAndChanges(
447         pointerIdValue: Long,
448         hitNodes: MutableObjectList<Node>
449     ) {
450         if (this.pointerIds.contains(pointerIdValue)) {
451             if (!hitNodes.contains(this)) {
452                 this.pointerIds.remove(pointerIdValue)
453                 this.relevantChanges.remove(pointerIdValue)
454             }
455         }
456 
457         children.forEach { it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes) }
458     }
459 
dispatchMainEventPassnull460     override fun dispatchMainEventPass(
461         changes: LongSparseArray<PointerInputChange>,
462         parentCoordinates: LayoutCoordinates,
463         internalPointerEvent: InternalPointerEvent,
464         isInBounds: Boolean
465     ): Boolean {
466         // TODO(b/158243568): The below dispatching operations may cause the pointerInputFilter to
467         //  become detached. Currently, they just no-op if it becomes detached and the detached
468         //  pointerInputFilters are removed from being tracked with the next event. I currently
469         //  believe they should be detached immediately. Though, it is possible they should be
470         //  detached after the conclusion of dispatch (so onCancel isn't called during calls
471         //  to onPointerEvent). As a result we guard each successive dispatch with the same check.
472         return dispatchIfNeeded {
473             val event = pointerEvent!!
474             val size = coordinates!!.size
475 
476             // Dispatch on the tunneling pass.
477             modifierNode.dispatchForKind(Nodes.PointerInput) {
478                 it.onPointerEvent(event, PointerEventPass.Initial, size)
479             }
480 
481             // Dispatch to children.
482             if (modifierNode.isAttached) {
483                 children.forEach {
484                     it.dispatchMainEventPass(
485                         // Pass only the already-filtered and position-translated changes down to
486                         // children
487                         relevantChanges,
488                         coordinates!!,
489                         internalPointerEvent,
490                         isInBounds
491                     )
492                 }
493             }
494 
495             if (modifierNode.isAttached) {
496                 // Dispatch on the bubbling pass.
497                 modifierNode.dispatchForKind(Nodes.PointerInput) {
498                     it.onPointerEvent(event, PointerEventPass.Main, size)
499                 }
500             }
501         }
502     }
503 
dispatchFinalEventPassnull504     override fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
505         // TODO(b/158243568): The below dispatching operations may cause the pointerInputFilter to
506         //  become detached. Currently, they just no-op if it becomes detached and the detached
507         //  pointerInputFilters are removed from being tracked with the next event. I currently
508         //  believe they should be detached immediately. Though, it is possible they should be
509         //  detached after the conclusion of dispatch (so onCancel isn't called during calls
510         //  to onPointerEvent). As a result we guard each successive dispatch with the same check.
511         val result = dispatchIfNeeded {
512             val event = pointerEvent!!
513             val size = coordinates!!.size
514             // Dispatch on the tunneling pass.
515             modifierNode.dispatchForKind(Nodes.PointerInput) {
516                 it.onPointerEvent(event, PointerEventPass.Final, size)
517             }
518 
519             // Dispatch to children.
520             if (modifierNode.isAttached) {
521                 children.forEach { it.dispatchFinalEventPass(internalPointerEvent) }
522             }
523         }
524         cleanUpHits(internalPointerEvent)
525         clearCache()
526         return result
527     }
528 
529     /**
530      * Calculates cached properties that will be stored in this [Node] for the duration of both
531      * [dispatchMainEventPass] and [dispatchFinalEventPass]. This allows us to avoid repeated work
532      * between passes, and within passes, as these properties won't change during the overall
533      * dispatch.
534      *
535      * @see clearCache
536      */
buildCachenull537     override fun buildCache(
538         changes: LongSparseArray<PointerInputChange>,
539         parentCoordinates: LayoutCoordinates,
540         internalPointerEvent: InternalPointerEvent,
541         isInBounds: Boolean
542     ): Boolean {
543         val childChanged =
544             super.buildCache(changes, parentCoordinates, internalPointerEvent, isInBounds)
545 
546         // Avoid future work if we know this node will no-op
547         if (!modifierNode.isAttached) return true
548 
549         modifierNode.dispatchForKind(Nodes.PointerInput) { coordinates = it.layoutCoordinates }
550 
551         // In some cases, undelegate() may be called and the modifierNode is still attached, but
552         // the [SuspendingPointerInputModifierNode] is no longer associated with it (since there
553         // are no [Nodes.PointerInput] kinds). In those cases, we skip triggering the event
554         // for this Node.
555         if (coordinates == null) return true
556 
557         @OptIn(ExperimentalComposeUiApi::class)
558         for (j in 0 until changes.size()) {
559             val keyValue = changes.keyAt(j)
560             val change = changes.valueAt(j)
561 
562             if (pointerIds.contains(keyValue)) {
563                 val prevPosition = change.previousPosition
564                 val currentPosition = change.position
565 
566                 if (prevPosition.isValid() && currentPosition.isValid()) {
567                     // And translate their position relative to the parent coordinates, to give us a
568                     // change local to the PointerInputFilter's coordinates
569                     val historical = ArrayList<HistoricalChange>(change.historical.size)
570 
571                     change.historical.fastForEach {
572                         val historicalPosition = it.position
573                         // In some rare cases, historic data may have an invalid position, that is,
574                         // Offset.Unspecified. In those cases, we don't want to include it in the
575                         // data returned to the developer because the values are invalid.
576                         if (historicalPosition.isValid()) {
577                             historical.add(
578                                 HistoricalChange(
579                                     it.uptimeMillis,
580                                     coordinates!!.localPositionOf(
581                                         parentCoordinates,
582                                         historicalPosition
583                                     ),
584                                     it.originalEventPosition
585                                 )
586                             )
587                         }
588                     }
589 
590                     relevantChanges.put(
591                         keyValue,
592                         change.copy(
593                             previousPosition =
594                                 coordinates!!.localPositionOf(parentCoordinates, prevPosition),
595                             currentPosition =
596                                 coordinates!!.localPositionOf(parentCoordinates, currentPosition),
597                             historical = historical
598                         )
599                     )
600                 }
601             }
602         }
603 
604         if (relevantChanges.isEmpty()) {
605             pointerIds.clear()
606             children.clear()
607             return true // not hit
608         }
609 
610         // Clean up any pointerIds that weren't dispatched
611         for (i in pointerIds.lastIndex downTo 0) {
612             val pointerId = pointerIds[i]
613             if (!changes.containsKey(pointerId.value)) {
614                 pointerIds.removeAt(i)
615             }
616         }
617 
618         val changesList = ArrayList<PointerInputChange>(relevantChanges.size())
619         for (i in 0 until relevantChanges.size()) {
620             changesList.add(relevantChanges.valueAt(i))
621         }
622         val event = PointerEvent(changesList, internalPointerEvent)
623 
624         val activeHoverChange =
625             event.changes.fastFirstOrNull { internalPointerEvent.activeHoverEvent(it.id) }
626 
627         if (activeHoverChange != null) {
628             if (!isInBounds) {
629                 isIn = false
630             } else if (!isIn && (activeHoverChange.pressed || activeHoverChange.previousPressed)) {
631                 // We have to recalculate isIn because we didn't redo hit testing
632                 val size = coordinates!!.size
633                 @Suppress("DEPRECATION")
634                 isIn = !activeHoverChange.isOutOfBounds(size)
635             }
636             if (
637                 isIn != wasIn &&
638                     (event.type == PointerEventType.Move ||
639                         event.type == PointerEventType.Enter ||
640                         event.type == PointerEventType.Exit)
641             ) {
642                 event.type =
643                     if (isIn) {
644                         PointerEventType.Enter
645                     } else {
646                         PointerEventType.Exit
647                     }
648             } else if (event.type == PointerEventType.Enter && wasIn && !hasExited) {
649                 event.type = PointerEventType.Move // We already knew that it was in.
650             } else if (event.type == PointerEventType.Exit && isIn && activeHoverChange.pressed) {
651                 event.type = PointerEventType.Move // We are still in.
652             }
653         }
654 
655         val changed =
656             childChanged ||
657                 event.type != PointerEventType.Move ||
658                 hasPositionChanged(pointerEvent, event)
659         pointerEvent = event
660         return changed
661     }
662 
hasPositionChangednull663     private fun hasPositionChanged(oldEvent: PointerEvent?, newEvent: PointerEvent): Boolean {
664         if (oldEvent == null || oldEvent.changes.size != newEvent.changes.size) {
665             return true
666         }
667         for (i in 0 until newEvent.changes.size) {
668             val old = oldEvent.changes[i]
669             val current = newEvent.changes[i]
670             if (old.position != current.position) {
671                 return true
672             }
673         }
674         return false
675     }
676 
677     /**
678      * Resets cached properties in case this node will continue to track different [pointerIds] than
679      * the ones we built the cache for, instead of being removed.
680      *
681      * @see buildCache
682      */
clearCachenull683     private fun clearCache() {
684         relevantChanges.clear()
685         coordinates = null
686     }
687 
688     /**
689      * Calls [block] if there are relevant changes, and if [modifierNode] is attached
690      *
691      * @return whether [block] was called
692      */
dispatchIfNeedednull693     private inline fun dispatchIfNeeded(block: () -> Unit): Boolean {
694         // If there are no relevant changes, there is nothing to process so return false.
695         if (relevantChanges.isEmpty()) return false
696         // If the input filter is not attached, avoid dispatching
697         if (!modifierNode.isAttached) return false
698 
699         block()
700 
701         // We dispatched to at least one pointer input filter so return true.
702         return true
703     }
704 
705     // TODO(shepshapard): Should some order of cancel dispatch be guaranteed? I think the answer is
706     //  essentially "no", but given that an order can be consistent... maybe we might as well
707     //  set an arbitrary standard and stick to it so user expectations are maintained.
708     /**
709      * Does a depth first traversal and invokes [PointerInputFilter.onCancel] during backtracking.
710      */
dispatchCancelnull711     override fun dispatchCancel() {
712         children.forEach { it.dispatchCancel() }
713         modifierNode.dispatchForKind(Nodes.PointerInput) { it.onCancelPointerInput() }
714     }
715 
markIsInnull716     fun markIsIn() {
717         isIn = true
718     }
719 
cleanUpHitsnull720     override fun cleanUpHits(internalPointerEvent: InternalPointerEvent) {
721         super.cleanUpHits(internalPointerEvent)
722 
723         val event = pointerEvent ?: return
724 
725         wasIn = isIn
726 
727         event.changes.fastForEach { change ->
728             // There are two scenarios where we need to remove the pointerIds:
729             //   1. Pointer is released AND event stream doesn't have an active hover.
730             //   2. Pointer is released AND is released outside the area.
731             val released = !change.pressed
732             val nonHoverEventStream = !internalPointerEvent.activeHoverEvent(change.id)
733             val outsideArea = !isIn
734 
735             val removePointerId = (released && nonHoverEventStream) || (released && outsideArea)
736 
737             if (removePointerId) {
738                 pointerIds.remove(change.id)
739             }
740         }
741 
742         isIn = false
743         hasExited = event.type == PointerEventType.Exit
744     }
745 
toStringnull746     override fun toString(): String {
747         return "Node(modifierNode=$modifierNode, children=$children, " + "pointerIds=$pointerIds)"
748     }
749 }
750