Skip to content

Commit

Permalink
Add PopupProperties.clippingEnabled setting (#740)
Browse files Browse the repository at this point in the history
* Add Popup clipping option

* Do not draw popup without valid parentBounds

* Update equals/hashCode

* Add default arguments

* Add tests

* Remove AlertDialog.shadow test

* Fix existing tests
  • Loading branch information
MatkovIvan committed Aug 9, 2023
1 parent f247627 commit d775feb
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 37 deletions.
Expand Up @@ -115,36 +115,6 @@ class DesktopAlertDialogTest {
}
}

// https://github.com/JetBrains/compose-multiplatform/issues/2857
@OptIn(ExperimentalMaterialApi::class)
@Test
fun `shadow drawn at content bounds`() {
// Show an AlertDialog with very large horizontal padding and check that the pixel
// at the edge of where the dialog would have been without padding has the same color as the
// background.
val screenshot = renderComposeScene(400, 400){
AlertDialog(
modifier = Modifier.size(width = 400.dp, height = 100.dp),
onDismissRequest = {},
title = {},
text = {},
dismissButton = {},
confirmButton = {},
dialogPadding = PaddingValues(horizontal = 150.dp)
)
}

val pixels = screenshot.toComposeImageBitmap().toPixelMap()
val backgroundPixel = pixels[0, 0]
val nearEdgeWithoutPaddingPixel = pixels[0, 200]
val nearRealEdgePixel = pixels[149, 200]

assertEquals(nearEdgeWithoutPaddingPixel, backgroundPixel)

// Also check that the shadow is present near the actual edge of the content
assertNotEquals(nearRealEdgePixel, backgroundPixel)
}

@OptIn(ExperimentalTestApi::class, ExperimentalMaterialApi::class)
@Test
fun `uses available width`() = runDesktopComposeUiTest(
Expand Down
Expand Up @@ -57,14 +57,19 @@ import androidx.compose.ui.unit.round
* focusable then this property does nothing.
* @property dismissOnClickOutside Whether the popup can be dismissed by clicking outside the
* popup's bounds. If true, clicking outside the popup will call onDismissRequest.
* @property clippingEnabled Whether to allow the popup window to extend beyond the bounds of the
* screen. By default the window is clipped to the screen boundaries. Setting this to false will
* allow windows to be accurately positioned.
* The default value is true.
* @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
* the platform default, which is smaller than the screen width.
*/
@Immutable
actual class PopupProperties @ExperimentalComposeUiApi constructor(
actual val focusable: Boolean,
actual val dismissOnBackPress: Boolean,
actual val dismissOnClickOutside: Boolean,
actual val focusable: Boolean = false,
actual val dismissOnBackPress: Boolean = true,
actual val dismissOnClickOutside: Boolean = true,
val clippingEnabled: Boolean = true,
val usePlatformDefaultWidth: Boolean = false,
) {
actual constructor(
Expand All @@ -85,6 +90,7 @@ actual class PopupProperties @ExperimentalComposeUiApi constructor(
if (focusable != other.focusable) return false
if (dismissOnBackPress != other.dismissOnBackPress) return false
if (dismissOnClickOutside != other.dismissOnClickOutside) return false
if (clippingEnabled != other.clippingEnabled) return false
if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false

return true
Expand All @@ -94,6 +100,7 @@ actual class PopupProperties @ExperimentalComposeUiApi constructor(
var result = focusable.hashCode()
result = 31 * result + dismissOnBackPress.hashCode()
result = 31 * result + dismissOnClickOutside.hashCode()
result = 31 * result + clippingEnabled.hashCode()
result = 31 * result + usePlatformDefaultWidth.hashCode()
return result
}
Expand Down Expand Up @@ -388,13 +395,14 @@ private fun PopupLayout(
onOutsidePointerEvent: ((PointerInputEvent) -> Unit)? = null,
content: @Composable () -> Unit
) {
var parentBounds by remember { mutableStateOf(IntRect.Zero) }
EmptyLayout(Modifier.parentBoundsInWindow { parentBounds = it })
var layoutParentBoundsInWindow: IntRect? by remember { mutableStateOf(null) }
EmptyLayout(Modifier.parentBoundsInWindow { layoutParentBoundsInWindow = it })
RootLayout(
modifier = modifier,
focusable = properties.focusable,
onOutsidePointerEvent = onOutsidePointerEvent
) { owner ->
val parentBounds = layoutParentBoundsInWindow ?: return@RootLayout
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val measurePolicy = rememberPopupMeasurePolicy(
Expand Down Expand Up @@ -436,9 +444,15 @@ private fun rememberPopupMeasurePolicy(
platformOffset = platformOffset,
usePlatformDefaultWidth = properties.usePlatformDefaultWidth
) { windowSize, contentSize ->
val position = popupPositionProvider.calculatePosition(
var position = popupPositionProvider.calculatePosition(
parentBounds, windowSize, layoutDirection, contentSize
)
if (properties.clippingEnabled) {
position = IntOffset(
x = position.x.coerceIn(0, windowSize.width - contentSize.width),
y = position.y.coerceIn(0, windowSize.height - contentSize.height)
)
}
onBoundsChanged(IntRect(position, contentSize))
position
}
Expand Down
Expand Up @@ -152,7 +152,8 @@ class PopupState(
onDismissRequest = onDismissRequest,
properties = PopupProperties(
focusable = focusable,
dismissOnClickOutside = dismissOnClickOutside
dismissOnClickOutside = dismissOnClickOutside,
clippingEnabled = false
)
) {
with(LocalDensity.current) {
Expand Down
Expand Up @@ -16,13 +16,16 @@

package androidx.compose.ui.window

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.FillBox
import androidx.compose.ui.Modifier
import androidx.compose.ui.PopupState
import androidx.compose.ui.assertReceived
import androidx.compose.ui.assertReceivedLast
Expand All @@ -36,14 +39,18 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.isEqualTo
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.runSkikoComposeUiTest
import androidx.compose.ui.touch
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlin.test.Test
import kotlin.test.fail

Expand Down Expand Up @@ -505,4 +512,46 @@ class PopupTest {
)
)
}

@Test
fun clippingEnabledPopup() = runSkikoComposeUiTest(
size = Size(100f, 100f)
) {
setContent {
Popup(
offset = IntOffset(80, 80)
) {
Box(Modifier.size(50.dp).testTag("box1"))
}
Popup(
offset = IntOffset(-30, -30)
) {
Box(Modifier.size(50.dp).testTag("box2"))
}
}
onNodeWithTag("box1").assertPositionInRootIsEqualTo(50.dp, 50.dp)
onNodeWithTag("box2").assertPositionInRootIsEqualTo(0.dp, 0.dp)
}

@Test
fun clippingDisabledPopup() = runSkikoComposeUiTest(
size = Size(100f, 100f)
) {
setContent {
Popup(
offset = IntOffset(80, 80),
properties = PopupProperties(clippingEnabled = false)
) {
Box(Modifier.size(50.dp).testTag("box1"))
}
Popup(
offset = IntOffset(-30, -30),
properties = PopupProperties(clippingEnabled = false)
) {
Box(Modifier.size(50.dp).testTag("box2"))
}
}
onNodeWithTag("box1").assertPositionInRootIsEqualTo(80.dp, 80.dp)
onNodeWithTag("box2").assertPositionInRootIsEqualTo((-30).dp, (-30).dp)
}
}

0 comments on commit d775feb

Please sign in to comment.