Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -122,6 +128,8 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
isFocusable = true
}

private var windowParent by mutableStateOf<Window?>(null)

private val _focusListeners = mutableSetOf<FocusListener?>()

private var _composeContainer: ComposeContainer? = null
Expand Down Expand Up @@ -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()
}
}
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -342,6 +360,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
if (isDisposeOnRemove) {
dispose()
}
windowParent = null
super.removeNotify()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -151,7 +150,7 @@ internal class ComposeWindowPanel(
)
composeContainer.setContent {
CompositionLocalProvider(
LocalWindow provides window
LocalAwtWindow provides window
) {
WindowContentLayout(modifier, content)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<java.awt.Window?> { null }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's better to provide a way to get Component where Compose is attached? SwingUtilities.getWindowAncestor might be called outside on the usage side

Copy link
Copy Markdown
Author

@m-sasha m-sasha Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that too, but if you just have the component, you won't know about the window changing (if it's removed and added somewhere else). At least not without adding AWT listeners, at which point I think it's too much AWT knowledge that we'd be asking from the developer.

It would also raise the question: which component exactly we'd want to report as "the" component.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ internal class WindowSkiaLayerComponent(
inputMethodsEnabled = enable
super.enableInputMethods(enable)
}

override fun dispose() {
super.dispose()
isDisposed = true
}
}

override val contentRoot: Component
Expand Down Expand Up @@ -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() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.*
Expand Down Expand Up @@ -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<CtxElement> {
Expand Down
Loading