diff --git a/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ProvidesLocalsTest.kt b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ProvidesLocalsTest.kt new file mode 100644 index 0000000000000..a787151949419 --- /dev/null +++ b/compose/ui/ui-test-junit4/src/desktopTest/kotlin/androidx/compose/ui/test/ProvidesLocalsTest.kt @@ -0,0 +1,40 @@ +/* + * 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.test + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.test.junit4.createComposeRule +import kotlin.test.assertNotEquals +import org.junit.Rule +import org.junit.Test + +class ProvidesLocalsTest { + + @get:Rule + val rule = createComposeRule() + + @OptIn(ExperimentalComposeUiApi::class) + @Test + fun providesWindowInfo() { + rule.setContent { + val windowInfo = LocalWindowInfo.current + assertNotEquals(0, windowInfo.containerSize.width) + assertNotEquals(0, windowInfo.containerSize.height) + } + } +} \ No newline at end of file diff --git a/compose/ui/ui-test-junit4/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt b/compose/ui/ui-test-junit4/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt index e0de1051c3360..895198528183a 100644 --- a/compose/ui/ui-test-junit4/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt +++ b/compose/ui/ui-test-junit4/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt @@ -17,25 +17,33 @@ package androidx.compose.ui.test import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.ComposeScene +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.InfiniteAnimationPolicy +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.SkiaRootForTest +import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsNode -import androidx.compose.ui.test.junit4.* import androidx.compose.ui.test.junit4.MainTestClockImpl import androidx.compose.ui.test.junit4.UncaughtExceptionHandler import androidx.compose.ui.test.junit4.isOnUiThread +import androidx.compose.ui.test.junit4.sleep import androidx.compose.ui.test.junit4.synchronized -import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.input.* +import androidx.compose.ui.text.input.EditCommand +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.ImeOptions +import androidx.compose.ui.text.input.PlatformTextInputService +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.cancellation.CancellationException @@ -189,6 +197,7 @@ class SkikoComposeUiTest( invalidate = { } ).apply { constraints = Constraints(maxWidth = surface.width, maxHeight = surface.height) + // TODO: Initialize WindowInfo once public way is available } private fun shouldPumpTime(): Boolean { @@ -282,12 +291,24 @@ class SkikoComposeUiTest( idlingResources.all { it.isIdleNow } } + @OptIn(ExperimentalComposeUiApi::class) override fun setContent(composable: @Composable () -> Unit) { + // TODO: Remove override once it's properly initialized + val overriddenComposable: @Composable () -> Unit = { + val windowInfo = object : WindowInfo by LocalWindowInfo.current { + override val containerSize: IntSize + get() = IntSize(surface.width, surface.height) + } + CompositionLocalProvider( + LocalWindowInfo provides windowInfo, + content = composable + ) + } if (isOnUiThread()) { - scene.setContent(content = composable) + scene.setContent(content = overriddenComposable) } else { runOnUiThread { - scene.setContent(content = composable) + scene.setContent(content = overriddenComposable) } // Only wait for idleness if not on the UI thread. If we are on the UI thread, the diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowInfo.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowInfo.android.kt new file mode 100644 index 0000000000000..0af4b3dfe6d50 --- /dev/null +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowInfo.android.kt @@ -0,0 +1,59 @@ +/* + * 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.platform + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers +import androidx.compose.ui.input.pointer.PointerKeyboardModifiers + +/** + * Provides information about the Window that is hosting this compose hierarchy. + */ +@Stable +actual interface WindowInfo { + /** + * Indicates whether the window hosting this compose hierarchy is in focus. + * + * When there are multiple windows visible, either in a multi-window environment or if a + * popup or dialog is visible, this property can be used to determine if the current window + * is in focus. + */ + actual val isWindowFocused: Boolean + + /** + * Indicates the state of keyboard modifiers (pressed or not). + */ + @ExperimentalComposeUiApi + actual val keyboardModifiers: PointerKeyboardModifiers +} + +internal class WindowInfoImpl : WindowInfo { + override var isWindowFocused: Boolean by mutableStateOf(false) + + @ExperimentalComposeUiApi + override var keyboardModifiers: PointerKeyboardModifiers by GlobalKeyboardModifiers + + companion object { + // One instance across all windows makes sense, since the state of KeyboardModifiers is + // common for all windows. + internal val GlobalKeyboardModifiers = mutableStateOf(EmptyPointerKeyboardModifiers()) + } +} diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt index c43cef7585957..a03b4c9e5cbed 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt @@ -19,19 +19,16 @@ package androidx.compose.ui.platform import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers import androidx.compose.ui.input.pointer.PointerKeyboardModifiers -import kotlinx.coroutines.flow.collect /** * Provides information about the Window that is hosting this compose hierarchy. */ @Stable -interface WindowInfo { +expect interface WindowInfo { /** * Indicates whether the window hosting this compose hierarchy is in focus. * @@ -44,10 +41,8 @@ interface WindowInfo { /** * Indicates the state of keyboard modifiers (pressed or not). */ - @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") - @get:ExperimentalComposeUiApi + @ExperimentalComposeUiApi val keyboardModifiers: PointerKeyboardModifiers - get() = WindowInfoImpl.GlobalKeyboardModifiers.value } @Composable @@ -58,23 +53,3 @@ internal fun WindowFocusObserver(onWindowFocusChanged: (isWindowFocused: Boolean snapshotFlow { windowInfo.isWindowFocused }.collect { callback.value(it) } } } - -internal class WindowInfoImpl : WindowInfo { - private val _isWindowFocused = mutableStateOf(false) - - override var isWindowFocused: Boolean - set(value) { _isWindowFocused.value = value } - get() = _isWindowFocused.value - - @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") - @get:ExperimentalComposeUiApi - override var keyboardModifiers: PointerKeyboardModifiers - get() = GlobalKeyboardModifiers.value - set(value) { GlobalKeyboardModifiers.value = value } - - companion object { - // One instance across all windows makes sense, since the state of KeyboardModifiers is - // common for all windows. - internal val GlobalKeyboardModifiers = mutableStateOf(EmptyPointerKeyboardModifiers()) - } -} diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeBridge.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeBridge.desktop.kt index 236eadc4e22ef..81ce83d8a9d91 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeBridge.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposeBridge.desktop.kt @@ -32,6 +32,7 @@ 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.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowExceptionHandler @@ -301,9 +302,15 @@ internal abstract class ComposeBridge { } protected fun updateSceneSize() { + val scale = component.density.density + val size = IntSize( + width = (component.width * scale).toInt(), + height = (component.height * scale).toInt() + ) + platform.windowInfo.containerSize = size scene.constraints = Constraints( - maxWidth = (component.width * scene.density.density).toInt(), - maxHeight = (component.height * scene.density.density).toInt() + maxWidth = size.width, + maxHeight = size.height ) } diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposeWindowTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposeWindowTest.kt index 7f0a76e82819b..4909643fb40a1 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposeWindowTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/awt/ComposeWindowTest.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.layout.layout import androidx.compose.ui.sendMouseEvent import androidx.compose.ui.window.WindowExceptionHandler import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.density import androidx.compose.ui.window.runApplicationTest @@ -151,6 +152,12 @@ class ComposeWindowTest { assertThat(window.preferredSize).isEqualTo(Dimension(234, 345)) window.pack() assertThat(window.size).isEqualTo(Dimension(234, 345)) + + assertThat(window.scene.platform.windowInfo.containerSize) + .isEqualTo(IntSize( + width = (234 * window.density.density).toInt(), + height = (345 * window.density.density).toInt(), + )) } finally { window.dispose() } @@ -176,6 +183,12 @@ class ComposeWindowTest { window.isVisible = true assertThat(window.preferredSize).isEqualTo(Dimension(300, 400)) assertThat(window.size).isEqualTo(Dimension(300, 400)) + + assertThat(window.scene.platform.windowInfo.containerSize) + .isEqualTo(IntSize( + width = (300 * window.density.density).toInt(), + height = (400 * window.density.density).toInt(), + )) } finally { window.dispose() } @@ -210,6 +223,12 @@ class ComposeWindowTest { ) ) ) + + assertThat(window.scene.platform.windowInfo.containerSize) + .isEqualTo(IntSize( + width = (300 * window.density.density).toInt(), + height = (400 * window.density.density).toInt(), + )) } finally { window.dispose() } diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopWindowInfoTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopWindowInfoTest.kt new file mode 100644 index 0000000000000..e7b5cef756257 --- /dev/null +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopWindowInfoTest.kt @@ -0,0 +1,42 @@ +/* + * 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.platform + +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.rememberWindowState +import androidx.compose.ui.window.runApplicationTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class DesktopWindowInfoTest { + + @Test + fun windowContainerSize() = runApplicationTest { + launchTestApplication { + val state = rememberWindowState( + size = DpSize(123.dp, 321.dp) + ) + Window(onCloseRequest = {}, state = state) { + val containerSize = window.scene.platform.windowInfo.containerSize + assertEquals(123, containerSize.width) + assertEquals(321, containerSize.height) + } + } + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt index 22d1b2b184929..65739a1ef730e 100644 --- a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt +++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.native.ComposeLayer import androidx.compose.ui.platform.JSTextInputService import androidx.compose.ui.platform.Platform import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.platform.WindowInfoImpl import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp @@ -51,8 +52,12 @@ internal actual class ComposeWindow(val canvasId: String) { fontScale = 1f ) + private val _windowInfo = WindowInfoImpl().apply { + isWindowFocused = true + } private val jsTextInputService = JSTextInputService() val platform = object : Platform by Platform.Empty { + override val windowInfo get() = _windowInfo override val textInputService = jsTextInputService override val viewConfiguration = object : ViewConfiguration { override val longPressTimeoutMillis: Long = 500 @@ -82,6 +87,7 @@ internal actual class ComposeWindow(val canvasId: String) { canvas.setAttribute("tabindex", "0") layer.layer.needRedraw() + _windowInfo.containerSize = IntSize(canvas.width, canvas.height) layer.setSize(canvas.width, canvas.height) } @@ -94,6 +100,7 @@ internal actual class ComposeWindow(val canvasId: String) { canvas.width = newSize.width canvas.height = newSize.height + _windowInfo.containerSize = IntSize(canvas.width, canvas.height) layer.layer.attachTo(canvas) layer.setSize(canvas.width, canvas.height) layer.layer.needRedraw() diff --git a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt index cdfae7d42b869..793445531b19e 100644 --- a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt +++ b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt @@ -18,18 +18,23 @@ package androidx.compose.ui.window import androidx.compose.runtime.Composable import androidx.compose.ui.createSkiaLayer -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.native.ComposeLayer import androidx.compose.ui.platform.MacosTextInputService import androidx.compose.ui.platform.Platform +import androidx.compose.ui.platform.WindowInfoImpl import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize import platform.AppKit.* import platform.Foundation.* import kotlinx.cinterop.* internal actual class ComposeWindow actual constructor() { private val macosTextInputService = MacosTextInputService() + private val _windowInfo = WindowInfoImpl().apply { + isWindowFocused = true + } val platform: Platform = object : Platform by Platform.Empty { + override val windowInfo get() = _windowInfo override val textInputService = macosTextInputService } val layer = ComposeLayer( @@ -56,11 +61,16 @@ internal actual class ComposeWindow actual constructor() { init { layer.layer.attachTo(nsWindow) nsWindow.orderFrontRegardless() - contentRect.useContents { - val scale = nsWindow.backingScaleFactor.toFloat() - layer.setDensity(Density(scale)) - layer.setSize((size.width * scale).toInt(), (size.height * scale).toInt()) + val scale = nsWindow.backingScaleFactor.toFloat() + val size = contentRect.useContents { + IntSize( + width = (size.width * scale).toInt(), + height = (size.height * scale).toInt() + ) } + _windowInfo.containerSize = size + layer.setDensity(Density(scale)) + layer.setSize(size.width, size.height) } /** diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/Platform.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/Platform.skiko.kt index 3292ce017766b..fd9b80c722a66 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/Platform.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/Platform.skiko.kt @@ -48,7 +48,6 @@ internal interface Platform { val textToolbar: TextToolbar companion object { - @OptIn(ExperimentalComposeUiApi::class) val Empty = object : Platform { override val windowInfo = WindowInfoImpl().apply { // true is a better default if platform doesn't provide WindowInfo. diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/WindowInfo.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/WindowInfo.skiko.kt new file mode 100644 index 0000000000000..eddbf3cd3e32b --- /dev/null +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/WindowInfo.skiko.kt @@ -0,0 +1,69 @@ +/* + * 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.platform + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.input.pointer.EmptyPointerKeyboardModifiers +import androidx.compose.ui.input.pointer.PointerKeyboardModifiers +import androidx.compose.ui.unit.IntSize + +/** + * Provides information about the Window that is hosting this compose hierarchy. + */ +@Stable +actual interface WindowInfo { + /** + * Indicates whether the window hosting this compose hierarchy is in focus. + * + * When there are multiple windows visible, either in a multi-window environment or if a + * popup or dialog is visible, this property can be used to determine if the current window + * is in focus. + */ + actual val isWindowFocused: Boolean + + /** + * Indicates the state of keyboard modifiers (pressed or not). + */ + @ExperimentalComposeUiApi + actual val keyboardModifiers: PointerKeyboardModifiers + + /** + * Size of the window's content container in pixels. + */ + @ExperimentalComposeUiApi + val containerSize: IntSize +} + +internal class WindowInfoImpl : WindowInfo { + override var isWindowFocused: Boolean by mutableStateOf(false) + + @ExperimentalComposeUiApi + override var keyboardModifiers: PointerKeyboardModifiers by GlobalKeyboardModifiers + + @ExperimentalComposeUiApi + override var containerSize: IntSize by mutableStateOf(IntSize.Zero) + + companion object { + // One instance across all windows makes sense, since the state of KeyboardModifiers is + // common for all windows. + internal val GlobalKeyboardModifiers = mutableStateOf(EmptyPointerKeyboardModifiers()) + } +} diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt new file mode 100644 index 0000000000000..41ee15231e7ec --- /dev/null +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/WindowInfoTest.kt @@ -0,0 +1,49 @@ +/* + * 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.platform + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertHeightIsEqualTo +import androidx.compose.ui.test.assertWidthIsEqualTo +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.runSkikoComposeUiTest +import androidx.compose.ui.unit.dp +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalTestApi::class) +class WindowInfoTest { + + @Test + fun windowContainerSize() = runSkikoComposeUiTest( + size = Size(123f, 321f) + ) { + setContent { + Box(Modifier.fillMaxSize().testTag("box")) + + val containerSize = LocalWindowInfo.current.containerSize + assertEquals(123, containerSize.width) + assertEquals(321, containerSize.height) + } + onNodeWithTag("box").assertWidthIsEqualTo(123.dp) + onNodeWithTag("box").assertHeightIsEqualTo(321.dp) + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt index b6227452849ae..5a60d40330e53 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt @@ -183,6 +183,10 @@ internal actual class ComposeWindow : UIViewController { } } + private val _windowInfo = WindowInfoImpl().apply { + isWindowFocused = true + } + @OverrideInit actual constructor() : super(nibName = null, bundle = null) @@ -341,15 +345,19 @@ internal actual class ComposeWindow : UIViewController { } private fun updateLayout(context: AttachedComposeContext) { - context.scene.density = density - context.scene.constraints = view.frame.useContents { - val scale = density.density - - Constraints( - maxWidth = (size.width * scale).roundToInt(), - maxHeight = (size.height * scale).roundToInt() + val scale = density.density + val size = view.frame.useContents { + IntSize( + width = (size.width * scale).roundToInt(), + height = (size.height * scale).roundToInt() ) } + _windowInfo.containerSize = size + context.scene.density = density + context.scene.constraints = Constraints( + maxWidth = size.width, + maxHeight = size.height + ) context.view.needRedraw() } @@ -523,6 +531,8 @@ internal actual class ComposeWindow : UIViewController { val inputTraits = inputServices.skikoUITextInputTraits val platform = object : Platform by Platform.Empty { + override val windowInfo: WindowInfo + get() = _windowInfo override val textInputService: PlatformTextInputService = inputServices override val viewConfiguration = object : ViewConfiguration {