Merge "Update multiplatform Dialog and Popup APIs" into androidx-main
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.jvm.kt
index 16f6d92..1a7bcdb 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.jvm.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.jvm.kt
@@ -64,6 +64,7 @@
 import androidx.compose.ui.window.DialogWindow
 import androidx.compose.ui.window.Notification
 import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
 import androidx.compose.ui.window.TrayState
 import androidx.compose.ui.window.Window
 import androidx.compose.ui.window.WindowPlacement
@@ -297,8 +298,10 @@
             Popup(
                 alignment = Alignment.TopCenter,
                 offset = IntOffset(0, 50),
-                focusable = true,
-                onDismissRequest = onDismiss
+                onDismissRequest = onDismiss,
+                properties = PopupProperties(
+                    focusable = true
+                )
             ) {
                 println("CompositionLocal value is ${LocalTest.current}.")
                 PopupContent(onDismiss)
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
index 5b16fcc..d28c709 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
@@ -40,6 +40,7 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
 import androidx.compose.ui.window.rememberCursorPositionProvider
 
 // Design of basic represenation is from Material specs:
@@ -68,9 +69,9 @@
         val isOpen = state.status is ContextMenuState.Status.Open
         if (isOpen) {
             Popup(
-                focusable = true,
+                popupPositionProvider = rememberCursorPositionProvider(),
                 onDismissRequest = { state.status = ContextMenuState.Status.Closed },
-                popupPositionProvider = rememberCursorPositionProvider()
+                properties = PopupProperties(focusable = true)
             ) {
                 Column(
                     modifier = Modifier
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt
index b4ffef2..a0abc54 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/BasicTooltip.desktop.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.window.Popup
 import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
 
 /**
  * BasicTooltipBox that wraps a composable with a tooltip.
@@ -53,13 +54,18 @@
     enableUserInput: Boolean,
     content: @Composable () -> Unit
 ) {
+    // TODO: Reuse android implementation - there is no platform specifics here.
+    //  Use expect/actual only for string resources
     Box(modifier = modifier) {
         content()
         if (state.isVisible) {
             Popup(
                 popupPositionProvider = positionProvider,
                 onDismissRequest = { state.dismiss() },
-                focusable = focusable
+                properties = PopupProperties(
+                    // TODO(b/326167778): focusable = true cannot work with mouse
+                    focusable = false
+                )
             ) { tooltip() }
         }
     }
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopAlertDialog.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopAlertDialog.desktop.kt
index 22dfdd9..f755c0c 100644
--- a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopAlertDialog.desktop.kt
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopAlertDialog.desktop.kt
@@ -191,6 +191,7 @@
         // [alignment] property of [Popup] and have to use [Box] that fills all the
         // available space. Also [Box] provides a dismiss request feature when clicked
         // outside of the [AlertDialog] content.
+        @Suppress("DEPRECATION") // Will be removed in aosp/3077146
         Popup(
             popupPositionProvider = object : PopupPositionProvider {
                 override fun calculatePosition(
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
index dc5e6c2..fad8335 100644
--- a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
@@ -170,6 +170,7 @@
             transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
         }
 
+        @Suppress("DEPRECATION") // Will be removed in aosp/3077093
         Popup(
             focusable = focusable,
             onDismissRequest = onDismissRequest,
@@ -294,6 +295,7 @@
     if (expandedStates.currentState || expandedStates.targetState) {
         val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
 
+        @Suppress("DEPRECATION") // Will be removed in aosp/3077093
         Popup(
             focusable = focusable,
             onDismissRequest = onDismissRequest,
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt
index 649eb3e..30845c6 100644
--- a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/internal/BasicTooltip.desktop.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.window.Popup
 import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
 
 /**
  * NOTICE:
@@ -56,13 +57,18 @@
     enableUserInput: Boolean,
     content: @Composable () -> Unit
 ) {
+    // TODO: Reuse android implementation - there is no platform specifics here.
+    //  Use expect/actual only for string resources
     Box(modifier = modifier) {
         content()
         if (state.isVisible) {
             Popup(
                 popupPositionProvider = positionProvider,
                 onDismissRequest = { state.dismiss() },
-                focusable = focusable
+                properties = PopupProperties(
+                    // TODO(b/326167778): focusable = true cannot work with mouse
+                    focusable = false
+                )
             ) { tooltip() }
         }
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt
index 7ad8bde..697c44e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.desktop.kt
@@ -16,29 +16,12 @@
 package androidx.compose.ui.window
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCompositionContext
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.LocalComposeScene
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.awt.LocalLayerContainer
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyEventType
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.type
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.SkiaBasedOwner
-import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.IntOffset
@@ -49,291 +32,23 @@
 import androidx.compose.ui.unit.round
 import java.awt.MouseInfo
 import javax.swing.SwingUtilities.convertPointFromScreen
-
-@Immutable
-actual class PopupProperties @ExperimentalComposeUiApi actual constructor(
-    actual val focusable: Boolean,
-    actual val dismissOnBackPress: Boolean,
-    actual val dismissOnClickOutside: Boolean,
-    actual val clippingEnabled: Boolean,
-) {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is PopupProperties) return false
-
-        if (focusable != other.focusable) return false
-        if (dismissOnBackPress != other.dismissOnBackPress) return false
-        if (dismissOnClickOutside != other.dismissOnClickOutside) return false
-        if (clippingEnabled != other.clippingEnabled) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = dismissOnBackPress.hashCode()
-        result = 31 * result + focusable.hashCode()
-        result = 31 * result + dismissOnBackPress.hashCode()
-        result = 31 * result + dismissOnClickOutside.hashCode()
-        result = 31 * result + clippingEnabled.hashCode()
-        return result
-    }
-}
+import kotlin.math.roundToInt
 
 /**
- * Opens a popup with the given content.
- *
- * The popup is positioned relative to its parent, using the [alignment] and [offset].
- * The popup is visible as long as it is part of the composition hierarchy.
- *
- * @sample androidx.compose.ui.samples.PopupSample
- *
- * @param alignment The alignment relative to the parent.
- * @param offset An offset from the original aligned position of the popup. Offset respects the
- * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
- * will be subtracted from it.
- * @param focusable Indicates if the popup can grab the focus.
- * @param onDismissRequest Executes when the user clicks outside of the popup.
- * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
- * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
- * Return true to stop propagation of this event. If you return false, the key event will be
- * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
- * it will be sent back up to the root using the onKeyEvent callback.
- * @param onKeyEvent This callback is invoked when the user interacts with the hardware
- * keyboard. While implementing this callback, return true to stop propagation of this event.
- * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
- * @param content The content to be displayed inside the popup.
+ * Returns a remembered value of the mouse cursor position or null if cursor is not inside a scene.
  */
 @Composable
-fun Popup(
-    alignment: Alignment = Alignment.TopStart,
-    offset: IntOffset = IntOffset(0, 0),
-    focusable: Boolean = false,
-    onDismissRequest: (() -> Unit)? = null,
-    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
-    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
-    content: @Composable () -> Unit
-) {
-    val popupPositioner = remember(alignment, offset) {
-        AlignmentOffsetPositionProvider(
-            alignment,
-            offset
+private fun rememberCursorPosition(): Offset? {
+    // TODO: Apply changes from https://github.com/JetBrains/compose-multiplatform-core/pull/432
+    val component = LocalLayerContainer.current
+    return remember {
+        val awtMousePosition = MouseInfo.getPointerInfo().location
+        convertPointFromScreen(awtMousePosition, component)
+        Offset(
+            (awtMousePosition.x * component.density.density),
+            (awtMousePosition.y * component.density.density)
         )
     }
-
-    Popup(
-        popupPositionProvider = popupPositioner,
-        onDismissRequest = onDismissRequest,
-        onKeyEvent = onKeyEvent,
-        onPreviewKeyEvent = onPreviewKeyEvent,
-        focusable = focusable,
-        content = content
-    )
-}
-
-/**
- * Opens a popup with the given content.
- *
- * The popup is positioned using a custom [popupPositionProvider].
- *
- * @sample androidx.compose.ui.samples.PopupSample
- *
- * @param popupPositionProvider Provides the screen position of the popup.
- * @param onDismissRequest Executes when the user clicks outside of the popup.
- * @param focusable Indicates if the popup can grab the focus.
- * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
- * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
- * Return true to stop propagation of this event. If you return false, the key event will be
- * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
- * it will be sent back up to the root using the onKeyEvent callback.
- * @param onKeyEvent This callback is invoked when the user interacts with the hardware
- * keyboard. While implementing this callback, return true to stop propagation of this event.
- * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
- * @param content The content to be displayed inside the popup.
- */
-@Composable
-fun Popup(
-    popupPositionProvider: PopupPositionProvider,
-    onDismissRequest: (() -> Unit)? = null,
-    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
-    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
-    focusable: Boolean = false,
-    content: @Composable () -> Unit
-) {
-    PopupLayout(
-        popupPositionProvider = popupPositionProvider,
-        focusable = focusable,
-        onDismissRequest = if (focusable) onDismissRequest else null,
-        onPreviewKeyEvent = onPreviewKeyEvent,
-        onKeyEvent = onKeyEvent,
-        content = content
-    )
-}
-
-/**
- * Opens a popup with the given content.
- *
- * A popup is a floating container that appears on top of the current activity.
- * It is especially useful for non-modal UI surfaces that remain hidden until they
- * are needed, for example floating menus like Cut/Copy/Paste.
- *
- * The popup is positioned relative to its parent, using the [alignment] and [offset].
- * The popup is visible as long as it is part of the composition hierarchy.
- *
- * @sample androidx.compose.ui.samples.PopupSample
- *
- * @param alignment The alignment relative to the parent.
- * @param offset An offset from the original aligned position of the popup. Offset respects the
- * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
- * will be subtracted from it.
- * @param onDismissRequest Executes when the user clicks outside of the popup.
- * @param properties [PopupProperties] for further customization of this popup's behavior.
- * @param content The content to be displayed inside the popup.
- */
-@Composable
-actual fun Popup(
-    alignment: Alignment,
-    offset: IntOffset,
-    onDismissRequest: (() -> Unit)?,
-    properties: PopupProperties,
-    content: @Composable () -> Unit
-) {
-    val popupPositioner = remember(alignment, offset) {
-        AlignmentOffsetPositionProvider(
-            alignment,
-            offset
-        )
-    }
-
-    Popup(
-        popupPositionProvider = popupPositioner,
-        onDismissRequest = onDismissRequest,
-        properties = properties,
-        content = content
-    )
-}
-
-/**
- * Opens a popup with the given content.
- *
- * The popup is positioned using a custom [popupPositionProvider].
- *
- * @sample androidx.compose.ui.samples.PopupSample
- *
- * @param popupPositionProvider Provides the screen position of the popup.
- * @param onDismissRequest Executes when the user clicks outside of the popup.
- * @param properties [PopupProperties] for further customization of this popup's behavior.
- * @param content The content to be displayed inside the popup.
- */
-@Composable
-actual fun Popup(
-    popupPositionProvider: PopupPositionProvider,
-    onDismissRequest: (() -> Unit)?,
-    properties: PopupProperties,
-    content: @Composable () -> Unit
-) {
-    PopupLayout(
-        popupPositionProvider,
-        properties.focusable,
-        if (properties.dismissOnClickOutside) onDismissRequest else null,
-        onKeyEvent = {
-            if (properties.dismissOnBackPress &&
-                it.type == KeyEventType.KeyDown && it.key == Key.Escape &&
-                onDismissRequest != null
-            ) {
-                onDismissRequest()
-                true
-            } else {
-                false
-            }
-        },
-        content = content
-    )
-}
-
-@Composable
-internal fun PopupLayout(
-    popupPositionProvider: PopupPositionProvider,
-    focusable: Boolean,
-    onDismissRequest: (() -> Unit)?,
-    modifier: Modifier = Modifier,
-    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
-    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
-    content: @Composable () -> Unit
-) {
-    val scene = LocalComposeScene.current
-    val density = LocalDensity.current
-
-    var parentBounds by remember { mutableStateOf(IntRect.Zero) }
-    var popupBounds by remember { mutableStateOf(IntRect.Zero) }
-
-    // getting parent bounds
-    Layout(
-        content = {},
-        modifier = Modifier.onGloballyPositioned { childCoordinates ->
-            val coordinates = childCoordinates.parentCoordinates!!
-            parentBounds = IntRect(
-                coordinates.localToWindow(Offset.Zero).round(),
-                coordinates.size
-            )
-        },
-        measurePolicy = { _, _ ->
-            layout(0, 0) {}
-        }
-    )
-
-    val parentComposition = rememberCompositionContext()
-    val (owner, composition) = remember {
-        val owner = SkiaBasedOwner(
-            platformInputService = scene.platformInputService,
-            component = scene.component,
-            density = density,
-            coroutineContext = parentComposition.effectCoroutineContext,
-            isPopup = true,
-            isFocusable = focusable,
-            onDismissRequest = onDismissRequest,
-            onPreviewKeyEvent = onPreviewKeyEvent,
-            onKeyEvent = onKeyEvent
-        )
-        scene.attach(owner)
-        val composition = owner.setContent(parent = parentComposition) {
-            Layout(
-                content = content,
-                modifier = modifier,
-                measurePolicy = { measurables, constraints ->
-                    val width = constraints.maxWidth
-                    val height = constraints.maxHeight
-
-                    layout(constraints.maxWidth, constraints.maxHeight) {
-                        measurables.forEach {
-                            val placeable = it.measure(constraints)
-                            val position = popupPositionProvider.calculatePosition(
-                                anchorBounds = parentBounds,
-                                windowSize = IntSize(width, height),
-                                layoutDirection = layoutDirection,
-                                popupContentSize = IntSize(placeable.width, placeable.height)
-                            )
-
-                            popupBounds = IntRect(
-                                position,
-                                IntSize(placeable.width, placeable.height)
-                            )
-                            owner.bounds = popupBounds
-                            placeable.place(position.x, position.y)
-                        }
-                    }
-                }
-            )
-        }
-        owner to composition
-    }
-    owner.density = density
-    DisposableEffect(Unit) {
-        onDispose {
-            scene.detach(owner)
-            composition.dispose()
-            owner.dispose()
-        }
-    }
 }
 
 /**
@@ -343,58 +58,122 @@
  * @param alignment The alignment of the popup relative to the current cursor position.
  * @param windowMargin Defines the area within the window that limits the placement of the popup.
  */
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun rememberCursorPositionProvider(
     offset: DpOffset = DpOffset.Zero,
     alignment: Alignment = Alignment.BottomEnd,
     windowMargin: Dp = 4.dp
-): PopupPositionProvider = with(LocalDensity.current) {
-    val component = LocalLayerContainer.current
-    val cursorPoint = remember {
-        val awtMousePosition = MouseInfo.getPointerInfo().location
-        convertPointFromScreen(awtMousePosition, component)
-        IntOffset(
-            (awtMousePosition.x * component.density.density).toInt(),
-            (awtMousePosition.y * component.density.density).toInt()
+): PopupPositionProvider {
+    val offsetPx = with(LocalDensity.current) {
+        Offset(offset.x.toPx(), offset.y.toPx())
+    }
+    val windowMarginPx = with(LocalDensity.current) {
+        windowMargin.roundToPx()
+    }
+    val cursorPosition = rememberCursorPosition()
+
+    if (cursorPosition == null) {
+        // if cursor is outside the scene, show popup under the parent component
+        return rememberComponentRectPositionProvider(
+            alignment = alignment,
+            offset = offset
         )
     }
-    val offsetPx = IntOffset(offset.x.roundToPx(), offset.y.roundToPx())
+
+    return remember(cursorPosition, offsetPx, alignment, windowMarginPx) {
+        PopupPositionProviderAtPosition(
+            positionPx = cursorPosition,
+            isRelativeToAnchor = false,
+            offsetPx = offsetPx,
+            alignment = alignment,
+            windowMarginPx = windowMarginPx
+        )
+    }
+}
+
+/**
+ * A [PopupPositionProvider] that positions the popup at the given position relative to the anchor.
+ *
+ * @param positionPx the offset, in pixels, relative to the anchor, to position the popup at.
+ * @param offset [DpOffset] to be added to the position of the popup.
+ * @param alignment The alignment of the popup relative to desired position.
+ * @param windowMargin Defines the area within the window that limits the placement of the popup.
+ */
+@ExperimentalComposeUiApi
+@Composable
+fun rememberPopupPositionProviderAtPosition(
+    positionPx: Offset,
+    offset: DpOffset = DpOffset.Zero,
+    alignment: Alignment = Alignment.BottomEnd,
+    windowMargin: Dp = 4.dp
+): PopupPositionProvider = with(LocalDensity.current) {
+    val offsetPx = Offset(offset.x.toPx(), offset.y.toPx())
     val windowMarginPx = windowMargin.roundToPx()
-    object : PopupPositionProvider {
-        override fun calculatePosition(
-            anchorBounds: IntRect,
-            windowSize: IntSize,
-            layoutDirection: LayoutDirection,
-            popupContentSize: IntSize
-        ) = with(density) {
-            val anchor = IntRect(cursorPoint, IntSize.Zero)
-            val tooltipArea = IntRect(
-                IntOffset(
-                    anchor.left - popupContentSize.width,
-                    anchor.top - popupContentSize.height,
-                ),
-                IntSize(
-                    popupContentSize.width * 2,
-                    popupContentSize.height * 2
-                )
+
+    remember(positionPx, offsetPx, alignment, windowMarginPx) {
+        PopupPositionProviderAtPosition(
+            positionPx = positionPx,
+            isRelativeToAnchor = true,
+            offsetPx = offsetPx,
+            alignment = alignment,
+            windowMarginPx = windowMarginPx
+        )
+    }
+}
+
+/**
+ * A [PopupPositionProvider] that positions the popup at the given offsets and alignment.
+ *
+ * @param positionPx The offset of the popup's location, in pixels.
+ * @param isRelativeToAnchor Whether [positionPx] is relative to the anchor bounds passed to
+ * [calculatePosition]. If `false`, it is relative to the window.
+ * @param offsetPx Extra offset to be added to the position of the popup, in pixels.
+ * @param alignment The alignment of the popup relative to desired position.
+ * @param windowMarginPx Defines the area within the window that limits the placement of the popup,
+ * in pixels.
+ */
+@ExperimentalComposeUiApi
+class PopupPositionProviderAtPosition(
+    val positionPx: Offset,
+    val isRelativeToAnchor: Boolean,
+    val offsetPx: Offset,
+    val alignment: Alignment = Alignment.BottomEnd,
+    val windowMarginPx: Int,
+) : PopupPositionProvider {
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        val anchor = IntRect(
+            offset = positionPx.round() +
+                (if (isRelativeToAnchor) anchorBounds.topLeft else IntOffset.Zero),
+            size = IntSize.Zero)
+        val tooltipArea = IntRect(
+            IntOffset(
+                anchor.left - popupContentSize.width,
+                anchor.top - popupContentSize.height,
+            ),
+            IntSize(
+                popupContentSize.width * 2,
+                popupContentSize.height * 2
             )
-            val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)
-            var x = tooltipArea.left + position.x + offsetPx.x
-            var y = tooltipArea.top + position.y + offsetPx.y
-            if (x + popupContentSize.width > windowSize.width - windowMarginPx) {
-                x -= popupContentSize.width
-            }
-            if (y + popupContentSize.height > windowSize.height - windowMarginPx) {
-                y -= popupContentSize.height + anchor.height
-            }
-            if (x < windowMarginPx) {
-                x = windowMarginPx
-            }
-            if (y < windowMarginPx) {
-                y = windowMarginPx
-            }
-            IntOffset(x, y)
+        )
+        val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)
+        var x = tooltipArea.left + position.x + offsetPx.x
+        var y = tooltipArea.top + position.y + offsetPx.y
+        if (x + popupContentSize.width > windowSize.width - windowMarginPx) {
+            x -= popupContentSize.width
         }
+        if (y + popupContentSize.height > windowSize.height - windowMarginPx) {
+            y -= popupContentSize.height + anchor.height
+        }
+        x = x.coerceAtLeast(windowMarginPx.toFloat())
+        y = y.coerceAtLeast(windowMarginPx.toFloat())
+
+        return IntOffset(x.roundToInt(), y.roundToInt())
     }
 }
 
@@ -410,28 +189,32 @@
     anchor: Alignment = Alignment.BottomCenter,
     alignment: Alignment = Alignment.BottomCenter,
     offset: DpOffset = DpOffset.Zero
-): PopupPositionProvider = with(LocalDensity.current) {
-    val offsetPx = IntOffset(offset.x.roundToPx(), offset.y.roundToPx())
-    return object : PopupPositionProvider {
-        override fun calculatePosition(
-            anchorBounds: IntRect,
-            windowSize: IntSize,
-            layoutDirection: LayoutDirection,
-            popupContentSize: IntSize
-        ): IntOffset {
-            val anchorPoint = anchor.align(IntSize.Zero, anchorBounds.size, layoutDirection)
-            val tooltipArea = IntRect(
-                IntOffset(
-                    anchorBounds.left + anchorPoint.x - popupContentSize.width,
-                    anchorBounds.top + anchorPoint.y - popupContentSize.height,
-                ),
-                IntSize(
-                    popupContentSize.width * 2,
-                    popupContentSize.height * 2
+): PopupPositionProvider {
+    val offsetPx = with(LocalDensity.current) {
+        IntOffset(offset.x.roundToPx(), offset.y.roundToPx())
+    }
+    return remember(anchor, alignment, offsetPx) {
+        object : PopupPositionProvider {
+            override fun calculatePosition(
+                anchorBounds: IntRect,
+                windowSize: IntSize,
+                layoutDirection: LayoutDirection,
+                popupContentSize: IntSize
+            ): IntOffset {
+                val anchorPoint = anchor.align(IntSize.Zero, anchorBounds.size, layoutDirection)
+                val tooltipArea = IntRect(
+                    IntOffset(
+                        anchorBounds.left + anchorPoint.x - popupContentSize.width,
+                        anchorBounds.top + anchorPoint.y - popupContentSize.height,
+                    ),
+                    IntSize(
+                        popupContentSize.width * 2,
+                        popupContentSize.height * 2
+                    )
                 )
-            )
-            val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)
-            return tooltipArea.topLeft + position + offsetPx
+                val position = alignment.align(popupContentSize, tooltipArea.size, layoutDirection)
+                return tooltipArea.topLeft + position + offsetPx
+            }
         }
     }
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Dialog.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Dialog.desktop.kt
index 4d9e671..f238d7d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Dialog.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Dialog.desktop.kt
@@ -18,26 +18,16 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.currentCompositionLocalContext
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.awt.ComposeDialog
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyEventType
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.type
 import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.ComponentUpdater
 import androidx.compose.ui.util.makeDisplayable
@@ -53,75 +43,6 @@
 import java.awt.event.WindowEvent
 import javax.swing.JDialog
 
-/**
- * Properties used to customize the behavior of a [Dialog].
- *
- * @property dismissOnBackPress whether the popup can be dismissed by pressing the back button
- *  * on Android or escape key on desktop.
- * If true, pressing the back button will call onDismissRequest.
- * @property dismissOnClickOutside whether the dialog can be dismissed by clicking outside the
- * dialog's bounds. If true, clicking outside the dialog will call onDismissRequest.
- * @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 DialogProperties actual constructor(
-    actual val dismissOnBackPress: Boolean,
-    actual val dismissOnClickOutside: Boolean,
-    actual val usePlatformDefaultWidth: Boolean,
-) {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is DialogProperties) return false
-
-        if (dismissOnBackPress != other.dismissOnBackPress) return false
-        if (dismissOnClickOutside != other.dismissOnClickOutside) return false
-        if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = dismissOnBackPress.hashCode()
-        result = 31 * result + dismissOnClickOutside.hashCode()
-        result = 31 * result + usePlatformDefaultWidth.hashCode()
-        return result
-    }
-}
-
-@Composable
-actual fun Dialog(
-    onDismissRequest: () -> Unit,
-    properties: DialogProperties,
-    content: @Composable () -> Unit
-) {
-    val popupPositioner = remember {
-        AlignmentOffsetPositionProvider(
-            alignment = Alignment.Center,
-            offset = IntOffset(0, 0)
-        )
-    }
-    PopupLayout(
-        popupPositionProvider = popupPositioner,
-        focusable = true,
-        if (properties.dismissOnClickOutside) onDismissRequest else null,
-        modifier = Modifier.drawBehind {
-            drawRect(Color.Black.copy(alpha = 0.6f))
-        },
-        onKeyEvent = {
-            if (properties.dismissOnBackPress &&
-                it.type == KeyEventType.KeyDown && it.key == Key.Escape
-            ) {
-                onDismissRequest()
-                true
-            } else {
-                false
-            }
-        },
-        content = content
-    )
-}
-
 @Deprecated(
     message = "Replaced by DialogWindow",
     replaceWith = ReplaceWith("DialogWindow(" +
@@ -155,26 +76,65 @@
     resizable,
     enabled,
     focusable,
+    alwaysOnTop = false,
     onPreviewKeyEvent,
     onKeyEvent,
     content
 )
 
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    message = "Replaced by an overload that also takes alwaysOnTop",
+)
+@Composable
+fun DialogWindow(
+    onCloseRequest: () -> Unit,
+    state: DialogState = rememberDialogState(),
+    visible: Boolean = true,
+    title: String = "Untitled",
+    icon: Painter? = null,
+    undecorated: Boolean = false,
+    transparent: Boolean = false,
+    resizable: Boolean = true,
+    enabled: Boolean = true,
+    focusable: Boolean = true,
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
+    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
+    content: @Composable DialogWindowScope.() -> Unit
+) {
+    DialogWindow(
+        onCloseRequest,
+        state,
+        visible,
+        title,
+        icon,
+        undecorated,
+        transparent,
+        resizable,
+        enabled,
+        focusable,
+        alwaysOnTop = false,
+        onPreviewKeyEvent,
+        onKeyEvent,
+        content
+    )
+}
+
 /**
  * Composes platform dialog in the current composition. When Dialog enters the composition,
  * a new platform dialog will be created and receives the focus. When Dialog leaves the
  * composition, dialog will be disposed and closed.
  *
- * Dialog is a modal window. It means it blocks the parent [Window] / [Dialog] in which composition
+ * Dialog is a modal window. It means it blocks the parent [Window] / [DialogWindow] in which composition
  * context it was created.
  *
  * Usage:
  * ```
  * @Composable
  * fun main() = application {
- *     val isDialogOpen by remember { mutableStateOf(true) }
+ *     var isDialogOpen by remember { mutableStateOf(true) }
  *     if (isDialogOpen) {
- *         Dialog(onCloseRequest = { isDialogOpen = false })
+ *         Dialog(onCloseRequest = { isDialogOpen = false }) {}
  *     }
  * }
  * ```
@@ -189,11 +149,11 @@
  * the native dialog will update its corresponding properties.
  * If [DialogState.position] is not [WindowPosition.isSpecified], then after the first show on the
  * screen [DialogState.position] will be set to the absolute values.
- * @param visible Is [Dialog] visible to user.
+ * @param visible Is [DialogWindow] visible to user.
  * If `false`:
- * - internal state of [Dialog] is preserved and will be restored next time the dialog
+ * - internal state of [DialogWindow] is preserved and will be restored next time the dialog
  * will be visible;
- * - native resources will not be released. They will be released only when [Dialog]
+ * - native resources will not be released. They will be released only when [DialogWindow]
  * will leave the composition.
  * @param title Title in the titlebar of the dialog
  * @param icon Icon in the titlebar of the window (for platforms which support this).
@@ -207,6 +167,7 @@
  * changing [state])
  * @param enabled Can dialog react to input events
  * @param focusable Can dialog receive focus
+ * @param alwaysOnTop Should the dialog always be on top of another windows and dialogs
  * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
  * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
  * Return true to stop propagation of this event. If you return false, the key event will be
@@ -229,6 +190,7 @@
     resizable: Boolean = true,
     enabled: Boolean = true,
     focusable: Boolean = true,
+    alwaysOnTop: Boolean = false,
     onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
     onKeyEvent: ((KeyEvent) -> Boolean) = { false },
     content: @Composable DialogWindowScope.() -> Unit
@@ -243,6 +205,7 @@
     val currentResizable by rememberUpdatedState(resizable)
     val currentEnabled by rememberUpdatedState(enabled)
     val currentFocusable by rememberUpdatedState(focusable)
+    val currentAlwaysOnTop by rememberUpdatedState(alwaysOnTop)
     val currentOnCloseRequest by rememberUpdatedState(onCloseRequest)
 
     val updater = remember(::ComponentUpdater)
@@ -281,6 +244,7 @@
                 set(currentResizable, dialog::setResizable)
                 set(currentEnabled, dialog::setEnabled)
                 set(currentFocusable, dialog::setFocusable)
+                set(currentAlwaysOnTop, dialog::setAlwaysOnTop)
                 set(state.size, dialog::setSizeSafely)
                 set(state.position, dialog::setPositionSafely)
             }
@@ -325,7 +289,7 @@
  * Once Dialog leaves the composition, [dispose] will be called to free resources that
  * obtained by the [ComposeDialog].
  *
- * Dialog is a modal window. It means it blocks the parent [Window] / [Dialog] in which composition
+ * Dialog is a modal window. It means it blocks the parent [Window] / [DialogWindow] in which composition
  * context it was created.
  *
  * The [update] block can be run multiple times (on the UI thread as well) due to recomposition,
@@ -334,13 +298,13 @@
  * Note the block will also be ran once right after the [create] block completes.
  *
  * Dialog is needed for creating dialog's that still can't be created with
- * the default Compose function [androidx.compose.ui.window.Dialog]
+ * the default Compose function [androidx.compose.ui.window.DialogWindow]
  *
  * @param visible Is [ComposeDialog] visible to user.
  * If `false`:
  * - internal state of [ComposeDialog] is preserved and will be restored next time the dialog
  * will be visible;
- * - native resources will not be released. They will be released only when [Dialog]
+ * - native resources will not be released. They will be released only when [DialogWindow]
  * will leave the composition.
  * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
  * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
@@ -393,12 +357,12 @@
 }
 
 /**
- * Receiver scope which is used by [androidx.compose.ui.window.Dialog].
+ * Receiver scope which is used by [androidx.compose.ui.window.DialogWindow].
  */
 @Stable
 interface DialogWindowScope : WindowScope {
     /**
-     * [ComposeDialog] that was created inside [androidx.compose.ui.window.Dialog].
+     * [ComposeDialog] that was created inside [androidx.compose.ui.window.DialogWindow].
      */
     override val window: ComposeDialog
 }
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt
new file mode 100644
index 0000000..9bc95d4
--- /dev/null
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2024 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.window
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.LocalComposeScene
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.SkiaBasedOwner
+import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * The default scrim opacity.
+ */
+private const val DefaultScrimOpacity = 0.6f
+private val DefaultScrimColor = Color.Black.copy(alpha = DefaultScrimOpacity)
+
+/**
+ * Properties used to customize the behavior of a [Dialog].
+ *
+ * @property dismissOnBackPress whether the popup can be dismissed by pressing the back button
+ *  * on Android or escape key on desktop.
+ * If true, pressing the back button will call onDismissRequest.
+ * @property dismissOnClickOutside whether the dialog can be dismissed by clicking outside the
+ * dialog's bounds. If true, clicking outside the dialog will call onDismissRequest.
+ * @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
+ * the platform default, which is smaller than the screen width.
+ * @property usePlatformInsets Whether the size of the dialog's content should be limited by
+ * platform insets.
+ * @property useSoftwareKeyboardInset Whether the size of the dialog's content should be limited by
+ * software keyboard inset.
+ * @property scrimColor Color of background fill.
+ */
+@Immutable
+actual class DialogProperties @ExperimentalComposeUiApi constructor(
+    actual val dismissOnBackPress: Boolean = true,
+    actual val dismissOnClickOutside: Boolean = true,
+    actual val usePlatformDefaultWidth: Boolean = true,
+    val usePlatformInsets: Boolean = true,
+    val useSoftwareKeyboardInset: Boolean = true,
+    val scrimColor: Color = DefaultScrimColor,
+) {
+    // Constructor with all non-experimental arguments.
+    @OptIn(ExperimentalComposeUiApi::class)
+    actual constructor(
+        dismissOnBackPress: Boolean,
+        dismissOnClickOutside: Boolean,
+        usePlatformDefaultWidth: Boolean,
+    ) : this(
+        dismissOnBackPress = dismissOnBackPress,
+        dismissOnClickOutside = dismissOnClickOutside,
+        usePlatformDefaultWidth = usePlatformDefaultWidth,
+        usePlatformInsets = true,
+        useSoftwareKeyboardInset = true,
+        scrimColor = DefaultScrimColor,
+    )
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DialogProperties) return false
+
+        if (dismissOnBackPress != other.dismissOnBackPress) return false
+        if (dismissOnClickOutside != other.dismissOnClickOutside) return false
+        if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false
+        if (usePlatformInsets != other.usePlatformInsets) return false
+        if (useSoftwareKeyboardInset != other.useSoftwareKeyboardInset) return false
+        if (scrimColor != other.scrimColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = dismissOnBackPress.hashCode()
+        result = 31 * result + dismissOnClickOutside.hashCode()
+        result = 31 * result + usePlatformDefaultWidth.hashCode()
+        result = 31 * result + usePlatformInsets.hashCode()
+        result = 31 * result + useSoftwareKeyboardInset.hashCode()
+        result = 31 * result + scrimColor.hashCode()
+        return result
+    }
+}
+
+@Composable
+actual fun Dialog(
+    onDismissRequest: () -> Unit,
+    properties: DialogProperties,
+    content: @Composable () -> Unit
+) {
+    DialogLayout(
+        if (properties.dismissOnClickOutside) onDismissRequest else null,
+        modifier = Modifier.drawBehind {
+            drawRect(properties.scrimColor)
+        },
+        onPreviewKeyEvent = { false },
+        onKeyEvent = {
+            if (properties.dismissOnBackPress && it.isDismissRequest()) {
+                onDismissRequest()
+                true
+            } else {
+                false
+            }
+        },
+        content = content
+    )
+}
+
+@Composable
+internal fun DialogLayout(
+    onDismissRequest: (() -> Unit)?,
+    modifier: Modifier = Modifier,
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean)?,
+    onKeyEvent: ((KeyEvent) -> Boolean)?,
+    content: @Composable () -> Unit
+) {
+    // TODO: Upstream ComposeScene refactor
+    val scene = LocalComposeScene.current
+    val density = LocalDensity.current
+
+    val parentComposition = rememberCompositionContext()
+    val (owner, composition) = remember {
+        val owner = SkiaBasedOwner(
+            platformInputService = scene.platformInputService,
+            component = scene.component,
+            density = density,
+            coroutineContext = parentComposition.effectCoroutineContext,
+            isPopup = true,
+            isFocusable = true,
+            onDismissRequest = onDismissRequest,
+            onPreviewKeyEvent = onPreviewKeyEvent ?: { false },
+            onKeyEvent = onKeyEvent ?: { false }
+        )
+        scene.attach(owner)
+        val composition = owner.setContent(parent = parentComposition) {
+            Layout(
+                content = content,
+                modifier = modifier,
+                measurePolicy = { measurables, constraints ->
+                    val width = constraints.maxWidth
+                    val height = constraints.maxHeight
+
+                    layout(constraints.maxWidth, constraints.maxHeight) {
+                        measurables.forEach {
+                            val placeable = it.measure(constraints)
+                            val position = Alignment.Center.align(
+                                size = IntSize(placeable.width, placeable.height),
+                                space = IntSize(width, height),
+                                layoutDirection = layoutDirection
+                            )
+                            owner.bounds = IntRect(
+                                position,
+                                IntSize(placeable.width, placeable.height)
+                            )
+                            placeable.place(position.x, position.y)
+                        }
+                    }
+                }
+            )
+        }
+        owner to composition
+    }
+    owner.density = density
+    DisposableEffect(Unit) {
+        onDispose {
+            scene.detach(owner)
+            composition.dispose()
+            owner.dispose()
+        }
+    }
+}
+
+private fun KeyEvent.isDismissRequest() =
+    type == KeyEventType.KeyDown && key == Key.Escape
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt
new file mode 100644
index 0000000..15b2c62
--- /dev/null
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt
@@ -0,0 +1,483 @@
+/*
+ * Copyright 2024 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.window
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.LocalComposeScene
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.SkiaBasedOwner
+import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.semantics.popup
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+
+/**
+ * Properties used to customize the behavior of a [Popup].
+ *
+ * @property focusable Whether the popup is focusable. When true, the popup will receive IME
+ * events and key presses, such as when the back button is pressed.
+ * @property dismissOnBackPress Whether the popup can be dismissed by pressing the back button
+ * on Android or escape key on desktop.
+ * If true, pressing the back button will call onDismissRequest. Note that [focusable] must be
+ * set to true in order to receive key events such as the back button - if the popup is not
+ * 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 popup's content should be limited to
+ * the platform default, which is smaller than the screen width.
+ * @property usePlatformInsets Whether the width of the popup's content should be limited by
+ * platform insets.
+ */
+@Immutable
+actual class PopupProperties @ExperimentalComposeUiApi constructor(
+    actual val focusable: Boolean = false,
+    actual val dismissOnBackPress: Boolean = true,
+    actual val dismissOnClickOutside: Boolean = true,
+    actual val clippingEnabled: Boolean = true,
+    val usePlatformDefaultWidth: Boolean = false,
+    val usePlatformInsets: Boolean = true,
+) {
+    // Constructor with all non-experimental arguments.
+    @OptIn(ExperimentalComposeUiApi::class)
+    actual constructor(
+        focusable: Boolean,
+        dismissOnBackPress: Boolean,
+        dismissOnClickOutside: Boolean,
+        clippingEnabled: Boolean,
+    ) : this(
+        focusable = focusable,
+        dismissOnBackPress = dismissOnBackPress,
+        dismissOnClickOutside = dismissOnClickOutside,
+        clippingEnabled = clippingEnabled,
+        usePlatformDefaultWidth = false,
+        usePlatformInsets = true,
+    )
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is PopupProperties) return false
+
+        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
+        if (usePlatformInsets != other.usePlatformInsets) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = focusable.hashCode()
+        result = 31 * result + dismissOnBackPress.hashCode()
+        result = 31 * result + dismissOnClickOutside.hashCode()
+        result = 31 * result + clippingEnabled.hashCode()
+        result = 31 * result + usePlatformDefaultWidth.hashCode()
+        result = 31 * result + usePlatformInsets.hashCode()
+        return result
+    }
+}
+
+/**
+ * Opens a popup with the given content.
+ *
+ * The popup is positioned relative to its parent, using the [alignment] and [offset].
+ * The popup is visible as long as it is part of the composition hierarchy.
+ *
+ * @sample androidx.compose.ui.samples.PopupSample
+ *
+ * @param alignment The alignment relative to the parent.
+ * @param offset An offset from the original aligned position of the popup. Offset respects the
+ * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
+ * will be subtracted from it.
+ * @param focusable Indicates if the popup can grab the focus.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
+ * Return true to stop propagation of this event. If you return false, the key event will be
+ * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
+ * it will be sent back up to the root using the onKeyEvent callback.
+ * @param onKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. While implementing this callback, return true to stop propagation of this event.
+ * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
+ * @param content The content to be displayed inside the popup.
+ */
+@Deprecated(
+    "Replaced by Popup with properties parameter",
+    ReplaceWith("Popup(alignment, offset, onDismissRequest, " +
+        "androidx.compose.ui.window.PopupProperties(focusable = focusable), " +
+        "onPreviewKeyEvent, onKeyEvent, content)")
+)
+@Composable
+fun Popup(
+    alignment: Alignment = Alignment.TopStart,
+    offset: IntOffset = IntOffset(0, 0),
+    focusable: Boolean = false,
+    onDismissRequest: (() -> Unit)? = null,
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
+    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
+    content: @Composable () -> Unit
+) = Popup(
+    alignment = alignment,
+    offset = offset,
+    onDismissRequest = onDismissRequest,
+    properties = PopupProperties(
+        focusable = focusable,
+        dismissOnBackPress = true,
+        dismissOnClickOutside = focusable
+
+    ),
+    onPreviewKeyEvent = onPreviewKeyEvent,
+    onKeyEvent = onKeyEvent,
+    content = content
+)
+
+/**
+ * Opens a popup with the given content.
+ *
+ * The popup is positioned using a custom [popupPositionProvider].
+ *
+ * @sample androidx.compose.ui.samples.PopupSample
+ *
+ * @param popupPositionProvider Provides the screen position of the popup.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param focusable Indicates if the popup can grab the focus.
+ * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
+ * Return true to stop propagation of this event. If you return false, the key event will be
+ * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
+ * it will be sent back up to the root using the onKeyEvent callback.
+ * @param onKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. While implementing this callback, return true to stop propagation of this event.
+ * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
+ * @param content The content to be displayed inside the popup.
+ */
+@Deprecated(
+    "Replaced by Popup with properties parameter",
+    ReplaceWith("Popup(popupPositionProvider, onDismissRequest, " +
+        "androidx.compose.ui.window.PopupProperties(focusable = focusable), " +
+        "onPreviewKeyEvent, onKeyEvent, content)")
+)
+@Composable
+fun Popup(
+    popupPositionProvider: PopupPositionProvider,
+    onDismissRequest: (() -> Unit)? = null,
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
+    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
+    focusable: Boolean = false,
+    content: @Composable () -> Unit
+) = Popup(
+    popupPositionProvider = popupPositionProvider,
+    onDismissRequest = onDismissRequest,
+    properties = PopupProperties(
+        focusable = focusable,
+        dismissOnBackPress = true,
+        dismissOnClickOutside = focusable
+
+    ),
+    onPreviewKeyEvent = onPreviewKeyEvent,
+    onKeyEvent = onKeyEvent,
+    content = content
+)
+
+/**
+ * Opens a popup with the given content.
+ *
+ * A popup is a floating container that appears on top of the current activity.
+ * It is especially useful for non-modal UI surfaces that remain hidden until they
+ * are needed, for example floating menus like Cut/Copy/Paste.
+ *
+ * The popup is positioned relative to its parent, using the [alignment] and [offset].
+ * The popup is visible as long as it is part of the composition hierarchy.
+ *
+ * @sample androidx.compose.ui.samples.PopupSample
+ *
+ * @param alignment The alignment relative to the parent.
+ * @param offset An offset from the original aligned position of the popup. Offset respects the
+ * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
+ * will be subtracted from it.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param properties [PopupProperties] for further customization of this popup's behavior.
+ * @param content The content to be displayed inside the popup.
+ */
+@Composable
+actual fun Popup(
+    alignment: Alignment,
+    offset: IntOffset,
+    onDismissRequest: (() -> Unit)?,
+    properties: PopupProperties,
+    content: @Composable () -> Unit
+): Unit = Popup(
+    alignment = alignment,
+    offset = offset,
+    onDismissRequest = onDismissRequest,
+    properties = properties,
+    onPreviewKeyEvent = null,
+    onKeyEvent = null,
+    content = content
+)
+
+/**
+ * Opens a popup with the given content.
+ *
+ * The popup is positioned using a custom [popupPositionProvider].
+ *
+ * @sample androidx.compose.ui.samples.PopupSample
+ *
+ * @param popupPositionProvider Provides the screen position of the popup.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param properties [PopupProperties] for further customization of this popup's behavior.
+ * @param content The content to be displayed inside the popup.
+ */
+@Composable
+actual fun Popup(
+    popupPositionProvider: PopupPositionProvider,
+    onDismissRequest: (() -> Unit)?,
+    properties: PopupProperties,
+    content: @Composable () -> Unit
+): Unit = Popup(
+    popupPositionProvider = popupPositionProvider,
+    onDismissRequest = onDismissRequest,
+    properties = properties,
+    onPreviewKeyEvent = null,
+    onKeyEvent = null,
+    content = content
+)
+
+/**
+ * Opens a popup with the given content.
+ *
+ * A popup is a floating container that appears on top of the current activity.
+ * It is especially useful for non-modal UI surfaces that remain hidden until they
+ * are needed, for example floating menus like Cut/Copy/Paste.
+ *
+ * The popup is positioned relative to its parent, using the [alignment] and [offset].
+ * The popup is visible as long as it is part of the composition hierarchy.
+ *
+ * @sample androidx.compose.ui.samples.PopupSample
+ *
+ * @param alignment The alignment relative to the parent.
+ * @param offset An offset from the original aligned position of the popup. Offset respects the
+ * Ltr/Rtl context, thus in Ltr it will be added to the original aligned position and in Rtl it
+ * will be subtracted from it.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param properties [PopupProperties] for further customization of this popup's behavior.
+ * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
+ * Return true to stop propagation of this event. If you return false, the key event will be
+ * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
+ * it will be sent back up to the root using the onKeyEvent callback.
+ * @param onKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. While implementing this callback, return true to stop propagation of this event.
+ * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
+ * @param content The content to be displayed inside the popup.
+ */
+@Composable
+fun Popup(
+    alignment: Alignment = Alignment.TopStart,
+    offset: IntOffset = IntOffset(0, 0),
+    onDismissRequest: (() -> Unit)? = null,
+    properties: PopupProperties = PopupProperties(),
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean)? = null,
+    onKeyEvent: ((KeyEvent) -> Boolean)? = null,
+    content: @Composable () -> Unit
+) {
+    val popupPositioner = remember(alignment, offset) {
+        AlignmentOffsetPositionProvider(alignment, offset)
+    }
+    Popup(
+        popupPositionProvider = popupPositioner,
+        onDismissRequest = onDismissRequest,
+        properties = properties,
+        onPreviewKeyEvent = onPreviewKeyEvent,
+        onKeyEvent = onKeyEvent,
+        content = content
+    )
+}
+
+/**
+ * Opens a popup with the given content.
+ *
+ * The popup is positioned using a custom [popupPositionProvider].
+ *
+ * @sample androidx.compose.ui.samples.PopupSample
+ *
+ * @param popupPositionProvider Provides the screen position of the popup.
+ * @param onDismissRequest Executes when the user clicks outside of the popup.
+ * @param properties [PopupProperties] for further customization of this popup's behavior.
+ * @param onPreviewKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
+ * Return true to stop propagation of this event. If you return false, the key event will be
+ * sent to this [onPreviewKeyEvent]'s child. If none of the children consume the event,
+ * it will be sent back up to the root using the onKeyEvent callback.
+ * @param onKeyEvent This callback is invoked when the user interacts with the hardware
+ * keyboard. While implementing this callback, return true to stop propagation of this event.
+ * If you return false, the key event will be sent to this [onKeyEvent]'s parent.
+ * @param content The content to be displayed inside the popup.
+ */
+@Composable
+fun Popup(
+    popupPositionProvider: PopupPositionProvider,
+    onDismissRequest: (() -> Unit)? = null,
+    properties: PopupProperties = PopupProperties(),
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean)? = null,
+    onKeyEvent: ((KeyEvent) -> Boolean)? = null,
+    content: @Composable () -> Unit
+) {
+    val currentOnDismissRequest by rememberUpdatedState(onDismissRequest)
+    val currentOnKeyEvent by rememberUpdatedState(onKeyEvent)
+
+    val overriddenOnKeyEvent = if (properties.dismissOnBackPress && onDismissRequest != null) {
+        // No need to remember this lambda, as it doesn't capture any values that can change.
+        { event: KeyEvent ->
+            val consumed = currentOnKeyEvent?.invoke(event) ?: false
+            if (!consumed && event.isDismissRequest()) {
+                currentOnDismissRequest?.invoke()
+                true
+            } else {
+                consumed
+            }
+        }
+    } else {
+        onKeyEvent
+    }
+    PopupLayout(
+        popupPositionProvider = popupPositionProvider,
+        focusable = properties.focusable,
+        onDismissRequest = onDismissRequest,
+        modifier = Modifier.semantics { popup() },
+        onPreviewKeyEvent = onPreviewKeyEvent,
+        onKeyEvent = overriddenOnKeyEvent,
+        content = content,
+    )
+}
+
+@Composable
+internal fun PopupLayout(
+    popupPositionProvider: PopupPositionProvider,
+    focusable: Boolean,
+    onDismissRequest: (() -> Unit)?,
+    modifier: Modifier = Modifier,
+    onPreviewKeyEvent: ((KeyEvent) -> Boolean)?,
+    onKeyEvent: ((KeyEvent) -> Boolean)?,
+    content: @Composable () -> Unit
+) {
+    // TODO: Upstream ComposeScene refactor
+    val scene = LocalComposeScene.current
+    val density = LocalDensity.current
+
+    var parentBounds by remember { mutableStateOf(IntRect.Zero) }
+    var popupBounds by remember { mutableStateOf(IntRect.Zero) }
+
+    // getting parent bounds
+    Layout(
+        content = {},
+        modifier = Modifier.onGloballyPositioned { childCoordinates ->
+            val coordinates = childCoordinates.parentCoordinates!!
+            parentBounds = IntRect(
+                coordinates.localToWindow(Offset.Zero).round(),
+                coordinates.size
+            )
+        },
+        measurePolicy = { _, _ ->
+            layout(0, 0) {}
+        }
+    )
+
+    val parentComposition = rememberCompositionContext()
+    val (owner, composition) = remember {
+        val owner = SkiaBasedOwner(
+            platformInputService = scene.platformInputService,
+            component = scene.component,
+            density = density,
+            coroutineContext = parentComposition.effectCoroutineContext,
+            isPopup = true,
+            isFocusable = focusable,
+            onDismissRequest = onDismissRequest,
+            onPreviewKeyEvent = onPreviewKeyEvent ?: { false },
+            onKeyEvent = onKeyEvent ?: { false }
+        )
+        scene.attach(owner)
+        val composition = owner.setContent(parent = parentComposition) {
+            Layout(
+                content = content,
+                modifier = modifier,
+                measurePolicy = { measurables, constraints ->
+                    val width = constraints.maxWidth
+                    val height = constraints.maxHeight
+
+                    layout(constraints.maxWidth, constraints.maxHeight) {
+                        measurables.forEach {
+                            val placeable = it.measure(constraints)
+                            val position = popupPositionProvider.calculatePosition(
+                                anchorBounds = parentBounds,
+                                windowSize = IntSize(width, height),
+                                layoutDirection = layoutDirection,
+                                popupContentSize = IntSize(placeable.width, placeable.height)
+                            )
+
+                            popupBounds = IntRect(
+                                position,
+                                IntSize(placeable.width, placeable.height)
+                            )
+                            owner.bounds = popupBounds
+                            placeable.place(position.x, position.y)
+                        }
+                    }
+                }
+            )
+        }
+        owner to composition
+    }
+    owner.density = density
+    DisposableEffect(Unit) {
+        onDispose {
+            scene.detach(owner)
+            composition.dispose()
+            owner.dispose()
+        }
+    }
+}
+
+private fun KeyEvent.isDismissRequest() =
+    type == KeyEventType.KeyDown && key == Key.Escape