diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt index 0c48e583ce07d..1d954e1764dad 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt @@ -155,6 +155,7 @@ internal class ComposeContainer( } fun dispose() { + _windowContainer?.removeComponentListener(this) mediator.dispose() layers.fastForEach(DesktopComposeSceneLayer::close) } 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 9a966f628bc64..dad5e503218ea 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 @@ -29,12 +29,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.sendMouseEvent import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.util.ThrowUncaughtExceptionRule import androidx.compose.ui.window.density import androidx.compose.ui.window.runApplicationTest import com.google.common.truth.Truth.assertThat @@ -54,9 +59,13 @@ import org.jetbrains.skiko.MainUIDispatcher import org.jetbrains.skiko.OS import org.jetbrains.skiko.SkiaLayerAnalytics import org.junit.Assume.assumeFalse +import org.junit.Rule import org.junit.Test class ComposePanelTest { + @get:Rule + val throwUncaughtExceptionRule = ThrowUncaughtExceptionRule() + @Test fun `don't override user preferred size`() { assumeFalse(GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance) @@ -270,6 +279,47 @@ class ComposePanelTest { } } + // https://github.com/JetBrains/compose-multiplatform/issues/4479 + @Test + fun `add, removing, add, set size`() { + assumeFalse(GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance) + + runBlocking(MainUIDispatcher) { + var size = Size.Zero + val composePanel = ComposePanel() + composePanel.setContent { + Box(Modifier.fillMaxSize().onGloballyPositioned { + size = it.size.toSize() + }) + } + + val frame = JFrame() + frame.isUndecorated = true + frame.size = Dimension(100, 100) + try { + val density = frame.contentPane.density.density + frame.contentPane.add(composePanel) + frame.isVisible = true + delay(1000) + assertEquals(Size(100f * density, 100f * density), size) + + frame.contentPane.remove(composePanel) + delay(1000) + assertEquals(Size(100f * density, 100f * density), size) + + frame.contentPane.add(composePanel) + delay(1000) + assertEquals(Size(100f * density, 100f * density), size) + + frame.size = Dimension(200, 100) + delay(1000) + assertEquals(Size(200f * density, 100f * density), size) + } finally { + frame.dispose() + } + } + } + @Test fun `initial panel size with border layout`() { assumeFalse(GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance) diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/util/ThrowUncaughtExceptionRule.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/util/ThrowUncaughtExceptionRule.kt new file mode 100644 index 0000000000000..af6e39947f4f8 --- /dev/null +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/util/ThrowUncaughtExceptionRule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 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.util + +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * A rule that throw an exception in the end of test if there were any uncaught exceptions. + * + * It is needed in cases, where exceptions are thrown outside testing thread. + * + * For example, AWT Event Thread can fire event independently, + * and its handler can throw an exception without failing the test. + * + * Usage: + * @get:Rule + * val throwUncaughtExceptionRule = ThrowUncaughtExceptionRule() + */ +class ThrowUncaughtExceptionRule : TestRule { + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + val oldHandler = Thread.getDefaultUncaughtExceptionHandler() + var exception: Throwable? = null + + Thread.setDefaultUncaughtExceptionHandler { t, e -> + if (exception != null) { + exception!!.addSuppressed(e) + } else { + exception = e + } + } + + try { + base.evaluate() + exception?.let { throw it } + } finally { + Thread.setDefaultUncaughtExceptionHandler(oldHandler) + } + } + } + } +} \ No newline at end of file