diff --git a/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsets.skiko.kt b/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsets.skiko.kt index 8d174155ebb51..94b58dee99e8f 100644 --- a/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsets.skiko.kt +++ b/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsets.skiko.kt @@ -94,4 +94,8 @@ internal fun PlatformInsets.toWindowInsets(): WindowInsets = object : WindowInse override fun getTop(density: Density): Int = top override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = right override fun getBottom(density: Density): Int = bottom + + override fun toString(): String { + return "PlatformInsets.toWindowInsets(getLeft=$left, getTop=$top, getRight=$right, getBottom=$bottom)" + } } diff --git a/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.skiko.kt b/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.skiko.kt index c942a76ff3132..f22bb8d536e8a 100644 --- a/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.skiko.kt +++ b/compose/foundation/foundation-layout/src/skikoMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.skiko.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.observeReads +import androidx.compose.ui.node.traverseAncestors import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.PlatformWindowInsets import androidx.compose.ui.platform.PlatformWindowInsetsProviderNode @@ -155,9 +156,10 @@ private val mandatorySystemGesturesPaddingLambda: PlatformWindowInsets.() -> Win @Stable private fun Modifier.windowInsetsPadding( inspectorInfo: InspectorInfo.() -> Unit, - insetsCalculation: PlatformWindowInsets.() -> WindowInsets -): Modifier = - this then PlatformWindowInsetsPaddingModifierElement(inspectorInfo, insetsCalculation) + insetsCalculation: PlatformWindowInsets.() -> WindowInsets, +): Modifier = this then + PlatformInsetsPaddingModifierElement(inspectorInfo) then + PlatformWindowInsetsPaddingModifierElement(inspectorInfo, insetsCalculation) private class PlatformWindowInsetsPaddingModifierElement( private val inspectorInfo: InspectorInfo.() -> Unit, @@ -178,9 +180,7 @@ private class PlatformWindowInsetsPaddingModifierNode( private var insetsGetter: PlatformWindowInsets.() -> WindowInsets, ): PlatformWindowInsetsProviderNode(), ObserverModifierNode { - private val insetsPaddingNode = delegate( - InsetsPaddingModifierNode(WindowInsets()) - ) + private var insetsPaddingNode: PlatformInsetsPaddingModifierNode? = null fun update(insetsGetter: (PlatformWindowInsets) -> WindowInsets) { if (this.insetsGetter !== insetsGetter) { @@ -194,9 +194,26 @@ private class PlatformWindowInsetsPaddingModifierNode( override fun onAttach() { super.onAttach() + traverseAncestors(InsetsConsumingModifierNodeKey) { node -> + if (node is PlatformInsetsPaddingModifierNode) { + insetsPaddingNode = node + false + } else true + } + onObservedReadsChanged() } + override fun onReset() { + insetsPaddingNode = null + super.onReset() + } + + override fun onDetach() { + insetsPaddingNode = null + super.onDetach() + } + override fun windowInsetsInvalidated() { super.windowInsetsInvalidated() @@ -205,7 +222,26 @@ private class PlatformWindowInsetsPaddingModifierNode( override fun onObservedReadsChanged() { observeReads { - insetsPaddingNode.update(windowInsets.insetsGetter()) + insetsPaddingNode?.update(windowInsets.insetsGetter()) } } -} \ No newline at end of file +} + +private class PlatformInsetsPaddingModifierElement( + private val inspectorInfo: InspectorInfo.() -> Unit, +): ModifierNodeElement() { + override fun create(): PlatformInsetsPaddingModifierNode = PlatformInsetsPaddingModifierNode() + override fun update(node: PlatformInsetsPaddingModifierNode) {} + override fun hashCode(): Int = 0 + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PlatformInsetsPaddingModifierElement) return false + return inspectorInfo === other.inspectorInfo + } + override fun InspectorInfo.inspectableProperties() = inspectorInfo() +} + +private class PlatformInsetsPaddingModifierNode: InsetsPaddingModifierNode(WindowInsets()) + +// TODO: https://youtrack.jetbrains.com/issue/CMP-9483 remove with a conflict after AOSP merge +internal const val InsetsConsumingModifierNodeKey = "androidx.compose.foundation.layout.ConsumedInsetsProvider" \ No newline at end of file diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/WindowInsetsTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/WindowInsetsTest.kt index 645ff37d6b058..b07422cb5aee7 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/WindowInsetsTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/layout/WindowInsetsTest.kt @@ -16,9 +16,15 @@ package androidx.compose.ui.layout +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -26,11 +32,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color 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.runInternalSkikoComposeUiTest +import androidx.compose.ui.unit.dp import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -177,6 +185,154 @@ class WindowInsetsTest { assertEquals(1, compositionCount) } } + + @Test + fun testConsumeWindowInsetsWithWindowInsetsPadding() = runInternalSkikoComposeUiTest { + var outerBoxRect = Rect.Zero + var innerBoxRect = Rect.Zero + + setContent { + Box(Modifier + .fillMaxSize() + .consumeWindowInsets(WindowInsets(top = 100.dp)) + .background(Color.Red) + .onGloballyPositioned { outerBoxRect = it.boundsInRoot() } + ) { + Box(Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets(top = 101.dp)) + .background(Color.Blue) + .onGloballyPositioned { innerBoxRect = it.boundsInRoot() } + ) + } + } + + assertEquals(Rect(outerBoxRect.left, 1f, outerBoxRect.right, outerBoxRect.bottom), innerBoxRect) + } + + @Test + fun testConsumeSystemWindowInsetsWithSystemPadding() = runInternalSkikoComposeUiTest( + windowInsets = TestWindowInsets(systemBarsInsets = mutableStateOf(PlatformInsets(top = 100))) + ) { + var outerBoxRect = Rect.Zero + var innerBoxRect = Rect.Zero + + setContent { + Box(Modifier + .fillMaxSize() + .consumeWindowInsets(WindowInsets.systemBars) + .background(Color.Red) + .onGloballyPositioned { outerBoxRect = it.boundsInRoot() } + ) { + Box(Modifier + .fillMaxSize() + .systemBarsPadding() + .background(Color.Blue) + .onGloballyPositioned { innerBoxRect = it.boundsInRoot() } + ) + } + } + + assertEquals(outerBoxRect, innerBoxRect) + } + + @Test + fun testConsumeWindowInsetsWithSystemPadding() = runInternalSkikoComposeUiTest( + windowInsets = TestWindowInsets(systemBarsInsets = mutableStateOf(PlatformInsets(top = 101))) + ) { + var outerBoxRect = Rect.Zero + var innerBoxRect = Rect.Zero + + setContent { + Box(Modifier + .fillMaxSize() + .consumeWindowInsets(WindowInsets(top = 100.dp)) + .background(Color.Red) + .onGloballyPositioned { outerBoxRect = it.boundsInRoot() } + ) { + Box(Modifier + .fillMaxSize() + .systemBarsPadding() + .background(Color.Blue) + .onGloballyPositioned { innerBoxRect = it.boundsInRoot() } + ) + } + } + + assertEquals(Rect(outerBoxRect.left, 1f, outerBoxRect.right, outerBoxRect.bottom), innerBoxRect) + } + + @Test + fun testConsumeWindowInsetsPaddingOnSystemInsetsChange() { + val systemBarsInsets = mutableStateOf(PlatformInsets.Zero) + + runInternalSkikoComposeUiTest( + windowInsets = TestWindowInsets(systemBarsInsets = systemBarsInsets) + ) { + var outerBoxRect = Rect.Zero + var innerBoxRect = Rect.Zero + + setContent { + Box(Modifier + .fillMaxSize() + .consumeWindowInsets(WindowInsets(top = 100.dp)) + .background(Color.Red) + .onGloballyPositioned { outerBoxRect = it.boundsInRoot() } + ) { + Box(Modifier + .fillMaxSize() + .systemBarsPadding() + .background(Color.Blue) + .onGloballyPositioned { innerBoxRect = it.boundsInRoot() } + ) + } + } + + assertEquals(outerBoxRect, innerBoxRect) + + systemBarsInsets.value = PlatformInsets(top = 101) + waitForIdle() + + assertEquals(Rect(outerBoxRect.left, 1f, outerBoxRect.right, outerBoxRect.bottom), innerBoxRect) + } + } + + @Test + fun testConsumeSystemWindowInsetsPaddingOnSystemInsetsChange() { + val systemBarsInsets = mutableStateOf(PlatformInsets.Zero) + + runInternalSkikoComposeUiTest( + windowInsets = TestWindowInsets(systemBarsInsets = systemBarsInsets) + ) { + var outerBoxRect = Rect.Zero + var innerBoxRect = Rect.Zero + val maxBottomInset = 100 + + setContent { + Box(Modifier + .fillMaxSize() + .consumeWindowInsets(WindowInsets.systemBars) + .background(Color.Red) + .onGloballyPositioned { outerBoxRect = it.boundsInRoot() } + ) { + Box(Modifier + .fillMaxSize() + .systemBarsPadding() + .background(Color.Blue) + .onGloballyPositioned { innerBoxRect = it.boundsInRoot() } + ) + } + } + + assertEquals(outerBoxRect, innerBoxRect) + + for (i in 1..maxBottomInset) { + systemBarsInsets.value = PlatformInsets(bottom = i) + waitForIdle() + assertEquals(outerBoxRect, innerBoxRect) + } + } + } } @Composable @@ -197,7 +353,8 @@ private fun TestContent( } private fun TestWindowInsets( - imeInsets: MutableState + imeInsets: MutableState = mutableStateOf(PlatformInsets.Zero), + systemBarsInsets: MutableState = mutableStateOf(PlatformInsets.Zero) ): PlatformWindowInsets = object : PlatformWindowInsets { override val ime: PlatformInsets get() = PlatformInsets( getBottom = { imeInsets.value.bottom }, @@ -205,4 +362,11 @@ private fun TestWindowInsets( getLeft = { imeInsets.value.left }, getRight = { imeInsets.value.right } ) + + override val systemBars: PlatformInsets get() = PlatformInsets( + getBottom = { systemBarsInsets.value.bottom }, + getTop = { systemBarsInsets.value.top }, + getLeft = { systemBarsInsets.value.left }, + getRight = { systemBarsInsets.value.right } + ) } \ No newline at end of file