blob: dc830200a271ab20e017567be28990eac9a7582f [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.
*/
@file:Suppress("DEPRECATION")
package androidx.compose.ui.platform
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.DefaultPointerButtons
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.PrimaryPressedPointerButtons
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.draganddrop.DragAndDropManager
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusDirection.Companion.Enter
import androidx.compose.ui.focus.FocusDirection.Companion.Exit
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.focus.FocusOwnerImpl
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.GraphicsContext
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.layer.GraphicsContext
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.input.InputMode.Companion.Keyboard
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.InputModeManagerImpl
import androidx.compose.ui.input.key.Key.Companion.Back
import androidx.compose.ui.input.key.Key.Companion.DirectionCenter
import androidx.compose.ui.input.key.Key.Companion.Tab
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.PointerIconService
import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.input.pointer.PointerInputEventProcessor
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.input.pointer.PositionCalculator
import androidx.compose.ui.input.pointer.ProcessResult
import androidx.compose.ui.input.pointer.TestPointerInputEventData
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.PlacementScope
import androidx.compose.ui.layout.RootMeasurePolicy
import androidx.compose.ui.modifier.ModifierLocalManager
import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNodeDrawScope
import androidx.compose.ui.node.MeasureAndLayoutDelegate
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.EmptySemanticsElement
import androidx.compose.ui.semantics.EmptySemanticsModifier
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.text.platform.FontLoader
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import kotlin.coroutines.CoroutineContext
private typealias Command = () -> Unit
@OptIn(
ExperimentalComposeUiApi::class,
InternalCoreApi::class,
InternalComposeUiApi::class
)
internal class SkiaBasedOwner(
private val platformInputService: PlatformInput,
private val component: PlatformComponent,
density: Density = Density(1f, 1f),
override val coroutineContext: CoroutineContext,
val isPopup: Boolean = false,
val isFocusable: Boolean = true,
val onDismissRequest: (() -> Unit)? = null,
private val onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
private val onKeyEvent: (KeyEvent) -> Boolean = { false },
) : Owner, RootForTest, SkiaRootForTest, PositionCalculator {
internal fun isHovered(point: Offset): Boolean {
val intOffset = IntOffset(point.x.toInt(), point.y.toInt())
return bounds.contains(intOffset)
}
internal var accessibilityController: AccessibilityController? = null
internal var bounds by mutableStateOf(IntRect.Zero)
override var density by mutableStateOf(density)
// TODO(demin): support RTL
override val layoutDirection: LayoutDirection = LayoutDirection.Ltr
override val sharedDrawScope = LayoutNodeDrawScope()
private val rootSemanticsNode = EmptySemanticsModifier()
private val semanticsModifier = EmptySemanticsElement(rootSemanticsNode)
override val focusOwner: FocusOwner = FocusOwnerImpl(
onRequestApplyChangesListener = ::registerOnEndApplyChangesListener,
onRequestFocusForOwner = { _, _ -> true }, // TODO request focus from framework.
onMoveFocusInterop = { _ -> true },
onClearFocusForOwner = {}, // TODO clear focus from framework.
onFocusRectInterop = { null },
onLayoutDirection = { layoutDirection } // TODO(demin): RTL [onRtlPropertiesChanged].
)
// TODO: Set the input mode. For now we don't support touch mode, (always in Key mode).
private val _inputModeManager = InputModeManagerImpl(
initialInputMode = Keyboard,
onRequestInputModeChange = {
// TODO: Change the input mode programmatically. For now we just return true if the
// requested input mode is Keyboard mode.
it == Keyboard
}
)
override val inputModeManager: InputModeManager
get() = _inputModeManager
override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
// TODO: set/clear _windowInfo.isWindowFocused when the window gains/loses focus.
private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
override val windowInfo: WindowInfo
get() = _windowInfo
// TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
// that this common logic can be used by all owners.
private val keyInputModifier = Modifier.onKeyEvent {
val focusDirection = getFocusDirection(it)
if (focusDirection == null || it.type != KeyDown) return@onKeyEvent false
// Consume the key event if we moved focus.
focusOwner.moveFocus(focusDirection)
}
@Suppress("unused") // to be used in JB fork (not all prerequisite changes added yet)
internal fun setCurrentKeyboardModifiers(modifiers: PointerKeyboardModifiers) {
_windowInfo.keyboardModifiers = modifiers
}
var constraints: Constraints = Constraints()
set(value) {
field = value
if (!isPopup) {
this.bounds = IntRect(
IntOffset(bounds.left, bounds.top),
IntSize(constraints.maxWidth, constraints.maxHeight)
)
}
}
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
it.modifier = semanticsModifier
.then(focusOwner.modifier)
.then(keyInputModifier)
.onPreviewKeyEvent(onPreviewKeyEvent)
.onKeyEvent(onKeyEvent)
}
override val rootForTest = this
override val snapshotObserver = OwnerSnapshotObserver { command ->
onDispatchCommand?.invoke(command)
}
private val pointerInputEventProcessor = PointerInputEventProcessor(root)
private val measureAndLayoutDelegate = MeasureAndLayoutDelegate(root)
private val endApplyChangesListeners = mutableVectorOf<(() -> Unit)?>()
init {
snapshotObserver.startObserving()
root.attach(this)
focusOwner.focusTransactionManager.withNewTransaction {
// TODO instead of taking focus here, call this when the owner gets focused.
focusOwner.takeFocus(Enter, previouslyFocusedRect = null)
}
}
fun dispose() {
snapshotObserver.stopObserving()
// we don't need to call root.detach() because root will be garbage collected
}
override val textInputService = TextInputService(platformInputService)
override val softwareKeyboardController: SoftwareKeyboardController =
DelegatingSoftwareKeyboardController(textInputService)
@Deprecated(
"fontLoader is deprecated, use fontFamilyResolver",
replaceWith = ReplaceWith("fontFamilyResolver")
)
override val fontLoader = FontLoader()
override val fontFamilyResolver = createFontFamilyResolver()
override val hapticFeedBack = DefaultHapticFeedback()
override val clipboardManager = PlatformClipboardManager()
override val accessibilityManager = DefaultAccessibilityManager()
override val graphicsContext: GraphicsContext = GraphicsContext()
override val textToolbar = DefaultTextToolbar()
override val semanticsOwner: SemanticsOwner = SemanticsOwner(root, rootSemanticsNode)
override val dragAndDropManager: DragAndDropManager get() = TODO("Not yet implemented")
override val autofillTree = AutofillTree()
override val autofill: Autofill? get() = null
override val viewConfiguration: ViewConfiguration = DefaultViewConfiguration(density)
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean =
sendKeyEvent(platformInputService, focusOwner, keyEvent)
override var showLayoutBounds = false
override fun requestFocus() = true
override fun onAttach(node: LayoutNode) = Unit
override fun onDetach(node: LayoutNode) {
measureAndLayoutDelegate.onNodeDetached(node)
snapshotObserver.clear(node)
needClearObservations = true
}
override val measureIteration: Long get() = measureAndLayoutDelegate.measureIteration
private var needLayout = true
private var needDraw = true
val needRender get() = needLayout || needDraw || needSendSyntheticEvents
var onNeedRender: (() -> Unit)? = null
var onDispatchCommand: ((Command) -> Unit)? = null
fun render(canvas: org.jetbrains.skia.Canvas) {
needLayout = false
measureAndLayout()
sendSyntheticEvents()
needDraw = false
draw(canvas)
clearInvalidObservations()
}
private var needClearObservations = false
private fun clearInvalidObservations() {
if (needClearObservations) {
snapshotObserver.clearInvalidObservations()
needClearObservations = false
}
}
private fun requestLayout() {
needLayout = true
needDraw = true
onNeedRender?.invoke()
}
private fun requestDraw() {
needDraw = true
onNeedRender?.invoke()
}
var contentSize = IntSize.Zero
private set
override val placementScope: Placeable.PlacementScope = PlacementScope(this)
override fun measureAndLayout(sendPointerUpdate: Boolean) {
measureAndLayoutDelegate.updateRootConstraints(constraints)
if (
measureAndLayoutDelegate.measureAndLayout(
scheduleSyntheticEvents.takeIf { sendPointerUpdate }
)
) {
requestDraw()
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
// Don't use mainOwner.root.width here, as it strictly coerced by [constraints]
contentSize = IntSize(
root.children.maxOfOrNull { it.outerCoordinator.measuredWidth } ?: 0,
root.children.maxOfOrNull { it.outerCoordinator.measuredHeight } ?: 0,
)
}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
measureAndLayoutDelegate.measureAndLayout(layoutNode, constraints)
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
measureAndLayoutDelegate.forceMeasureTheSubtree(layoutNode, affectsLookahead)
}
override fun onRequestMeasure(
layoutNode: LayoutNode,
affectsLookahead: Boolean,
forceRequest: Boolean,
scheduleMeasureAndLayout: Boolean
) {
if (affectsLookahead) {
if (measureAndLayoutDelegate.requestLookaheadRemeasure(layoutNode, forceRequest) &&
scheduleMeasureAndLayout
) {
requestLayout()
}
} else if (measureAndLayoutDelegate.requestRemeasure(layoutNode, forceRequest) &&
scheduleMeasureAndLayout
) {
requestLayout()
}
}
override fun onRequestRelayout(
layoutNode: LayoutNode,
affectsLookahead: Boolean,
forceRequest: Boolean
) {
if (affectsLookahead) {
if (measureAndLayoutDelegate.requestLookaheadRelayout(layoutNode, forceRequest)) {
requestLayout()
}
} else if (measureAndLayoutDelegate.requestRelayout(layoutNode, forceRequest)) {
requestLayout()
}
}
override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
measureAndLayoutDelegate.requestOnPositionedCallback(layoutNode)
requestLayout()
}
override fun createLayer(
drawBlock: (Canvas, GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit,
explicitLayer: GraphicsLayer?
) = SkiaLayer(
density,
invalidateParentLayer = {
invalidateParentLayer()
requestDraw()
},
drawBlock = drawBlock,
onDestroy = { needClearObservations = true }
)
override fun onSemanticsChange() {
accessibilityController?.onSemanticsChange()
}
override fun onLayoutChange(layoutNode: LayoutNode) {
accessibilityController?.onLayoutChange(layoutNode)
}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
return when (keyEvent.key) {
Tab -> if (keyEvent.isShiftPressed) Previous else Next
DirectionCenter -> Enter
Back -> Exit
else -> null
}
}
override fun calculatePositionInWindow(localPosition: Offset): Offset = localPosition
override fun calculateLocalPosition(positionInWindow: Offset): Offset = positionInWindow
override fun localToScreen(localPosition: Offset): Offset = localPosition
override fun localToScreen(localTransform: Matrix) {}
override fun screenToLocal(positionOnScreen: Offset): Offset = positionOnScreen
fun draw(canvas: org.jetbrains.skia.Canvas) {
root.draw(canvas.asComposeCanvas(), null)
}
private var desiredPointerIcon: PointerIcon? = null
private var needSendSyntheticEvents = false
private var lastPointerEvent: PointerInputEvent? = null
private val scheduleSyntheticEvents: () -> Unit = {
// we can't send event synchronously, as we can have call of `measureAndLayout`
// inside the event handler. So we can have a situation when we call event handler inside
// event handler. And that can lead to unpredictable behaviour.
// Nature of synthetic events doesn't require that they should be fired
// synchronously on layout change.
needSendSyntheticEvents = true
onNeedRender?.invoke()
}
// TODO(demin) should we repeat all events, or only which are make sense?
// For example, touch Move after touch Release doesn't make sense,
// and an application can handle it in a wrong way
// Desktop doesn't support touch at the moment, but when it will, we should resolve this.
private fun sendSyntheticEvents() {
if (needSendSyntheticEvents) {
needSendSyntheticEvents = false
val lastPointerEvent = lastPointerEvent
if (lastPointerEvent != null) {
doProcessPointerInput(
PointerInputEvent(
PointerEventType.Move,
lastPointerEvent.uptime,
lastPointerEvent.pointers,
lastPointerEvent.buttons,
lastPointerEvent.keyboardModifiers,
lastPointerEvent.mouseEvent
)
)
}
}
}
internal fun processPointerInput(event: PointerInputEvent): ProcessResult {
measureAndLayout()
sendSyntheticEvents()
desiredPointerIcon = null
lastPointerEvent = event
return doProcessPointerInput(event)
}
private fun doProcessPointerInput(event: PointerInputEvent): ProcessResult {
return pointerInputEventProcessor.process(
event,
this,
isInBounds = event.pointers.all {
it.position.x in 0f..root.width.toFloat() &&
it.position.y in 0f..root.height.toFloat()
}
).also {
if (it.dispatchedToAPointerInputModifier) {
setPointerIcon(component, desiredPointerIcon)
}
}
}
override fun processPointerInput(timeMillis: Long, pointers: List<TestPointerInputEventData>) {
val isPressed = pointers.any { it.down }
processPointerInput(
PointerInputEvent(
PointerEventType.Unknown,
timeMillis,
pointers.map { it.toPointerInputEventData() },
if (isPressed) PrimaryPressedPointerButtons else DefaultPointerButtons
)
)
}
override fun onEndApplyChanges() {
// Listeners can add more items to the list and we want to ensure that they
// are executed after being added, so loop until the list is empty
while (endApplyChangesListeners.isNotEmpty()) {
val size = endApplyChangesListeners.size
for (i in 0 until size) {
val listener = endApplyChangesListeners[i]
// null out the item so that if the listener is re-added then we execute it again.
endApplyChangesListeners[i] = null
listener?.invoke()
}
// Remove all the items that were visited. Removing items shifts all items after
// to the front of the list, so removing in a chunk is cheaper than removing one-by-one
endApplyChangesListeners.removeRange(0, size)
}
}
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
if (listener !in endApplyChangesListeners) {
endApplyChangesListeners += listener
}
}
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
measureAndLayoutDelegate.registerOnLayoutCompletedListener(listener)
requestLayout()
}
override suspend fun textInputSession(
session: suspend PlatformTextInputSessionScope.() -> Nothing
): Nothing {
component.textInputSession(session)
}
// A Stub for the PointerIconService required in Owner.kt
override val pointerIconService: PointerIconService =
object : PointerIconService {
override fun getIcon(): PointerIcon {
return desiredPointerIcon ?: PointerIcon.Default
}
override fun setIcon(value: PointerIcon?) {
desiredPointerIcon = value
}
}
}
internal expect fun sendKeyEvent(
platformInputService: PlatformInput,
focusOwner: FocusOwner,
keyEvent: KeyEvent
): Boolean
internal expect fun setPointerIcon(
containerCursor: PlatformComponentWithCursor?,
icon: PointerIcon?
)