Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PopupProperties.clippingEnabled setting #740

Merged
merged 7 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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`() {
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
// 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
}
}