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