[go: nahoru, domu]

blob: eb9c39a8cb9c18ce8654443fb3ebef5e37271886 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.platform
import androidx.compose.animation.core.MonotonicFrameAnimationClock
import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.staticAmbientOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.mouse.MouseScrollEvent
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.input.pointer.PointerInputEventData
import androidx.compose.ui.input.pointer.PointerType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import org.jetbrains.skija.Canvas
import java.awt.event.InputMethodEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
internal val DesktopOwnersAmbient = staticAmbientOf<DesktopOwners>()
@OptIn(ExperimentalCoroutinesApi::class)
internal class DesktopOwners(
coroutineScope: CoroutineScope,
component: DesktopComponent = DummyDesktopComponent,
invalidate: () -> Unit = {},
) {
private val _invalidate = invalidate
@Volatile
private var hasPendingDraws = false
private var invalidateScheduled = false
private var willRenderInThisFrame = false
fun invalidate() {
if (!willRenderInThisFrame) {
invalidateScheduled = true
hasPendingDraws = true
_invalidate()
}
}
val list = LinkedHashSet<DesktopOwner>()
var keyboard: Keyboard? = null
private var pointerId = 0L
private var isMousePressed = false
private val dispatcher = FlushCoroutineDispatcher(coroutineScope)
private val frameClock = BroadcastFrameClock(::invalidate)
private val coroutineContext = dispatcher + frameClock
internal val animationClock = MonotonicFrameAnimationClock(
CoroutineScope(coroutineScope.coroutineContext + coroutineContext)
)
internal val recomposer = Recomposer(coroutineContext)
internal val platformInputService: DesktopPlatformInput = DesktopPlatformInput(component)
init {
// TODO(demin): Experimental API (CoroutineStart.UNDISPATCHED).
// Decide what to do before release (copy paste or use different approach).
coroutineScope.launch(coroutineContext, start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
}
/**
* Returns true if there are pending recompositions, draws or dispatched tasks.
* Can be called from any thread.
*/
fun hasInvalidations() = hasPendingDraws ||
recomposer.hasPendingWork ||
dispatcher.hasTasks()
fun register(desktopOwner: DesktopOwner) {
list.add(desktopOwner)
invalidate()
}
fun unregister(desktopOwner: DesktopOwner) {
list.remove(desktopOwner)
invalidate()
}
fun onFrame(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
invalidateScheduled = false
willRenderInThisFrame = true
try {
// We must see the actual state before we will render the frame
Snapshot.sendApplyNotifications()
dispatcher.flush()
frameClock.sendFrame(nanoTime)
for (owner in list) {
owner.setSize(width, height)
owner.measureAndLayout()
}
} finally {
willRenderInThisFrame = false
}
for (owner in list) {
owner.draw(canvas)
}
if (frameClock.hasAwaiters) {
_invalidate()
}
if (!invalidateScheduled) {
hasPendingDraws = false
}
}
val lastOwner: DesktopOwner?
get() = list.lastOrNull()
fun onMousePressed(x: Int, y: Int, nativeEvent: MouseEvent? = null) {
isMousePressed = true
lastOwner?.processPointerInput(pointerInputEvent(nativeEvent, x, y, isMousePressed))
}
fun onMouseReleased(x: Int, y: Int, nativeEvent: MouseEvent? = null) {
isMousePressed = false
lastOwner?.processPointerInput(pointerInputEvent(nativeEvent, x, y, isMousePressed))
pointerId += 1
}
fun onMouseDragged(x: Int, y: Int, nativeEvent: MouseEvent? = null) {
lastOwner?.processPointerInput(pointerInputEvent(nativeEvent, x, y, isMousePressed))
}
fun onMouseScroll(x: Int, y: Int, event: MouseScrollEvent) {
val position = Offset(x.toFloat(), y.toFloat())
lastOwner?.onMouseScroll(position, event)
}
fun onMouseMoved(x: Int, y: Int) {
val position = Offset(x.toFloat(), y.toFloat())
lastOwner?.onPointerMove(position)
}
private fun consumeKeyEventOr(event: KeyEvent, or: () -> Unit) {
val consumed = list.lastOrNull()?.sendKeyEvent(ComposeKeyEvent(event)) ?: false
if (!consumed) {
or()
}
}
fun onKeyPressed(event: KeyEvent) = consumeKeyEventOr(event) {
platformInputService.onKeyPressed(event.keyCode, event.keyChar)
}
fun onKeyReleased(event: KeyEvent) = consumeKeyEventOr(event) {
platformInputService.onKeyReleased(event.keyCode, event.keyChar)
}
fun onKeyTyped(event: KeyEvent) = consumeKeyEventOr(event) {
platformInputService.onKeyTyped(event.keyChar)
}
fun onInputMethodEvent(event: InputMethodEvent) {
if (!event.isConsumed()) {
when (event.id) {
InputMethodEvent.INPUT_METHOD_TEXT_CHANGED -> {
platformInputService.replaceInputMethodText(event)
event.consume()
}
InputMethodEvent.CARET_POSITION_CHANGED -> {
platformInputService.inputMethodCaretPositionChanged(event)
event.consume()
}
}
}
}
private fun pointerInputEvent(
nativeEvent: MouseEvent?,
x: Int,
y: Int,
down: Boolean
): PointerInputEvent {
val time = System.nanoTime() / 1_000_000L
return PointerInputEvent(
time,
listOf(
PointerInputEventData(
PointerId(pointerId),
time,
Offset(x.toFloat(), y.toFloat()),
down,
PointerType.Mouse
)
),
nativeEvent
)
}
}