Skip to content

Commit

Permalink
Fix applying WindowInsets inside Popup/Dialog (#832)
Browse files Browse the repository at this point in the history
## Proposed Changes

- Unwrap unnecessary state for `LocalSafeArea`/`LocalLayoutMargins`
- Override  `LocalSafeArea`/`LocalLayoutMargins` for separate owners.
- Replace `IOSInsets` to `PlatformInsets` - [Don't use data classes in
an
API](https://kotlinlang.org/docs/jvm-api-guidelines-backward-compatibility.html#don-t-use-data-classes-in-an-api)

## Changes in API

```diff
-androidx.compose.ui.uikit.IOSInsets
+androidx.compose.ui.platform.PlatformInsets
```
```diff
-androidx.compose.ui.uikit.LocalSafeAreaState
+androidx.compose.ui.platform.LocalSafeArea
```
```diff
-androidx.compose.ui.uikit.LocalLayoutMarginsState
+androidx.compose.ui.platform.LocalLayoutMargins
```

## Testing

```kt
Box(modifier = Modifier
    .fillMaxSize()
    .windowInsetsPadding(WindowInsets.systemBars)
    .background(Color.Green)
)
Popup {
    Box(modifier = Modifier
        .fillMaxSize()
        .windowInsetsPadding(WindowInsets.systemBars)
        .background(Color.Red)
    )
}
```

Android | iOS (Before) | iOS (After)
--- | --- | ---
<img
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/8fe492a6-007e-4cbf-9c8a-fa6eea9bb056"
height="600"> | <img
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/3168efba-0072-47aa-853a-1c44bd68e4e0"
height="600"> | <img
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/3845efe8-eb8e-4c71-87e2-ea44dd73a91e"
height="600">

## Issues Fixed

Fixes (partially)
JetBrains/compose-multiplatform#3701
  • Loading branch information
MatkovIvan authored Sep 21, 2023
1 parent ee1756a commit 2be70cf
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ package androidx.compose.foundation.layout

import androidx.compose.runtime.Composable
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.LocalLayoutMargins
import androidx.compose.ui.platform.LocalSafeArea
import androidx.compose.ui.platform.PlatformInsets
import androidx.compose.ui.uikit.*
import androidx.compose.ui.unit.dp

private val ZeroInsets = WindowInsets(0, 0, 0, 0)

@OptIn(InternalComposeApi::class)
private fun IOSInsets.toWindowInsets() = WindowInsets(
@OptIn(ExperimentalComposeUiApi::class)
private fun PlatformInsets.toWindowInsets() = WindowInsets(
top = top,
bottom = bottom,
left = left,
Expand All @@ -36,16 +40,16 @@ private fun IOSInsets.toWindowInsets() = WindowInsets(
*/
private val WindowInsets.Companion.iosSafeArea: WindowInsets
@Composable
@OptIn(InternalComposeApi::class)
get() = LocalSafeAreaState.current.value.toWindowInsets()
@OptIn(InternalComposeApi::class, ExperimentalComposeUiApi::class)
get() = LocalSafeArea.current.toWindowInsets()

/**
* This insets represents iOS layoutMargins.
*/
private val WindowInsets.Companion.layoutMargins: WindowInsets
@Composable
@OptIn(InternalComposeApi::class)
get() = LocalLayoutMarginsState.current.value.toWindowInsets()
@OptIn(InternalComposeApi::class, ExperimentalComposeUiApi::class)
get() = LocalLayoutMargins.current.toWindowInsets()

/**
* An insets type representing the window of a caption bar.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@
package androidx.compose.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Density
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.ui.platform.PlatformInsets

@OptIn(InternalComposeApi::class)
@Composable
internal actual fun Density.platformPadding(): RootLayoutPadding =
RootLayoutPadding.Zero
internal actual fun platformInsets(): PlatformInsets =
PlatformInsets.Zero

@Composable
internal actual fun platformOwnerContent(content: @Composable () -> Unit) {
content()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/**
* This class represents platform insets.
*/
@ExperimentalComposeUiApi
@Immutable
class PlatformInsets(
@Stable
val top: Dp = 0.dp,
@Stable
val bottom: Dp = 0.dp,
@Stable
val left: Dp = 0.dp,
@Stable
val right: Dp = 0.dp,
) {
companion object {
val Zero = PlatformInsets(0.dp, 0.dp, 0.dp, 0.dp)
}

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

if (top != other.top) return false
if (bottom != other.bottom) return false
if (left != other.left) return false
if (right != other.right) return false

return true
}

override fun hashCode(): Int {
var result = top.hashCode()
result = 31 * result + bottom.hashCode()
result = 31 * result + left.hashCode()
result = 31 * result + right.hashCode()
return result
}

override fun toString(): String {
return "PlatformInsets(top=$top, bottom=$bottom, left=$left, right=$right)"
}
}

internal fun PlatformInsets.exclude(insets: PlatformInsets) = PlatformInsets(
left = (left - insets.left).coerceAtLeast(0.dp),
top = (top - insets.top).coerceAtLeast(0.dp),
right = (right - insets.right).coerceAtLeast(0.dp),
bottom = (bottom - insets.bottom).coerceAtLeast(0.dp)
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.PlatformInsets
import androidx.compose.ui.requireCurrent
import androidx.compose.ui.semantics.dialog
import androidx.compose.ui.semantics.semantics
Expand Down Expand Up @@ -169,15 +170,15 @@ private fun DialogLayout(
onOutsidePointerEvent: ((PointerInputEvent) -> Unit)? = null,
content: @Composable () -> Unit
) {
val platformInsets = platformInsets()
RootLayout(
modifier = modifier,
focusable = true,
onOutsidePointerEvent = onOutsidePointerEvent
) { owner ->
val density = LocalDensity.current
val measurePolicy = rememberDialogMeasurePolicy(
properties = properties,
platformPadding = with(density) { platformPadding() }
platformInsets = platformInsets
) {
owner.bounds = it
}
Expand All @@ -191,14 +192,14 @@ private fun DialogLayout(
@Composable
private fun rememberDialogMeasurePolicy(
properties: DialogProperties,
platformPadding: RootLayoutPadding,
platformInsets: PlatformInsets,
onBoundsChanged: (IntRect) -> Unit
) = remember(properties, platformPadding, onBoundsChanged) {
) = remember(properties, platformInsets, onBoundsChanged) {
RootMeasurePolicy(
platformPadding = platformPadding,
platformInsets = platformInsets,
usePlatformDefaultWidth = properties.usePlatformDefaultWidth
) { windowSize, contentSize ->
val position = positionWithPadding(platformPadding, windowSize) {
val position = positionWithInsets(platformInsets, windowSize) {
it.center - contentSize.center
}
onBoundsChanged(IntRect(position, contentSize))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.PlatformInsets
import androidx.compose.ui.semantics.popup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset
Expand Down Expand Up @@ -421,6 +421,7 @@ private fun PopupLayout(
onOutsidePointerEvent: ((PointerInputEvent) -> Unit)? = null,
content: @Composable () -> Unit
) {
val platformInsets = platformInsets()
var layoutParentBoundsInWindow: IntRect? by remember { mutableStateOf(null) }
EmptyLayout(Modifier.parentBoundsInWindow { layoutParentBoundsInWindow = it })
RootLayout(
Expand All @@ -429,12 +430,11 @@ private fun PopupLayout(
onOutsidePointerEvent = onOutsidePointerEvent
) { owner ->
val parentBounds = layoutParentBoundsInWindow ?: return@RootLayout
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val measurePolicy = rememberPopupMeasurePolicy(
popupPositionProvider = popupPositionProvider,
properties = properties,
platformPadding = with(density) { platformPadding() },
platformInsets = platformInsets,
layoutDirection = layoutDirection,
parentBounds = parentBounds
) {
Expand All @@ -461,16 +461,16 @@ private fun Modifier.parentBoundsInWindow(
private fun rememberPopupMeasurePolicy(
popupPositionProvider: PopupPositionProvider,
properties: PopupProperties,
platformPadding: RootLayoutPadding,
platformInsets: PlatformInsets,
layoutDirection: LayoutDirection,
parentBounds: IntRect,
onBoundsChanged: (IntRect) -> Unit
) = remember(popupPositionProvider, properties, platformPadding, layoutDirection, parentBounds, onBoundsChanged) {
) = remember(popupPositionProvider, properties, platformInsets, layoutDirection, parentBounds, onBoundsChanged) {
RootMeasurePolicy(
platformPadding = platformPadding,
platformInsets = platformInsets,
usePlatformDefaultWidth = properties.usePlatformDefaultWidth
) { windowSize, contentSize ->
var position = positionWithPadding(platformPadding, windowSize) {
var position = positionWithInsets(platformInsets, windowSize) {
popupPositionProvider.calculatePosition(
parentBounds, it, layoutDirection, contentSize
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerInputEvent
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.PlatformInsets
import androidx.compose.ui.platform.SkiaBasedOwner
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.requireCurrent
Expand Down Expand Up @@ -71,7 +73,9 @@ internal fun RootLayout(
)
scene.attach(owner)
owner to owner.setContent(parent = parentComposition) {
content(owner)
platformOwnerContent {
content(owner)
}
}
}
DisposableEffect(Unit) {
Expand Down Expand Up @@ -99,12 +103,12 @@ internal fun EmptyLayout(
)

internal fun RootMeasurePolicy(
platformPadding: RootLayoutPadding,
platformInsets: PlatformInsets,
usePlatformDefaultWidth: Boolean,
calculatePosition: (windowSize: IntSize, contentSize: IntSize) -> IntOffset,
calculatePosition: MeasureScope.(windowSize: IntSize, contentSize: IntSize) -> IntOffset,
) = MeasurePolicy {measurables, constraints ->
val platformConstraints = applyPlatformConstrains(
constraints, platformPadding, usePlatformDefaultWidth
constraints, platformInsets, usePlatformDefaultWidth
)
val placeables = measurables.fastMap { it.measure(platformConstraints) }
val windowSize = IntSize(constraints.maxWidth, constraints.maxHeight)
Expand All @@ -122,13 +126,12 @@ internal fun RootMeasurePolicy(

private fun Density.applyPlatformConstrains(
constraints: Constraints,
platformPadding: RootLayoutPadding,
platformInsets: PlatformInsets,
usePlatformDefaultWidth: Boolean
): Constraints {
val platformConstraints = constraints.offset(
horizontal = -(platformPadding.left + platformPadding.right),
vertical = -(platformPadding.top + platformPadding.bottom)
)
val horizontal = platformInsets.left.roundToPx() + platformInsets.right.roundToPx()
val vertical = platformInsets.top.roundToPx() + platformInsets.bottom.roundToPx()
val platformConstraints = constraints.offset(-horizontal, -vertical)
return if (usePlatformDefaultWidth) {
platformConstraints.constrain(
platformDefaultConstrains(constraints)
Expand All @@ -138,32 +141,30 @@ private fun Density.applyPlatformConstrains(
}
}

internal data class RootLayoutPadding(
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
) {
companion object {
val Zero = RootLayoutPadding(0, 0, 0, 0)
}
}

internal fun positionWithPadding(
padding: RootLayoutPadding,
internal fun MeasureScope.positionWithInsets(
insets: PlatformInsets,
size: IntSize,
calculatePosition: (size: IntSize) -> IntOffset,
): IntOffset {
val horizontal = insets.left.roundToPx() + insets.right.roundToPx()
val vertical = insets.top.roundToPx() + insets.bottom.roundToPx()
val sizeWithPadding = IntSize(
width = size.width - padding.left - padding.right,
height = size.height - padding.top - padding.bottom
width = size.width - horizontal,
height = size.height - vertical
)
val position = calculatePosition(sizeWithPadding)
return position + IntOffset(padding.left, padding.top)
val offset = IntOffset(
x = insets.left.roundToPx(),
y = insets.top.roundToPx()
)
return position + offset
}

@Composable
internal expect fun Density.platformPadding(): RootLayoutPadding
internal expect fun platformInsets(): PlatformInsets

@Composable
internal expect fun platformOwnerContent(content: @Composable () -> Unit)

private fun Density.platformDefaultConstrains(
constraints: Constraints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,23 @@
* limitations under the License.
*/

package androidx.compose.ui.uikit
package androidx.compose.ui.platform

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.State
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

/**
* Composition local for SafeArea of ComposeUIViewController
*/
@InternalComposeApi
val LocalSafeAreaState = staticCompositionLocalOf<State<IOSInsets>> {
error("CompositionLocal LocalSafeAreaTopState not present")
val LocalSafeArea = staticCompositionLocalOf<PlatformInsets> {
error("CompositionLocal LocalSafeArea not present")
}

/**
* Composition local for layoutMargins of ComposeUIViewController
*/
@InternalComposeApi
val LocalLayoutMarginsState = staticCompositionLocalOf<State<IOSInsets>> {
error("CompositionLocal LocalLayoutMarginsState not present")
val LocalLayoutMargins = staticCompositionLocalOf<PlatformInsets> {
error("CompositionLocal LocalLayoutMargins not present")
}

/**
* This class represents iOS Insets.
* It contains equals and hashcode and can be used as Compose State<IOSInsets>.
*/
@Immutable
@InternalComposeApi
data class IOSInsets(
val top: Dp = 0.dp,
val bottom: Dp = 0.dp,
val left: Dp = 0.dp,
val right: Dp = 0.dp,
)
Loading

0 comments on commit 2be70cf

Please sign in to comment.