Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2021 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 |
| 17 | |
| 18 | import androidx.compose.runtime.BroadcastFrameClock |
| 19 | import androidx.compose.runtime.Composable |
| 20 | import androidx.compose.runtime.Composition |
| 21 | import androidx.compose.runtime.CompositionContext |
| 22 | import androidx.compose.runtime.CompositionLocalProvider |
| 23 | import androidx.compose.runtime.LaunchedEffect |
| 24 | import androidx.compose.runtime.Recomposer |
| 25 | import androidx.compose.runtime.rememberCoroutineScope |
| 26 | import androidx.compose.runtime.snapshots.Snapshot |
| 27 | import androidx.compose.runtime.staticCompositionLocalOf |
| 28 | import androidx.compose.runtime.withFrameNanos |
| 29 | import androidx.compose.ui.geometry.Offset |
| 30 | import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 31 | import androidx.compose.ui.input.pointer.PointerEventType |
| 32 | import androidx.compose.ui.input.pointer.PointerInputEvent |
| 33 | import androidx.compose.ui.input.pointer.PointerType |
| 34 | import androidx.compose.ui.node.LayoutNode |
| 35 | import androidx.compose.ui.platform.PlatformComponent |
| 36 | import androidx.compose.ui.platform.SkiaBasedOwner |
| 37 | import androidx.compose.ui.platform.PlatformInput |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 38 | import androidx.compose.ui.platform.DummyPlatformComponent |
| 39 | import androidx.compose.ui.platform.EmptyDispatcher |
| 40 | import androidx.compose.ui.platform.FlushCoroutineDispatcher |
| 41 | import androidx.compose.ui.platform.GlobalSnapshotManager |
| 42 | import androidx.compose.ui.platform.setContent |
| 43 | import androidx.compose.ui.node.RootForTest |
| 44 | import androidx.compose.ui.unit.Constraints |
| 45 | import androidx.compose.ui.unit.Density |
| 46 | import androidx.compose.ui.unit.IntSize |
| 47 | import androidx.compose.ui.unit.dp |
| 48 | import kotlinx.coroutines.CoroutineScope |
| 49 | import kotlinx.coroutines.CoroutineStart |
| 50 | import kotlinx.coroutines.Job |
| 51 | import kotlinx.coroutines.launch |
| 52 | import org.jetbrains.skia.Canvas |
| 53 | import kotlin.coroutines.CoroutineContext |
| 54 | import kotlin.jvm.Volatile |
| 55 | |
| 56 | internal val LocalComposeScene = staticCompositionLocalOf<ComposeScene> { |
| 57 | error("CompositionLocal LocalComposeScene not provided") |
| 58 | } |
| 59 | |
| 60 | /** |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 61 | * A virtual container that encapsulates Compose UI content. UI content can be constructed via |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 62 | * [setContent] method and with any Composable that manipulates [LayoutNode] tree. |
| 63 | * |
| 64 | * To draw content on [Canvas], you can use [render] method. |
| 65 | * |
| 66 | * To specify available size for the content, you should use [constraints]. |
| 67 | * |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 68 | * After [ComposeScene] will no longer needed, you should call [close] method, so all resources |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 69 | * and subscriptions will be properly closed. Otherwise there can be a memory leak. |
| 70 | */ |
| 71 | class ComposeScene internal constructor( |
| 72 | coroutineContext: CoroutineContext, |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 73 | internal val component: PlatformComponent, |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 74 | density: Density, |
| 75 | private val invalidate: () -> Unit |
| 76 | ) { |
| 77 | /** |
| 78 | * Constructs [ComposeScene] |
| 79 | * |
| 80 | * @param coroutineContext Context which will be used to launch effects ([LaunchedEffect], |
| 81 | * [rememberCoroutineScope]) and run recompositions. |
| 82 | * @param density Initial density of the content which will be used to convert [dp] units. |
| 83 | * @param invalidate Callback which will be called when the content need to be recomposed or |
| 84 | * rerendered. If you draw your content using [render] method, in this callback you should |
| 85 | * schedule the next [render] in your rendering loop. |
| 86 | */ |
| 87 | constructor( |
| 88 | coroutineContext: CoroutineContext = EmptyDispatcher, |
| 89 | density: Density = Density(1f), |
| 90 | invalidate: () -> Unit = {} |
| 91 | ) : this( |
| 92 | coroutineContext, |
| 93 | DummyPlatformComponent, |
| 94 | density, |
| 95 | invalidate |
| 96 | ) |
| 97 | |
| 98 | private var isInvalidationDisabled = false |
| 99 | |
| 100 | @Volatile |
| 101 | private var hasPendingDraws = true |
Igor Demin | d59b883 | 2021-10-14 20:07:07 +0300 | [diff] [blame] | 102 | private inline fun <T> postponeInvalidation(block: () -> T): T { |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 103 | isInvalidationDisabled = true |
Igor Demin | d59b883 | 2021-10-14 20:07:07 +0300 | [diff] [blame] | 104 | val result = try { |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 105 | block() |
| 106 | } finally { |
| 107 | isInvalidationDisabled = false |
| 108 | } |
| 109 | invalidateIfNeeded() |
Igor Demin | d59b883 | 2021-10-14 20:07:07 +0300 | [diff] [blame] | 110 | return result |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 111 | } |
| 112 | |
| 113 | private fun invalidateIfNeeded() { |
Igor Demin | d59b883 | 2021-10-14 20:07:07 +0300 | [diff] [blame] | 114 | hasPendingDraws = frameClock.hasAwaiters || list.any(SkiaBasedOwner::needRender) |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 115 | if (hasPendingDraws && !isInvalidationDisabled) { |
| 116 | invalidate() |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | private val list = LinkedHashSet<SkiaBasedOwner>() |
| 121 | private val listCopy = mutableListOf<SkiaBasedOwner>() |
| 122 | |
| 123 | private inline fun forEachOwner(action: (SkiaBasedOwner) -> Unit) { |
| 124 | listCopy.addAll(list) |
| 125 | listCopy.forEach(action) |
| 126 | listCopy.clear() |
| 127 | } |
| 128 | |
| 129 | /** |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 130 | * All currently registered [RootForTest]s. After calling [setContent] the first root |
| 131 | * will be added. If there is an any [Popup] is present in the content, it will be added as |
| 132 | * another [RootForTest] |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 133 | */ |
| 134 | val roots: Set<RootForTest> get() = list |
| 135 | |
| 136 | private var pointerId = 0L |
| 137 | private var isMousePressed = false |
| 138 | |
| 139 | private val job = Job() |
| 140 | private val dispatcher = FlushCoroutineDispatcher(CoroutineScope(coroutineContext + job)) |
| 141 | private val frameClock = BroadcastFrameClock(onNewAwaiters = ::invalidateIfNeeded) |
| 142 | private val coroutineScope = CoroutineScope(coroutineContext + job + dispatcher + frameClock) |
| 143 | |
| 144 | private val recomposer = Recomposer(coroutineScope.coroutineContext) |
| 145 | internal val platformInputService: PlatformInput = PlatformInput(component) |
| 146 | |
| 147 | private var mainOwner: SkiaBasedOwner? = null |
| 148 | private var composition: Composition? = null |
| 149 | |
| 150 | /** |
| 151 | * Density of the content which will be used to convert [dp] units. |
| 152 | */ |
| 153 | var density: Density = density |
| 154 | set(value) { |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 155 | check(!isClosed) { "ComposeScene is closed" } |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 156 | field = value |
| 157 | mainOwner?.density = value |
| 158 | } |
| 159 | |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 160 | private var isClosed = false |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 161 | |
| 162 | init { |
| 163 | GlobalSnapshotManager.ensureStarted() |
| 164 | coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { |
| 165 | recomposer.runRecomposeAndApplyChanges() |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * Close all resources and subscriptions. Not calling this method when [ComposeScene] is no |
| 171 | * longer needed will cause a memory leak. |
| 172 | * |
| 173 | * All effects launched via [LaunchedEffect] or [rememberCoroutineScope] will be cancelled |
| 174 | * (but not immediately). |
| 175 | * |
| 176 | * After calling this method, you cannot call any other method of this [ComposeScene]. |
| 177 | */ |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 178 | fun close() { |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 179 | composition?.dispose() |
| 180 | mainOwner?.dispose() |
| 181 | recomposer.cancel() |
| 182 | job.cancel() |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 183 | isClosed = true |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 184 | } |
| 185 | |
| 186 | private fun dispatchCommand(command: () -> Unit) { |
| 187 | coroutineScope.launch { |
| 188 | command() |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Returns true if there are pending recompositions, renders or dispatched tasks. |
| 194 | * Can be called from any thread. |
| 195 | */ |
| 196 | fun hasInvalidations() = hasPendingDraws || |
| 197 | recomposer.hasPendingWork || |
| 198 | dispatcher.hasTasks() |
| 199 | |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 200 | internal fun attach(owner: SkiaBasedOwner) { |
| 201 | check(!isClosed) { "ComposeScene is closed" } |
| 202 | list.add(owner) |
| 203 | owner.onNeedRender = ::invalidateIfNeeded |
| 204 | owner.onDispatchCommand = ::dispatchCommand |
| 205 | owner.constraints = constraints |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 206 | invalidateIfNeeded() |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 207 | if (owner.isFocusable) { |
| 208 | focusedOwner = owner |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 209 | } |
| 210 | } |
| 211 | |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 212 | internal fun detach(owner: SkiaBasedOwner) { |
| 213 | check(!isClosed) { "ComposeScene is closed" } |
| 214 | list.remove(owner) |
| 215 | owner.onDispatchCommand = null |
| 216 | owner.onNeedRender = null |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 217 | invalidateIfNeeded() |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 218 | if (owner == focusedOwner) { |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 219 | focusedOwner = list.lastOrNull { it.isFocusable } |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Update the composition with the content described by the [content] composable. After this |
| 225 | * has been called the changes to produce the initial composition has been calculated and |
| 226 | * applied to the composition. |
| 227 | * |
| 228 | * Will throw an [IllegalStateException] if the composition has been disposed. |
| 229 | * |
| 230 | * @param content Content of the [ComposeScene] |
| 231 | */ |
| 232 | fun setContent( |
| 233 | content: @Composable () -> Unit |
| 234 | ) = setContent( |
| 235 | parentComposition = null, |
| 236 | content = content |
| 237 | ) |
| 238 | |
| 239 | // TODO(demin): We should configure routing of key events if there |
| 240 | // are any popups/root present: |
| 241 | // - ComposeScene.sendKeyEvent |
| 242 | // - ComposeScene.onPreviewKeyEvent (or Window.onPreviewKeyEvent) |
| 243 | // - Popup.onPreviewKeyEvent |
| 244 | // - NestedPopup.onPreviewKeyEvent |
| 245 | // - NestedPopup.onKeyEvent |
| 246 | // - Popup.onKeyEvent |
| 247 | // - ComposeScene.onKeyEvent |
| 248 | // Currently we have this routing: |
| 249 | // - [active Popup or the main content].onPreviewKeyEvent |
| 250 | // - [active Popup or the main content].onKeyEvent |
| 251 | // After we change routing, we can remove onPreviewKeyEvent/onKeyEvent from this method |
| 252 | internal fun setContent( |
| 253 | parentComposition: CompositionContext? = null, |
| 254 | onPreviewKeyEvent: (ComposeKeyEvent) -> Boolean = { false }, |
| 255 | onKeyEvent: (ComposeKeyEvent) -> Boolean = { false }, |
| 256 | content: @Composable () -> Unit |
| 257 | ) { |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 258 | check(!isClosed) { "ComposeScene is closed" } |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 259 | composition?.dispose() |
| 260 | mainOwner?.dispose() |
| 261 | val mainOwner = SkiaBasedOwner( |
| 262 | platformInputService, |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 263 | component, |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 264 | density, |
| 265 | onPreviewKeyEvent = onPreviewKeyEvent, |
| 266 | onKeyEvent = onKeyEvent |
| 267 | ) |
| 268 | attach(mainOwner) |
| 269 | composition = mainOwner.setContent(parentComposition ?: recomposer) { |
| 270 | CompositionLocalProvider( |
| 271 | LocalComposeScene provides this, |
| 272 | content = content |
| 273 | ) |
| 274 | } |
| 275 | this.mainOwner = mainOwner |
| 276 | |
| 277 | // to perform all pending work synchronously. to start LaunchedEffect for example |
| 278 | dispatcher.flush() |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Set constraints, which will be used to measure and layout content. |
| 283 | */ |
| 284 | var constraints: Constraints = Constraints() |
| 285 | set(value) { |
| 286 | field = value |
| 287 | forEachOwner { |
| 288 | it.constraints = constraints |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * Returns the current content size |
| 294 | */ |
| 295 | val contentSize: IntSize |
| 296 | get() { |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 297 | check(!isClosed) { "ComposeScene is closed" } |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 298 | val mainOwner = mainOwner ?: return IntSize.Zero |
| 299 | mainOwner.measureAndLayout() |
| 300 | return IntSize(mainOwner.root.width, mainOwner.root.height) |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Render the current content on [canvas]. Passed [nanoTime] will be used to drive all |
| 305 | * animations in the content (or any other code, which uses [withFrameNanos] |
| 306 | */ |
| 307 | fun render(canvas: Canvas, nanoTime: Long) { |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 308 | check(!isClosed) { "ComposeScene is closed" } |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 309 | postponeInvalidation { |
| 310 | // TODO(https://github.com/JetBrains/compose-jb/issues/1135): |
| 311 | // Temporarily workaround for flaky tests in WithComposeUiTest. |
| 312 | // It fails when we remove synchronized and run: |
| 313 | // ./gradlew desktopTest -Pandroidx.compose.multiplatformEnabled=true |
| 314 | // We should make a minimal reproducer, and fix race condition somewhere |
| 315 | // else, not here. |
| 316 | // See also GlobalSnapshotManager. |
| 317 | synchronized(Snapshot.current) { |
| 318 | // We must see the actual state before we will render the frame |
| 319 | Snapshot.sendApplyNotifications() |
| 320 | dispatcher.flush() |
| 321 | frameClock.sendFrame(nanoTime) |
| 322 | } |
| 323 | |
| 324 | forEachOwner { |
| 325 | it.render(canvas) |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | private var focusedOwner: SkiaBasedOwner? = null |
| 331 | private val hoveredOwner: SkiaBasedOwner? |
| 332 | get() = list.lastOrNull { it.isHovered(pointLocation) } ?: list.lastOrNull() |
| 333 | |
| 334 | private fun SkiaBasedOwner?.isAbove( |
| 335 | targetOwner: SkiaBasedOwner? |
| 336 | ) = list.indexOf(this) > list.indexOf(targetOwner) |
| 337 | |
| 338 | // TODO(demin): return Boolean (when it is consumed). |
| 339 | // see ComposeLayer todo about AWTDebounceEventQueue |
| 340 | /** |
| 341 | * Send pointer event to the content. |
| 342 | * |
| 343 | * @param eventType Indicates the primary reason that the event was sent. |
| 344 | * @param position The [Offset] of the current pointer event, relative to the content. |
Igor Demin | 7b79943 | 2021-10-20 16:43:36 +0300 | [diff] [blame] | 345 | * @param scrollDelta scroll delta for the PointerEventType.Scroll event |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 346 | * @param timeMillis The time of the current pointer event, in milliseconds. The start (`0`) time |
| 347 | * is platform-dependent. |
| 348 | * @param type The device type that produced the event, such as [mouse][PointerType.Mouse], |
| 349 | * or [touch][PointerType.Touch]. |
| 350 | * @param nativeEvent The original native event. |
| 351 | */ |
| 352 | @OptIn(ExperimentalComposeUiApi::class) |
| 353 | fun sendPointerEvent( |
| 354 | eventType: PointerEventType, |
| 355 | position: Offset, |
Igor Demin | 7b79943 | 2021-10-20 16:43:36 +0300 | [diff] [blame] | 356 | scrollDelta: Offset = Offset(0f, 0f), |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 357 | timeMillis: Long = System.nanoTime() / 1_000_000L, |
| 358 | type: PointerType = PointerType.Mouse, |
| 359 | nativeEvent: Any? = null, |
| 360 | // TODO(demin): support PointerButtons, PointerKeyboardModifiers |
| 361 | // buttons: PointerButtons? = null, |
| 362 | // keyboardModifiers: PointerKeyboardModifiers? = null, |
Igor Demin | d59b883 | 2021-10-14 20:07:07 +0300 | [diff] [blame] | 363 | ): Unit = postponeInvalidation { |
Igor Demin | 5c3047f | 2022-01-18 14:28:53 +0300 | [diff] [blame] | 364 | check(!isClosed) { "ComposeScene is closed" } |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 365 | when (eventType) { |
| 366 | PointerEventType.Press -> isMousePressed = true |
| 367 | PointerEventType.Release -> isMousePressed = false |
| 368 | } |
| 369 | val event = pointerInputEvent( |
Igor Demin | 7b79943 | 2021-10-20 16:43:36 +0300 | [diff] [blame] | 370 | eventType, |
| 371 | position, |
| 372 | timeMillis, |
| 373 | nativeEvent, |
| 374 | type, |
| 375 | isMousePressed, |
| 376 | pointerId, |
| 377 | scrollDelta |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 378 | ) |
| 379 | when (eventType) { |
| 380 | PointerEventType.Press -> onMousePressed(event) |
| 381 | PointerEventType.Release -> onMouseReleased(event) |
| 382 | PointerEventType.Move -> { |
| 383 | pointLocation = position |
| 384 | hoveredOwner?.processPointerInput(event) |
| 385 | } |
| 386 | PointerEventType.Enter -> hoveredOwner?.processPointerInput(event) |
| 387 | PointerEventType.Exit -> hoveredOwner?.processPointerInput(event) |
Igor Demin | 7b79943 | 2021-10-20 16:43:36 +0300 | [diff] [blame] | 388 | PointerEventType.Scroll -> hoveredOwner?.processPointerInput(event) |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 389 | } |
| 390 | } |
| 391 | |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 392 | private fun onMousePressed(event: PointerInputEvent) { |
| 393 | val currentOwner = hoveredOwner |
| 394 | if (currentOwner != null) { |
| 395 | if (focusedOwner.isAbove(currentOwner)) { |
| 396 | focusedOwner?.onDismissRequest?.invoke() |
| 397 | } else { |
| 398 | currentOwner.processPointerInput(event) |
| 399 | } |
| 400 | } else { |
| 401 | focusedOwner?.processPointerInput(event) |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | private fun onMouseReleased(event: PointerInputEvent) { |
| 406 | val owner = hoveredOwner ?: focusedOwner |
| 407 | owner?.processPointerInput(event) |
| 408 | pointerId += 1 |
| 409 | } |
| 410 | |
| 411 | private var pointLocation = Offset.Zero |
| 412 | |
| 413 | /** |
| 414 | * Send [KeyEvent] to the content. |
| 415 | * @return true if the event was consumed by the content |
| 416 | */ |
Igor Demin | d59b883 | 2021-10-14 20:07:07 +0300 | [diff] [blame] | 417 | fun sendKeyEvent(event: ComposeKeyEvent): Boolean = postponeInvalidation { |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 418 | return focusedOwner?.sendKeyEvent(event) == true |
| 419 | } |
| 420 | |
| 421 | internal fun onInputMethodEvent(event: Any) = this.onPlatformInputMethodEvent(event) |
| 422 | } |
| 423 | |
| 424 | internal expect fun ComposeScene.onPlatformInputMethodEvent(event: Any) |
| 425 | |
| 426 | internal expect fun pointerInputEvent( |
| 427 | eventType: PointerEventType, |
| 428 | position: Offset, |
| 429 | timeMillis: Long, |
| 430 | nativeEvent: Any?, |
| 431 | type: PointerType, |
| 432 | isMousePressed: Boolean, |
Igor Demin | 7b79943 | 2021-10-20 16:43:36 +0300 | [diff] [blame] | 433 | pointerId: Long, |
| 434 | scrollDelta: Offset |
Alexander Gorshenev | 4b920e6 | 2021-10-13 15:59:48 +0300 | [diff] [blame] | 435 | ): PointerInputEvent |