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 bac9d7a31092b..90f712c339685 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 @@ -17,6 +17,10 @@ package androidx.compose.ui.awt import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.ComposeFeatureFlags import androidx.compose.ui.ComposeUiFlags import androidx.compose.ui.ExperimentalComposeUiApi @@ -37,10 +41,12 @@ import java.awt.ComponentOrientation import java.awt.Container import java.awt.Dimension import java.awt.FocusTraversalPolicy +import java.awt.Window import java.awt.event.FocusEvent import java.awt.event.FocusListener import java.util.* import javax.swing.JLayeredPane +import javax.swing.SwingUtilities import javax.swing.SwingUtilities.isEventDispatchThread import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -122,6 +128,8 @@ class ComposePanel @ExperimentalComposeUiApi constructor( isFocusable = true } + private var windowParent by mutableStateOf(null) + private val _focusListeners = mutableSetOf() private var _composeContainer: ComposeContainer? = null @@ -231,8 +239,17 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // to keep the lambda describing composable content and set the content only when // everything is ready to avoid accidental crashes and memory leaks on all supported OS // types. - _composeContent = content - _composeContainer?.setContent(content) + val wrappedContent = wrapContent(content) + _composeContent = wrappedContent + _composeContainer?.setContent(wrappedContent) + } + + private fun wrapContent(content: @Composable () -> Unit): @Composable () -> Unit { + return { + CompositionLocalProvider(LocalAwtWindow provides windowParent) { + content() + } + } } /** @@ -286,6 +303,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( // content. val composeContainer = _composeContainer ?: createComposeContainer().also { _composeContainer = it + windowParent = SwingUtilities.getWindowAncestor(this) it.redispatchUnconsumedMouseWheelEvents = redispatchUnconsumedMouseWheelEvents @OptIn(InternalCoreApi::class) it.showLayoutBounds = showLayoutBounds @@ -342,6 +360,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor( if (isDisposeOnRemove) { dispose() } + windowParent = null super.removeNotify() } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt index 0331c73d7c6e3..8e761ffe59e4c 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.LayerType import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.scene.ComposeContainer -import androidx.compose.ui.window.LocalWindow import androidx.savedstate.SavedState import java.awt.Component import java.awt.Container @@ -151,7 +150,7 @@ internal class ComposeWindowPanel( ) composeContainer.setContent { CompositionLocalProvider( - LocalWindow provides window + LocalAwtWindow provides window ) { WindowContentLayout(modifier, content) } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/LocalAwtWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/LocalAwtWindow.kt new file mode 100644 index 0000000000000..559e189ced840 --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/LocalAwtWindow.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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.staticCompositionLocalOf +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.ImageComposeScene + +/** + * Window-owner of the current composition. + * + * This could be: + * - A Compose window, e.g., [ComposeWindow] or [ComposeDialog] + * - A non-Compose window, e.g., [java.awt.Frame] or [java.awt.Dialog] if Compose is embedded into + * Swing via [ComposePanel]. + * - `null`, if the current composition is not inside a window, such as in unit tests, or with + * [ImageComposeScene]. + */ +@ExperimentalComposeUiApi +val LocalAwtWindow = staticCompositionLocalOf { null } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingDialog.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingDialog.desktop.kt index b0aef19c747be..454bf989dbe38 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingDialog.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingDialog.desktop.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.window.DialogModalityType import androidx.compose.ui.window.DialogState import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.DialogWindowScope -import androidx.compose.ui.window.LocalWindow import androidx.compose.ui.window.LocalWindowExceptionHandlerFactory import androidx.compose.ui.window.UndecoratedWindowDecoration import androidx.compose.ui.window.WindowDecoration @@ -201,7 +200,7 @@ fun SwingDialog( init: (ComposeDialog) -> Unit, content: @Composable DialogWindowScope.() -> Unit ) { - val owner = LocalWindow.current + val owner = LocalAwtWindow.current val currentState by rememberUpdatedState(state) val currentTitle by rememberUpdatedState(title) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowContentLayout.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowContentLayout.desktop.kt index e09d1d5227073..5bdc86cc7f235 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowContentLayout.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowContentLayout.desktop.kt @@ -23,8 +23,6 @@ import androidx.compose.ui.layout.layoutId import androidx.compose.ui.unit.Constraints import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMaxOfOrDefault -import androidx.compose.ui.util.fastMaxOfOrNull -import androidx.compose.ui.window.LocalWindow /** * Base layout for full-window Compose content. @@ -34,7 +32,7 @@ internal fun WindowContentLayout( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { - val window = requireNotNull(LocalWindow.current) + val window = requireNotNull(LocalAwtWindow.current) Layout( content = content, modifier = modifier, diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt index 0aad4db202855..ffa29d3fd3702 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt @@ -116,6 +116,11 @@ internal class WindowSkiaLayerComponent( inputMethodsEnabled = enable super.enableInputMethods(enable) } + + override fun dispose() { + super.dispose() + isDisposed = true + } } override val contentRoot: Component @@ -150,16 +155,21 @@ internal class WindowSkiaLayerComponent( override val windowHandle by hierarchyRoot::windowHandle + private var isDisposed = false + init { hierarchyRoot.renderDelegate = renderDelegate } override fun dispose() { hierarchyRoot.dispose() + isDisposed = true } override fun onComposeInvalidation() { - hierarchyRoot.needRender() + if (!isDisposed) { + hierarchyRoot.needRender() + } } override fun renderImmediately() { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/LocalWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/LocalWindow.kt deleted file mode 100644 index 4208152b9da25..0000000000000 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/LocalWindow.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 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.window - -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.awt.ComposeDialog -import androidx.compose.ui.awt.ComposePanel -import androidx.compose.ui.awt.ComposeWindow -import java.awt.Window - -/** - * Window-owner of the current composition (for example, [ComposeWindow] or [ComposeDialog]). - * If the composition is not inside Window (for example, [ComposePanel]), then return null - */ -internal val LocalWindow = compositionLocalOf { null } \ No newline at end of file diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt index 9ae13b3375313..06b7aa77ee489 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt @@ -73,6 +73,7 @@ import com.google.common.truth.Truth.assertThat import java.awt.BorderLayout import java.awt.Dimension import java.awt.GraphicsEnvironment +import java.awt.Window import java.awt.event.MouseEvent import javax.swing.BoxLayout import javax.swing.JFrame @@ -938,4 +939,27 @@ class ComposePanelTest { frame.dispose() } } + + @Test + fun `ComposePanel provides LocalAwtWindow`() = runApplicationTest { + var localWindow: Window? = null + val composePanel = ComposePanel().apply { + size = Dimension(300, 300) + setContent { + localWindow = LocalAwtWindow.current + } + } + val frame = JFrame().apply { + contentPane.add(composePanel) + size = Dimension(300, 300) + } + + try { + frame.isVisible = true + awaitIdle() + assertNotNull(localWindow) + } finally { + frame.dispose() + } + } } diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DialogWindowTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DialogWindowTest.kt index d8182b016a113..b4d108e997d2a 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DialogWindowTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DialogWindowTest.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.* import androidx.compose.ui.awt.ComposeDialog +import androidx.compose.ui.awt.LocalAwtWindow import androidx.compose.ui.awt.SwingDialog import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -53,6 +54,7 @@ import javax.swing.JFrame import kotlin.concurrent.thread import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlinx.coroutines.delay class DialogWindowTest { @@ -827,6 +829,18 @@ class DialogWindowTest { content() } } + + @Test + fun dialogWindowComposableProvidesLocalAwtWindow() = runApplicationTest { + var localWindow: Window? = null + launchTestApplication { + DialogWindow(onCloseRequest = ::exitApplication) { + localWindow = LocalAwtWindow.current + } + } + awaitIdle() + assertNotNull(localWindow) + } } private fun assertDialogStateEquals(expected: DialogState, actual: DialogState) { diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTest.kt index 374375ce5f0fe..3ca3a5a5aa53e 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTest.kt @@ -25,6 +25,7 @@ import androidx.compose.material.Slider import androidx.compose.runtime.* import androidx.compose.ui.* import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.awt.LocalAwtWindow import androidx.compose.ui.awt.SwingWindow import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -46,6 +47,7 @@ import kotlin.math.roundToInt import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.* @@ -880,6 +882,18 @@ class WindowTest { content() } } + + @Test + fun windowComposableProvidesLocalAwtWindow() = runApplicationTest { + var localWindow: Window? = null + launchTestApplication { + Window(onCloseRequest = ::exitApplication) { + localWindow = LocalAwtWindow.current + } + } + awaitIdle() + assertNotNull(localWindow) + } } private object CtxElement : CoroutineContext.Element, CoroutineContext.Key {