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