Skip to content

Commit d813291

Browse files
authored
Rename LocalWindow to LocalAwtWindow, make it public and provide it in ComposePanel (JetBrains#3007)
1 parent 6b80ffc commit d813291

10 files changed

Lines changed: 121 additions & 39 deletions

File tree

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
package androidx.compose.ui.awt
1818

1919
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.CompositionLocalProvider
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.setValue
2024
import androidx.compose.ui.ComposeFeatureFlags
2125
import androidx.compose.ui.ComposeUiFlags
2226
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -37,10 +41,12 @@ import java.awt.ComponentOrientation
3741
import java.awt.Container
3842
import java.awt.Dimension
3943
import java.awt.FocusTraversalPolicy
44+
import java.awt.Window
4045
import java.awt.event.FocusEvent
4146
import java.awt.event.FocusListener
4247
import java.util.*
4348
import javax.swing.JLayeredPane
49+
import javax.swing.SwingUtilities
4450
import javax.swing.SwingUtilities.isEventDispatchThread
4551
import kotlin.coroutines.CoroutineContext
4652
import kotlin.coroutines.EmptyCoroutineContext
@@ -122,6 +128,8 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
122128
isFocusable = true
123129
}
124130

131+
private var windowParent by mutableStateOf<Window?>(null)
132+
125133
private val _focusListeners = mutableSetOf<FocusListener?>()
126134

127135
private var _composeContainer: ComposeContainer? = null
@@ -231,8 +239,17 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
231239
// to keep the lambda describing composable content and set the content only when
232240
// everything is ready to avoid accidental crashes and memory leaks on all supported OS
233241
// types.
234-
_composeContent = content
235-
_composeContainer?.setContent(content)
242+
val wrappedContent = wrapContent(content)
243+
_composeContent = wrappedContent
244+
_composeContainer?.setContent(wrappedContent)
245+
}
246+
247+
private fun wrapContent(content: @Composable () -> Unit): @Composable () -> Unit {
248+
return {
249+
CompositionLocalProvider(LocalAwtWindow provides windowParent) {
250+
content()
251+
}
252+
}
236253
}
237254

238255
/**
@@ -286,6 +303,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
286303
// content.
287304
val composeContainer = _composeContainer ?: createComposeContainer().also {
288305
_composeContainer = it
306+
windowParent = SwingUtilities.getWindowAncestor(this)
289307
it.redispatchUnconsumedMouseWheelEvents = redispatchUnconsumedMouseWheelEvents
290308
@OptIn(InternalCoreApi::class)
291309
it.showLayoutBounds = showLayoutBounds
@@ -342,6 +360,7 @@ class ComposePanel @ExperimentalComposeUiApi constructor(
342360
if (isDisposeOnRemove) {
343361
dispose()
344362
}
363+
windowParent = null
345364
super.removeNotify()
346365
}
347366

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeWindowPanel.desktop.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import androidx.compose.ui.LayerType
2323
import androidx.compose.ui.Modifier
2424
import androidx.compose.ui.input.key.KeyEvent
2525
import androidx.compose.ui.scene.ComposeContainer
26-
import androidx.compose.ui.window.LocalWindow
2726
import androidx.savedstate.SavedState
2827
import java.awt.Component
2928
import java.awt.Container
@@ -151,7 +150,7 @@ internal class ComposeWindowPanel(
151150
)
152151
composeContainer.setContent {
153152
CompositionLocalProvider(
154-
LocalWindow provides window
153+
LocalAwtWindow provides window
155154
) {
156155
WindowContentLayout(modifier, content)
157156
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.ui.awt
18+
19+
import androidx.compose.runtime.staticCompositionLocalOf
20+
import androidx.compose.ui.ExperimentalComposeUiApi
21+
import androidx.compose.ui.ImageComposeScene
22+
23+
/**
24+
* Window-owner of the current composition.
25+
*
26+
* This could be:
27+
* - A Compose window, e.g., [ComposeWindow] or [ComposeDialog]
28+
* - A non-Compose window, e.g., [java.awt.Frame] or [java.awt.Dialog] if Compose is embedded into
29+
* Swing via [ComposePanel].
30+
* - `null`, if the current composition is not inside a window, such as in unit tests, or with
31+
* [ImageComposeScene].
32+
*/
33+
@ExperimentalComposeUiApi
34+
val LocalAwtWindow = staticCompositionLocalOf<java.awt.Window?> { null }

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingDialog.desktop.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import androidx.compose.ui.window.DialogModalityType
4141
import androidx.compose.ui.window.DialogState
4242
import androidx.compose.ui.window.DialogWindow
4343
import androidx.compose.ui.window.DialogWindowScope
44-
import androidx.compose.ui.window.LocalWindow
4544
import androidx.compose.ui.window.LocalWindowExceptionHandlerFactory
4645
import androidx.compose.ui.window.UndecoratedWindowDecoration
4746
import androidx.compose.ui.window.WindowDecoration
@@ -201,7 +200,7 @@ fun SwingDialog(
201200
init: (ComposeDialog) -> Unit,
202201
content: @Composable DialogWindowScope.() -> Unit
203202
) {
204-
val owner = LocalWindow.current
203+
val owner = LocalAwtWindow.current
205204

206205
val currentState by rememberUpdatedState(state)
207206
val currentTitle by rememberUpdatedState(title)

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/WindowContentLayout.desktop.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import androidx.compose.ui.layout.layoutId
2323
import androidx.compose.ui.unit.Constraints
2424
import androidx.compose.ui.util.fastForEach
2525
import androidx.compose.ui.util.fastMaxOfOrDefault
26-
import androidx.compose.ui.util.fastMaxOfOrNull
27-
import androidx.compose.ui.window.LocalWindow
2826

2927
/**
3028
* Base layout for full-window Compose content.
@@ -34,7 +32,7 @@ internal fun WindowContentLayout(
3432
modifier: Modifier = Modifier,
3533
content: @Composable () -> Unit
3634
) {
37-
val window = requireNotNull(LocalWindow.current)
35+
val window = requireNotNull(LocalAwtWindow.current)
3836
Layout(
3937
content = content,
4038
modifier = modifier,

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/skia/WindowSkiaLayerComponent.desktop.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ internal class WindowSkiaLayerComponent(
116116
inputMethodsEnabled = enable
117117
super.enableInputMethods(enable)
118118
}
119+
120+
override fun dispose() {
121+
super.dispose()
122+
isDisposed = true
123+
}
119124
}
120125

121126
override val contentRoot: Component
@@ -150,16 +155,21 @@ internal class WindowSkiaLayerComponent(
150155

151156
override val windowHandle by hierarchyRoot::windowHandle
152157

158+
private var isDisposed = false
159+
153160
init {
154161
hierarchyRoot.renderDelegate = renderDelegate
155162
}
156163

157164
override fun dispose() {
158165
hierarchyRoot.dispose()
166+
isDisposed = true
159167
}
160168

161169
override fun onComposeInvalidation() {
162-
hierarchyRoot.needRender()
170+
if (!isDisposed) {
171+
hierarchyRoot.needRender()
172+
}
163173
}
164174

165175
override fun renderImmediately() {

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/LocalWindow.kt

Lines changed: 0 additions & 29 deletions
This file was deleted.

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposePanelTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import com.google.common.truth.Truth.assertThat
7373
import java.awt.BorderLayout
7474
import java.awt.Dimension
7575
import java.awt.GraphicsEnvironment
76+
import java.awt.Window
7677
import java.awt.event.MouseEvent
7778
import javax.swing.BoxLayout
7879
import javax.swing.JFrame
@@ -938,4 +939,27 @@ class ComposePanelTest {
938939
frame.dispose()
939940
}
940941
}
942+
943+
@Test
944+
fun `ComposePanel provides LocalAwtWindow`() = runApplicationTest {
945+
var localWindow: Window? = null
946+
val composePanel = ComposePanel().apply {
947+
size = Dimension(300, 300)
948+
setContent {
949+
localWindow = LocalAwtWindow.current
950+
}
951+
}
952+
val frame = JFrame().apply {
953+
contentPane.add(composePanel)
954+
size = Dimension(300, 300)
955+
}
956+
957+
try {
958+
frame.isVisible = true
959+
awaitIdle()
960+
assertNotNull(localWindow)
961+
} finally {
962+
frame.dispose()
963+
}
964+
}
941965
}

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/DialogWindowTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.runtime.*
2424
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
2525
import androidx.compose.ui.*
2626
import androidx.compose.ui.awt.ComposeDialog
27+
import androidx.compose.ui.awt.LocalAwtWindow
2728
import androidx.compose.ui.awt.SwingDialog
2829
import androidx.compose.ui.focus.FocusRequester
2930
import androidx.compose.ui.focus.focusRequester
@@ -53,6 +54,7 @@ import javax.swing.JFrame
5354
import kotlin.concurrent.thread
5455
import kotlin.test.Test
5556
import kotlin.test.assertEquals
57+
import kotlin.test.assertNotNull
5658
import kotlinx.coroutines.delay
5759

5860
class DialogWindowTest {
@@ -827,6 +829,18 @@ class DialogWindowTest {
827829
content()
828830
}
829831
}
832+
833+
@Test
834+
fun dialogWindowComposableProvidesLocalAwtWindow() = runApplicationTest {
835+
var localWindow: Window? = null
836+
launchTestApplication {
837+
DialogWindow(onCloseRequest = ::exitApplication) {
838+
localWindow = LocalAwtWindow.current
839+
}
840+
}
841+
awaitIdle()
842+
assertNotNull(localWindow)
843+
}
830844
}
831845

832846
private fun assertDialogStateEquals(expected: DialogState, actual: DialogState) {

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.material.Slider
2525
import androidx.compose.runtime.*
2626
import androidx.compose.ui.*
2727
import androidx.compose.ui.awt.ComposeWindow
28+
import androidx.compose.ui.awt.LocalAwtWindow
2829
import androidx.compose.ui.awt.SwingWindow
2930
import androidx.compose.ui.focus.FocusRequester
3031
import androidx.compose.ui.focus.focusRequester
@@ -46,6 +47,7 @@ import kotlin.math.roundToInt
4647
import kotlin.test.Test
4748
import kotlin.test.assertEquals
4849
import kotlin.test.assertFalse
50+
import kotlin.test.assertNotNull
4951
import kotlin.time.Duration.Companion.milliseconds
5052
import kotlin.time.Duration.Companion.seconds
5153
import kotlinx.coroutines.*
@@ -880,6 +882,18 @@ class WindowTest {
880882
content()
881883
}
882884
}
885+
886+
@Test
887+
fun windowComposableProvidesLocalAwtWindow() = runApplicationTest {
888+
var localWindow: Window? = null
889+
launchTestApplication {
890+
Window(onCloseRequest = ::exitApplication) {
891+
localWindow = LocalAwtWindow.current
892+
}
893+
}
894+
awaitIdle()
895+
assertNotNull(localWindow)
896+
}
883897
}
884898

885899
private object CtxElement : CoroutineContext.Element, CoroutineContext.Key<CtxElement> {

0 commit comments

Comments
 (0)