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 
17 package androidx.compose.ui.test
18 
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.platform.ViewConfiguration
22 import androidx.compose.ui.semantics.SemanticsNode
23 import androidx.compose.ui.unit.Density
24 import androidx.compose.ui.unit.IntSize
25 import kotlin.math.roundToInt
26 
27 /**
28  * The receiver scope of the multi-modal input injection lambda from [performMultiModalInput].
29  *
30  * [MultiModalInjectionScope] brings together the receiver scopes of all individual modalities,
31  * allowing you to inject gestures that consist of events from different modalities, like touch and
32  * mouse. For each modality, there is a function to which you pass a lambda in which you can inject
33  * events for that modality: currently, we have [touch], [mouse] and [key] functions. See their
34  * respective docs for more information.
35  *
36  * Note that all events generated by the gesture methods are batched together and sent as a whole
37  * after [performMultiModalInput] has executed its code block.
38  *
39  * Example of performing a click via touch input followed by drag and drop via mouse input:
40  *
41  * @sample androidx.compose.ui.test.samples.multiModalInputClickDragDrop
42  * @see InjectionScope
43  * @see TouchInjectionScope
44  * @see MouseInjectionScope
45  * @see KeyInjectionScope
46  * @see RotaryInjectionScope
47  */
48 // TODO(fresen): add better multi modal example when we have key input support
49 sealed interface MultiModalInjectionScope : InjectionScope {
50     /** Injects all touch events sent by the given [block] */
touchnull51     fun touch(block: TouchInjectionScope.() -> Unit)
52 
53     /** Injects all mouse events sent by the given [block] */
54     fun mouse(block: MouseInjectionScope.() -> Unit)
55 
56     /** Injects all key events sent by the given [block] */
57     @ExperimentalTestApi fun key(block: KeyInjectionScope.() -> Unit)
58 
59     /** Injects all rotary events sent by the given [block] */
60     @ExperimentalTestApi fun rotary(block: RotaryInjectionScope.() -> Unit)
61 }
62 
63 internal class MultiModalInjectionScopeImpl(node: SemanticsNode, testContext: TestContext) :
64     MultiModalInjectionScope, Density by node.layoutInfo.density {
65     // TODO(b/133217292): Better error: explain which gesture couldn't be performed
66     private var _semanticsNode: SemanticsNode? = node
67     private val semanticsNode
68         get() =
69             checkNotNull(_semanticsNode) {
70                 "Can't query SemanticsNode, InjectionScope has already been disposed"
71             }
72 
73     // TODO(b/133217292): Better error: explain which gesture couldn't be performed
74     private var _inputDispatcher: InputDispatcher? =
75         createInputDispatcher(
76             testContext,
77             checkNotNull(semanticsNode.root) { "null semantics root" }
78         )
79     internal val inputDispatcher
80         get() =
81             checkNotNull(_inputDispatcher) {
82                 "Can't send gesture, InjectionScope has already been disposed"
83             }
84 
85     /**
86      * Returns and stores the visible bounds of the [semanticsNode] we're interacting with. This
87      * applies clipping, which is almost always the correct thing to do when injecting gestures, as
88      * gestures operate on visible UI.
89      */
90     private val boundsInRoot: Rect by lazy { semanticsNode.boundsInRoot }
91 
92     /**
93      * Returns the size of the visible part of the node we're interacting with. This is contrary to
94      * [SemanticsNode.size], which returns the unclipped size of the node.
95      */
96     override val visibleSize: IntSize by lazy {
97         IntSize(boundsInRoot.width.roundToInt(), boundsInRoot.height.roundToInt())
98     }
99 
100     /**
101      * Transforms the [position] to root coordinates.
102      *
103      * @param position A position in local coordinates
104      * @return [position] transformed to coordinates relative to the containing root.
105      */
106     internal fun localToRoot(position: Offset): Offset {
107         return if (position.isValid()) {
108             position + boundsInRoot.topLeft
109         } else {
110             // Allows invalid position to still pass back through Compose (for testing)
111             position
112         }
113     }
114 
115     internal fun rootToLocal(position: Offset): Offset {
116         return if (position.isValid()) {
117             position - boundsInRoot.topLeft
118         } else {
119             // Allows invalid position to still pass back through Compose (for testing)
120             position
121         }
122     }
123 
124     override val viewConfiguration: ViewConfiguration
125         get() = semanticsNode.layoutInfo.viewConfiguration
126 
127     internal fun dispose() {
128         _semanticsNode = null
129         _inputDispatcher?.also {
130             _inputDispatcher = null
131             try {
132                 it.flush()
133             } finally {
134                 it.dispose()
135             }
136         }
137     }
138 
139     /**
140      * Adds the given [durationMillis] to the current event time, delaying the next event by that
141      * time. Only valid when a gesture has already been started, or when a finished gesture is
142      * resumed.
143      */
144     override fun advanceEventTime(durationMillis: Long) {
145         inputDispatcher.advanceEventTime(durationMillis)
146     }
147 
148     private val touchScope: TouchInjectionScope = TouchInjectionScopeImpl(this)
149 
150     private val mouseScope: MouseInjectionScope = MouseInjectionScopeImpl(this)
151 
152     @ExperimentalTestApi private val keyScope: KeyInjectionScope = KeyInjectionScopeImpl(this)
153 
154     @ExperimentalTestApi
155     private val rotaryScope: RotaryInjectionScope = RotaryInjectionScopeImpl(this)
156 
157     override fun touch(block: TouchInjectionScope.() -> Unit) {
158         block.invoke(touchScope)
159     }
160 
161     override fun mouse(block: MouseInjectionScope.() -> Unit) {
162         block.invoke(mouseScope)
163     }
164 
165     @ExperimentalTestApi
166     override fun key(block: KeyInjectionScope.() -> Unit) {
167         block.invoke(keyScope)
168     }
169 
170     @ExperimentalTestApi
171     override fun rotary(block: RotaryInjectionScope.() -> Unit) {
172         block.invoke(rotaryScope)
173     }
174 }
175