1 /*
2  * Copyright 2019 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 package androidx.compose.ui.test
17 
18 import androidx.compose.ui.geometry.Offset
19 import androidx.compose.ui.input.key.Key
20 import androidx.compose.ui.node.RootForTest
21 
createInputDispatchernull22 internal expect fun createInputDispatcher(
23     testContext: TestContext,
24     root: RootForTest
25 ): InputDispatcher
26 
27 /**
28  * Dispatcher to inject any kind of input. An [InputDispatcher] is created at the beginning of
29  * [performMultiModalInput] or the single modality alternatives, and disposed at the end of that
30  * method. The state of all input modalities is persisted and restored on the next invocation of
31  * [performMultiModalInput] (or an alternative).
32  *
33  * Dispatching input happens in two stages. In the first stage, all events are generated (enqueued),
34  * using the `enqueue*` methods, and in the second stage all events are injected. Clients of
35  * [InputDispatcher] should only call methods for the first stage listed below, the second stage is
36  * handled by [performMultiModalInput] and friends.
37  *
38  * Touch input:
39  * * [getCurrentTouchPosition]
40  * * [enqueueTouchDown]
41  * * [enqueueTouchMove]
42  * * [updateTouchPointer]
43  * * [enqueueTouchUp]
44  * * [enqueueTouchCancel]
45  *
46  * Mouse input:
47  * * [currentMousePosition]
48  * * [enqueueMousePress]
49  * * [enqueueMouseMove]
50  * * [updateMousePosition]
51  * * [enqueueMouseRelease]
52  * * [enqueueMouseCancel]
53  * * [enqueueMouseScroll]
54  *
55  * Rotary input:
56  * * [enqueueRotaryScrollHorizontally]
57  * * [enqueueRotaryScrollVertically]
58  *
59  * Key input:
60  * * [enqueueKeyDown]
61  * * [enqueueKeyUp]
62  *
63  * Chaining methods:
64  * * [advanceEventTime]
65  *
66  * [exitHoverOnPress] and [moveOnScroll] allow controlling Android-specific behaviors that may not
67  * be appropriate on other platforms. While it is a quick and simple solution, if more significant
68  * differences are discovered, this problem may need to be revisited for a more robust solution.
69  *
70  * Note that the extra events sent due to [exitHoverOnPress] and [moveOnScroll] are in fact filtered
71  * out on Android before they reach any Compose elements. They nevertheless need to be sent for the
72  * benefit of any interop Android views inside Compose, which expect an Android-native model of the
73  * event stream.
74  */
75 internal abstract class InputDispatcher(
76     private val testContext: TestContext,
77     private val root: RootForTest,
78     private val exitHoverOnPress: Boolean = true,
79     private val moveOnScroll: Boolean = true
80 ) {
81     companion object {
82         /**
83          * The default time between two successively injected events, 16 milliseconds. Events are
84          * normally sent on every frame and thus follow the frame rate. On a 60Hz screen this is
85          * ~16ms per frame.
86          */
87         var eventPeriodMillis = 16L
88             internal set
89 
90         /**
91          * The delay between a down event on a particular [Key] and the first repeat event on that
92          * same key.
93          */
94         const val InitialRepeatDelay = 500L
95 
96         /**
97          * The interval between subsequent repeats (after the initial repeat) on a particular key.
98          */
99         const val SubsequentRepeatDelay = 50L
100     }
101 
102     /** The eventTime of the next event. */
103     protected var currentTime = testContext.currentTime
104 
105     /** The state of the current touch gesture. If `null`, no touch gesture is in progress. */
106     protected var partialGesture: PartialGesture? = null
107 
108     /**
109      * The state of the mouse. The mouse state is always available. It starts at [Offset.Zero] in
110      * not-entered state.
111      */
112     protected var mouseInputState: MouseInputState = MouseInputState()
113 
114     /**
115      * The state of the keyboard keys. The key input state is always available. It starts with no
116      * keys pressed down and the [KeyInputState.downTime] set to zero.
117      */
118     protected var keyInputState: KeyInputState = KeyInputState()
119 
120     /** The state of the rotary button. */
121     protected var rotaryInputState: RotaryInputState = RotaryInputState()
122 
123     /**
124      * Indicates if a gesture is in progress or not. A gesture is in progress if at least one finger
125      * is (still) touching the screen.
126      */
127     val isTouchInProgress: Boolean
128         get() = partialGesture != null
129 
130     /** Indicates whether caps lock is on or not. */
131     val isCapsLockOn: Boolean
132         get() = keyInputState.capsLockOn
133 
134     /** Indicates whether num lock is on or not. */
135     val isNumLockOn: Boolean
136         get() = keyInputState.numLockOn
137 
138     /** Indicates whether scroll lock is on or not. */
139     val isScrollLockOn: Boolean
140         get() = keyInputState.scrollLockOn
141 
142     init {
143         val rootHash = identityHashCode(root)
144         val state = testContext.states.remove(rootHash)
145         if (state != null) {
146             partialGesture = state.partialGesture
147             mouseInputState = state.mouseInputState
148             keyInputState = state.keyInputState
149         }
150     }
151 
152     protected open fun saveState(root: RootForTest?) {
153         if (root != null) {
154             val rootHash = identityHashCode(root)
155             testContext.states[rootHash] =
156                 InputDispatcherState(partialGesture, mouseInputState, keyInputState)
157         }
158     }
159 
160     private val TestContext.currentTime
161         get() = testOwner.mainClock.currentTime
162 
163     private val RootForTest.bounds
164         get() = semanticsOwner.rootSemanticsNode.boundsInRoot
165 
166     protected fun isWithinRootBounds(position: Offset): Boolean = root.bounds.contains(position)
167 
168     /**
169      * Increases the current event time by [durationMillis].
170      *
171      * Depending on the [keyInputState], there may be repeat key events that need to be sent within
172      * the given duration. If there are, the clock will be forwarded until it is time for the repeat
173      * key event, the key event will be sent, and then the clock will be forwarded by the remaining
174      * duration.
175      *
176      * @param durationMillis The duration of the delay. Must be positive
177      */
178     fun advanceEventTime(durationMillis: Long = eventPeriodMillis) {
179         require(durationMillis >= 0) {
180             "duration of a delay can only be positive, not $durationMillis"
181         }
182 
183         val endTime = currentTime + durationMillis
184         keyInputState.sendRepeatKeysIfNeeded(endTime)
185         currentTime = endTime
186     }
187 
188     /**
189      * During a touch gesture, returns the position of the last touch event of the given
190      * [pointerId]. Returns `null` if no touch gesture is in progress for that [pointerId].
191      *
192      * @param pointerId The id of the pointer for which to return the current position
193      * @return The current position of the pointer with the given [pointerId], or `null` if the
194      *   pointer is not currently in use
195      */
196     fun getCurrentTouchPosition(pointerId: Int): Offset? {
197         return partialGesture?.lastPositions?.get(pointerId)
198     }
199 
200     /**
201      * The current position of the mouse. If no mouse event has been sent yet, will be
202      * [Offset.Zero].
203      */
204     val currentMousePosition: Offset
205         get() = mouseInputState.lastPosition
206 
207     /**
208      * Indicates if the given [key] is pressed down or not.
209      *
210      * @param key The key to be checked.
211      * @return true if given [key] is pressed, otherwise false.
212      */
213     fun isKeyDown(key: Key): Boolean = keyInputState.isKeyDown(key)
214 
215     /**
216      * Generates a down touch event at [position] for the pointer with the given [pointerId]. Starts
217      * a new touch gesture if no other [pointerId]s are down. Only possible if the [pointerId] is
218      * not currently being used, although pointer ids may be reused during a touch gesture.
219      *
220      * @param pointerId The id of the pointer, can be any number not yet in use by another pointer
221      * @param position The coordinate of the down event
222      * @see enqueueTouchMove
223      * @see updateTouchPointer
224      * @see enqueueTouchUp
225      * @see enqueueTouchCancel
226      */
227     fun enqueueTouchDown(pointerId: Int, position: Offset) {
228         var gesture = partialGesture
229 
230         // Check if this pointer is not already down
231         require(gesture == null || !gesture.lastPositions.containsKey(pointerId)) {
232             "Cannot send DOWN event, a gesture is already in progress for pointer $pointerId"
233         }
234 
235         if (mouseInputState.hasAnyButtonPressed) {
236             // If mouse buttons are down, a touch gesture cancels the mouse gesture
237             mouseInputState.enqueueCancel()
238         } else if (mouseInputState.isEntered) {
239             // If no mouse buttons were down, we may have been in hovered state
240             mouseInputState.exitHover()
241         }
242 
243         // Send a MOVE event if pointers have changed since the last event
244         gesture?.flushPointerUpdates()
245 
246         // Start a new gesture, or add the pointerId to the existing gesture
247         if (gesture == null) {
248             gesture = PartialGesture(currentTime, position, pointerId)
249             partialGesture = gesture
250         } else {
251             gesture.lastPositions[pointerId] = position
252         }
253 
254         // Send the DOWN event
255         gesture.enqueueDown(pointerId)
256     }
257 
258     /**
259      * Generates a move touch event without moving any of the pointers. Use this to commit all
260      * changes in pointer location made with [updateTouchPointer]. The generated event will contain
261      * the current position of all pointers.
262      *
263      * @see enqueueTouchDown
264      * @see updateTouchPointer
265      * @see enqueueTouchUp
266      * @see enqueueTouchCancel
267      * @see enqueueTouchMoves
268      */
269     fun enqueueTouchMove() {
270         val gesture =
271             checkNotNull(partialGesture) { "Cannot send MOVE event, no gesture is in progress" }
272         gesture.enqueueMove()
273         gesture.hasPointerUpdates = false
274     }
275 
276     /**
277      * Enqueue the current time+coordinates as a move event, with the historical parameters
278      * preceding it (so that they are ultimately available from methods like
279      * MotionEvent.getHistoricalX).
280      *
281      * @see enqueueTouchMove
282      * @see TouchInjectionScope.moveWithHistory
283      */
284     fun enqueueTouchMoves(
285         relativeHistoricalTimes: List<Long>,
286         historicalCoordinates: List<List<Offset>>
287     ) {
288         val gesture =
289             checkNotNull(partialGesture) { "Cannot send MOVE event, no gesture is in progress" }
290         gesture.enqueueMoves(relativeHistoricalTimes, historicalCoordinates)
291         gesture.hasPointerUpdates = false
292     }
293 
294     /**
295      * Updates the position of the touch pointer with the given [pointerId] to the given [position],
296      * but does not generate a move touch event. Use this to move multiple pointers simultaneously.
297      * To generate the next move touch event, which will contain the current position of _all_
298      * pointers (not just the moved ones), call [enqueueTouchMove]. If you move one or more pointers
299      * and then call [enqueueTouchDown], without calling [enqueueTouchMove] first, a move event will
300      * be generated right before that down event.
301      *
302      * @param pointerId The id of the pointer to move, as supplied in [enqueueTouchDown]
303      * @param position The position to move the pointer to
304      * @see enqueueTouchDown
305      * @see enqueueTouchMove
306      * @see enqueueTouchUp
307      * @see enqueueTouchCancel
308      */
309     fun updateTouchPointer(pointerId: Int, position: Offset) {
310         val gesture = partialGesture
311 
312         // Check if this pointer is in the gesture
313         check(gesture != null) { "Cannot move pointers, no gesture is in progress" }
314         require(gesture.lastPositions.containsKey(pointerId)) {
315             "Cannot move pointer $pointerId, it is not active in the current gesture"
316         }
317 
318         gesture.lastPositions[pointerId] = position
319         gesture.hasPointerUpdates = true
320     }
321 
322     /**
323      * Generates an up touch event for the given [pointerId] at the current position of that
324      * pointer.
325      *
326      * @param pointerId The id of the pointer to lift up, as supplied in [enqueueTouchDown]
327      * @see enqueueTouchDown
328      * @see updateTouchPointer
329      * @see enqueueTouchMove
330      * @see enqueueTouchCancel
331      */
332     fun enqueueTouchUp(pointerId: Int) {
333         val gesture = partialGesture
334 
335         // Check if this pointer is in the gesture
336         check(gesture != null) { "Cannot send UP event, no gesture is in progress" }
337         require(gesture.lastPositions.containsKey(pointerId)) {
338             "Cannot send UP event for pointer $pointerId, it is not active in the current gesture"
339         }
340 
341         // First send the UP event
342         gesture.enqueueUp(pointerId)
343 
344         // Then remove the pointer, and end the gesture if no pointers are left
345         gesture.lastPositions.remove(pointerId)
346         if (gesture.lastPositions.isEmpty()) {
347             partialGesture = null
348         }
349     }
350 
351     /**
352      * Generates a cancel touch event for the current touch gesture. Sent automatically when mouse
353      * events are sent while a touch gesture is in progress.
354      *
355      * @see enqueueTouchDown
356      * @see updateTouchPointer
357      * @see enqueueTouchMove
358      * @see enqueueTouchUp
359      */
360     fun enqueueTouchCancel() {
361         val gesture =
362             checkNotNull(partialGesture) { "Cannot send CANCEL event, no gesture is in progress" }
363         gesture.enqueueCancel()
364         partialGesture = null
365     }
366 
367     /**
368      * Generates a move event with all pointer locations, if any of the pointers has been moved by
369      * [updateTouchPointer] since the last move event.
370      */
371     private fun PartialGesture.flushPointerUpdates() {
372         if (hasPointerUpdates) {
373             enqueueTouchMove()
374         }
375     }
376 
377     /**
378      * Generates a mouse button pressed event for the given [buttonId]. This will generate all
379      * required associated events as well, such as a down event if it is the first button being
380      * pressed and an optional hover exit event.
381      *
382      * @param buttonId The id of the mouse button. This is platform dependent, use the values
383      *   defined by [MouseButton.buttonId].
384      */
385     fun enqueueMousePress(buttonId: Int) {
386         val mouse = mouseInputState
387 
388         check(!mouse.isButtonPressed(buttonId)) {
389             "Cannot send mouse button down event, button $buttonId is already pressed"
390         }
391         check(isWithinRootBounds(currentMousePosition) || mouse.hasAnyButtonPressed) {
392             "Cannot start a mouse gesture outside the Compose root bounds, mouse position is " +
393                 "$currentMousePosition and bounds are ${root.bounds}"
394         }
395         if (partialGesture != null) {
396             enqueueTouchCancel()
397         }
398 
399         // Down time is when the first button is pressed
400         if (mouse.hasNoButtonsPressed) {
401             mouse.downTime = currentTime
402         }
403         mouse.setButtonBit(buttonId)
404 
405         // Exit hovering if necessary (Android-specific behavior)
406         if (exitHoverOnPress) {
407             if (mouse.isEntered) {
408                 mouse.exitHover()
409             }
410         }
411         // down/move + press
412         mouse.enqueuePress(buttonId)
413     }
414 
415     /**
416      * Generates a mouse move or hover event to the given [position]. If buttons are pressed, a move
417      * event is generated, otherwise generates a hover event.
418      *
419      * @param position The new mouse position
420      */
421     fun enqueueMouseMove(position: Offset) {
422         val mouse = mouseInputState
423 
424         // Touch needs to be cancelled, even if mouse is out of bounds
425         if (partialGesture != null) {
426             enqueueTouchCancel()
427         }
428 
429         updateMousePosition(position)
430         val isWithinBounds = isWithinRootBounds(position)
431 
432         if (isWithinBounds && !mouse.isEntered && mouse.hasNoButtonsPressed) {
433             // If not yet hovering and no buttons pressed, enter hover state
434             mouse.enterHover()
435         } else if (!isWithinBounds && mouse.isEntered) {
436             // If hovering, exit now
437             mouse.exitHover()
438         }
439         mouse.enqueueMove()
440     }
441 
442     /**
443      * Updates the mouse position without sending an event. Useful if down, up or scroll events need
444      * to be injected on a different location than the preceding move event.
445      *
446      * @param position The new mouse position
447      */
448     fun updateMousePosition(position: Offset) {
449         mouseInputState.lastPosition = position
450         // Contrary to touch input, we don't need to store that the position has changed, because
451         // all events that are affected send the current position regardless.
452     }
453 
454     /**
455      * Generates a mouse button released event for the given [buttonId]. This will generate all
456      * required associated events as well, such as an up and hover enter event if it is the last
457      * button being released.
458      *
459      * @param buttonId The id of the mouse button. This is platform dependent, use the values
460      *   defined by [MouseButton.buttonId].
461      */
462     fun enqueueMouseRelease(buttonId: Int) {
463         val mouse = mouseInputState
464 
465         check(mouse.isButtonPressed(buttonId)) {
466             "Cannot send mouse button up event, button $buttonId is not pressed"
467         }
468         check(partialGesture == null) {
469             "Touch gesture can't be in progress, mouse buttons are down"
470         }
471 
472         mouse.unsetButtonBit(buttonId)
473         mouse.enqueueRelease(buttonId)
474 
475         // When no buttons remaining, enter hover state immediately (Android-specific behavior)
476         if (exitHoverOnPress) {
477             if (mouse.hasNoButtonsPressed && isWithinRootBounds(currentMousePosition)) {
478                 mouse.enterHover()
479                 mouse.enqueueMove()
480             }
481         }
482     }
483 
484     /**
485      * Generates a mouse hover enter event on the given [position].
486      *
487      * @param position The new mouse position
488      */
489     fun enqueueMouseEnter(position: Offset) {
490         val mouse = mouseInputState
491 
492         check(!mouse.isEntered) { "Cannot send mouse hover enter event, mouse is already hovering" }
493         check(mouse.hasNoButtonsPressed) {
494             "Cannot send mouse hover enter event, mouse buttons are down"
495         }
496         check(isWithinRootBounds(position)) {
497             "Cannot send mouse hover enter event, $position is out of bounds"
498         }
499 
500         updateMousePosition(position)
501         mouse.enterHover()
502     }
503 
504     /**
505      * Generates a mouse hover exit event on the given [position].
506      *
507      * @param position The new mouse position
508      */
509     fun enqueueMouseExit(position: Offset) {
510         val mouse = mouseInputState
511 
512         check(mouse.isEntered) { "Cannot send mouse hover exit event, mouse is not hovering" }
513 
514         updateMousePosition(position)
515         mouse.exitHover()
516     }
517 
518     /**
519      * Generates a mouse cancel event. Can only be done if no mouse buttons are currently pressed.
520      * Sent automatically if a touch event is sent while mouse buttons are down.
521      */
522     fun enqueueMouseCancel() {
523         val mouse = mouseInputState
524         check(mouse.hasAnyButtonPressed) {
525             "Cannot send mouse cancel event, no mouse buttons are pressed"
526         }
527         mouse.clearButtonState()
528         mouse.enqueueCancel()
529     }
530 
531     /**
532      * Generates a scroll event on [scrollWheel] by [delta].
533      *
534      * Positive [delta] values correspond to scrolling forward (new content appears at the bottom of
535      * a column, or at the end of a row), negative values correspond to scrolling backward (new
536      * content appears at the top of a column, or at the start of a row).
537      */
538     fun enqueueMouseScroll(delta: Float, scrollWheel: ScrollWheel) {
539         val mouse = mouseInputState
540 
541         if (moveOnScroll) {
542             // On Android a scroll is always preceded by a move(/hover) event
543             enqueueMouseMove(currentMousePosition)
544         }
545         if (isWithinRootBounds(currentMousePosition)) {
546             mouse.enqueueScroll(delta, scrollWheel)
547         }
548     }
549 
550     /**
551      * Generates a key down event for the given [key].
552      *
553      * @param key The keyboard key to be pushed down. Platform specific.
554      */
555     fun enqueueKeyDown(key: Key) {
556         val keyboard = keyInputState
557 
558         check(!keyboard.isKeyDown(key)) {
559             "Cannot send key down event, Key($key) is already pressed down."
560         }
561 
562         // TODO(Onadim): Figure out whether key input needs to enqueue a touch cancel.
563         // Down time is the time of the most recent key down event, which is now.
564         keyboard.downTime = currentTime
565 
566         // Add key to pressed keys.
567         keyboard.setKeyDown(key)
568 
569         keyboard.enqueueDown(key)
570     }
571 
572     /**
573      * Generates a key up event for the given [key].
574      *
575      * @param key The keyboard key to be released. Platform specific.
576      */
577     fun enqueueKeyUp(key: Key) {
578         val keyboard = keyInputState
579 
580         check(keyboard.isKeyDown(key)) {
581             "Cannot send key up event, Key($key) is not pressed down."
582         }
583 
584         // TODO(Onadim): Figure out whether key input needs to enqueue a touch cancel.
585         // Remove key from pressed keys.
586         keyboard.setKeyUp(key)
587 
588         // Send the up event
589         keyboard.enqueueUp(key)
590     }
591 
592     fun enqueueRotaryScrollHorizontally(horizontalScrollPixels: Float) {
593         // TODO(b/214437966): figure out if ongoing scroll events need to be cancelled.
594         rotaryInputState.enqueueRotaryScrollHorizontally(horizontalScrollPixels)
595     }
596 
597     fun enqueueRotaryScrollVertically(verticalScrollPixels: Float) {
598         // TODO(b/214437966): figure out if ongoing scroll events need to be cancelled.
599         rotaryInputState.enqueueRotaryScrollVertically(verticalScrollPixels)
600     }
601 
602     private fun MouseInputState.enterHover() {
603         enqueueEnter()
604         isEntered = true
605     }
606 
607     private fun MouseInputState.exitHover() {
608         enqueueExit()
609         isEntered = false
610     }
611 
612     /**
613      * Sends any and all repeat key events that are required between [currentTime] and [endTime].
614      *
615      * Mutates the value of [currentTime] in order to send each of the repeat events at exactly the
616      * time it should be sent.
617      *
618      * @param endTime All repeats set to occur before this time will be sent.
619      */
620     // TODO(b/236623354): Extend repeat key event support to [MainTestClock.advanceTimeBy].
621     private fun KeyInputState.sendRepeatKeysIfNeeded(endTime: Long) {
622 
623         // Return if there is no key to repeat or if it is not yet time to repeat it.
624         if (repeatKey == null || endTime - downTime < InitialRepeatDelay) return
625 
626         // Initial repeat
627         if (lastRepeatTime <= downTime) {
628             // Not yet had a repeat on this key, but it needs at least the initial one.
629             check(repeatCount == 0) { "repeatCount should be reset to 0 when downTime updates" }
630             repeatCount = 1
631 
632             lastRepeatTime = downTime + InitialRepeatDelay
633             currentTime = lastRepeatTime
634 
635             enqueueRepeat()
636         }
637 
638         // Subsequent repeats
639         val numRepeats: Int = ((endTime - lastRepeatTime) / SubsequentRepeatDelay).toInt()
640 
641         repeat(numRepeats) {
642             repeatCount += 1
643             lastRepeatTime += SubsequentRepeatDelay
644             currentTime = lastRepeatTime
645             enqueueRepeat()
646         }
647     }
648 
649     /**
650      * Enqueues a key down event on the repeat key, if there is one. If the repeat key is null, an
651      * [IllegalStateException] is thrown.
652      */
653     private fun KeyInputState.enqueueRepeat() {
654         val repKey =
655             checkNotNull(repeatKey) {
656                 "A repeat key event cannot be sent if the repeat key is null."
657             }
658         keyInputState.enqueueDown(repKey)
659     }
660 
661     /**
662      * Sends all enqueued events and blocks while they are dispatched. If an exception is thrown
663      * during the process, all events that haven't yet been dispatched will be dropped.
664      */
665     abstract fun flush()
666 
667     protected abstract fun PartialGesture.enqueueDown(pointerId: Int)
668 
669     protected abstract fun PartialGesture.enqueueMove()
670 
671     protected abstract fun PartialGesture.enqueueMoves(
672         relativeHistoricalTimes: List<Long>,
673         historicalCoordinates: List<List<Offset>>
674     )
675 
676     protected abstract fun PartialGesture.enqueueUp(pointerId: Int)
677 
678     protected abstract fun PartialGesture.enqueueCancel()
679 
680     protected abstract fun MouseInputState.enqueuePress(buttonId: Int)
681 
682     protected abstract fun MouseInputState.enqueueMove()
683 
684     protected abstract fun MouseInputState.enqueueRelease(buttonId: Int)
685 
686     protected abstract fun MouseInputState.enqueueEnter()
687 
688     protected abstract fun MouseInputState.enqueueExit()
689 
690     protected abstract fun MouseInputState.enqueueCancel()
691 
692     protected abstract fun KeyInputState.enqueueDown(key: Key)
693 
694     protected abstract fun KeyInputState.enqueueUp(key: Key)
695 
696     /**
697      * Used to control lock key toggling behaviour on different platforms. Defaults to Android-style
698      * toggling. To change toggling behaviour, override this method and switch to using
699      * [LockKeyState.isLockKeyOnExcludingOffPress], or implement a different toggling behaviour.
700      */
701     protected open val KeyInputState.capsLockOn: Boolean
702         get() = capsLockState.isLockKeyOnIncludingOffPress
703 
704     /**
705      * Used to control lock key toggling behaviour on different platforms. Defaults to Android-style
706      * toggling. To change toggling behaviour, override this method and switch to using
707      * [LockKeyState.isLockKeyOnExcludingOffPress], or implement a different toggling behaviour.
708      */
709     protected open val KeyInputState.numLockOn: Boolean
710         get() = numLockState.isLockKeyOnIncludingOffPress
711 
712     /**
713      * Used to control lock key toggling behaviour on different platforms. Defaults to Android-style
714      * toggling. To change toggling behaviour, override this method and switch to using
715      * [LockKeyState.isLockKeyOnExcludingOffPress], or implement a different toggling behaviour.
716      */
717     protected open val KeyInputState.scrollLockOn: Boolean
718         get() = scrollLockState.isLockKeyOnIncludingOffPress
719 
720     protected abstract fun MouseInputState.enqueueScroll(delta: Float, scrollWheel: ScrollWheel)
721 
722     protected abstract fun RotaryInputState.enqueueRotaryScrollHorizontally(
723         horizontalScrollPixels: Float
724     )
725 
726     protected abstract fun RotaryInputState.enqueueRotaryScrollVertically(
727         verticalScrollPixels: Float
728     )
729 
730     /**
731      * Called when this [InputDispatcher] is about to be discarded, from
732      * [MultiModalInjectionScopeImpl.dispose].
733      */
734     fun dispose() {
735         saveState(root)
736         onDispose()
737     }
738 
739     /**
740      * Override this method to take platform specific action when this dispatcher is disposed. E.g.
741      * to recycle event objects that the dispatcher still holds on to.
742      */
743     protected open fun onDispose() {}
744 }
745 
746 /**
747  * The state of the current gesture. Contains the current position of all pointers and the down time
748  * (start time) of the gesture. For the current time, see [InputDispatcher.currentTime].
749  *
750  * @param downTime The time of the first down event of this gesture
751  * @param startPosition The position of the first down event of this gesture
752  * @param pointerId The pointer id of the first down event of this gesture
753  */
754 internal class PartialGesture(val downTime: Long, startPosition: Offset, pointerId: Int) {
755     val lastPositions = mutableMapOf(Pair(pointerId, startPosition))
756     var hasPointerUpdates: Boolean = false
757 }
758 
759 /**
760  * The current mouse state. Contains the current mouse position, which buttons are pressed, if it is
761  * hovering over the current node and the down time of the mouse (which is the time of the last
762  * mouse down event).
763  */
764 internal class MouseInputState {
765     var downTime: Long = 0
766     val pressedButtons: MutableSet<Int> = mutableSetOf()
767     var lastPosition: Offset = Offset.Zero
768     var isEntered: Boolean = false
769 
770     val hasAnyButtonPressed
771         get() = pressedButtons.isNotEmpty()
772 
773     val hasOneButtonPressed
774         get() = pressedButtons.size == 1
775 
776     val hasNoButtonsPressed
777         get() = pressedButtons.isEmpty()
778 
isButtonPressednull779     fun isButtonPressed(buttonId: Int): Boolean {
780         return pressedButtons.contains(buttonId)
781     }
782 
setButtonBitnull783     fun setButtonBit(buttonId: Int) {
784         pressedButtons.add(buttonId)
785     }
786 
unsetButtonBitnull787     fun unsetButtonBit(buttonId: Int) {
788         pressedButtons.remove(buttonId)
789     }
790 
clearButtonStatenull791     fun clearButtonState() {
792         pressedButtons.clear()
793     }
794 }
795 
796 /**
797  * Toggling states for lock keys.
798  *
799  * Note that lock keys may not be toggled in the same way across all platforms.
800  *
801  * Take caps lock as an example; consistently, all platforms turn caps lock on upon the first key
802  * down event, and it stays on after the subsequent key up. However, on some platforms caps lock
803  * will turn off immediately upon the next key down event (MacOS for example), whereas other
804  * platforms (e.g. Linux, Android) wait for the next key up event before turning caps lock off.
805  *
806  * This enum breaks the lock key state down into four possible options - depending upon the
807  * interpretation of these four states, Android-like or MacOS-like behaviour can both be achieved.
808  *
809  * To get Android-like behaviour, use [isLockKeyOnIncludingOffPress], whereas for MacOS-style
810  * behaviour, use [isLockKeyOnExcludingOffPress].
811  */
812 internal enum class LockKeyState(val state: Int) {
813     UP_AND_OFF(0),
814     DOWN_AND_ON(1),
815     UP_AND_ON(2),
816     DOWN_AND_OPTIONAL(3);
817 
818     /**
819      * Whether or not the lock key is on. The lock key is considered on from the start of the "on
820      * press" until the end of the "off press", i.e. from the first key down event to the second key
821      * up event of the corresponding lock key.
822      */
823     val isLockKeyOnIncludingOffPress
824         get() = state > 0
825 
826     /**
827      * Whether or not the lock key is on. The lock key is considered on from the start of the "on
828      * press" until the start of the "off press", i.e. from the first key down event to the second
829      * key down event of the corresponding lock key.
830      */
831     val isLockKeyOnExcludingOffPress
832         get() = this == DOWN_AND_ON || this == UP_AND_ON
833 
834     /** Returns the next state in the cycle of lock key states. */
nextnull835     fun next(): LockKeyState {
836         return when (this) {
837             UP_AND_OFF -> DOWN_AND_ON
838             DOWN_AND_ON -> UP_AND_ON
839             UP_AND_ON -> DOWN_AND_OPTIONAL
840             DOWN_AND_OPTIONAL -> UP_AND_OFF
841         }
842     }
843 }
844 
845 /**
846  * The current key input state. Contains the keys that are pressed, the down time of the keyboard
847  * (which is the time of the last key down event), the state of the lock keys and the device ID.
848  */
849 internal class KeyInputState {
850     private val downKeys: HashSet<Key> = hashSetOf()
851 
852     var downTime = 0L
853     var repeatKey: Key? = null
854     var repeatCount = 0
855     var lastRepeatTime = downTime
856     var capsLockState: LockKeyState = LockKeyState.UP_AND_OFF
857     var numLockState: LockKeyState = LockKeyState.UP_AND_OFF
858     var scrollLockState: LockKeyState = LockKeyState.UP_AND_OFF
859 
isKeyDownnull860     fun isKeyDown(key: Key): Boolean = downKeys.contains(key)
861 
862     fun setKeyUp(key: Key) {
863         downKeys.remove(key)
864         if (key == repeatKey) {
865             repeatKey = null
866             repeatCount = 0
867         }
868         updateLockKeys(key)
869     }
870 
setKeyDownnull871     fun setKeyDown(key: Key) {
872         downKeys.add(key)
873         repeatKey = key
874         repeatCount = 0
875         updateLockKeys(key)
876     }
877 
878     /** Updates lock key state values. */
updateLockKeysnull879     private fun updateLockKeys(key: Key) {
880         when (key) {
881             Key.CapsLock -> capsLockState = capsLockState.next()
882             Key.NumLock -> numLockState = numLockState.next()
883             Key.ScrollLock -> scrollLockState = scrollLockState.next()
884         }
885     }
886 }
887 
888 /**
889  * We don't have any state associated with RotaryInput, but we use a RotaryInputState class for
890  * consistency with the other APIs.
891  */
892 internal class RotaryInputState
893 
894 /**
895  * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored when
896  * the [GestureScope] is recreated.
897  *
898  * @param partialGesture The state of an incomplete gesture. If no gesture was in progress when the
899  *   state of the [InputDispatcher] was saved, this will be `null`.
900  * @param mouseInputState The state of the mouse.
901  * @param keyInputState The state of the keyboard.
902  */
903 internal data class InputDispatcherState(
904     val partialGesture: PartialGesture?,
905     val mouseInputState: MouseInputState,
906     val keyInputState: KeyInputState
907 )
908