Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
353d272
Remove unused imports
svastven Nov 13, 2025
3744760
Make toWindowInsets function internal
svastven Nov 13, 2025
cc993aa
Align PlatformWindowInsets safeDrawing and safeContent to Android
svastven Nov 13, 2025
02b0f53
Migrate window insets padding modifiers from composed API to InsetsPa…
svastven Nov 13, 2025
f5f62c7
Add test to WindowInsetsPaddingTest
svastven Nov 13, 2025
428f757
Remove unused import
svastven Nov 13, 2025
d885be1
Lint
svastven Nov 13, 2025
7903e54
Update compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/Ro…
svastven Nov 13, 2025
308ab48
Update exclude function implementation to use PlatformWindowInsets in…
svastven Nov 13, 2025
6f6912b
Disable useSoftwareKeyboardInset in ModalBottomSheetDialog properties
svastven Nov 13, 2025
1ec3640
Disable useSoftwareKeyboardInset in tests
svastven Nov 13, 2025
b506bd6
Remove redundant test
svastven Nov 13, 2025
226c7ef
Add windowInsets parameter to internal skiko compose ui test
svastven Nov 14, 2025
3061bcc
Do not provide windowInsets with LocalPlatformWindowInsets but instea…
svastven Nov 14, 2025
1f3a00b
Provide windowInsets to ui test directly instead of LocalPlatformWind…
svastven Nov 14, 2025
d6ec7e7
Remove unused imports
svastven Nov 14, 2025
20adaf1
Revert test changes
svastven Nov 14, 2025
06b723d
Update platform window insets tree propagation
svastven Dec 1, 2025
c11ea11
Merge branch 'jb-main' into svastven/migrate-insets-modifiers
svastven Dec 1, 2025
3c8d61e
Remove extra line
svastven Dec 1, 2025
3cf6884
Rename ime padding insets animation tests
svastven Dec 1, 2025
e0117d0
Revert removed imports
svastven Dec 2, 2025
98f0d4a
Fix platform insets calculation to use ancestorWindowInsets
svastven Dec 2, 2025
c807cfb
Make EmptyPlatformWindowInsets object internal API
svastven Dec 2, 2025
854d8f7
Merge branch 'jb-main' into svastven/migrate-insets-modifiers
svastven Dec 2, 2025
43f11c9
Track changes in insetsGetter and update insets padding node state
svastven Dec 3, 2025
43e9cf7
Update taverseKey of PlatformWindowInsetsProviderNode
svastven Dec 3, 2025
12bdfa0
MR updates
svastven Dec 3, 2025
77b12f7
Unify EmptyPlatformWindowInsets
svastven Dec 3, 2025
b8ed6c6
Add TODOs
svastven Dec 3, 2025
2bbfee6
Replace equals operator
svastven Dec 3, 2025
49fe3e0
Dump API
svastven Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import kotlin.jvm.JvmName

// TODO: https://youtrack.jetbrains.com/issue/CMP-9379

actual val WindowInsets.Companion.captionBar: WindowInsets
@Composable
get() = LocalPlatformWindowInsets.current.captionBar.toWindowInsets()
Expand Down Expand Up @@ -87,7 +89,7 @@ actual val WindowInsets.Companion.safeContent: WindowInsets
@Composable
get() = LocalPlatformWindowInsets.current.safeContent.toWindowInsets()

private fun PlatformInsets.toWindowInsets(): WindowInsets = object : WindowInsets {
internal fun PlatformInsets.toWindowInsets(): WindowInsets = object : WindowInsets {
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = left
override fun getTop(density: Density): Int = top
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = right
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,153 +14,198 @@
* limitations under the License.
*/

@file:OptIn(InternalComposeUiApi::class)

package androidx.compose.foundation.layout

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.ProvidableModifierLocal
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.PlatformWindowInsets
import androidx.compose.ui.platform.PlatformWindowInsetsProviderNode
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.offset
import androidx.compose.ui.platform.safeContent
import androidx.compose.ui.platform.safeDrawing
import androidx.compose.ui.platform.safeGestures

actual fun Modifier.safeDrawingPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "safeDrawingPadding" }) {
WindowInsets.safeDrawing
}
windowInsetsPadding(
debugInspectorInfo { name = "safeDrawingPadding" },
safeDrawingPaddingLambda
)

private val safeDrawingPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
safeDrawing.toWindowInsets()
}

actual fun Modifier.safeGesturesPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "safeGesturesPadding" }) {
WindowInsets.safeGestures
}
windowInsetsPadding(
debugInspectorInfo { name = "safeGesturesPadding" },
safeGesturesPaddingLambda
)

private val safeGesturesPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
safeGestures.toWindowInsets()
}

actual fun Modifier.safeContentPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "safeContentPadding" }) {
WindowInsets.safeContent
}
windowInsetsPadding(
debugInspectorInfo { name = "safeContentPadding" },
safeContentPaddingLambda
)

private val safeContentPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
safeContent.toWindowInsets()
}

actual fun Modifier.systemBarsPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "systemBarsPadding" }) {
WindowInsets.systemBars
}
windowInsetsPadding(
debugInspectorInfo { name = "systemBarsPadding" },
systemBarsPaddingLambda
)

private val systemBarsPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
systemBars.toWindowInsets()
}

actual fun Modifier.displayCutoutPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "displayCutoutPadding" }) {
WindowInsets.displayCutout
}
windowInsetsPadding(
debugInspectorInfo { name = "displayCutoutPadding" },
displayCutoutPaddingLambda
)

private val displayCutoutPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
displayCutout.toWindowInsets()
}

actual fun Modifier.statusBarsPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "statusBarsPadding" }) {
WindowInsets.statusBars
}
windowInsetsPadding(
debugInspectorInfo { name = "statusBarsPadding" },
statusBarsPaddingLambda
)

private val statusBarsPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
statusBars.toWindowInsets()
}

actual fun Modifier.imePadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "imePadding" }) {
WindowInsets.ime
}
windowInsetsPadding(
debugInspectorInfo { name = "imePadding" },
imePaddingLambda
)

private val imePaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
ime.toWindowInsets()
}

actual fun Modifier.navigationBarsPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "navigationBarsPadding" }) {
WindowInsets.navigationBars
}
windowInsetsPadding(
debugInspectorInfo { name = "navigationBarsPadding" },
navigationBarsPaddingLambda
)

private val navigationBarsPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
navigationBars.toWindowInsets()
}

actual fun Modifier.captionBarPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "captionBarPadding" }) {
WindowInsets.captionBar
}
windowInsetsPadding(
debugInspectorInfo { name = "captionBarPadding" },
captionBarPaddingLambda
)

private val captionBarPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
captionBar.toWindowInsets()
}

actual fun Modifier.waterfallPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "waterfallPadding" }) {
WindowInsets.waterfall
}
windowInsetsPadding(
debugInspectorInfo { name = "waterfallPadding" },
waterfallPaddingLambda
)

private val waterfallPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
waterfall.toWindowInsets()
}

actual fun Modifier.systemGesturesPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "systemGesturesPadding" }) {
WindowInsets.systemGestures
}
windowInsetsPadding(
debugInspectorInfo { name = "systemGesturesPadding" },
systemGesturesPaddingLambda
)

private val systemGesturesPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
systemGestures.toWindowInsets()
}

actual fun Modifier.mandatorySystemGesturesPadding(): Modifier =
windowInsetsPadding(debugInspectorInfo { name = "mandatorySystemGesturesPadding" }) {
WindowInsets.mandatorySystemGestures
}
windowInsetsPadding(
debugInspectorInfo { name = "mandatorySystemGesturesPadding" },
mandatorySystemGesturesPaddingLambda
)

// FIXME: Should be replaced with non-composed InsetsPaddingModifierElement
// https://youtrack.jetbrains.com/issue/CMP-8998
@Suppress("NOTHING_TO_INLINE")
@Stable
private inline fun Modifier.windowInsetsPadding(
noinline inspectorInfo: InspectorInfo.() -> Unit,
crossinline insetsCalculation: @Composable () -> WindowInsets
): Modifier = composed(inspectorInfo) {
val insets = insetsCalculation()
_InsetsPaddingModifier(insets)
private val mandatorySystemGesturesPaddingLambda: PlatformWindowInsets.() -> WindowInsets = {
mandatorySystemGestures.toWindowInsets()
}

private val ModifierLocalConsumedWindowInsets = modifierLocalOf { WindowInsets(0, 0, 0, 0) }
private class _InsetsPaddingModifier(private val insets: WindowInsets) :
LayoutModifier, ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
private var unconsumedInsets: WindowInsets by mutableStateOf(insets)
private var consumedInsets: WindowInsets by mutableStateOf(insets)

override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val left = unconsumedInsets.getLeft(this, layoutDirection)
val top = unconsumedInsets.getTop(this)
val right = unconsumedInsets.getRight(this, layoutDirection)
val bottom = unconsumedInsets.getBottom(this)

val horizontal = left + right
val vertical = top + bottom

val childConstraints = constraints.offset(-horizontal, -vertical)
val placeable = measurable.measure(childConstraints)

val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) { placeable.place(left, top) }
@Stable
private fun Modifier.windowInsetsPadding(
inspectorInfo: InspectorInfo.() -> Unit,
insetsCalculation: PlatformWindowInsets.() -> WindowInsets
): Modifier =
this then PlatformWindowInsetsPaddingModifierElement(inspectorInfo, insetsCalculation)

private class PlatformWindowInsetsPaddingModifierElement(
private val inspectorInfo: InspectorInfo.() -> Unit,
private val insetsGetter: PlatformWindowInsets.() -> WindowInsets
): ModifierNodeElement<PlatformWindowInsetsPaddingModifierNode>() {
override fun create(): PlatformWindowInsetsPaddingModifierNode = PlatformWindowInsetsPaddingModifierNode(insetsGetter)
override fun update(node: PlatformWindowInsetsPaddingModifierNode) = node.update(insetsGetter)
override fun InspectorInfo.inspectableProperties() = inspectorInfo()
override fun hashCode(): Int = insetsGetter.hashCode()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PlatformWindowInsetsPaddingModifierElement) return false
return insetsGetter === other.insetsGetter
}
}

private class PlatformWindowInsetsPaddingModifierNode(
private var insetsGetter: PlatformWindowInsets.() -> WindowInsets,
): PlatformWindowInsetsProviderNode(), ObserverModifierNode {

override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
val consumed = ModifierLocalConsumedWindowInsets.current
unconsumedInsets = insets.exclude(consumed)
consumedInsets = consumed.union(insets)
private val insetsPaddingNode = delegate(
InsetsPaddingModifierNode(WindowInsets())
)

fun update(insetsGetter: (PlatformWindowInsets) -> WindowInsets) {
if (this.insetsGetter !== insetsGetter) {
this.insetsGetter = insetsGetter
windowInsetsInvalidated()
}
}

override val key: ProvidableModifierLocal<WindowInsets>
get() = ModifierLocalConsumedWindowInsets
override fun calculatePlatformInsets(ancestorWindowInsets: PlatformWindowInsets): PlatformWindowInsets = ancestorWindowInsets

override val value: WindowInsets
get() = consumedInsets
override fun onAttach() {
super.onAttach()

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other !is _InsetsPaddingModifier) {
return false
}
onObservedReadsChanged()
}

override fun windowInsetsInvalidated() {
super.windowInsetsInvalidated()

return other.insets == insets
onObservedReadsChanged()
}

override fun hashCode(): Int = insets.hashCode()
}
override fun onObservedReadsChanged() {
observeReads {
insetsPaddingNode.update(windowInsets.insetsGetter())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformDragAndDropManager
import androidx.compose.ui.platform.PlatformDragAndDropSource
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
import androidx.compose.ui.platform.PlatformWindowInsets
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.scene.CanvasLayersComposeScene
import androidx.compose.ui.scene.ComposeScene
Expand Down Expand Up @@ -113,6 +114,7 @@ fun runInternalSkikoComposeUiTest(
runTestContext: CoroutineContext = EmptyCoroutineContext,
testTimeout: Duration = Duration.INFINITE,
semanticsOwnerListener: PlatformContext.SemanticsOwnerListener? = null,
windowInsets: PlatformWindowInsets? = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that it's the right way to expose it to public (under annotation but anyway) testing API.

In general, I'd keep this out of scope for now. Our own tests have a number of examples of how to provide PlatformContext inside tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should only be as part of the internal API since the constructor is marked as internal API and also it can only be provided to the runInternalSkikoComposeUiTest which is marked internal. I followed similar approach as with the semanticsOwnerListener above, which can also be provided only in terms of the internal API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, adding it here indeed doesn't mean that we'll need to support compatiblity forever.
But I still think that the existing pattern (an arg above) is not the right way to expose it.

We might change it in the future, so I won't block PR because of it

coroutineDispatcher: TestDispatcher = defaultTestDispatcher(),
block: suspend SkikoComposeUiTest.() -> Unit
): TestResult {
Expand All @@ -125,6 +127,7 @@ fun runInternalSkikoComposeUiTest(
testTimeout = testTimeout,
density = density,
semanticsOwnerListener = semanticsOwnerListener,
windowInsets = windowInsets,
coroutineDispatcher = coroutineDispatcher,
).runTest(block)
}
Expand Down Expand Up @@ -158,6 +161,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
private val testTimeout: Duration = Duration.INFINITE,
override val density: Density = Density(1f),
private val semanticsOwnerListener: PlatformContext.SemanticsOwnerListener?,
private val windowInsets: PlatformWindowInsets?,
private val coroutineDispatcher: TestDispatcher = defaultTestDispatcher(),
) : ComposeUiTest {
init {
Expand All @@ -178,6 +182,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
effectContext = effectContext,
density = density,
semanticsOwnerListener = null,
windowInsets = null,
)

constructor(
Expand All @@ -195,6 +200,7 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
testTimeout = testTimeout,
density = density,
semanticsOwnerListener = null,
windowInsets = null,
)

private val composeRootRegistry = ComposeRootRegistry()
Expand Down Expand Up @@ -520,6 +526,8 @@ open class SkikoComposeUiTest @InternalTestApi constructor(

override val semanticsOwnerListener: PlatformContext.SemanticsOwnerListener?
get() = [email protected]
override val windowInsets: PlatformWindowInsets
get() = [email protected] ?: super.windowInsets

override val dragAndDropManager: PlatformDragAndDropManager = TestDragAndDropManager()

Expand Down
2 changes: 2 additions & 0 deletions compose/ui/ui/api/ui.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -3906,6 +3906,7 @@ final val androidx.compose.ui.platform/androidx_compose_ui_platform_InspectorVal
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_DefaultViewConfiguration$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_DefaultViewConfiguration$stableprop|#static{}androidx_compose_ui_platform_PlatformContext_DefaultViewConfiguration$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_Empty$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_Empty$stableprop|#static{}androidx_compose_ui_platform_PlatformContext_Empty$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop|#static{}androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop|#static{}androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop|#static{}androidx_compose_ui_platform_ValueElement$stableprop[0]
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop|#static{}androidx_compose_ui_platform_ValueElementSequence$stableprop[0]
final val androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop|#static{}androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop[0]
Expand Down Expand Up @@ -4366,6 +4367,7 @@ final fun androidx.compose.ui.platform/androidx_compose_ui_platform_InspectorVal
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_DefaultViewConfiguration$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_DefaultViewConfiguration$stableprop_getter|androidx_compose_ui_platform_PlatformContext_DefaultViewConfiguration$stableprop_getter(){}[0]
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_Empty$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_Empty$stableprop_getter|androidx_compose_ui_platform_PlatformContext_Empty$stableprop_getter(){}[0]
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop_getter|androidx_compose_ui_platform_PlatformContext_EmptyFocusManager$stableprop_getter(){}[0]
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop_getter|androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop_getter(){}[0]
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop_getter|androidx_compose_ui_platform_ValueElement$stableprop_getter(){}[0]
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter|androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(){}[0]
final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop_getter|androidx_compose_ui_scene_ComposeSceneContext_Empty$stableprop_getter(){}[0]
Expand Down
Loading