From 20269e29480d0880ab1bb476c7f8a885eb964952 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 31 May 2023 16:17:04 +0200 Subject: [PATCH 01/19] Introduce ComposeSwingLayer that uses offscreen rendering on awt.Graphics for smooth Swing interop --- .../compose/ui/awt/ComposePanel.desktop.kt | 4 +- .../ui/awt/ComposeSwingLayer.desktop.kt | 520 ++++++++++++++++++ 2 files changed, 522 insertions(+), 2 deletions(-) create mode 100644 compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index 7914c18e33394..8abf03057cad5 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -77,7 +77,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( private val _focusListeners = mutableSetOf() private var _isFocusable = true private var _isRequestFocusEnabled = false - private var layer: ComposeLayer? = null + private var layer: ComposeSwingLayer? = null private val clipMap = mutableMapOf() private var content: (@Composable () -> Unit)? = null @@ -149,7 +149,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // After [super.addNotify] is called we can safely initialize the layer and composable // content. - layer = ComposeLayer(skiaLayerAnalytics).apply { + layer = ComposeSwingLayerImpl(skiaLayerAnalytics).apply { scene.releaseFocus() component.setSize(width, height) component.isFocusable = _isFocusable diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt new file mode 100644 index 0000000000000..d91b7355d478d --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -0,0 +1,520 @@ +/* + * Copyright 2023 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:OptIn(ExperimentalComposeUiApi::class) + +package androidx.compose.ui.awt + +import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalContext +import androidx.compose.ui.ComposeScene +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.platform.* +import androidx.compose.ui.semantics.SemanticsOwner +import androidx.compose.ui.toPointerKeyboardModifiers +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.WindowExceptionHandler +import androidx.compose.ui.window.density +import androidx.compose.ui.window.layoutDirection +import java.awt.* +import java.awt.Cursor +import java.awt.event.* +import java.awt.event.KeyEvent +import java.awt.im.InputMethodRequests +import javax.accessibility.Accessible +import javax.accessibility.AccessibleContext +import javax.swing.SwingUtilities +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineExceptionHandler +import org.jetbrains.skia.Canvas +import org.jetbrains.skiko.* +import org.jetbrains.skiko.swing.SkiaSwingLayer +import org.jetbrains.skiko.swing.SkiaSwingLayerComponent + +internal interface ComposeSwingLayer { + val component: SkiaSwingLayerComponent + + val scene: ComposeScene + + var exceptionHandler: WindowExceptionHandler? + + fun setContent(content: @Composable () -> Unit) + + fun dispose() +} + +internal class ComposeSwingLayerImpl( + private val skiaLayerAnalytics: SkiaLayerAnalytics +) : ComposeSwingLayer { + private var isDisposed = false + + internal val sceneAccessible = ComposeSceneAccessible( + ownersProvider = { scene.owners.asReversed() }, + mainOwnerProvider = { scene.mainOwner } + ) + + private val skikoView = object : SkikoView { + override val input: SkikoInput + get() = object : SkikoInput { + + } + + override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { + catchExceptions { + scene.render(canvas, nanoTime) + } + } + } + + private val _component = ComponentImpl() + + override val component: SkiaSwingLayerComponent get() = _component + + @OptIn(ExperimentalComposeUiApi::class) + private val coroutineExceptionHandler = object : + AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { + override fun handleException(context: CoroutineContext, exception: Throwable) { + exceptionHandler?.onException(exception) ?: throw exception + } + } + + @OptIn(ExperimentalComposeUiApi::class) + override var exceptionHandler: WindowExceptionHandler? = null + + @OptIn(ExperimentalComposeUiApi::class) + private fun catchExceptions(body: () -> Unit) { + try { + body() + } catch (e: Throwable) { + exceptionHandler?.onException(e) ?: throw e + } + } + + private val platform = object : Platform by Platform.Empty { + override fun setPointerIcon(pointerIcon: PointerIcon) { + _component.cursor = (pointerIcon as? AwtCursor)?.cursor ?: Cursor(Cursor.DEFAULT_CURSOR) + } + + override fun accessibilityController(owner: SemanticsOwner) = + AccessibilityControllerImpl(owner, _component, onFocusReceived = { + _component.requestNativeFocusOnAccessible(it) + }) + + override val windowInfo = WindowInfoImpl() + + override val textInputService = PlatformInput(_component) + + override val layoutDirection: LayoutDirection + get() = component.layoutDirection + + override val focusManager = object : FocusManager { + override fun clearFocus(force: Boolean) { + val root = component.rootPane + root?.focusTraversalPolicy?.getDefaultComponent(root)?.requestFocusInWindow() + } + + override fun moveFocus(focusDirection: FocusDirection): Boolean = + when (focusDirection) { + FocusDirection.Next -> { + val toFocus = _component.focusCycleRootAncestor?.let { root -> + val policy = root.focusTraversalPolicy + policy.getComponentAfter(root, _component) + ?: policy.getDefaultComponent(root) + } + val hasFocus = toFocus?.hasFocus() == true + !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_FORWARD) == true + } + + FocusDirection.Previous -> { + val toFocus = _component.focusCycleRootAncestor?.let { root -> + val policy = root.focusTraversalPolicy + policy.getComponentBefore(root, _component) + ?: policy.getDefaultComponent(root) + } + val hasFocus = toFocus?.hasFocus() == true + !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_BACKWARD) == true + } + + else -> false + } + } + + override fun requestFocusForOwner(): Boolean { + return component.hasFocus() || component.requestFocusInWindow() + } + + override val viewConfiguration = object : ViewConfiguration { + override val longPressTimeoutMillis: Long = 500 + override val doubleTapTimeoutMillis: Long = 300 + override val doubleTapMinTimeMillis: Long = 40 + override val touchSlop: Float get() = with(_component.density) { 18.dp.toPx() } + } + } + + override val scene = ComposeScene( + MainUIDispatcher + coroutineExceptionHandler, + platform, + Density(1f), + _component::repaint + ) + + private val density get() = _component.density.density + + /** + * Keyboard modifiers state might be changed when window is not focused, so window doesn't + * receive any key events. + * This flag is set when window focus changes. Then we can rely on it when handling the + * first movementEvent to get the actual keyboard modifiers state from it. + * After window gains focus, the first motionEvent.metaState (after focus gained) is used + * to update windowInfo.keyboardModifiers. + * + * TODO: needs to be set `true` when focus changes: + * (Window focus change is implemented in JB fork, but not upstreamed yet). + */ + private var keyboardModifiersRequireUpdate = false + + @OptIn(ExperimentalSkikoApi::class) + private inner class ComponentImpl : + SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics), + PlatformComponent { + var currentInputMethodRequests: InputMethodRequests? = null + + private var window: Window? = null + private var windowListener = object : WindowFocusListener { + override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() + override fun windowLostFocus(e: WindowEvent) = refreshWindowFocus() + } + + override fun addNotify() { + super.addNotify() + resetDensity() + initContent() + updateSceneSize() + window = SwingUtilities.getWindowAncestor(this) + window?.addWindowFocusListener(windowListener) + refreshWindowFocus() + } + + override fun removeNotify() { + window?.removeWindowFocusListener(windowListener) + window = null + refreshWindowFocus() + super.removeNotify() + } + + override fun paint(g: Graphics) { + resetDensity() + super.paint(g) + } + + override fun getInputMethodRequests() = currentInputMethodRequests + + override fun enableInput(inputMethodRequests: InputMethodRequests) { + currentInputMethodRequests = inputMethodRequests + enableInputMethods(true) + val focusGainedEvent = FocusEvent(this, FocusEvent.FOCUS_GAINED) + inputContext.dispatchEvent(focusGainedEvent) + } + + override fun disableInput() { + currentInputMethodRequests = null + } + + override fun doLayout() { + super.doLayout() + updateSceneSize() + } + + private fun updateSceneSize() { + this@ComposeSwingLayerImpl.scene.constraints = Constraints( + maxWidth = (width * density.density).toInt().coerceAtLeast(0), + maxHeight = (height * density.density).toInt().coerceAtLeast(0) + ) + } + + override fun getPreferredSize(): Dimension { + return if (isPreferredSizeSet) super.getPreferredSize() else Dimension( + (this@ComposeSwingLayerImpl.scene.contentSize.width / density.density).toInt(), + (this@ComposeSwingLayerImpl.scene.contentSize.height / density.density).toInt() + ) + } + + override val locationOnScreen: Point + @Suppress("ACCIDENTAL_OVERRIDE") // KT-47743 + get() = super.getLocationOnScreen() + + override var density: Density = Density(1f) + private set + + private fun resetDensity() { + density = (this as SkiaSwingLayer).density + if (this@ComposeSwingLayerImpl.scene.density != density) { + this@ComposeSwingLayerImpl.scene.density = density + updateSceneSize() + } + } + + private fun refreshWindowFocus() { + platform.windowInfo.isWindowFocused = window?.isFocused ?: false + keyboardModifiersRequireUpdate = true + } + + fun setCurrentKeyboardModifiers(modifiers: PointerKeyboardModifiers) { + platform.windowInfo.keyboardModifiers = modifiers + } + + override fun getAccessibleContext(): AccessibleContext? { + return sceneAccessible.accessibleContext + } + } + + init { + _component.addInputMethodListener(object : InputMethodListener { + override fun caretPositionChanged(event: InputMethodEvent?) { + if (isDisposed) return + // Which OSes and which input method could produce such events? We need to have some + // specific cases in mind before implementing this + } + + override fun inputMethodTextChanged(event: InputMethodEvent) { + if (isDisposed) return + catchExceptions { + platform.textInputService.inputMethodTextChanged(event) + } + } + }) + + _component.addFocusListener(object : FocusListener { + override fun focusGained(e: FocusEvent) { + // We don't reset focus for Compose when the component loses focus temporary. + // Partially because we don't support restoring focus after clearing it. + // Focus can be lost temporary when another window or popup takes focus. + if (!e.isTemporary) { + scene.requestFocus() + } + } + + override fun focusLost(e: FocusEvent) { + // We don't reset focus for Compose when the component loses focus temporary. + // Partially because we don't support restoring focus after clearing it. + // Focus can be lost temporary when another window or popup takes focus. + if (!e.isTemporary) { + scene.releaseFocus() + } + } + }) + + _component.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(event: MouseEvent) = Unit + override fun mousePressed(event: MouseEvent) = onMouseEvent(event) + override fun mouseReleased(event: MouseEvent) = onMouseEvent(event) + override fun mouseEntered(event: MouseEvent) = onMouseEvent(event) + override fun mouseExited(event: MouseEvent) = onMouseEvent(event) + }) + _component.addMouseMotionListener(object : MouseMotionAdapter() { + override fun mouseDragged(event: MouseEvent) = onMouseEvent(event) + override fun mouseMoved(event: MouseEvent) = onMouseEvent(event) + }) + _component.addMouseWheelListener { event -> + onMouseWheelEvent(event) + } + _component.focusTraversalKeysEnabled = false + _component.addKeyListener(object : KeyAdapter() { + override fun keyPressed(event: KeyEvent) = onKeyEvent(event) + override fun keyReleased(event: KeyEvent) = onKeyEvent(event) + override fun keyTyped(event: KeyEvent) = onKeyEvent(event) + }) + } + + private fun onMouseEvent(event: MouseEvent) = catchExceptions { + // AWT can send events after the window is disposed + if (isDisposed) return@catchExceptions + if (keyboardModifiersRequireUpdate) { + keyboardModifiersRequireUpdate = false + _component.setCurrentKeyboardModifiers(event.keyboardModifiers) + } + scene.onMouseEvent(density, event) + } + + private fun onMouseWheelEvent(event: MouseWheelEvent) = catchExceptions { + if (isDisposed) return@catchExceptions + scene.onMouseWheelEvent(density, event) + } + + private fun onKeyEvent(event: KeyEvent) = catchExceptions { + if (isDisposed) return@catchExceptions + platform.textInputService.onKeyEvent(event) + _component.setCurrentKeyboardModifiers(event.toPointerKeyboardModifiers()) + if (scene.sendKeyEvent(ComposeKeyEvent(event))) { + event.consume() + } + } + + override fun dispose() { + check(!isDisposed) + scene.close() + _component.dispose() + _initContent = null + isDisposed = true + } + + var compositionLocalContext: CompositionLocalContext? by scene::compositionLocalContext + + override fun setContent( + content: @Composable () -> Unit + ) { + // If we call it before attaching, everything probably will be fine, + // but the first composition will be useless, as we set density=1 + // (we don't know the real density if we have unattached component) + _initContent = { + catchExceptions { + scene.setContent( + content = content + ) + } + } + initContent() + } + + private var _initContent: (() -> Unit)? = null + + private fun initContent() { + if (_component.isDisplayable) { + _initContent?.invoke() + _initContent = null + } + } +} + +@Suppress("ControlFlowWithEmptyBody") +@OptIn(ExperimentalComposeUiApi::class) +private fun ComposeScene.onMouseEvent( + density: Float, + event: MouseEvent +) { + val eventType = when (event.id) { + MouseEvent.MOUSE_PRESSED -> PointerEventType.Press + MouseEvent.MOUSE_RELEASED -> PointerEventType.Release + MouseEvent.MOUSE_DRAGGED -> PointerEventType.Move + MouseEvent.MOUSE_MOVED -> PointerEventType.Move + MouseEvent.MOUSE_ENTERED -> PointerEventType.Enter + MouseEvent.MOUSE_EXITED -> PointerEventType.Exit + else -> PointerEventType.Unknown + } + sendPointerEvent( + eventType = eventType, + position = Offset(event.x.toFloat(), event.y.toFloat()) * density, + timeMillis = event.`when`, + type = PointerType.Mouse, + buttons = event.buttons, + keyboardModifiers = event.keyboardModifiers, + nativeEvent = event, + button = event.getPointerButton() + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +private fun MouseEvent.getPointerButton(): PointerButton? { + if (button == MouseEvent.NOBUTTON) return null + return when (button) { + MouseEvent.BUTTON2 -> PointerButton.Tertiary + MouseEvent.BUTTON3 -> PointerButton.Secondary + else -> PointerButton(button - 1) + } +} + +@Suppress("ControlFlowWithEmptyBody") +@OptIn(ExperimentalComposeUiApi::class) +private fun ComposeScene.onMouseWheelEvent( + density: Float, + event: MouseWheelEvent +) { + sendPointerEvent( + eventType = PointerEventType.Scroll, + position = Offset(event.x.toFloat(), event.y.toFloat()) * density, + scrollDelta = if (event.isShiftDown) { + Offset(event.preciseWheelRotation.toFloat(), 0f) + } else { + Offset(0f, event.preciseWheelRotation.toFloat()) + }, + timeMillis = event.`when`, + type = PointerType.Mouse, + buttons = event.buttons, + keyboardModifiers = event.keyboardModifiers, + nativeEvent = event + ) +} + + +@OptIn(ExperimentalComposeUiApi::class) +private val MouseEvent.buttons + get() = PointerButtons( + // We should check [event.button] because of case where [event.modifiersEx] does not provide + // info about the pressed mouse button when using touchpad on MacOS 12 (AWT only). + // When the [Tap to click] feature is activated on Mac OS 12, half of all clicks are not + // handled because [event.modifiersEx] may not provide info about the pressed mouse button. + isPrimaryPressed = ((modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0 + || (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON1)) + && !isMacOsCtrlClick, + isSecondaryPressed = (modifiersEx and MouseEvent.BUTTON3_DOWN_MASK) != 0 + || (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON3) + || isMacOsCtrlClick, + isTertiaryPressed = (modifiersEx and MouseEvent.BUTTON2_DOWN_MASK) != 0 + || (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON2), + isBackPressed = (modifiersEx and MouseEvent.getMaskForButton(4)) != 0 + || (id == MouseEvent.MOUSE_PRESSED && button == 4), + isForwardPressed = (modifiersEx and MouseEvent.getMaskForButton(5)) != 0 + || (id == MouseEvent.MOUSE_PRESSED && button == 5), + ) + +@OptIn(ExperimentalComposeUiApi::class) +private val MouseEvent.keyboardModifiers + get() = PointerKeyboardModifiers( + isCtrlPressed = (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0, + isMetaPressed = (modifiersEx and InputEvent.META_DOWN_MASK) != 0, + isAltPressed = (modifiersEx and InputEvent.ALT_DOWN_MASK) != 0, + isShiftPressed = (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0, + isAltGraphPressed = (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) != 0, + isSymPressed = false, + isFunctionPressed = false, + isCapsLockOn = getLockingKeyStateSafe(KeyEvent.VK_CAPS_LOCK), + isScrollLockOn = getLockingKeyStateSafe(KeyEvent.VK_SCROLL_LOCK), + isNumLockOn = getLockingKeyStateSafe(KeyEvent.VK_NUM_LOCK), + ) + +private fun getLockingKeyStateSafe( + mask: Int +): Boolean = try { + Toolkit.getDefaultToolkit().getLockingKeyState(mask) +} catch (_: Exception) { + false +} + +private val MouseEvent.isMacOsCtrlClick + get() = ( + hostOs.isMacOS && + ((modifiersEx and InputEvent.BUTTON1_DOWN_MASK) != 0) && + ((modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0) + ) From 55dcf44d525abeb2b6e2521a3d4c8e6d9242204b Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 12 Jun 2023 16:42:55 +0200 Subject: [PATCH 02/19] Make ComposeLayer as base class for standalone window and swing interop --- .../compose/ui/awt/ComposeLayer.desktop.kt | 330 ++++++------- .../compose/ui/awt/ComposePanel.desktop.kt | 2 +- .../ui/awt/ComposeSwingLayer.desktop.kt | 446 ++---------------- .../ui/awt/ComposeWindowDelegate.desktop.kt | 2 +- .../compose/ui/awt/ComposeWindowLayer.kt | 131 +++++ 5 files changed, 304 insertions(+), 607 deletions(-) create mode 100644 compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 558411f493b17..fd5d40c5dcc2b 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -20,7 +20,6 @@ package androidx.compose.ui.awt import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.ComposeScene import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.focus.FocusDirection @@ -30,39 +29,70 @@ import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.toPointerKeyboardModifiers -import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowExceptionHandler import androidx.compose.ui.window.density import androidx.compose.ui.window.layoutDirection -import java.awt.* +import java.awt.Component import java.awt.Cursor +import java.awt.Point +import java.awt.Toolkit import java.awt.event.* import java.awt.event.KeyEvent import java.awt.im.InputMethodRequests import javax.accessibility.Accessible -import javax.swing.SwingUtilities +import javax.swing.JComponent import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineExceptionHandler import org.jetbrains.skia.Canvas -import org.jetbrains.skiko.* -import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.MainUIDispatcher +import org.jetbrains.skiko.SkikoInput +import org.jetbrains.skiko.SkikoView +import org.jetbrains.skiko.hostOs -internal class ComposeLayer( - private val skiaLayerAnalytics: SkiaLayerAnalytics -) { +internal abstract class ComposeLayer { private var isDisposed = false - internal val sceneAccessible = ComposeSceneAccessible( + val sceneAccessible = ComposeSceneAccessible( ownersProvider = { scene.owners.asReversed() }, mainOwnerProvider = { scene.mainOwner } ) - private val _component = ComponentImpl() - val component: SkiaLayer get() = _component + protected abstract val componentLayer: JComponent + + // Needed for case when componentLayer is a wrapper for another Component that need to acquire focus events + // e.g. canvas in case of ComposeWindowLayer + // TODO: can we use [componentLayer] here? + protected abstract val focusComponentDelegate: Component + + protected var currentInputMethodRequests: InputMethodRequests? = null + + private val platformComponent: PlatformComponent = object : PlatformComponent { + override fun enableInput(inputMethodRequests: InputMethodRequests) { + currentInputMethodRequests = inputMethodRequests + componentLayer.enableInputMethods(true) + val focusGainedEvent = FocusEvent(focusComponentDelegate, FocusEvent.FOCUS_GAINED) + componentLayer.inputContext.dispatchEvent(focusGainedEvent) + } + + override fun disableInput() { + currentInputMethodRequests = null + } + + override val locationOnScreen: Point + get() = componentLayer.locationOnScreen + override val density: Density + get() = componentLayer.density + } + + protected abstract fun requestNativeFocusOnAccessible(accessible: Accessible) + + protected abstract fun onComposeInvalidation() + + protected abstract fun disposeComponentLayer() @OptIn(ExperimentalComposeUiApi::class) private val coroutineExceptionHandler = object : @@ -84,72 +114,31 @@ internal class ComposeLayer( } } - private val platform = object : Platform by Platform.Empty { - override fun setPointerIcon(pointerIcon: PointerIcon) { - _component.cursor = (pointerIcon as? AwtCursor)?.cursor ?: Cursor(Cursor.DEFAULT_CURSOR) - } - - override fun accessibilityController(owner: SemanticsOwner) = - AccessibilityControllerImpl(owner, _component, onFocusReceived = { - _component.requestNativeFocusOnAccessible(it) - }) + protected val platform: DesktopPlatform = DesktopPlatform() - override val windowInfo = WindowInfoImpl() - - override val textInputService = PlatformInput(_component) + internal val scene = ComposeScene( + MainUIDispatcher + coroutineExceptionHandler, + platform, + Density(1f), + invalidate = { + onComposeInvalidation() + }, + ) - override val layoutDirection: LayoutDirection - get() = component.layoutDirection + protected val skikoView = object : SkikoView { + override val input: SkikoInput + get() = object : SkikoInput { - override val focusManager = object : FocusManager { - override fun clearFocus(force: Boolean) { - val root = component.rootPane - root?.focusTraversalPolicy?.getDefaultComponent(root)?.requestFocusInWindow() } - override fun moveFocus(focusDirection: FocusDirection): Boolean = when (focusDirection) { - FocusDirection.Next -> { - val toFocus = _component.focusCycleRootAncestor?.let { root -> - val policy = root.focusTraversalPolicy - policy.getComponentAfter(root, _component) - ?: policy.getDefaultComponent(root) - } - val hasFocus = toFocus?.hasFocus() == true - !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_FORWARD) == true - } - FocusDirection.Previous -> { - val toFocus = _component.focusCycleRootAncestor?.let { root -> - val policy = root.focusTraversalPolicy - policy.getComponentBefore(root, _component) - ?: policy.getDefaultComponent(root) - } - val hasFocus = toFocus?.hasFocus() == true - !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_BACKWARD) == true - } - else -> false + override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { + catchExceptions { + scene.render(canvas, nanoTime) } } - - override fun requestFocusForOwner(): Boolean { - return component.hasFocus() || component.requestFocusInWindow() - } - - override val viewConfiguration = object : ViewConfiguration { - override val longPressTimeoutMillis: Long = 500 - override val doubleTapTimeoutMillis: Long = 300 - override val doubleTapMinTimeMillis: Long = 40 - override val touchSlop: Float get() = with(_component.density) { 18.dp.toPx() } - } } - internal val scene = ComposeScene( - MainUIDispatcher + coroutineExceptionHandler, - platform, - Density(1f), - _component::needRedraw - ) - - private val density get() = _component.density.density + private val density get() = platformComponent.density.density /** * Keyboard modifiers state might be changed when window is not focused, so window doesn't @@ -162,116 +151,11 @@ internal class ComposeLayer( * TODO: needs to be set `true` when focus changes: * (Window focus change is implemented in JB fork, but not upstreamed yet). */ - private var keyboardModifiersRequireUpdate = false - - private inner class ComponentImpl : - SkiaLayer( - externalAccessibleFactory = { sceneAccessible }, - analytics = skiaLayerAnalytics - ), - Accessible, PlatformComponent { - var currentInputMethodRequests: InputMethodRequests? = null - - private var window: Window? = null - private var windowListener = object : WindowFocusListener { - override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() - override fun windowLostFocus(e: WindowEvent) = refreshWindowFocus() - } - - override fun addNotify() { - super.addNotify() - resetDensity() - initContent() - updateSceneSize() - window = SwingUtilities.getWindowAncestor(this) - window?.addWindowFocusListener(windowListener) - refreshWindowFocus() - } - - override fun removeNotify() { - window?.removeWindowFocusListener(windowListener) - window = null - refreshWindowFocus() - super.removeNotify() - } - - override fun paint(g: Graphics) { - resetDensity() - super.paint(g) - } - - override fun getInputMethodRequests() = currentInputMethodRequests - - override fun enableInput(inputMethodRequests: InputMethodRequests) { - currentInputMethodRequests = inputMethodRequests - enableInputMethods(true) - val focusGainedEvent = FocusEvent(this.canvas, FocusEvent.FOCUS_GAINED) - inputContext.dispatchEvent(focusGainedEvent) - } - - override fun disableInput() { - currentInputMethodRequests = null - } - - override fun doLayout() { - super.doLayout() - updateSceneSize() - } - - private fun updateSceneSize() { - this@ComposeLayer.scene.constraints = Constraints( - maxWidth = (width * density.density).toInt().coerceAtLeast(0), - maxHeight = (height * density.density).toInt().coerceAtLeast(0) - ) - } - - override fun getPreferredSize(): Dimension { - return if (isPreferredSizeSet) super.getPreferredSize() else Dimension( - (this@ComposeLayer.scene.contentSize.width / density.density).toInt(), - (this@ComposeLayer.scene.contentSize.height / density.density).toInt() - ) - } - - override val locationOnScreen: Point - @Suppress("ACCIDENTAL_OVERRIDE") // KT-47743 - get() = super.getLocationOnScreen() - - override var density: Density = Density(1f) - private set - - private fun resetDensity() { - density = (this as SkiaLayer).density - if (this@ComposeLayer.scene.density != density) { - this@ComposeLayer.scene.density = density - updateSceneSize() - } - } + protected var keyboardModifiersRequireUpdate = false - private fun refreshWindowFocus() { - platform.windowInfo.isWindowFocused = window?.isFocused ?: false - keyboardModifiersRequireUpdate = true - } - - fun setCurrentKeyboardModifiers(modifiers: PointerKeyboardModifiers) { - platform.windowInfo.keyboardModifiers = modifiers - } - } - - init { - _component.skikoView = object : SkikoView { - override val input: SkikoInput - get() = object: SkikoInput { - - } - - override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { - catchExceptions { - scene.render(canvas, nanoTime) - } - } - } - - _component.addInputMethodListener(object : InputMethodListener { + @OptIn(ExperimentalComposeUiApi::class) + protected fun attachComposeToComponent() { + componentLayer.addInputMethodListener(object : InputMethodListener { override fun caretPositionChanged(event: InputMethodEvent?) { if (isDisposed) return // Which OSes and which input method could produce such events? We need to have some @@ -286,7 +170,7 @@ internal class ComposeLayer( } }) - _component.addFocusListener(object : FocusListener { + componentLayer.addFocusListener(object : FocusListener { override fun focusGained(e: FocusEvent) { // We don't reset focus for Compose when the component loses focus temporary. // Partially because we don't support restoring focus after clearing it. @@ -306,22 +190,22 @@ internal class ComposeLayer( } }) - _component.addMouseListener(object : MouseAdapter() { + componentLayer.addMouseListener(object : MouseAdapter() { override fun mouseClicked(event: MouseEvent) = Unit override fun mousePressed(event: MouseEvent) = onMouseEvent(event) override fun mouseReleased(event: MouseEvent) = onMouseEvent(event) override fun mouseEntered(event: MouseEvent) = onMouseEvent(event) override fun mouseExited(event: MouseEvent) = onMouseEvent(event) }) - _component.addMouseMotionListener(object : MouseMotionAdapter() { + componentLayer.addMouseMotionListener(object : MouseMotionAdapter() { override fun mouseDragged(event: MouseEvent) = onMouseEvent(event) override fun mouseMoved(event: MouseEvent) = onMouseEvent(event) }) - _component.addMouseWheelListener { event -> + componentLayer.addMouseWheelListener { event -> onMouseWheelEvent(event) } - _component.focusTraversalKeysEnabled = false - _component.addKeyListener(object : KeyAdapter() { + componentLayer.focusTraversalKeysEnabled = false + componentLayer.addKeyListener(object : KeyAdapter() { override fun keyPressed(event: KeyEvent) = onKeyEvent(event) override fun keyReleased(event: KeyEvent) = onKeyEvent(event) override fun keyTyped(event: KeyEvent) = onKeyEvent(event) @@ -333,7 +217,7 @@ internal class ComposeLayer( if (isDisposed) return@catchExceptions if (keyboardModifiersRequireUpdate) { keyboardModifiersRequireUpdate = false - _component.setCurrentKeyboardModifiers(event.keyboardModifiers) + setCurrentKeyboardModifiers(event.keyboardModifiers) } scene.onMouseEvent(density, event) } @@ -346,7 +230,7 @@ internal class ComposeLayer( private fun onKeyEvent(event: KeyEvent) = catchExceptions { if (isDisposed) return@catchExceptions platform.textInputService.onKeyEvent(event) - _component.setCurrentKeyboardModifiers(event.toPointerKeyboardModifiers()) + setCurrentKeyboardModifiers(event.toPointerKeyboardModifiers()) if (scene.sendKeyEvent(ComposeKeyEvent(event))) { event.consume() } @@ -355,13 +239,11 @@ internal class ComposeLayer( fun dispose() { check(!isDisposed) scene.close() - _component.dispose() + disposeComponentLayer() _initContent = null isDisposed = true } - var compositionLocalContext: CompositionLocalContext? by scene::compositionLocalContext - fun setContent( onPreviewKeyEvent: (ComposeKeyEvent) -> Boolean = { false }, onKeyEvent: (ComposeKeyEvent) -> Boolean = { false }, @@ -384,12 +266,78 @@ internal class ComposeLayer( private var _initContent: (() -> Unit)? = null - private fun initContent() { - if (_component.isDisplayable) { + protected fun initContent() { + if (componentLayer.isDisplayable) { _initContent?.invoke() _initContent = null } } + + private fun setCurrentKeyboardModifiers(modifiers: PointerKeyboardModifiers) { + platform.windowInfo.keyboardModifiers = modifiers + } + + protected inner class DesktopPlatform : Platform by Platform.Empty { + override fun setPointerIcon(pointerIcon: PointerIcon) { + componentLayer.cursor = + (pointerIcon as? AwtCursor)?.cursor ?: Cursor(Cursor.DEFAULT_CURSOR) + } + + override fun accessibilityController(owner: SemanticsOwner) = + AccessibilityControllerImpl(owner, platformComponent, onFocusReceived = { + requestNativeFocusOnAccessible(it) + }) + + override val windowInfo = WindowInfoImpl() + + override val textInputService = PlatformInput(platformComponent) + + override val layoutDirection: LayoutDirection + get() = componentLayer.layoutDirection + + override val focusManager = object : FocusManager { + override fun clearFocus(force: Boolean) { + val root = componentLayer.rootPane + root?.focusTraversalPolicy?.getDefaultComponent(root)?.requestFocusInWindow() + } + + override fun moveFocus(focusDirection: FocusDirection): Boolean = + when (focusDirection) { + FocusDirection.Next -> { + val toFocus = componentLayer.focusCycleRootAncestor?.let { root -> + val policy = root.focusTraversalPolicy + policy.getComponentAfter(root, componentLayer) + ?: policy.getDefaultComponent(root) + } + val hasFocus = toFocus?.hasFocus() == true + !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_FORWARD) == true + } + + FocusDirection.Previous -> { + val toFocus = componentLayer.focusCycleRootAncestor?.let { root -> + val policy = root.focusTraversalPolicy + policy.getComponentBefore(root, componentLayer) + ?: policy.getDefaultComponent(root) + } + val hasFocus = toFocus?.hasFocus() == true + !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_BACKWARD) == true + } + + else -> false + } + } + + override fun requestFocusForOwner(): Boolean { + return componentLayer.hasFocus() || componentLayer.requestFocusInWindow() + } + + override val viewConfiguration = object : ViewConfiguration { + override val longPressTimeoutMillis: Long = 500 + override val doubleTapTimeoutMillis: Long = 300 + override val doubleTapMinTimeMillis: Long = 40 + override val touchSlop: Float get() = with(platformComponent.density) { 18.dp.toPx() } + } + } } @Suppress("ControlFlowWithEmptyBody") diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index 8abf03057cad5..f52794dac34f1 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -149,7 +149,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // After [super.addNotify] is called we can safely initialize the layer and composable // content. - layer = ComposeSwingLayerImpl(skiaLayerAnalytics).apply { + layer = ComposeSwingLayer(skiaLayerAnalytics).apply { scene.releaseFocus() component.setSize(width, height) component.isFocusable = _isFocusable diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index d91b7355d478d..fa1e11a60118b 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -18,187 +18,56 @@ package androidx.compose.ui.awt -import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalContext -import androidx.compose.ui.ComposeScene import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.FocusManager -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.* -import androidx.compose.ui.platform.* -import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.toPointerKeyboardModifiers import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.WindowExceptionHandler import androidx.compose.ui.window.density -import androidx.compose.ui.window.layoutDirection -import java.awt.* -import java.awt.Cursor -import java.awt.event.* -import java.awt.event.KeyEvent +import java.awt.Component +import java.awt.Dimension +import java.awt.Graphics +import java.awt.Window +import java.awt.event.WindowEvent +import java.awt.event.WindowFocusListener import java.awt.im.InputMethodRequests import javax.accessibility.Accessible import javax.accessibility.AccessibleContext +import javax.swing.JComponent import javax.swing.SwingUtilities -import kotlin.coroutines.AbstractCoroutineContextElement -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineExceptionHandler -import org.jetbrains.skia.Canvas -import org.jetbrains.skiko.* +import org.jetbrains.skiko.ExperimentalSkikoApi +import org.jetbrains.skiko.SkiaLayerAnalytics import org.jetbrains.skiko.swing.SkiaSwingLayer import org.jetbrains.skiko.swing.SkiaSwingLayerComponent -internal interface ComposeSwingLayer { - val component: SkiaSwingLayerComponent - - val scene: ComposeScene - - var exceptionHandler: WindowExceptionHandler? - - fun setContent(content: @Composable () -> Unit) - - fun dispose() -} - -internal class ComposeSwingLayerImpl( +internal class ComposeSwingLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeSwingLayer { - private var isDisposed = false - - internal val sceneAccessible = ComposeSceneAccessible( - ownersProvider = { scene.owners.asReversed() }, - mainOwnerProvider = { scene.mainOwner } - ) - - private val skikoView = object : SkikoView { - override val input: SkikoInput - get() = object : SkikoInput { - - } - - override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { - catchExceptions { - scene.render(canvas, nanoTime) - } - } - } - +) : ComposeLayer() { private val _component = ComponentImpl() + val component: SkiaSwingLayerComponent get() = _component - override val component: SkiaSwingLayerComponent get() = _component + override val componentLayer: JComponent + get() = _component + override val focusComponentDelegate: Component + get() = _component - @OptIn(ExperimentalComposeUiApi::class) - private val coroutineExceptionHandler = object : - AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler { - override fun handleException(context: CoroutineContext, exception: Throwable) { - exceptionHandler?.onException(exception) ?: throw exception - } + override fun requestNativeFocusOnAccessible(accessible: Accessible) { + // TODO: support a11y } - @OptIn(ExperimentalComposeUiApi::class) - override var exceptionHandler: WindowExceptionHandler? = null - - @OptIn(ExperimentalComposeUiApi::class) - private fun catchExceptions(body: () -> Unit) { - try { - body() - } catch (e: Throwable) { - exceptionHandler?.onException(e) ?: throw e - } + override fun onComposeInvalidation() { + _component.repaint() } - private val platform = object : Platform by Platform.Empty { - override fun setPointerIcon(pointerIcon: PointerIcon) { - _component.cursor = (pointerIcon as? AwtCursor)?.cursor ?: Cursor(Cursor.DEFAULT_CURSOR) - } - - override fun accessibilityController(owner: SemanticsOwner) = - AccessibilityControllerImpl(owner, _component, onFocusReceived = { - _component.requestNativeFocusOnAccessible(it) - }) - - override val windowInfo = WindowInfoImpl() - - override val textInputService = PlatformInput(_component) - - override val layoutDirection: LayoutDirection - get() = component.layoutDirection - - override val focusManager = object : FocusManager { - override fun clearFocus(force: Boolean) { - val root = component.rootPane - root?.focusTraversalPolicy?.getDefaultComponent(root)?.requestFocusInWindow() - } - - override fun moveFocus(focusDirection: FocusDirection): Boolean = - when (focusDirection) { - FocusDirection.Next -> { - val toFocus = _component.focusCycleRootAncestor?.let { root -> - val policy = root.focusTraversalPolicy - policy.getComponentAfter(root, _component) - ?: policy.getDefaultComponent(root) - } - val hasFocus = toFocus?.hasFocus() == true - !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_FORWARD) == true - } - - FocusDirection.Previous -> { - val toFocus = _component.focusCycleRootAncestor?.let { root -> - val policy = root.focusTraversalPolicy - policy.getComponentBefore(root, _component) - ?: policy.getDefaultComponent(root) - } - val hasFocus = toFocus?.hasFocus() == true - !hasFocus && toFocus?.requestFocusInWindow(FocusEvent.Cause.TRAVERSAL_BACKWARD) == true - } - - else -> false - } - } - - override fun requestFocusForOwner(): Boolean { - return component.hasFocus() || component.requestFocusInWindow() - } - - override val viewConfiguration = object : ViewConfiguration { - override val longPressTimeoutMillis: Long = 500 - override val doubleTapTimeoutMillis: Long = 300 - override val doubleTapMinTimeMillis: Long = 40 - override val touchSlop: Float get() = with(_component.density) { 18.dp.toPx() } - } + init { + attachComposeToComponent() } - override val scene = ComposeScene( - MainUIDispatcher + coroutineExceptionHandler, - platform, - Density(1f), - _component::repaint - ) - - private val density get() = _component.density.density - - /** - * Keyboard modifiers state might be changed when window is not focused, so window doesn't - * receive any key events. - * This flag is set when window focus changes. Then we can rely on it when handling the - * first movementEvent to get the actual keyboard modifiers state from it. - * After window gains focus, the first motionEvent.metaState (after focus gained) is used - * to update windowInfo.keyboardModifiers. - * - * TODO: needs to be set `true` when focus changes: - * (Window focus change is implemented in JB fork, but not upstreamed yet). - */ - private var keyboardModifiersRequireUpdate = false + override fun disposeComponentLayer() { + _component.dispose() + } @OptIn(ExperimentalSkikoApi::class) private inner class ComponentImpl : - SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics), - PlatformComponent { + SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { var currentInputMethodRequests: InputMethodRequests? = null private var window: Window? = null @@ -231,24 +100,13 @@ internal class ComposeSwingLayerImpl( override fun getInputMethodRequests() = currentInputMethodRequests - override fun enableInput(inputMethodRequests: InputMethodRequests) { - currentInputMethodRequests = inputMethodRequests - enableInputMethods(true) - val focusGainedEvent = FocusEvent(this, FocusEvent.FOCUS_GAINED) - inputContext.dispatchEvent(focusGainedEvent) - } - - override fun disableInput() { - currentInputMethodRequests = null - } - override fun doLayout() { super.doLayout() updateSceneSize() } private fun updateSceneSize() { - this@ComposeSwingLayerImpl.scene.constraints = Constraints( + this@ComposeSwingLayer.scene.constraints = Constraints( maxWidth = (width * density.density).toInt().coerceAtLeast(0), maxHeight = (height * density.density).toInt().coerceAtLeast(0) ) @@ -256,22 +114,14 @@ internal class ComposeSwingLayerImpl( override fun getPreferredSize(): Dimension { return if (isPreferredSizeSet) super.getPreferredSize() else Dimension( - (this@ComposeSwingLayerImpl.scene.contentSize.width / density.density).toInt(), - (this@ComposeSwingLayerImpl.scene.contentSize.height / density.density).toInt() + (this@ComposeSwingLayer.scene.contentSize.width / density.density).toInt(), + (this@ComposeSwingLayer.scene.contentSize.height / density.density).toInt() ) } - override val locationOnScreen: Point - @Suppress("ACCIDENTAL_OVERRIDE") // KT-47743 - get() = super.getLocationOnScreen() - - override var density: Density = Density(1f) - private set - private fun resetDensity() { - density = (this as SkiaSwingLayer).density - if (this@ComposeSwingLayerImpl.scene.density != density) { - this@ComposeSwingLayerImpl.scene.density = density + if (this@ComposeSwingLayer.scene.density != density) { + this@ComposeSwingLayer.scene.density = density updateSceneSize() } } @@ -281,240 +131,8 @@ internal class ComposeSwingLayerImpl( keyboardModifiersRequireUpdate = true } - fun setCurrentKeyboardModifiers(modifiers: PointerKeyboardModifiers) { - platform.windowInfo.keyboardModifiers = modifiers - } - override fun getAccessibleContext(): AccessibleContext? { return sceneAccessible.accessibleContext } } - - init { - _component.addInputMethodListener(object : InputMethodListener { - override fun caretPositionChanged(event: InputMethodEvent?) { - if (isDisposed) return - // Which OSes and which input method could produce such events? We need to have some - // specific cases in mind before implementing this - } - - override fun inputMethodTextChanged(event: InputMethodEvent) { - if (isDisposed) return - catchExceptions { - platform.textInputService.inputMethodTextChanged(event) - } - } - }) - - _component.addFocusListener(object : FocusListener { - override fun focusGained(e: FocusEvent) { - // We don't reset focus for Compose when the component loses focus temporary. - // Partially because we don't support restoring focus after clearing it. - // Focus can be lost temporary when another window or popup takes focus. - if (!e.isTemporary) { - scene.requestFocus() - } - } - - override fun focusLost(e: FocusEvent) { - // We don't reset focus for Compose when the component loses focus temporary. - // Partially because we don't support restoring focus after clearing it. - // Focus can be lost temporary when another window or popup takes focus. - if (!e.isTemporary) { - scene.releaseFocus() - } - } - }) - - _component.addMouseListener(object : MouseAdapter() { - override fun mouseClicked(event: MouseEvent) = Unit - override fun mousePressed(event: MouseEvent) = onMouseEvent(event) - override fun mouseReleased(event: MouseEvent) = onMouseEvent(event) - override fun mouseEntered(event: MouseEvent) = onMouseEvent(event) - override fun mouseExited(event: MouseEvent) = onMouseEvent(event) - }) - _component.addMouseMotionListener(object : MouseMotionAdapter() { - override fun mouseDragged(event: MouseEvent) = onMouseEvent(event) - override fun mouseMoved(event: MouseEvent) = onMouseEvent(event) - }) - _component.addMouseWheelListener { event -> - onMouseWheelEvent(event) - } - _component.focusTraversalKeysEnabled = false - _component.addKeyListener(object : KeyAdapter() { - override fun keyPressed(event: KeyEvent) = onKeyEvent(event) - override fun keyReleased(event: KeyEvent) = onKeyEvent(event) - override fun keyTyped(event: KeyEvent) = onKeyEvent(event) - }) - } - - private fun onMouseEvent(event: MouseEvent) = catchExceptions { - // AWT can send events after the window is disposed - if (isDisposed) return@catchExceptions - if (keyboardModifiersRequireUpdate) { - keyboardModifiersRequireUpdate = false - _component.setCurrentKeyboardModifiers(event.keyboardModifiers) - } - scene.onMouseEvent(density, event) - } - - private fun onMouseWheelEvent(event: MouseWheelEvent) = catchExceptions { - if (isDisposed) return@catchExceptions - scene.onMouseWheelEvent(density, event) - } - - private fun onKeyEvent(event: KeyEvent) = catchExceptions { - if (isDisposed) return@catchExceptions - platform.textInputService.onKeyEvent(event) - _component.setCurrentKeyboardModifiers(event.toPointerKeyboardModifiers()) - if (scene.sendKeyEvent(ComposeKeyEvent(event))) { - event.consume() - } - } - - override fun dispose() { - check(!isDisposed) - scene.close() - _component.dispose() - _initContent = null - isDisposed = true - } - - var compositionLocalContext: CompositionLocalContext? by scene::compositionLocalContext - - override fun setContent( - content: @Composable () -> Unit - ) { - // If we call it before attaching, everything probably will be fine, - // but the first composition will be useless, as we set density=1 - // (we don't know the real density if we have unattached component) - _initContent = { - catchExceptions { - scene.setContent( - content = content - ) - } - } - initContent() - } - - private var _initContent: (() -> Unit)? = null - - private fun initContent() { - if (_component.isDisplayable) { - _initContent?.invoke() - _initContent = null - } - } -} - -@Suppress("ControlFlowWithEmptyBody") -@OptIn(ExperimentalComposeUiApi::class) -private fun ComposeScene.onMouseEvent( - density: Float, - event: MouseEvent -) { - val eventType = when (event.id) { - MouseEvent.MOUSE_PRESSED -> PointerEventType.Press - MouseEvent.MOUSE_RELEASED -> PointerEventType.Release - MouseEvent.MOUSE_DRAGGED -> PointerEventType.Move - MouseEvent.MOUSE_MOVED -> PointerEventType.Move - MouseEvent.MOUSE_ENTERED -> PointerEventType.Enter - MouseEvent.MOUSE_EXITED -> PointerEventType.Exit - else -> PointerEventType.Unknown - } - sendPointerEvent( - eventType = eventType, - position = Offset(event.x.toFloat(), event.y.toFloat()) * density, - timeMillis = event.`when`, - type = PointerType.Mouse, - buttons = event.buttons, - keyboardModifiers = event.keyboardModifiers, - nativeEvent = event, - button = event.getPointerButton() - ) -} - -@OptIn(ExperimentalComposeUiApi::class) -private fun MouseEvent.getPointerButton(): PointerButton? { - if (button == MouseEvent.NOBUTTON) return null - return when (button) { - MouseEvent.BUTTON2 -> PointerButton.Tertiary - MouseEvent.BUTTON3 -> PointerButton.Secondary - else -> PointerButton(button - 1) - } -} - -@Suppress("ControlFlowWithEmptyBody") -@OptIn(ExperimentalComposeUiApi::class) -private fun ComposeScene.onMouseWheelEvent( - density: Float, - event: MouseWheelEvent -) { - sendPointerEvent( - eventType = PointerEventType.Scroll, - position = Offset(event.x.toFloat(), event.y.toFloat()) * density, - scrollDelta = if (event.isShiftDown) { - Offset(event.preciseWheelRotation.toFloat(), 0f) - } else { - Offset(0f, event.preciseWheelRotation.toFloat()) - }, - timeMillis = event.`when`, - type = PointerType.Mouse, - buttons = event.buttons, - keyboardModifiers = event.keyboardModifiers, - nativeEvent = event - ) -} - - -@OptIn(ExperimentalComposeUiApi::class) -private val MouseEvent.buttons - get() = PointerButtons( - // We should check [event.button] because of case where [event.modifiersEx] does not provide - // info about the pressed mouse button when using touchpad on MacOS 12 (AWT only). - // When the [Tap to click] feature is activated on Mac OS 12, half of all clicks are not - // handled because [event.modifiersEx] may not provide info about the pressed mouse button. - isPrimaryPressed = ((modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0 - || (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON1)) - && !isMacOsCtrlClick, - isSecondaryPressed = (modifiersEx and MouseEvent.BUTTON3_DOWN_MASK) != 0 - || (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON3) - || isMacOsCtrlClick, - isTertiaryPressed = (modifiersEx and MouseEvent.BUTTON2_DOWN_MASK) != 0 - || (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON2), - isBackPressed = (modifiersEx and MouseEvent.getMaskForButton(4)) != 0 - || (id == MouseEvent.MOUSE_PRESSED && button == 4), - isForwardPressed = (modifiersEx and MouseEvent.getMaskForButton(5)) != 0 - || (id == MouseEvent.MOUSE_PRESSED && button == 5), - ) - -@OptIn(ExperimentalComposeUiApi::class) -private val MouseEvent.keyboardModifiers - get() = PointerKeyboardModifiers( - isCtrlPressed = (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0, - isMetaPressed = (modifiersEx and InputEvent.META_DOWN_MASK) != 0, - isAltPressed = (modifiersEx and InputEvent.ALT_DOWN_MASK) != 0, - isShiftPressed = (modifiersEx and InputEvent.SHIFT_DOWN_MASK) != 0, - isAltGraphPressed = (modifiersEx and InputEvent.ALT_GRAPH_DOWN_MASK) != 0, - isSymPressed = false, - isFunctionPressed = false, - isCapsLockOn = getLockingKeyStateSafe(KeyEvent.VK_CAPS_LOCK), - isScrollLockOn = getLockingKeyStateSafe(KeyEvent.VK_SCROLL_LOCK), - isNumLockOn = getLockingKeyStateSafe(KeyEvent.VK_NUM_LOCK), - ) - -private fun getLockingKeyStateSafe( - mask: Int -): Boolean = try { - Toolkit.getDefaultToolkit().getLockingKeyState(mask) -} catch (_: Exception) { - false -} - -private val MouseEvent.isMacOsCtrlClick - get() = ( - hostOs.isMacOS && - ((modifiersEx and InputEvent.BUTTON1_DOWN_MASK) != 0) && - ((modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0) - ) +} \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt index 4b6590e082074..3ff30eb84753c 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt @@ -49,7 +49,7 @@ internal class ComposeWindowDelegate( // (see https://github.com/JetBrains/compose-jb/issues/1688), // so we nullify layer on dispose, to prevent keeping // big objects in memory (like the whole LayoutNode tree of the window) - private var _layer: ComposeLayer? = ComposeLayer(skiaLayerAnalytics) + private var _layer: ComposeWindowLayer? = ComposeWindowLayer(skiaLayerAnalytics) private val layer get() = requireNotNull(_layer) { "ComposeLayer is disposed" diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt new file mode 100644 index 0000000000000..20cd53a9f8a97 --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2023 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.awt + +import androidx.compose.runtime.CompositionLocalContext +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.window.density +import java.awt.Component +import java.awt.Dimension +import java.awt.Graphics +import java.awt.Window +import java.awt.event.WindowEvent +import java.awt.event.WindowFocusListener +import javax.accessibility.Accessible +import javax.swing.JComponent +import javax.swing.SwingUtilities +import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.SkiaLayerAnalytics + +internal class ComposeWindowLayer( + private val skiaLayerAnalytics: SkiaLayerAnalytics +) : ComposeLayer() { + private val _component = ComponentImpl() + val component: SkiaLayer get() = _component + + override val componentLayer: JComponent + get() = _component + override val focusComponentDelegate: Component + get() = _component.canvas + + var compositionLocalContext: CompositionLocalContext? by scene::compositionLocalContext + + init { + _component.skikoView = skikoView + attachComposeToComponent() + } + + override fun requestNativeFocusOnAccessible(accessible: Accessible) { + _component.requestNativeFocusOnAccessible(accessible) + } + + override fun onComposeInvalidation() { + _component.needRedraw() + } + + override fun disposeComponentLayer() { + _component.dispose() + } + + private inner class ComponentImpl : + SkiaLayer( + externalAccessibleFactory = { sceneAccessible }, + analytics = skiaLayerAnalytics + ), + Accessible { + private var window: Window? = null + private var windowListener = object : WindowFocusListener { + override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() + override fun windowLostFocus(e: WindowEvent) = refreshWindowFocus() + } + + override fun addNotify() { + super.addNotify() + resetDensity() + initContent() + updateSceneSize() + window = SwingUtilities.getWindowAncestor(this) + window?.addWindowFocusListener(windowListener) + refreshWindowFocus() + } + + override fun removeNotify() { + window?.removeWindowFocusListener(windowListener) + window = null + refreshWindowFocus() + super.removeNotify() + } + + override fun paint(g: Graphics) { + resetDensity() + super.paint(g) + } + + override fun getInputMethodRequests() = currentInputMethodRequests + + override fun doLayout() { + super.doLayout() + updateSceneSize() + } + + private fun updateSceneSize() { + this@ComposeWindowLayer.scene.constraints = Constraints( + maxWidth = (width * density.density).toInt().coerceAtLeast(0), + maxHeight = (height * density.density).toInt().coerceAtLeast(0) + ) + } + + override fun getPreferredSize(): Dimension { + return if (isPreferredSizeSet) super.getPreferredSize() else Dimension( + (this@ComposeWindowLayer.scene.contentSize.width / density.density).toInt(), + (this@ComposeWindowLayer.scene.contentSize.height / density.density).toInt() + ) + } + + private fun resetDensity() { + if (this@ComposeWindowLayer.scene.density != density) { + this@ComposeWindowLayer.scene.density = density + updateSceneSize() + } + } + + private fun refreshWindowFocus() { + this@ComposeWindowLayer.platform.windowInfo.isWindowFocused = window?.isFocused ?: false + keyboardModifiersRequireUpdate = true + } + } +} From c438c71d94f8dcc3400c5fbb4e294571cdf636ff Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 12 Jun 2023 16:48:38 +0200 Subject: [PATCH 03/19] Make ComposeLayer as base class for standalone window and swing interop --- .../kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index fa1e11a60118b..d13a44171e165 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -68,8 +68,6 @@ internal class ComposeSwingLayer( @OptIn(ExperimentalSkikoApi::class) private inner class ComponentImpl : SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { - var currentInputMethodRequests: InputMethodRequests? = null - private var window: Window? = null private var windowListener = object : WindowFocusListener { override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() From b3315067b51ce22addb579fc09ef022afac36697 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 12 Jun 2023 17:18:58 +0200 Subject: [PATCH 04/19] Move functions to ComposeLayer base class --- .../compose/ui/awt/ComposeLayer.desktop.kt | 50 +++++++++++++++-- .../ui/awt/ComposeSwingLayer.desktop.kt | 56 ++++--------------- .../compose/ui/awt/ComposeWindowLayer.kt | 54 ++++-------------- 3 files changed, 66 insertions(+), 94 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index fd5d40c5dcc2b..4a0a0a4932d02 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -29,16 +29,14 @@ import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.toPointerKeyboardModifiers +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowExceptionHandler import androidx.compose.ui.window.density import androidx.compose.ui.window.layoutDirection -import java.awt.Component -import java.awt.Cursor -import java.awt.Point -import java.awt.Toolkit +import java.awt.* import java.awt.event.* import java.awt.event.KeyEvent import java.awt.im.InputMethodRequests @@ -70,6 +68,13 @@ internal abstract class ComposeLayer { protected var currentInputMethodRequests: InputMethodRequests? = null + private val windowFocusListener = object : WindowFocusListener { + override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() + override fun windowLostFocus(e: WindowEvent) = refreshWindowFocus() + } + + private var window: Window? = null + private val platformComponent: PlatformComponent = object : PlatformComponent { override fun enableInput(inputMethodRequests: InputMethodRequests) { currentInputMethodRequests = inputMethodRequests @@ -138,6 +143,12 @@ internal abstract class ComposeLayer { } } + protected val sceneDimension: Dimension + get() = Dimension( + (scene.contentSize.width / componentLayer.density.density).toInt(), + (scene.contentSize.height / componentLayer.density.density).toInt() + ) + private val density get() = platformComponent.density.density /** @@ -277,6 +288,37 @@ internal abstract class ComposeLayer { platform.windowInfo.keyboardModifiers = modifiers } + protected fun updateSceneSize() { + scene.constraints = Constraints( + maxWidth = (componentLayer.width * componentLayer.density.density).toInt() + .coerceAtLeast(0), + maxHeight = (componentLayer.height * componentLayer.density.density).toInt() + .coerceAtLeast(0) + ) + } + + protected fun resetSceneDensity() { + if (scene.density != componentLayer.density) { + scene.density = componentLayer.density + updateSceneSize() + } + } + + protected fun updateWindowState(window: Window?) { + if (window != null) { + window.addWindowFocusListener(windowFocusListener) + } else { + window?.removeWindowFocusListener(windowFocusListener) + } + this.window = window + refreshWindowFocus() + } + + private fun refreshWindowFocus() { + platform.windowInfo.isWindowFocused = window?.isFocused ?: false + keyboardModifiersRequireUpdate = true + } + protected inner class DesktopPlatform : Platform by Platform.Empty { override fun setPointerIcon(pointerIcon: PointerIcon) { componentLayer.cursor = diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index d13a44171e165..3700c45268745 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -18,17 +18,10 @@ package androidx.compose.ui.awt -import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.window.density import java.awt.Component import java.awt.Dimension import java.awt.Graphics -import java.awt.Window -import java.awt.event.WindowEvent -import java.awt.event.WindowFocusListener -import java.awt.im.InputMethodRequests import javax.accessibility.Accessible import javax.accessibility.AccessibleContext import javax.swing.JComponent @@ -41,7 +34,7 @@ import org.jetbrains.skiko.swing.SkiaSwingLayerComponent internal class ComposeSwingLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics ) : ComposeLayer() { - private val _component = ComponentImpl() + private val _component = ComposeSwingSkiaLayer() val component: SkiaSwingLayerComponent get() = _component override val componentLayer: JComponent @@ -65,34 +58,27 @@ internal class ComposeSwingLayer( _component.dispose() } + /** + * See also backendLayer for standalone compose [androidx.compose.ui.awt.ComposeWindowLayer.ComposeWindowSkiaLayer] + */ @OptIn(ExperimentalSkikoApi::class) - private inner class ComponentImpl : + private inner class ComposeSwingSkiaLayer : SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { - private var window: Window? = null - private var windowListener = object : WindowFocusListener { - override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() - override fun windowLostFocus(e: WindowEvent) = refreshWindowFocus() - } - override fun addNotify() { super.addNotify() - resetDensity() + resetSceneDensity() initContent() updateSceneSize() - window = SwingUtilities.getWindowAncestor(this) - window?.addWindowFocusListener(windowListener) - refreshWindowFocus() + updateWindowState(SwingUtilities.getWindowAncestor(this)) } override fun removeNotify() { - window?.removeWindowFocusListener(windowListener) - window = null - refreshWindowFocus() + updateWindowState(window = null) super.removeNotify() } override fun paint(g: Graphics) { - resetDensity() + resetSceneDensity() super.paint(g) } @@ -103,30 +89,8 @@ internal class ComposeSwingLayer( updateSceneSize() } - private fun updateSceneSize() { - this@ComposeSwingLayer.scene.constraints = Constraints( - maxWidth = (width * density.density).toInt().coerceAtLeast(0), - maxHeight = (height * density.density).toInt().coerceAtLeast(0) - ) - } - override fun getPreferredSize(): Dimension { - return if (isPreferredSizeSet) super.getPreferredSize() else Dimension( - (this@ComposeSwingLayer.scene.contentSize.width / density.density).toInt(), - (this@ComposeSwingLayer.scene.contentSize.height / density.density).toInt() - ) - } - - private fun resetDensity() { - if (this@ComposeSwingLayer.scene.density != density) { - this@ComposeSwingLayer.scene.density = density - updateSceneSize() - } - } - - private fun refreshWindowFocus() { - platform.windowInfo.isWindowFocused = window?.isFocused ?: false - keyboardModifiersRequireUpdate = true + return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension } override fun getAccessibleContext(): AccessibleContext? { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt index 20cd53a9f8a97..1f539d483ebcc 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -17,14 +17,9 @@ package androidx.compose.ui.awt import androidx.compose.runtime.CompositionLocalContext -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.window.density import java.awt.Component import java.awt.Dimension import java.awt.Graphics -import java.awt.Window -import java.awt.event.WindowEvent -import java.awt.event.WindowFocusListener import javax.accessibility.Accessible import javax.swing.JComponent import javax.swing.SwingUtilities @@ -34,7 +29,7 @@ import org.jetbrains.skiko.SkiaLayerAnalytics internal class ComposeWindowLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics ) : ComposeLayer() { - private val _component = ComponentImpl() + private val _component = ComposeWindowSkiaLayer() val component: SkiaLayer get() = _component override val componentLayer: JComponent @@ -61,37 +56,30 @@ internal class ComposeWindowLayer( _component.dispose() } - private inner class ComponentImpl : + /** + * See also backend layer for swing interop: [androidx.compose.ui.awt.ComposeSwingLayer.ComposeSwingSkiaLayer] + */ + private inner class ComposeWindowSkiaLayer : SkiaLayer( externalAccessibleFactory = { sceneAccessible }, analytics = skiaLayerAnalytics ), Accessible { - private var window: Window? = null - private var windowListener = object : WindowFocusListener { - override fun windowGainedFocus(e: WindowEvent) = refreshWindowFocus() - override fun windowLostFocus(e: WindowEvent) = refreshWindowFocus() - } - override fun addNotify() { super.addNotify() - resetDensity() + resetSceneDensity() initContent() updateSceneSize() - window = SwingUtilities.getWindowAncestor(this) - window?.addWindowFocusListener(windowListener) - refreshWindowFocus() + updateWindowState(SwingUtilities.getWindowAncestor(this)) } override fun removeNotify() { - window?.removeWindowFocusListener(windowListener) - window = null - refreshWindowFocus() + updateWindowState(null) super.removeNotify() } override fun paint(g: Graphics) { - resetDensity() + resetSceneDensity() super.paint(g) } @@ -102,30 +90,8 @@ internal class ComposeWindowLayer( updateSceneSize() } - private fun updateSceneSize() { - this@ComposeWindowLayer.scene.constraints = Constraints( - maxWidth = (width * density.density).toInt().coerceAtLeast(0), - maxHeight = (height * density.density).toInt().coerceAtLeast(0) - ) - } - override fun getPreferredSize(): Dimension { - return if (isPreferredSizeSet) super.getPreferredSize() else Dimension( - (this@ComposeWindowLayer.scene.contentSize.width / density.density).toInt(), - (this@ComposeWindowLayer.scene.contentSize.height / density.density).toInt() - ) - } - - private fun resetDensity() { - if (this@ComposeWindowLayer.scene.density != density) { - this@ComposeWindowLayer.scene.density = density - updateSceneSize() - } - } - - private fun refreshWindowFocus() { - this@ComposeWindowLayer.platform.windowInfo.isWindowFocused = window?.isFocused ?: false - keyboardModifiersRequireUpdate = true + return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension } } } From 54c5e3600f50ce1a4c65bd9c03bfe26c246e1297 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 12 Jun 2023 17:26:49 +0200 Subject: [PATCH 05/19] Cleanup --- .../compose/ui/awt/ComposeLayer.desktop.kt | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 4a0a0a4932d02..18a36de3e825d 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -162,7 +162,7 @@ internal abstract class ComposeLayer { * TODO: needs to be set `true` when focus changes: * (Window focus change is implemented in JB fork, but not upstreamed yet). */ - protected var keyboardModifiersRequireUpdate = false + private var keyboardModifiersRequireUpdate = false @OptIn(ExperimentalComposeUiApi::class) protected fun attachComposeToComponent() { @@ -305,11 +305,8 @@ internal abstract class ComposeLayer { } protected fun updateWindowState(window: Window?) { - if (window != null) { - window.addWindowFocusListener(windowFocusListener) - } else { - window?.removeWindowFocusListener(windowFocusListener) - } + this.window?.removeWindowFocusListener(windowFocusListener) + window?.addWindowFocusListener(windowFocusListener) this.window = window refreshWindowFocus() } @@ -382,8 +379,6 @@ internal abstract class ComposeLayer { } } -@Suppress("ControlFlowWithEmptyBody") -@OptIn(ExperimentalComposeUiApi::class) private fun ComposeScene.onMouseEvent( density: Float, event: MouseEvent @@ -409,7 +404,6 @@ private fun ComposeScene.onMouseEvent( ) } -@OptIn(ExperimentalComposeUiApi::class) private fun MouseEvent.getPointerButton(): PointerButton? { if (button == MouseEvent.NOBUTTON) return null return when (button) { @@ -419,8 +413,6 @@ private fun MouseEvent.getPointerButton(): PointerButton? { } } -@Suppress("ControlFlowWithEmptyBody") -@OptIn(ExperimentalComposeUiApi::class) private fun ComposeScene.onMouseWheelEvent( density: Float, event: MouseWheelEvent @@ -442,7 +434,6 @@ private fun ComposeScene.onMouseWheelEvent( } -@OptIn(ExperimentalComposeUiApi::class) private val MouseEvent.buttons get() = PointerButtons( // We should check [event.button] because of case where [event.modifiersEx] does not provide // info about the pressed mouse button when using touchpad on MacOS 12 (AWT only). @@ -462,7 +453,6 @@ private val MouseEvent.buttons get() = PointerButtons( || (id == MouseEvent.MOUSE_PRESSED && button == 5), ) -@OptIn(ExperimentalComposeUiApi::class) private val MouseEvent.keyboardModifiers get() = PointerKeyboardModifiers( isCtrlPressed = (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0, isMetaPressed = (modifiersEx and InputEvent.META_DOWN_MASK) != 0, From 108036437a3356c2c9e65cf8f7a79252e09d9d5e Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 12 Jun 2023 17:42:32 +0200 Subject: [PATCH 06/19] Add some docs --- .../androidx/compose/ui/awt/ComposeLayer.desktop.kt | 6 ++++++ .../androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt | 8 ++++++++ .../kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 18a36de3e825d..18f19629408ac 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -51,6 +51,12 @@ import org.jetbrains.skiko.SkikoInput import org.jetbrains.skiko.SkikoView import org.jetbrains.skiko.hostOs +/** + * Provides a base implementation for integrating a Compose scene with AWT/Swing. + * It allows setting Compose content by [setContent], this content should be drawn on [componentLayer]. + * + * Inheritors should call [attachComposeToComponent], so events that came to [componentLayer] will be transferred to [ComposeScene] + */ internal abstract class ComposeLayer { private var isDisposed = false diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index 3700c45268745..ec6d9af1a6f90 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -31,6 +31,14 @@ import org.jetbrains.skiko.SkiaLayerAnalytics import org.jetbrains.skiko.swing.SkiaSwingLayer import org.jetbrains.skiko.swing.SkiaSwingLayerComponent +/** + * Provides [component] that can be used as a Swing component. + * Content set in [setContent] will be drawn on this [component]. + * + * [ComposeSwingLayer] provides smooth integration with Swing, so z-ordering, double-buffering etc. from Swing will be taken into account. + * + * However, if smooth interop with Swing is not needed, consider using [androidx.compose.ui.awt.ComposeWindowLayer] + */ internal class ComposeSwingLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics ) : ComposeLayer() { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt index 1f539d483ebcc..22c73222a6aab 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -26,6 +26,11 @@ import javax.swing.SwingUtilities import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayerAnalytics +/** + * Provides a heavyweight AWT [component] used to render content (from [setContent]) on-screen with Skia. + * + * If smooth interop with Swing is needed, consider using [androidx.compose.ui.awt.ComposeWindowLayer] + */ internal class ComposeWindowLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics ) : ComposeLayer() { From 49d087ec42f481df7c03545146e905afae5c11e8 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 12 Jun 2023 18:09:10 +0200 Subject: [PATCH 07/19] Allow choosing off-screen and on-screen rendering for ComposePanel by system property --- .../compose/ui/awt/ComposeLayer.desktop.kt | 77 ++++++++++--------- .../compose/ui/awt/ComposePanel.desktop.kt | 45 ++++++----- .../ui/awt/ComposeSwingLayer.desktop.kt | 15 ++-- .../ui/awt/ComposeWindowDelegate.desktop.kt | 8 +- .../compose/ui/awt/ComposeWindowLayer.kt | 18 +++-- 5 files changed, 92 insertions(+), 71 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 18f19629408ac..712edc458fe43 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -20,6 +20,7 @@ package androidx.compose.ui.awt import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.ComposeScene import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.focus.FocusDirection @@ -37,6 +38,7 @@ import androidx.compose.ui.window.WindowExceptionHandler import androidx.compose.ui.window.density import androidx.compose.ui.window.layoutDirection import java.awt.* +import java.awt.Cursor import java.awt.event.* import java.awt.event.KeyEvent import java.awt.im.InputMethodRequests @@ -46,18 +48,15 @@ import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineExceptionHandler import org.jetbrains.skia.Canvas -import org.jetbrains.skiko.MainUIDispatcher -import org.jetbrains.skiko.SkikoInput -import org.jetbrains.skiko.SkikoView -import org.jetbrains.skiko.hostOs +import org.jetbrains.skiko.* /** * Provides a base implementation for integrating a Compose scene with AWT/Swing. - * It allows setting Compose content by [setContent], this content should be drawn on [componentLayer]. + * It allows setting Compose content by [setContent], this content should be drawn on [component]. * - * Inheritors should call [attachComposeToComponent], so events that came to [componentLayer] will be transferred to [ComposeScene] + * Inheritors should call [attachComposeToComponent], so events that came to [component] will be transferred to [ComposeScene] */ -internal abstract class ComposeLayer { +internal abstract class ComposeLayer { private var isDisposed = false val sceneAccessible = ComposeSceneAccessible( @@ -65,7 +64,11 @@ internal abstract class ComposeLayer { mainOwnerProvider = { scene.mainOwner } ) - protected abstract val componentLayer: JComponent + abstract val component: T + + abstract val renderApi: GraphicsApi + + abstract val clipComponents: MutableList // Needed for case when componentLayer is a wrapper for another Component that need to acquire focus events // e.g. canvas in case of ComposeWindowLayer @@ -84,9 +87,9 @@ internal abstract class ComposeLayer { private val platformComponent: PlatformComponent = object : PlatformComponent { override fun enableInput(inputMethodRequests: InputMethodRequests) { currentInputMethodRequests = inputMethodRequests - componentLayer.enableInputMethods(true) + component.enableInputMethods(true) val focusGainedEvent = FocusEvent(focusComponentDelegate, FocusEvent.FOCUS_GAINED) - componentLayer.inputContext.dispatchEvent(focusGainedEvent) + component.inputContext.dispatchEvent(focusGainedEvent) } override fun disableInput() { @@ -94,9 +97,9 @@ internal abstract class ComposeLayer { } override val locationOnScreen: Point - get() = componentLayer.locationOnScreen + get() = component.locationOnScreen override val density: Density - get() = componentLayer.density + get() = component.density } protected abstract fun requestNativeFocusOnAccessible(accessible: Accessible) @@ -136,6 +139,8 @@ internal abstract class ComposeLayer { }, ) + var compositionLocalContext: CompositionLocalContext? by scene::compositionLocalContext + protected val skikoView = object : SkikoView { override val input: SkikoInput get() = object : SkikoInput { @@ -151,8 +156,8 @@ internal abstract class ComposeLayer { protected val sceneDimension: Dimension get() = Dimension( - (scene.contentSize.width / componentLayer.density.density).toInt(), - (scene.contentSize.height / componentLayer.density.density).toInt() + (scene.contentSize.width / component.density.density).toInt(), + (scene.contentSize.height / component.density.density).toInt() ) private val density get() = platformComponent.density.density @@ -172,7 +177,7 @@ internal abstract class ComposeLayer { @OptIn(ExperimentalComposeUiApi::class) protected fun attachComposeToComponent() { - componentLayer.addInputMethodListener(object : InputMethodListener { + component.addInputMethodListener(object : InputMethodListener { override fun caretPositionChanged(event: InputMethodEvent?) { if (isDisposed) return // Which OSes and which input method could produce such events? We need to have some @@ -187,7 +192,7 @@ internal abstract class ComposeLayer { } }) - componentLayer.addFocusListener(object : FocusListener { + component.addFocusListener(object : FocusListener { override fun focusGained(e: FocusEvent) { // We don't reset focus for Compose when the component loses focus temporary. // Partially because we don't support restoring focus after clearing it. @@ -207,29 +212,29 @@ internal abstract class ComposeLayer { } }) - componentLayer.addMouseListener(object : MouseAdapter() { + component.addMouseListener(object : MouseAdapter() { override fun mouseClicked(event: MouseEvent) = Unit override fun mousePressed(event: MouseEvent) = onMouseEvent(event) override fun mouseReleased(event: MouseEvent) = onMouseEvent(event) override fun mouseEntered(event: MouseEvent) = onMouseEvent(event) override fun mouseExited(event: MouseEvent) = onMouseEvent(event) }) - componentLayer.addMouseMotionListener(object : MouseMotionAdapter() { + component.addMouseMotionListener(object : MouseMotionAdapter() { override fun mouseDragged(event: MouseEvent) = onMouseEvent(event) override fun mouseMoved(event: MouseEvent) = onMouseEvent(event) }) - componentLayer.addMouseWheelListener { event -> + component.addMouseWheelListener { event -> onMouseWheelEvent(event) } - componentLayer.focusTraversalKeysEnabled = false - componentLayer.addKeyListener(object : KeyAdapter() { + component.focusTraversalKeysEnabled = false + component.addKeyListener(object : KeyAdapter() { override fun keyPressed(event: KeyEvent) = onKeyEvent(event) override fun keyReleased(event: KeyEvent) = onKeyEvent(event) override fun keyTyped(event: KeyEvent) = onKeyEvent(event) }) } - private fun onMouseEvent(event: MouseEvent) = catchExceptions { + private fun onMouseEvent(event: MouseEvent): Unit = catchExceptions { // AWT can send events after the window is disposed if (isDisposed) return@catchExceptions if (keyboardModifiersRequireUpdate) { @@ -239,7 +244,7 @@ internal abstract class ComposeLayer { scene.onMouseEvent(density, event) } - private fun onMouseWheelEvent(event: MouseWheelEvent) = catchExceptions { + private fun onMouseWheelEvent(event: MouseWheelEvent): Unit = catchExceptions { if (isDisposed) return@catchExceptions scene.onMouseWheelEvent(density, event) } @@ -284,7 +289,7 @@ internal abstract class ComposeLayer { private var _initContent: (() -> Unit)? = null protected fun initContent() { - if (componentLayer.isDisplayable) { + if (component.isDisplayable) { _initContent?.invoke() _initContent = null } @@ -296,16 +301,16 @@ internal abstract class ComposeLayer { protected fun updateSceneSize() { scene.constraints = Constraints( - maxWidth = (componentLayer.width * componentLayer.density.density).toInt() + maxWidth = (component.width * component.density.density).toInt() .coerceAtLeast(0), - maxHeight = (componentLayer.height * componentLayer.density.density).toInt() + maxHeight = (component.height * component.density.density).toInt() .coerceAtLeast(0) ) } protected fun resetSceneDensity() { - if (scene.density != componentLayer.density) { - scene.density = componentLayer.density + if (scene.density != component.density) { + scene.density = component.density updateSceneSize() } } @@ -324,7 +329,7 @@ internal abstract class ComposeLayer { protected inner class DesktopPlatform : Platform by Platform.Empty { override fun setPointerIcon(pointerIcon: PointerIcon) { - componentLayer.cursor = + component.cursor = (pointerIcon as? AwtCursor)?.cursor ?: Cursor(Cursor.DEFAULT_CURSOR) } @@ -338,20 +343,20 @@ internal abstract class ComposeLayer { override val textInputService = PlatformInput(platformComponent) override val layoutDirection: LayoutDirection - get() = componentLayer.layoutDirection + get() = component.layoutDirection override val focusManager = object : FocusManager { override fun clearFocus(force: Boolean) { - val root = componentLayer.rootPane + val root = component.rootPane root?.focusTraversalPolicy?.getDefaultComponent(root)?.requestFocusInWindow() } override fun moveFocus(focusDirection: FocusDirection): Boolean = when (focusDirection) { FocusDirection.Next -> { - val toFocus = componentLayer.focusCycleRootAncestor?.let { root -> + val toFocus = component.focusCycleRootAncestor?.let { root -> val policy = root.focusTraversalPolicy - policy.getComponentAfter(root, componentLayer) + policy.getComponentAfter(root, component) ?: policy.getDefaultComponent(root) } val hasFocus = toFocus?.hasFocus() == true @@ -359,9 +364,9 @@ internal abstract class ComposeLayer { } FocusDirection.Previous -> { - val toFocus = componentLayer.focusCycleRootAncestor?.let { root -> + val toFocus = component.focusCycleRootAncestor?.let { root -> val policy = root.focusTraversalPolicy - policy.getComponentBefore(root, componentLayer) + policy.getComponentBefore(root, component) ?: policy.getDefaultComponent(root) } val hasFocus = toFocus?.hasFocus() == true @@ -373,7 +378,7 @@ internal abstract class ComposeLayer { } override fun requestFocusForOwner(): Boolean { - return componentLayer.hasFocus() || componentLayer.requestFocusInWindow() + return component.hasFocus() || component.requestFocusInWindow() } override val viewConfiguration = object : ViewConfiguration { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index f52794dac34f1..fa5b5c0f4a248 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -20,17 +20,14 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.window.WindowExceptionHandler -import org.jetbrains.skiko.ClipComponent -import org.jetbrains.skiko.GraphicsApi -import java.awt.Color -import java.awt.Component -import java.awt.Container -import java.awt.Dimension -import java.awt.FocusTraversalPolicy +import java.awt.* import java.awt.event.FocusEvent import java.awt.event.FocusListener +import javax.swing.JComponent import javax.swing.JLayeredPane import javax.swing.SwingUtilities.isEventDispatchThread +import org.jetbrains.skiko.ClipComponent +import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.SkiaLayerAnalytics /** @@ -77,7 +74,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( private val _focusListeners = mutableSetOf() private var _isFocusable = true private var _isRequestFocusEnabled = false - private var layer: ComposeSwingLayer? = null + private var layer: ComposeLayer? = null private val clipMap = mutableMapOf() private var content: (@Composable () -> Unit)? = null @@ -133,23 +130,35 @@ class ComposePanel @ExperimentalComposeUiApi constructor( } val clipComponent = ClipComponent(component) clipMap[component] = clipComponent - layer!!.component.clipComponents.add(clipComponent) + layer!!.clipComponents.add(clipComponent) return super.add(component, Integer.valueOf(0)) } override fun remove(component: Component) { - layer!!.component.clipComponents.remove(clipMap[component]!!) + layer!!.clipComponents.remove(clipMap[component]!!) clipMap.remove(component) super.remove(component) } - @OptIn(ExperimentalComposeUiApi::class) override fun addNotify() { super.addNotify() // After [super.addNotify] is called we can safely initialize the layer and composable // content. - layer = ComposeSwingLayer(skiaLayerAnalytics).apply { + layer = createComposeLayer() + initContent() + super.add(layer!!.component, Integer.valueOf(1)) + } + + @OptIn(ExperimentalComposeUiApi::class) + private fun createComposeLayer(): ComposeLayer { + val renderOnGraphics = System.getProperty("compose.swing.render.on.graphics") == "true" + val layer: ComposeLayer = if (renderOnGraphics) { + ComposeSwingLayer(skiaLayerAnalytics) + } else { + ComposeWindowLayer(skiaLayerAnalytics) + } + return layer.apply { scene.releaseFocus() component.setSize(width, height) component.isFocusable = _isFocusable @@ -161,14 +170,16 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // The focus can be switched from the child component inside SwingPanel. // In that case, SwingPanel will take care of it. if (!isParentOf(e.oppositeComponent)) { - layer?.scene?.requestFocus() + layer.scene.requestFocus() when (e.cause) { FocusEvent.Cause.TRAVERSAL_FORWARD -> { - layer?.scene?.moveFocus(FocusDirection.Next) + layer.scene.moveFocus(FocusDirection.Next) } + FocusEvent.Cause.TRAVERSAL_BACKWARD -> { - layer?.scene?.moveFocus(FocusDirection.Previous) + layer.scene.moveFocus(FocusDirection.Previous) } + else -> Unit } } @@ -177,8 +188,6 @@ class ComposePanel @ExperimentalComposeUiApi constructor( override fun focusLost(e: FocusEvent) = Unit }) } - initContent() - super.add(layer!!.component, Integer.valueOf(1)) } override fun removeNotify() { @@ -257,5 +266,5 @@ class ComposePanel @ExperimentalComposeUiApi constructor( * environment variable. */ val renderApi: GraphicsApi - get() = if (layer != null) layer!!.component.renderApi else GraphicsApi.UNKNOWN + get() = if (layer != null) layer!!.renderApi else GraphicsApi.UNKNOWN } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index ec6d9af1a6f90..d1c56157d8627 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -24,9 +24,10 @@ import java.awt.Dimension import java.awt.Graphics import javax.accessibility.Accessible import javax.accessibility.AccessibleContext -import javax.swing.JComponent import javax.swing.SwingUtilities +import org.jetbrains.skiko.ClipRectangle import org.jetbrains.skiko.ExperimentalSkikoApi +import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.SkiaLayerAnalytics import org.jetbrains.skiko.swing.SkiaSwingLayer import org.jetbrains.skiko.swing.SkiaSwingLayerComponent @@ -41,12 +42,16 @@ import org.jetbrains.skiko.swing.SkiaSwingLayerComponent */ internal class ComposeSwingLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeLayer() { +) : ComposeLayer() { private val _component = ComposeSwingSkiaLayer() - val component: SkiaSwingLayerComponent get() = _component + override val component: SkiaSwingLayerComponent get() = _component + + override val renderApi: GraphicsApi + get() = _component.renderApi + + override val clipComponents: MutableList + get() = _component.clipComponents - override val componentLayer: JComponent - get() = _component override val focusComponentDelegate: Component get() = _component diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt index 3ff30eb84753c..ff5c593ff88a8 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt @@ -49,7 +49,7 @@ internal class ComposeWindowDelegate( // (see https://github.com/JetBrains/compose-jb/issues/1688), // so we nullify layer on dispose, to prevent keeping // big objects in memory (like the whole LayoutNode tree of the window) - private var _layer: ComposeWindowLayer? = ComposeWindowLayer(skiaLayerAnalytics) + private var _layer: ComposeLayer? = ComposeWindowLayer(skiaLayerAnalytics) private val layer get() = requireNotNull(_layer) { "ComposeLayer is disposed" @@ -67,12 +67,12 @@ internal class ComposeWindowDelegate( override fun add(component: Component): Component { val clipComponent = ClipComponent(component) clipMap[component] = clipComponent - layer.component.clipComponents.add(clipComponent) + layer.clipComponents.add(clipComponent) return add(component, Integer.valueOf(0)) } override fun remove(component: Component) { - layer.component.clipComponents.remove(clipMap[component]!!) + layer.clipComponents.remove(clipMap[component]!!) clipMap.remove(component) super.remove(component) } @@ -223,7 +223,7 @@ internal class ComposeWindowDelegate( get() = layer.component.windowHandle val renderApi: GraphicsApi - get() = layer.component.renderApi + get() = layer.renderApi var isTransparent: Boolean get() = layer.component.transparency diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt index 22c73222a6aab..834b659a9438a 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -16,13 +16,13 @@ package androidx.compose.ui.awt -import androidx.compose.runtime.CompositionLocalContext import java.awt.Component import java.awt.Dimension import java.awt.Graphics import javax.accessibility.Accessible -import javax.swing.JComponent import javax.swing.SwingUtilities +import org.jetbrains.skiko.ClipRectangle +import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayerAnalytics @@ -33,17 +33,19 @@ import org.jetbrains.skiko.SkiaLayerAnalytics */ internal class ComposeWindowLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeLayer() { +) : ComposeLayer() { private val _component = ComposeWindowSkiaLayer() - val component: SkiaLayer get() = _component + override val component: SkiaLayer get() = _component + + override val renderApi: GraphicsApi + get() = _component.renderApi + + override val clipComponents: MutableList + get() = _component.clipComponents - override val componentLayer: JComponent - get() = _component override val focusComponentDelegate: Component get() = _component.canvas - var compositionLocalContext: CompositionLocalContext? by scene::compositionLocalContext - init { _component.skikoView = skikoView attachComposeToComponent() From eaabd757833425c918869592d80230e08a7b2ea4 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Fri, 16 Jun 2023 16:40:01 +0200 Subject: [PATCH 08/19] Get rid of generic type --- .../kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt | 4 ++-- .../kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt | 6 +++--- .../androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt | 2 +- .../compose/ui/awt/ComposeWindowDelegate.desktop.kt | 2 +- .../kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 712edc458fe43..11acecbd69cec 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -56,7 +56,7 @@ import org.jetbrains.skiko.* * * Inheritors should call [attachComposeToComponent], so events that came to [component] will be transferred to [ComposeScene] */ -internal abstract class ComposeLayer { +internal abstract class ComposeLayer { private var isDisposed = false val sceneAccessible = ComposeSceneAccessible( @@ -64,7 +64,7 @@ internal abstract class ComposeLayer { mainOwnerProvider = { scene.mainOwner } ) - abstract val component: T + abstract val component: JComponent abstract val renderApi: GraphicsApi diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index fa5b5c0f4a248..078133e5ffcba 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -74,7 +74,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( private val _focusListeners = mutableSetOf() private var _isFocusable = true private var _isRequestFocusEnabled = false - private var layer: ComposeLayer? = null + private var layer: ComposeLayer? = null private val clipMap = mutableMapOf() private var content: (@Composable () -> Unit)? = null @@ -151,9 +151,9 @@ class ComposePanel @ExperimentalComposeUiApi constructor( } @OptIn(ExperimentalComposeUiApi::class) - private fun createComposeLayer(): ComposeLayer { + private fun createComposeLayer(): ComposeLayer { val renderOnGraphics = System.getProperty("compose.swing.render.on.graphics") == "true" - val layer: ComposeLayer = if (renderOnGraphics) { + val layer: ComposeLayer = if (renderOnGraphics) { ComposeSwingLayer(skiaLayerAnalytics) } else { ComposeWindowLayer(skiaLayerAnalytics) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index d1c56157d8627..e855682860690 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -42,7 +42,7 @@ import org.jetbrains.skiko.swing.SkiaSwingLayerComponent */ internal class ComposeSwingLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeLayer() { +) : ComposeLayer() { private val _component = ComposeSwingSkiaLayer() override val component: SkiaSwingLayerComponent get() = _component diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt index ff5c593ff88a8..8606480e114da 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt @@ -49,7 +49,7 @@ internal class ComposeWindowDelegate( // (see https://github.com/JetBrains/compose-jb/issues/1688), // so we nullify layer on dispose, to prevent keeping // big objects in memory (like the whole LayoutNode tree of the window) - private var _layer: ComposeLayer? = ComposeWindowLayer(skiaLayerAnalytics) + private var _layer: ComposeWindowLayer? = ComposeWindowLayer(skiaLayerAnalytics) private val layer get() = requireNotNull(_layer) { "ComposeLayer is disposed" diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt index 834b659a9438a..d778ce6021c30 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -33,7 +33,7 @@ import org.jetbrains.skiko.SkiaLayerAnalytics */ internal class ComposeWindowLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeLayer() { +) : ComposeLayer() { private val _component = ComposeWindowSkiaLayer() override val component: SkiaLayer get() = _component From 2e8fa9d42e957bb106b462480859226f3f3464b4 Mon Sep 17 00:00:00 2001 From: Nikolai Rykunov Date: Fri, 16 Jun 2023 16:46:06 +0200 Subject: [PATCH 09/19] Update compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt Co-authored-by: Ivan Matkov --- .../kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 11acecbd69cec..44b407b8383cf 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -98,6 +98,7 @@ internal abstract class ComposeLayer { override val locationOnScreen: Point get() = component.locationOnScreen + override val density: Density get() = component.density } From fb8fb1098181405ca091e9ab0b08eaefe0310e77 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 21 Jun 2023 15:11:45 +0200 Subject: [PATCH 10/19] Formatting --- .../kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt | 1 + .../kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index e855682860690..c8437535cda72 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -77,6 +77,7 @@ internal class ComposeSwingLayer( @OptIn(ExperimentalSkikoApi::class) private inner class ComposeSwingSkiaLayer : SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { + override fun addNotify() { super.addNotify() resetSceneDensity() diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt index d778ce6021c30..cf7cb10152c4b 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -72,6 +72,7 @@ internal class ComposeWindowLayer( analytics = skiaLayerAnalytics ), Accessible { + override fun addNotify() { super.addNotify() resetSceneDensity() From 261c8343e6caa3ea8730d37b25ec88d6a5f7fee7 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 21 Jun 2023 15:14:47 +0200 Subject: [PATCH 11/19] Use toBoolean for system property --- .../kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index 078133e5ffcba..cb6797f861042 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -152,7 +152,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( @OptIn(ExperimentalComposeUiApi::class) private fun createComposeLayer(): ComposeLayer { - val renderOnGraphics = System.getProperty("compose.swing.render.on.graphics") == "true" + val renderOnGraphics = System.getProperty("compose.swing.render.on.graphics").toBoolean() val layer: ComposeLayer = if (renderOnGraphics) { ComposeSwingLayer(skiaLayerAnalytics) } else { From 26b9538724e9dfc78e028cb5433dd397c0f08915 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 21 Jun 2023 15:25:25 +0200 Subject: [PATCH 12/19] Use scene density in update scene size and don't coerce the result since width and height shouldn't be negative --- .../kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 44b407b8383cf..201168649852d 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -302,10 +302,8 @@ internal abstract class ComposeLayer { protected fun updateSceneSize() { scene.constraints = Constraints( - maxWidth = (component.width * component.density.density).toInt() - .coerceAtLeast(0), - maxHeight = (component.height * component.density.density).toInt() - .coerceAtLeast(0) + maxWidth = (component.width * scene.density.density).toInt(), + maxHeight = (component.height * scene.density.density).toInt() ) } From 3ac03ec2525b309e1c0da608f3b23e9a1c344202 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Wed, 21 Jun 2023 17:44:18 +0200 Subject: [PATCH 13/19] Rename --- .../kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt | 2 +- .../androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt | 4 ++-- .../kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index 201168649852d..c7076a34b99b4 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -314,7 +314,7 @@ internal abstract class ComposeLayer { } } - protected fun updateWindowState(window: Window?) { + protected fun setParentWindow(window: Window?) { this.window?.removeWindowFocusListener(windowFocusListener) window?.addWindowFocusListener(windowFocusListener) this.window = window diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index c8437535cda72..c9ba620c1a2a2 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -83,11 +83,11 @@ internal class ComposeSwingLayer( resetSceneDensity() initContent() updateSceneSize() - updateWindowState(SwingUtilities.getWindowAncestor(this)) + setParentWindow(SwingUtilities.getWindowAncestor(this)) } override fun removeNotify() { - updateWindowState(window = null) + setParentWindow(window = null) super.removeNotify() } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt index cf7cb10152c4b..7a94f79ea3b0b 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt @@ -78,11 +78,11 @@ internal class ComposeWindowLayer( resetSceneDensity() initContent() updateSceneSize() - updateWindowState(SwingUtilities.getWindowAncestor(this)) + setParentWindow(SwingUtilities.getWindowAncestor(this)) } override fun removeNotify() { - updateWindowState(null) + setParentWindow(null) super.removeNotify() } From 4f1c1e3cf97a8763c40e4f019c0b7fe5fdd91bd1 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Fri, 30 Jun 2023 12:03:36 +0200 Subject: [PATCH 14/19] Update skiko version and use SkiaSwingLayer instead of SkiaSwingLayerComponent --- .../androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt | 5 +++-- gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index c9ba620c1a2a2..3afb3c24d72e5 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -30,7 +30,6 @@ import org.jetbrains.skiko.ExperimentalSkikoApi import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.SkiaLayerAnalytics import org.jetbrains.skiko.swing.SkiaSwingLayer -import org.jetbrains.skiko.swing.SkiaSwingLayerComponent /** * Provides [component] that can be used as a Swing component. @@ -44,7 +43,9 @@ internal class ComposeSwingLayer( private val skiaLayerAnalytics: SkiaLayerAnalytics ) : ComposeLayer() { private val _component = ComposeSwingSkiaLayer() - override val component: SkiaSwingLayerComponent get() = _component + + @OptIn(ExperimentalSkikoApi::class) + override val component: SkiaSwingLayer get() = _component override val renderApi: GraphicsApi get() = _component.renderApi diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d80efb1f4d9d6..9ff77e05227d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ moshi = "1.13.0" protobuf = "3.21.8" paparazzi = "1.0.0" paparazziNative = "2022.1.1-canary-f5f9f71" -skiko = "0.7.63" +skiko = "0.7.67" sqldelight = "1.3.0" retrofit = "2.7.2" wire = "4.5.1" From b6a889d37ddcbd7af3182f51f050508b9081ea18 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 3 Jul 2023 12:09:07 +0200 Subject: [PATCH 15/19] Cleanup names and use anonymous objects instead of naming ones --- .../compose/ui/awt/ComposeLayer.desktop.kt | 2 +- .../compose/ui/awt/ComposePanel.desktop.kt | 11 ++- .../ui/awt/ComposeSwingLayer.desktop.kt | 90 +++++++++---------- .../ui/awt/ComposeWindowDelegate.desktop.kt | 2 +- ...eWindowLayer.kt => WindowComposeBridge.kt} | 86 +++++++++--------- 5 files changed, 93 insertions(+), 98 deletions(-) rename compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/{ComposeWindowLayer.kt => WindowComposeBridge.kt} (64%) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt index c7076a34b99b4..236eadc4e22ef 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeLayer.desktop.kt @@ -56,7 +56,7 @@ import org.jetbrains.skiko.* * * Inheritors should call [attachComposeToComponent], so events that came to [component] will be transferred to [ComposeScene] */ -internal abstract class ComposeLayer { +internal abstract class ComposeBridge { private var isDisposed = false val sceneAccessible = ComposeSceneAccessible( diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index cb6797f861042..2ab9cc4b92dfc 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.window.WindowExceptionHandler import java.awt.* import java.awt.event.FocusEvent import java.awt.event.FocusListener -import javax.swing.JComponent import javax.swing.JLayeredPane import javax.swing.SwingUtilities.isEventDispatchThread import org.jetbrains.skiko.ClipComponent @@ -74,7 +73,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( private val _focusListeners = mutableSetOf() private var _isFocusable = true private var _isRequestFocusEnabled = false - private var layer: ComposeLayer? = null + private var layer: ComposeBridge? = null private val clipMap = mutableMapOf() private var content: (@Composable () -> Unit)? = null @@ -151,12 +150,12 @@ class ComposePanel @ExperimentalComposeUiApi constructor( } @OptIn(ExperimentalComposeUiApi::class) - private fun createComposeLayer(): ComposeLayer { + private fun createComposeLayer(): ComposeBridge { val renderOnGraphics = System.getProperty("compose.swing.render.on.graphics").toBoolean() - val layer: ComposeLayer = if (renderOnGraphics) { - ComposeSwingLayer(skiaLayerAnalytics) + val layer: ComposeBridge = if (renderOnGraphics) { + SwingComposeBridge(skiaLayerAnalytics) } else { - ComposeWindowLayer(skiaLayerAnalytics) + WindowComposeBridge(skiaLayerAnalytics) } return layer.apply { scene.releaseFocus() diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index 3afb3c24d72e5..6dad76fb8d647 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -35,14 +35,53 @@ import org.jetbrains.skiko.swing.SkiaSwingLayer * Provides [component] that can be used as a Swing component. * Content set in [setContent] will be drawn on this [component]. * - * [ComposeSwingLayer] provides smooth integration with Swing, so z-ordering, double-buffering etc. from Swing will be taken into account. + * [SwingComposeBridge] provides smooth integration with Swing, so z-ordering, double-buffering etc. from Swing will be taken into account. * - * However, if smooth interop with Swing is not needed, consider using [androidx.compose.ui.awt.ComposeWindowLayer] + * However, if smooth interop with Swing is not needed, consider using [androidx.compose.ui.awt.WindowComposeBridge] */ -internal class ComposeSwingLayer( +internal class SwingComposeBridge( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeLayer() { - private val _component = ComposeSwingSkiaLayer() +) : ComposeBridge() { + /** + * See also backendLayer for standalone Compose in [androidx.compose.ui.awt.WindowComposeBridge] + */ + @OptIn(ExperimentalSkikoApi::class) + private val _component = + object : SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { + + override fun addNotify() { + super.addNotify() + resetSceneDensity() + initContent() + updateSceneSize() + setParentWindow(SwingUtilities.getWindowAncestor(this)) + } + + override fun removeNotify() { + setParentWindow(window = null) + super.removeNotify() + } + + override fun paint(g: Graphics) { + resetSceneDensity() + super.paint(g) + } + + override fun getInputMethodRequests() = currentInputMethodRequests + + override fun doLayout() { + super.doLayout() + updateSceneSize() + } + + override fun getPreferredSize(): Dimension { + return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension + } + + override fun getAccessibleContext(): AccessibleContext? { + return sceneAccessible.accessibleContext + } + } @OptIn(ExperimentalSkikoApi::class) override val component: SkiaSwingLayer get() = _component @@ -71,45 +110,4 @@ internal class ComposeSwingLayer( override fun disposeComponentLayer() { _component.dispose() } - - /** - * See also backendLayer for standalone compose [androidx.compose.ui.awt.ComposeWindowLayer.ComposeWindowSkiaLayer] - */ - @OptIn(ExperimentalSkikoApi::class) - private inner class ComposeSwingSkiaLayer : - SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { - - override fun addNotify() { - super.addNotify() - resetSceneDensity() - initContent() - updateSceneSize() - setParentWindow(SwingUtilities.getWindowAncestor(this)) - } - - override fun removeNotify() { - setParentWindow(window = null) - super.removeNotify() - } - - override fun paint(g: Graphics) { - resetSceneDensity() - super.paint(g) - } - - override fun getInputMethodRequests() = currentInputMethodRequests - - override fun doLayout() { - super.doLayout() - updateSceneSize() - } - - override fun getPreferredSize(): Dimension { - return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension - } - - override fun getAccessibleContext(): AccessibleContext? { - return sceneAccessible.accessibleContext - } - } } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt index 8606480e114da..46e483210320d 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt @@ -49,7 +49,7 @@ internal class ComposeWindowDelegate( // (see https://github.com/JetBrains/compose-jb/issues/1688), // so we nullify layer on dispose, to prevent keeping // big objects in memory (like the whole LayoutNode tree of the window) - private var _layer: ComposeWindowLayer? = ComposeWindowLayer(skiaLayerAnalytics) + private var _layer: WindowComposeBridge? = WindowComposeBridge(skiaLayerAnalytics) private val layer get() = requireNotNull(_layer) { "ComposeLayer is disposed" diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt similarity index 64% rename from compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt rename to compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt index 7a94f79ea3b0b..8c8bbb2318291 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowLayer.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt @@ -29,12 +29,50 @@ import org.jetbrains.skiko.SkiaLayerAnalytics /** * Provides a heavyweight AWT [component] used to render content (from [setContent]) on-screen with Skia. * - * If smooth interop with Swing is needed, consider using [androidx.compose.ui.awt.ComposeWindowLayer] + * If smooth interop with Swing is needed, consider using [androidx.compose.ui.awt.SwingComposeBridge] */ -internal class ComposeWindowLayer( +internal class WindowComposeBridge( private val skiaLayerAnalytics: SkiaLayerAnalytics -) : ComposeLayer() { - private val _component = ComposeWindowSkiaLayer() +) : ComposeBridge() { + /** + * See also backend layer for swing interop in [androidx.compose.ui.awt.SwingComposeBridge] + */ + private val _component = + object : SkiaLayer( + externalAccessibleFactory = { sceneAccessible }, + analytics = skiaLayerAnalytics + ), Accessible { + + override fun addNotify() { + super.addNotify() + resetSceneDensity() + initContent() + updateSceneSize() + setParentWindow(SwingUtilities.getWindowAncestor(this)) + } + + override fun removeNotify() { + setParentWindow(null) + super.removeNotify() + } + + override fun paint(g: Graphics) { + resetSceneDensity() + super.paint(g) + } + + override fun getInputMethodRequests() = currentInputMethodRequests + + override fun doLayout() { + super.doLayout() + updateSceneSize() + } + + override fun getPreferredSize(): Dimension { + return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension + } + } + override val component: SkiaLayer get() = _component override val renderApi: GraphicsApi @@ -62,44 +100,4 @@ internal class ComposeWindowLayer( override fun disposeComponentLayer() { _component.dispose() } - - /** - * See also backend layer for swing interop: [androidx.compose.ui.awt.ComposeSwingLayer.ComposeSwingSkiaLayer] - */ - private inner class ComposeWindowSkiaLayer : - SkiaLayer( - externalAccessibleFactory = { sceneAccessible }, - analytics = skiaLayerAnalytics - ), - Accessible { - - override fun addNotify() { - super.addNotify() - resetSceneDensity() - initContent() - updateSceneSize() - setParentWindow(SwingUtilities.getWindowAncestor(this)) - } - - override fun removeNotify() { - setParentWindow(null) - super.removeNotify() - } - - override fun paint(g: Graphics) { - resetSceneDensity() - super.paint(g) - } - - override fun getInputMethodRequests() = currentInputMethodRequests - - override fun doLayout() { - super.doLayout() - updateSceneSize() - } - - override fun getPreferredSize(): Dimension { - return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension - } - } } From 1eb5914215b67fde4b3534d94145e8d348a83b8f Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 3 Jul 2023 12:13:28 +0200 Subject: [PATCH 16/19] Add gradle config for offscreen rendering swing example --- compose/desktop/desktop/samples/build.gradle | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/compose/desktop/desktop/samples/build.gradle b/compose/desktop/desktop/samples/build.gradle index d5b239ca193b2..7c5479b2369a3 100644 --- a/compose/desktop/desktop/samples/build.gradle +++ b/compose/desktop/desktop/samples/build.gradle @@ -90,6 +90,16 @@ task runSwing(type: JavaExec) { compilation.runtimeDependencyFiles } +task runSwingOffscreenRendering(type: JavaExec) { + dependsOn(":compose:desktop:desktop:jar") + main = "androidx.compose.desktop.examples.swingexample.Main_jvmKt" + def compilation = kotlin.jvm().compilations["main"] + classpath = + compilation.output.allOutputs + + compilation.runtimeDependencyFiles + jvmArgs("-Dcompose.swing.render.on.graphics=true") +} + task runMouseClicks(type: JavaExec) { dependsOn(":compose:desktop:desktop:jar") main = "androidx.compose.desktop.examples.mouseclicks.Main_jvmKt" From 13c0010f0a9b8adf67f90e393a872a8a2e79bb2f Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 3 Jul 2023 12:17:46 +0200 Subject: [PATCH 17/19] Fix swing interop example --- .../androidx/compose/desktop/examples/swingexample/Main.jvm.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.jvm.kt index 0aa702a7e9524..ee311a82af389 100644 --- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.jvm.kt +++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.jvm.kt @@ -143,6 +143,8 @@ fun SwingComposeWindow() { size = IntSize(40, 40), action = { panel.remove(composePanelBottom) + panel.revalidate() + panel.repaint() } ), BorderLayout.SOUTH From 5b2e9dd8702031dc88d71878667643faf09722c8 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 3 Jul 2023 13:44:10 +0200 Subject: [PATCH 18/19] Rename layer -> bridge --- .../compose/ui/awt/ComposePanel.desktop.kt | 54 +++++++-------- .../ui/awt/ComposeWindowDelegate.desktop.kt | 66 +++++++++---------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt index 2ab9cc4b92dfc..608adc5f05bf0 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt @@ -73,17 +73,17 @@ class ComposePanel @ExperimentalComposeUiApi constructor( private val _focusListeners = mutableSetOf() private var _isFocusable = true private var _isRequestFocusEnabled = false - private var layer: ComposeBridge? = null + private var bridge: ComposeBridge? = null private val clipMap = mutableMapOf() private var content: (@Composable () -> Unit)? = null override fun setBounds(x: Int, y: Int, width: Int, height: Int) { - layer?.component?.setSize(width, height) + bridge?.component?.setSize(width, height) super.setBounds(x, y, width, height) } override fun getPreferredSize(): Dimension? { - return if (isPreferredSizeSet) super.getPreferredSize() else layer?.component?.preferredSize + return if (isPreferredSizeSet) super.getPreferredSize() else bridge?.component?.preferredSize } /** @@ -109,12 +109,12 @@ class ComposePanel @ExperimentalComposeUiApi constructor( var exceptionHandler: WindowExceptionHandler? = null set(value) { field = value - layer?.exceptionHandler = value + bridge?.exceptionHandler = value } private fun initContent() { - if (layer != null && content != null) { - layer!!.setContent { + if (bridge != null && content != null) { + bridge!!.setContent { CompositionLocalProvider( LocalLayerContainer provides this, content = content!! @@ -124,17 +124,17 @@ class ComposePanel @ExperimentalComposeUiApi constructor( } override fun add(component: Component): Component { - if (layer == null) { + if (bridge == null) { return component } val clipComponent = ClipComponent(component) clipMap[component] = clipComponent - layer!!.clipComponents.add(clipComponent) + bridge!!.clipComponents.add(clipComponent) return super.add(component, Integer.valueOf(0)) } override fun remove(component: Component) { - layer!!.clipComponents.remove(clipMap[component]!!) + bridge!!.clipComponents.remove(clipMap[component]!!) clipMap.remove(component) super.remove(component) } @@ -144,9 +144,9 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // After [super.addNotify] is called we can safely initialize the layer and composable // content. - layer = createComposeLayer() + bridge = createComposeLayer() initContent() - super.add(layer!!.component, Integer.valueOf(1)) + super.add(bridge!!.component, Integer.valueOf(1)) } @OptIn(ExperimentalComposeUiApi::class) @@ -190,22 +190,22 @@ class ComposePanel @ExperimentalComposeUiApi constructor( } override fun removeNotify() { - if (layer != null) { - layer!!.dispose() - super.remove(layer!!.component) - layer = null + if (bridge != null) { + bridge!!.dispose() + super.remove(bridge!!.component) + bridge = null } super.removeNotify() } override fun addFocusListener(l: FocusListener?) { - layer?.component?.addFocusListener(l) + bridge?.component?.addFocusListener(l) _focusListeners.add(l) } override fun removeFocusListener(l: FocusListener?) { - layer?.component?.removeFocusListener(l) + bridge?.component?.removeFocusListener(l) _focusListeners.remove(l) } @@ -213,42 +213,42 @@ class ComposePanel @ExperimentalComposeUiApi constructor( override fun setFocusable(focusable: Boolean) { _isFocusable = focusable - layer?.component?.isFocusable = focusable + bridge?.component?.isFocusable = focusable } override fun isRequestFocusEnabled(): Boolean = _isRequestFocusEnabled override fun setRequestFocusEnabled(requestFocusEnabled: Boolean) { _isRequestFocusEnabled = requestFocusEnabled - layer?.component?.isRequestFocusEnabled = requestFocusEnabled + bridge?.component?.isRequestFocusEnabled = requestFocusEnabled } override fun hasFocus(): Boolean { - return layer?.component?.hasFocus() ?: false + return bridge?.component?.hasFocus() ?: false } override fun isFocusOwner(): Boolean { - return layer?.component?.isFocusOwner ?: false + return bridge?.component?.isFocusOwner ?: false } override fun requestFocus() { - layer?.component?.requestFocus() + bridge?.component?.requestFocus() } override fun requestFocus(temporary: Boolean): Boolean { - return layer?.component?.requestFocus(temporary) ?: false + return bridge?.component?.requestFocus(temporary) ?: false } override fun requestFocus(cause: FocusEvent.Cause?) { - layer?.component?.requestFocus(cause) + bridge?.component?.requestFocus(cause) } override fun requestFocusInWindow(): Boolean { - return layer?.component?.requestFocusInWindow() ?: false + return bridge?.component?.requestFocusInWindow() ?: false } override fun requestFocusInWindow(cause: FocusEvent.Cause?): Boolean { - return layer?.component?.requestFocusInWindow(cause) ?: false + return bridge?.component?.requestFocusInWindow(cause) ?: false } override fun setFocusTraversalKeysEnabled(focusTraversalKeysEnabled: Boolean) { @@ -265,5 +265,5 @@ class ComposePanel @ExperimentalComposeUiApi constructor( * environment variable. */ val renderApi: GraphicsApi - get() = if (layer != null) layer!!.renderApi else GraphicsApi.UNKNOWN + get() = if (bridge != null) bridge!!.renderApi else GraphicsApi.UNKNOWN } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt index 46e483210320d..81b7fcb4cab40 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowDelegate.desktop.kt @@ -49,49 +49,49 @@ internal class ComposeWindowDelegate( // (see https://github.com/JetBrains/compose-jb/issues/1688), // so we nullify layer on dispose, to prevent keeping // big objects in memory (like the whole LayoutNode tree of the window) - private var _layer: WindowComposeBridge? = WindowComposeBridge(skiaLayerAnalytics) - private val layer - get() = requireNotNull(_layer) { + private var _bridge: WindowComposeBridge? = WindowComposeBridge(skiaLayerAnalytics) + private val bridge + get() = requireNotNull(_bridge) { "ComposeLayer is disposed" } internal val windowAccessible: Accessible - get() = layer.sceneAccessible + get() = bridge.sceneAccessible val undecoratedWindowResizer = UndecoratedWindowResizer(window) private val _pane = object : JLayeredPane() { override fun setBounds(x: Int, y: Int, width: Int, height: Int) { - layer.component.setSize(width, height) + bridge.component.setSize(width, height) super.setBounds(x, y, width, height) } override fun add(component: Component): Component { val clipComponent = ClipComponent(component) clipMap[component] = clipComponent - layer.clipComponents.add(clipComponent) + bridge.clipComponents.add(clipComponent) return add(component, Integer.valueOf(0)) } override fun remove(component: Component) { - layer.clipComponents.remove(clipMap[component]!!) + bridge.clipComponents.remove(clipMap[component]!!) clipMap.remove(component) super.remove(component) } override fun addNotify() { super.addNotify() - layer.component.requestFocus() + bridge.component.requestFocus() } override fun getPreferredSize() = - if (isPreferredSizeSet) super.getPreferredSize() else layer.component.preferredSize + if (isPreferredSizeSet) super.getPreferredSize() else bridge.component.preferredSize init { layout = null - super.add(layer.component, 1) + super.add(bridge.component, 1) } fun dispose() { - super.remove(layer.component) + super.remove(bridge.component) } } @@ -120,15 +120,15 @@ internal class ComposeWindowDelegate( } var fullscreen: Boolean - get() = layer.component.fullscreen + get() = bridge.component.fullscreen set(value) { - layer.component.fullscreen = value + bridge.component.fullscreen = value } var compositionLocalContext: CompositionLocalContext? - get() = layer.compositionLocalContext + get() = bridge.compositionLocalContext set(value) { - layer.compositionLocalContext = value + bridge.compositionLocalContext = value } fun setContent( @@ -136,7 +136,7 @@ internal class ComposeWindowDelegate( onKeyEvent: (KeyEvent) -> Boolean = { false }, content: @Composable () -> Unit ) { - layer.setContent( + bridge.setContent( onPreviewKeyEvent = onPreviewKeyEvent, onKeyEvent = onKeyEvent, ) { @@ -165,7 +165,7 @@ internal class ComposeWindowDelegate( if (it.layoutId == "UndecoratedWindowResizer") it else null } val resizerPlaceable = resizerMeasurable?.let { - val density = layer.component.density.density + val density = bridge.component.density.density val resizerWidth = (window.width * density).toInt() val resizerHeight = (window.height * density).toInt() it.measure( @@ -199,41 +199,41 @@ internal class ComposeWindowDelegate( fun dispose() { if (!isDisposed) { - layer.dispose() + bridge.dispose() _pane.dispose() - _layer = null + _bridge = null isDisposed = true } } fun onRenderApiChanged(action: () -> Unit) { - layer.component.onStateChanged(SkiaLayer.PropertyKind.Renderer) { + bridge.component.onStateChanged(SkiaLayer.PropertyKind.Renderer) { action() } } @ExperimentalComposeUiApi var exceptionHandler: WindowExceptionHandler? - get() = layer.exceptionHandler + get() = bridge.exceptionHandler set(value) { - layer.exceptionHandler = value + bridge.exceptionHandler = value } val windowHandle: Long - get() = layer.component.windowHandle + get() = bridge.component.windowHandle val renderApi: GraphicsApi - get() = layer.renderApi + get() = bridge.renderApi var isTransparent: Boolean - get() = layer.component.transparency + get() = bridge.component.transparency set(value) { - if (value != layer.component.transparency) { + if (value != bridge.component.transparency) { check(isUndecorated()) { "Transparent window should be undecorated!" } check(!window.isDisplayable) { "Cannot change transparency if window is already displayable." } - layer.component.transparency = value + bridge.component.transparency = value if (value) { if (hostOs != OS.Windows) { window.background = Color(0, 0, 0, 0) @@ -245,26 +245,26 @@ internal class ComposeWindowDelegate( } fun addMouseListener(listener: MouseListener) { - layer.component.addMouseListener(listener) + bridge.component.addMouseListener(listener) } fun removeMouseListener(listener: MouseListener) { - layer.component.removeMouseListener(listener) + bridge.component.removeMouseListener(listener) } fun addMouseMotionListener(listener: MouseMotionListener) { - layer.component.addMouseMotionListener(listener) + bridge.component.addMouseMotionListener(listener) } fun removeMouseMotionListener(listener: MouseMotionListener) { - layer.component.removeMouseMotionListener(listener) + bridge.component.removeMouseMotionListener(listener) } fun addMouseWheelListener(listener: MouseWheelListener) { - layer.component.addMouseWheelListener(listener) + bridge.component.addMouseWheelListener(listener) } fun removeMouseWheelListener(listener: MouseWheelListener) { - layer.component.removeMouseWheelListener(listener) + bridge.component.removeMouseWheelListener(listener) } } From 26264b5a1e3c0ad64f52419d9d56ff4dfade0585 Mon Sep 17 00:00:00 2001 From: Nikolay Rykunov Date: Mon, 3 Jul 2023 13:46:29 +0200 Subject: [PATCH 19/19] Remove redundant _component backing property --- .../ui/awt/ComposeSwingLayer.desktop.kt | 18 ++--- .../compose/ui/awt/WindowComposeBridge.kt | 72 +++++++++---------- 2 files changed, 41 insertions(+), 49 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt index 6dad76fb8d647..709d1e8741cde 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeSwingLayer.desktop.kt @@ -39,16 +39,15 @@ import org.jetbrains.skiko.swing.SkiaSwingLayer * * However, if smooth interop with Swing is not needed, consider using [androidx.compose.ui.awt.WindowComposeBridge] */ +@OptIn(ExperimentalSkikoApi::class) internal class SwingComposeBridge( private val skiaLayerAnalytics: SkiaLayerAnalytics ) : ComposeBridge() { /** * See also backendLayer for standalone Compose in [androidx.compose.ui.awt.WindowComposeBridge] */ - @OptIn(ExperimentalSkikoApi::class) - private val _component = + override val component: SkiaSwingLayer = object : SkiaSwingLayer(skikoView = skikoView, analytics = skiaLayerAnalytics) { - override fun addNotify() { super.addNotify() resetSceneDensity() @@ -83,24 +82,21 @@ internal class SwingComposeBridge( } } - @OptIn(ExperimentalSkikoApi::class) - override val component: SkiaSwingLayer get() = _component - override val renderApi: GraphicsApi - get() = _component.renderApi + get() = component.renderApi override val clipComponents: MutableList - get() = _component.clipComponents + get() = component.clipComponents override val focusComponentDelegate: Component - get() = _component + get() = component override fun requestNativeFocusOnAccessible(accessible: Accessible) { // TODO: support a11y } override fun onComposeInvalidation() { - _component.repaint() + component.repaint() } init { @@ -108,6 +104,6 @@ internal class SwingComposeBridge( } override fun disposeComponentLayer() { - _component.dispose() + component.dispose() } } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt index 8c8bbb2318291..6cb847d2c32c0 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowComposeBridge.kt @@ -37,67 +37,63 @@ internal class WindowComposeBridge( /** * See also backend layer for swing interop in [androidx.compose.ui.awt.SwingComposeBridge] */ - private val _component = - object : SkiaLayer( - externalAccessibleFactory = { sceneAccessible }, - analytics = skiaLayerAnalytics - ), Accessible { - - override fun addNotify() { - super.addNotify() - resetSceneDensity() - initContent() - updateSceneSize() - setParentWindow(SwingUtilities.getWindowAncestor(this)) - } - - override fun removeNotify() { - setParentWindow(null) - super.removeNotify() - } + override val component: SkiaLayer = object : SkiaLayer( + externalAccessibleFactory = { sceneAccessible }, + analytics = skiaLayerAnalytics + ), Accessible { + override fun addNotify() { + super.addNotify() + resetSceneDensity() + initContent() + updateSceneSize() + setParentWindow(SwingUtilities.getWindowAncestor(this)) + } - override fun paint(g: Graphics) { - resetSceneDensity() - super.paint(g) - } + override fun removeNotify() { + setParentWindow(null) + super.removeNotify() + } - override fun getInputMethodRequests() = currentInputMethodRequests + override fun paint(g: Graphics) { + resetSceneDensity() + super.paint(g) + } - override fun doLayout() { - super.doLayout() - updateSceneSize() - } + override fun getInputMethodRequests() = currentInputMethodRequests - override fun getPreferredSize(): Dimension { - return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension - } + override fun doLayout() { + super.doLayout() + updateSceneSize() } - override val component: SkiaLayer get() = _component + override fun getPreferredSize(): Dimension { + return if (isPreferredSizeSet) super.getPreferredSize() else sceneDimension + } + } override val renderApi: GraphicsApi - get() = _component.renderApi + get() = component.renderApi override val clipComponents: MutableList - get() = _component.clipComponents + get() = component.clipComponents override val focusComponentDelegate: Component - get() = _component.canvas + get() = component.canvas init { - _component.skikoView = skikoView + component.skikoView = skikoView attachComposeToComponent() } override fun requestNativeFocusOnAccessible(accessible: Accessible) { - _component.requestNativeFocusOnAccessible(accessible) + component.requestNativeFocusOnAccessible(accessible) } override fun onComposeInvalidation() { - _component.needRedraw() + component.needRedraw() } override fun disposeComponentLayer() { - _component.dispose() + component.dispose() } }