diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt index 55da14adad075..2c46d7febb94e 100644 --- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt +++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ComposeView.android.kt @@ -637,14 +637,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 companion object } -/** - * Flag to disable WindowInsetsRulers. System UI needs to disable WindowInsets Rulers for all - * ComposeViews, so this is a global switch. We don't want to have them add a ComposeView and the - * new ComposeView suddenly requests WindowInsets updates, changing the behavior so that the insets - * suddenly notify. - */ -internal var areWindowInsetsRulersEnabled = true - /** * Used to disable [androidx.compose.ui.layout.WindowInsetsRulers]. This can be used when UI never * reads WindowInsets across all ComposeViews to reduce the overhead of requesting WindowInsets @@ -652,7 +644,7 @@ internal var areWindowInsetsRulersEnabled = true * the application. This should be called before the first [ComposeView] is created. */ fun ComposeView.Companion.disableWindowInsetsRulers() { - areWindowInsetsRulersEnabled = false + androidx.compose.ui.disableWindowInsetsRulers() } @OptIn(ExperimentalComposeUiApi::class) diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DisableWindowInsetsRulers.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DisableWindowInsetsRulers.kt new file mode 100644 index 0000000000000..b24faf4536a74 --- /dev/null +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DisableWindowInsetsRulers.kt @@ -0,0 +1,36 @@ +/* + * 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 + +/** + * Flag to disable WindowInsetsRulers. Some integrations need to disable WindowInsets Rulers for all + * Compose roots, so this is a global switch. We don't want them to add a new Compose root and have + * it suddenly request WindowInsets updates, changing the behavior so that the insets suddenly + * notify. + */ +internal var areWindowInsetsRulersEnabled: Boolean = true + +/** + * Used to disable [androidx.compose.ui.layout.WindowInsetsRulers]. This can be used when UI never + * reads WindowInsets across all Compose roots to reduce the overhead of requesting WindowInsets + * updates. Only call this when no Compose roots will ever need to handle insets over the lifetime of + * the application. This should be called before the first Compose root is created. + */ +@ExperimentalComposeUiApi +fun disableWindowInsetsRulers() { + areWindowInsetsRulersEnabled = false +} diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeUiFlags.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeUiFlags.skiko.kt index 7d1e4ea9db654..d6fc0bafcb9a1 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeUiFlags.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeUiFlags.skiko.kt @@ -30,10 +30,6 @@ internal object SkikoComposeUiFlags { @Suppress("MutableBareField") @JvmField var isDialogAnimationEnabled: Boolean = true - - @Suppress("MutableBareField") - @JvmField - var areWindowInsetsRulersEnabled: Boolean = true } /** @@ -60,20 +56,3 @@ var ComposeUiFlags.isClearFocusOnMouseDownEnabled by SkikoComposeUiFlags::isClea */ @ExperimentalComposeUiApi var ComposeUiFlags.isDialogAnimationEnabled by SkikoComposeUiFlags::isDialogAnimationEnabled - -/** - * Enable WindowInsets rulers: - * * `SystemBarsRulers` - * * `ImeRulers` - * * `StatusBarsRulers` - * * `NavigationBarsRulers` - * * `CaptionBarRulers` - * * `MandatorySystemGesturesRulers` - * * `TappableElementRulers` - * * `WaterfallRulers` - * * `SafeDrawingRulers` - * * `SafeGesturesRulers` - * * `SafeContentRulers` - */ -@ExperimentalComposeUiApi -var ComposeUiFlags.areWindowInsetsRulersEnabled by SkikoComposeUiFlags::areWindowInsetsRulersEnabled diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index b8ccca31e031e..e36e5df7a87d8 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt @@ -1011,7 +1011,7 @@ private object IdentityPositionCalculator : PositionCalculator { } private fun Modifier.rulerProvider(windowInsets: PlatformWindowInsets) = - if (ComposeUiFlags.areWindowInsetsRulersEnabled) then(RulerProviderModifierElement(windowInsets)) else this + if (areWindowInsetsRulersEnabled) then(RulerProviderModifierElement(windowInsets)) else this private data class RootWindowInsetsProviderModifierElement( val windowInsets: PlatformWindowInsets, diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/DisableWindowInsetsRulersTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/DisableWindowInsetsRulersTest.kt new file mode 100644 index 0000000000000..75053ea144ab0 --- /dev/null +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/DisableWindowInsetsRulersTest.kt @@ -0,0 +1,87 @@ +/* + * 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.layout + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.areWindowInsetsRulersEnabled +import androidx.compose.ui.platform.PlatformInsets +import androidx.compose.ui.platform.PlatformWindowInsets +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.InternalTestApi +import androidx.compose.ui.test.v2.runInternalSkikoComposeUiTest +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertTrue + +@OptIn(ExperimentalTestApi::class, InternalTestApi::class) +class DisableWindowInsetsRulersTest { + + @AfterTest + fun tearDown() { + areWindowInsetsRulersEnabled = true + } + + @Test + fun disableWindowInsetsRulers() { + androidx.compose.ui.disableWindowInsetsRulers() + + runInternalSkikoComposeUiTest( + windowInsets = TestWindowInsets(systemBarsInsets = mutableStateOf(PlatformInsets(top = 100))) + ) { + var left = 0f + var top = 0f + var right = 0f + var bottom = 0f + + setContent { + Box( + Modifier.fillMaxSize().layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + left = WindowInsetsRulers.StatusBars.current.left.current(Float.NaN) + top = WindowInsetsRulers.StatusBars.current.top.current(Float.NaN) + right = WindowInsetsRulers.StatusBars.current.right.current(Float.NaN) + bottom = WindowInsetsRulers.StatusBars.current.bottom.current(Float.NaN) + } + } + ) + } + + waitForIdle() + + assertTrue(left.isNaN()) + assertTrue(top.isNaN()) + assertTrue(right.isNaN()) + assertTrue(bottom.isNaN()) + } + } +} + +private fun TestWindowInsets( + systemBarsInsets: androidx.compose.runtime.State = mutableStateOf(PlatformInsets.Zero) +): PlatformWindowInsets = object : PlatformWindowInsets { + override val statusBars: PlatformInsets get() = PlatformInsets( + getBottom = { systemBarsInsets.value.bottom }, + getTop = { systemBarsInsets.value.top }, + getLeft = { systemBarsInsets.value.left }, + getRight = { systemBarsInsets.value.right } + ) +} diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layout/WindowInsetsRulersTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layout/WindowInsetsRulersTest.kt index 43728929bfda9..55a27c34fd827 100644 --- a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layout/WindowInsetsRulersTest.kt +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layout/WindowInsetsRulersTest.kt @@ -22,9 +22,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.areWindowInsetsRulersEnabled import androidx.compose.ui.layout.Placeable.PlacementScope import androidx.compose.ui.layout.WindowInsetsRulers.Companion.DisplayCutout +import androidx.compose.ui.layout.WindowInsetsRulers.Companion.StatusBars import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.test.UIKitInstrumentedTest import androidx.compose.ui.test.runUIKitInstrumentedTest @@ -33,9 +36,11 @@ import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toPlatformInsets import kotlin.math.roundToInt +import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertTrue import kotlinx.cinterop.ExperimentalForeignApi import platform.UIKit.UIInterfaceOrientationLandscapeLeft import platform.UIKit.UIInterfaceOrientationLandscapeRight @@ -43,13 +48,18 @@ import platform.UIKit.UIInterfaceOrientationPortrait import platform.UIKit.UIInterfaceOrientationPortraitUpsideDown import platform.UIKit.UIView -@OptIn(ExperimentalForeignApi::class) +@OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) class WindowInsetsRulersTest { private var contentSize: IntSize = IntSize.Zero private var insetsRect: IntRect? = null private val displayCutoutRects = mutableObjectListOf() + @AfterTest + fun tearDown() { + areWindowInsetsRulersEnabled = true + } + @Test fun testDisplayCutoutsForPortrait() = runUIKitInstrumentedTest( ignoreIf = UIKitInstrumentedTest.isRunningOnIPad, @@ -209,6 +219,36 @@ class WindowInsetsRulersTest { assertEquals(boundingRectFromDisplayCutouts, insetsRect) } + @Test + fun testDisableWindowInsetsRulers() = runUIKitInstrumentedTest { + androidx.compose.ui.disableWindowInsetsRulers() + + var left = 0f + var top = 0f + var right = 0f + var bottom = 0f + + setContent { + Box( + Modifier.fillMaxSize().layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + left = StatusBars.current.left.current(Float.NaN) + top = StatusBars.current.top.current(Float.NaN) + right = StatusBars.current.right.current(Float.NaN) + bottom = StatusBars.current.bottom.current(Float.NaN) + } + } + ) + } + + assertTrue(left.isNaN()) + assertTrue(top.isNaN()) + assertTrue(right.isNaN()) + assertTrue(bottom.isNaN()) + } + private val boundingRectFromDisplayCutouts: IntRect get() { var left = contentSize.width var top = contentSize.height