From e8854e957d6ad1503a49c75cd52ef0ec74ed4122 Mon Sep 17 00:00:00 2001 From: Ivan Matkov Date: Tue, 12 Mar 2024 23:27:52 +0100 Subject: [PATCH] Fix click outside and scrim drawing for `WINDOW` layers --- .../androidx/compose/ui/awt/Utils.desktop.kt | 19 ++- .../ui/scene/ComposeContainer.desktop.kt | 68 +++++++- .../scene/DesktopComposeSceneLayer.desktop.kt | 147 +++++++++++++++++- .../scene/SwingComposeSceneLayer.desktop.kt | 109 ++----------- .../scene/WindowComposeSceneLayer.desktop.kt | 132 +++++----------- .../ui/scene/MultiLayerComposeScene.skiko.kt | 14 +- .../compose/ui/window/Dialog.skiko.kt | 9 ++ 7 files changed, 293 insertions(+), 205 deletions(-) diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/Utils.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/Utils.desktop.kt index 25e5b9bfcc436..38f2466fa74bc 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/Utils.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/Utils.desktop.kt @@ -17,10 +17,13 @@ package androidx.compose.ui.awt import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntRect import java.awt.Component -import java.awt.Transparency +import java.awt.Rectangle import javax.swing.JComponent -import org.jetbrains.skiko.ClipRectangle +import kotlin.math.ceil +import kotlin.math.floor import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.OS import org.jetbrains.skiko.hostOs @@ -36,6 +39,18 @@ internal fun Component.isParentOf(component: Component?): Boolean { return false } +internal fun IntRect.toAwtRectangle(density: Density): Rectangle { + val left = floor(left / density.density).toInt() + val top = floor(top / density.density).toInt() + val right = ceil(right / density.density).toInt() + val bottom = ceil(bottom / density.density).toInt() + val width = right - left + val height = bottom - top + return Rectangle( + left, top, width, height + ) +} + internal fun Color.toAwtColor() = java.awt.Color(red, green, blue, alpha) internal fun getTransparentWindowBackground( diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt index 410dcb282efdc..64e57ea8c7293 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt @@ -16,8 +16,8 @@ package androidx.compose.ui.scene -import java.awt.event.MouseEvent as AwtMouseEvent import java.awt.event.KeyEvent as AwtKeyEvent +import java.awt.event.MouseEvent as AwtMouseEvent import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider @@ -36,6 +36,7 @@ import androidx.compose.ui.skiko.OverlaySkikoViewDecorator import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachReversed import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Popup import androidx.compose.ui.window.WindowExceptionHandler @@ -123,6 +124,7 @@ internal class ComposeContainer( }, eventFilter = AwtEventFilters( OnlyValidPrimaryMouseButtonFilter, + DetectEventOutsideLayer(), FocusableLayerEventFilter() ), coroutineContext = coroutineContext, @@ -197,7 +199,9 @@ internal class ComposeContainer( * Callback to let layers draw overlay on main [mediator]. */ private fun onRenderOverlay(canvas: Canvas, width: Int, height: Int) { - layers.fastForEach { it.onRenderOverlay(canvas, width, height) } + layers.fastForEach { + it.onRenderOverlay(canvas, width, height, windowContext.isWindowTransparent) + } } fun onChangeWindowTransparency(value: Boolean) { @@ -312,6 +316,7 @@ internal class ComposeContainer( LayerType.OnWindow -> WindowComposeSceneLayer( composeContainer = this, skiaLayerAnalytics = skiaLayerAnalytics, + transparent = true, // TODO: Consider allowing opaque window layers density = density, layoutDirection = layoutDirection, focusable = focusable, @@ -329,12 +334,55 @@ internal class ComposeContainer( } } + /** + * Generates a sequence of layers that are positioned above the given layer in the layers list. + * + * @param layer the layer to find layers above + * @return a sequence of layers positioned above the given layer + */ + fun layersAbove(layer: DesktopComposeSceneLayer) = sequence { + var isAbove = false + for (i in layers) { + if (i == layer) { + isAbove = true + } else if (isAbove) { + yield(i) + } + } + } + + /** + * Notify layers about change in layers list. Required for additional invalidation and + * re-drawing if needed. + * + * @param layer the layer that triggered the change + */ + private fun onLayersChange(layer: DesktopComposeSceneLayer) { + layers.fastForEach { + if (it != layer) { + it.onLayersChange() + } + } + } + + /** + * Attaches a [DesktopComposeSceneLayer] to the list of layers. + * + * @param layer the layer to attach + */ fun attachLayer(layer: DesktopComposeSceneLayer) { layers.add(layer) + onLayersChange(layer) } + /** + * Detaches a [DesktopComposeSceneLayer] from the list of layers. + * + * @param layer the layer to detach + */ fun detachLayer(layer: DesktopComposeSceneLayer) { layers.remove(layer) + onLayersChange(layer) } fun createComposeSceneContext(platformContext: PlatformContext): ComposeSceneContext = @@ -363,6 +411,22 @@ internal class ComposeContainer( } } + /** + * Detect and trigger [DesktopComposeSceneLayer.onMouseEventOutside] if event happened below + * focused layer. + */ + private inner class DetectEventOutsideLayer : AwtEventFilter { + override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean { + layers.fastForEachReversed { + it.onMouseEventOutside(event) + if (it.focusable) { + return true + } + } + return true + } + } + private inner class FocusableLayerEventFilter : AwtEventFilter { private val noFocusableLayers get() = layers.all { !it.focusable } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/DesktopComposeSceneLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/DesktopComposeSceneLayer.desktop.kt index 8f3b53280b6fd..221ac910b3e74 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/DesktopComposeSceneLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/DesktopComposeSceneLayer.desktop.kt @@ -16,6 +16,21 @@ package androidx.compose.ui.scene +import androidx.annotation.CallSuper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalContext +import androidx.compose.ui.awt.AwtEventFilter +import androidx.compose.ui.awt.AwtEventFilters +import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter +import androidx.compose.ui.awt.toAwtRectangle +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.fastForEachReversed +import java.awt.event.KeyEvent +import java.awt.event.MouseEvent +import javax.swing.SwingUtilities import org.jetbrains.skia.Canvas /** @@ -24,7 +39,67 @@ import org.jetbrains.skia.Canvas * @see SwingComposeSceneLayer * @see WindowComposeSceneLayer */ -internal abstract class DesktopComposeSceneLayer : ComposeSceneLayer { +internal abstract class DesktopComposeSceneLayer( + protected val composeContainer: ComposeContainer, + density: Density, + layoutDirection: LayoutDirection, +) : ComposeSceneLayer { + protected val windowContainer get() = composeContainer.windowContainer + protected val layersAbove get() = composeContainer.layersAbove(this) + protected val eventFilter get() = AwtEventFilters( + OnlyValidPrimaryMouseButtonFilter, + DetectEventOutsideLayer(), + FocusableLayerEventFilter() + ) + + protected abstract val mediator: ComposeSceneMediator? + + private var outsidePointerCallback: ((eventType: PointerEventType) -> Unit)? = null + private var isClosed = false + + final override var density: Density = density + set(value) { + field = value + mediator?.onChangeDensity(value) + } + + final override var layoutDirection: LayoutDirection = layoutDirection + set(value) { + field = value + mediator?.onChangeLayoutDirection(value) + } + + final override var compositionLocalContext: CompositionLocalContext? + get() = mediator?.compositionLocalContext + set(value) { mediator?.compositionLocalContext = value } + + @CallSuper + override fun close() { + isClosed = true + } + + final override fun setContent(content: @Composable () -> Unit) { + mediator?.setContent(content) + } + + final override fun setKeyEventListener( + onPreviewKeyEvent: ((androidx.compose.ui.input.key.KeyEvent) -> Boolean)?, + onKeyEvent: ((androidx.compose.ui.input.key.KeyEvent) -> Boolean)? + ) { + mediator?.setKeyEventListeners( + onPreviewKeyEvent = onPreviewKeyEvent ?: { false }, + onKeyEvent = onKeyEvent ?: { false } + ) + } + + final override fun setOutsidePointerEventListener( + onOutsidePointerEvent: ((eventType: PointerEventType) -> Unit)? + ) { + outsidePointerCallback = onOutsidePointerEvent + } + + override fun calculateLocalPosition(positionInWindow: IntOffset) = + positionInWindow // [ComposeScene] is equal to [windowContainer] for the layer. /** * Called when the focus of the window containing main Compose view has changed. @@ -45,12 +120,72 @@ internal abstract class DesktopComposeSceneLayer : ComposeSceneLayer { } /** - * Renders the overlay on the main Compose view canvas. + * Called when the layers in [composeContainer] have changed. + */ + open fun onLayersChange() { + } + + /** + * Renders an overlay on the canvas. + * + * @param canvas the canvas to render on + * @param width the width of the overlay + * @param height the height of the overlay + * @param transparent a flag indicating whether [canvas] is transparent + */ + open fun onRenderOverlay(canvas: Canvas, width: Int, height: Int, transparent: Boolean) { + } + + /** + * This method is called when a mouse event occurs outside of this layer. * - * @param canvas the canvas of the main Compose view - * @param width the width of the canvas - * @param height the height of the canvas + * @param event the mouse event + */ + fun onMouseEventOutside(event: MouseEvent) { + if (isClosed || !event.isMainAction() || inBounds(event)) { + return + } + val eventType = when (event.id) { + MouseEvent.MOUSE_PRESSED -> PointerEventType.Press + MouseEvent.MOUSE_RELEASED -> PointerEventType.Release + else -> return + } + outsidePointerCallback?.invoke(eventType) + } + + private fun inBounds(event: MouseEvent): Boolean { + val point = if (event.component != windowContainer) { + SwingUtilities.convertPoint(event.component, event.point, windowContainer) + } else { + event.point + } + return boundsInWindow.toAwtRectangle(density).contains(point) + } + + /** + * Detect and trigger [DesktopComposeSceneLayer.onMouseEventOutside] if event happened below + * focused layer. */ - open fun onRenderOverlay(canvas: Canvas, width: Int, height: Int) { + private inner class DetectEventOutsideLayer : AwtEventFilter { + override fun shouldSendMouseEvent(event: MouseEvent): Boolean { + layersAbove.toList().fastForEachReversed { + it.onMouseEventOutside(event) + if (it.focusable) { + return true + } + } + return true + } + } + + private inner class FocusableLayerEventFilter : AwtEventFilter { + private val noFocusableLayersAbove: Boolean + get() = layersAbove.all { !it.focusable } + + override fun shouldSendMouseEvent(event: MouseEvent): Boolean = noFocusableLayersAbove + override fun shouldSendKeyEvent(event: KeyEvent): Boolean = focusable && noFocusableLayersAbove } } + +private fun MouseEvent.isMainAction() = + button == MouseEvent.BUTTON1 diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/SwingComposeSceneLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/SwingComposeSceneLayer.desktop.kt index 9efc7cd17ec48..91054dfd017af 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/SwingComposeSceneLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/SwingComposeSceneLayer.desktop.kt @@ -16,43 +16,33 @@ package androidx.compose.ui.scene -import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext -import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.awt.toAwtColor +import androidx.compose.ui.awt.toAwtRectangle import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.scene.skia.SkiaLayerComponent import androidx.compose.ui.scene.skia.SwingSkiaLayerComponent import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.window.density import java.awt.Dimension import java.awt.Graphics -import java.awt.Rectangle import java.awt.event.MouseAdapter import java.awt.event.MouseEvent -import java.awt.event.MouseListener import javax.swing.JLayeredPane import javax.swing.SwingUtilities -import kotlin.math.ceil -import kotlin.math.floor import org.jetbrains.skiko.SkiaLayerAnalytics internal class SwingComposeSceneLayer( - private val composeContainer: ComposeContainer, + composeContainer: ComposeContainer, private val skiaLayerAnalytics: SkiaLayerAnalytics, density: Density, layoutDirection: LayoutDirection, focusable: Boolean, compositionContext: CompositionContext -) : DesktopComposeSceneLayer() { - private val windowContainer get() = composeContainer.windowContainer - +) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) { private val backgroundMouseListener = object : MouseAdapter() { override fun mousePressed(event: MouseEvent) = onMouseEventOutside(event) override fun mouseReleased(event: MouseEvent) = onMouseEventOutside(event) @@ -61,9 +51,9 @@ internal class SwingComposeSceneLayer( private val container = object : JLayeredPane() { override fun addNotify() { super.addNotify() - _mediator?.onComponentAttached() + mediator?.onComponentAttached() _boundsInWindow?.let { - _mediator?.contentComponent?.bounds = it.toAwtRectangle(density) + mediator?.contentComponent?.bounds = it.toAwtRectangle(density) } } @@ -76,6 +66,7 @@ internal class SwingComposeSceneLayer( } }.also { it.layout = null + it.isFocusable = focusable it.isOpaque = false it.background = Color.Transparent.toAwtColor() it.size = Dimension(windowContainer.width, windowContainer.height) @@ -85,37 +76,25 @@ internal class SwingComposeSceneLayer( // TODO: Do not clip this from main scene if layersContainer == main container windowContainer.add(it, JLayeredPane.POPUP_LAYER, 0) } + private var containerSize = IntSize.Zero set(value) { if (field.width != value.width || field.height != value.height) { field = value container.setBounds(0, 0, value.width, value.height) if (_boundsInWindow == null) { - _mediator?.contentComponent?.size = container.size + mediator?.contentComponent?.size = container.size } - _mediator?.onChangeComponentSize() + mediator?.onChangeComponentSize() } } - private var _mediator: ComposeSceneMediator? = null - private var outsidePointerCallback: ((eventType: PointerEventType) -> Unit)? = null - - override var density: Density = density - set(value) { - field = value - _mediator?.onChangeDensity(value) - } - - override var layoutDirection: LayoutDirection = layoutDirection - set(value) { - field = value - _mediator?.onChangeLayoutDirection(value) - } + override var mediator: ComposeSceneMediator? = null override var focusable: Boolean = focusable set(value) { field = value - // TODO: Pass it to mediator/scene + container.isFocusable = value } private var _boundsInWindow: IntRect? = null @@ -127,13 +106,9 @@ internal class SwingComposeSceneLayer( /* source = */ windowContainer, /* aRectangle = */ value.toAwtRectangle(container.density), /* destination = */ container) - _mediator?.contentComponent?.bounds = localBounds + mediator?.contentComponent?.bounds = localBounds } - override var compositionLocalContext: CompositionLocalContext? - get() = _mediator?.compositionLocalContext - set(value) { _mediator?.compositionLocalContext = value } - override var scrimColor: Color? = null set(value) { field = value @@ -142,12 +117,13 @@ internal class SwingComposeSceneLayer( } init { - _mediator = ComposeSceneMediator( + mediator = ComposeSceneMediator( container = container, windowContext = composeContainer.windowContext, exceptionHandler = { composeContainer.exceptionHandler?.onException(it) ?: throw it }, + eventFilter = eventFilter, coroutineContext = compositionContext.effectCoroutineContext, skiaLayerComponentFactory = ::createSkiaLayerComponent, composeSceneFactory = ::createComposeScene, @@ -159,56 +135,20 @@ internal class SwingComposeSceneLayer( } override fun close() { + super.close() composeContainer.detachLayer(this) - _mediator?.dispose() - _mediator = null + mediator?.dispose() + mediator = null windowContainer.remove(container) windowContainer.invalidate() windowContainer.repaint() } - override fun setContent(content: @Composable () -> Unit) { - _mediator?.setContent(content) - } - - override fun setKeyEventListener( - onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, - onKeyEvent: ((KeyEvent) -> Boolean)? - ) { - _mediator?.setKeyEventListeners( - onPreviewKeyEvent = onPreviewKeyEvent ?: { false }, - onKeyEvent = onKeyEvent ?: { false } - ) - } - - override fun setOutsidePointerEventListener( - onOutsidePointerEvent: ((eventType: PointerEventType) -> Unit)? - ) { - outsidePointerCallback = onOutsidePointerEvent - } - override fun onChangeWindowSize() { containerSize = IntSize(windowContainer.width, windowContainer.height) } - private fun onMouseEventOutside(event: MouseEvent) { - // TODO: Filter/consume based on [focused] flag - if (!event.isMainAction()) { - return - } - val eventType = when (event.id) { - MouseEvent.MOUSE_PRESSED -> PointerEventType.Press - MouseEvent.MOUSE_RELEASED -> PointerEventType.Release - else -> return - } - outsidePointerCallback?.invoke(eventType) - } - - override fun calculateLocalPosition(positionInWindow: IntOffset): IntOffset { - return positionInWindow - } - private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent { return SwingSkiaLayerComponent( mediator = mediator, @@ -230,18 +170,3 @@ internal class SwingComposeSceneLayer( ) } } - -private fun IntRect.toAwtRectangle(density: Density): Rectangle { - val left = floor(left / density.density).toInt() - val top = floor(top / density.density).toInt() - val right = ceil(right / density.density).toInt() - val bottom = ceil(bottom / density.density).toInt() - val width = right - left - val height = bottom - top - return Rectangle( - left, top, width, height - ) -} - -private fun MouseEvent.isMainAction() = - button == MouseEvent.BUTTON1 diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt index a50a0f03c7bae..64b491a278e59 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/WindowComposeSceneLayer.desktop.kt @@ -16,52 +16,45 @@ package androidx.compose.ui.scene -import androidx.compose.runtime.Composable +import org.jetbrains.skia.Rect as SkRect import androidx.compose.runtime.CompositionContext -import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.awt.getTransparentWindowBackground import androidx.compose.ui.awt.setTransparent -import androidx.compose.ui.awt.toAwtColor +import androidx.compose.ui.awt.toAwtRectangle import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.scene.skia.SkiaLayerComponent import androidx.compose.ui.scene.skia.WindowSkiaLayerComponent +import androidx.compose.ui.skiko.OverlaySkikoViewDecorator 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.toOffset import androidx.compose.ui.window.density +import androidx.compose.ui.window.getDialogScrimBlendMode import androidx.compose.ui.window.layoutDirectionFor import androidx.compose.ui.window.sizeInPx import java.awt.Point -import java.awt.Rectangle import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent -import java.awt.event.WindowEvent -import java.awt.event.WindowFocusListener import javax.swing.JDialog import javax.swing.JLayeredPane -import kotlin.math.ceil -import kotlin.math.floor import org.jetbrains.skia.Canvas import org.jetbrains.skiko.SkiaLayerAnalytics internal class WindowComposeSceneLayer( - private val composeContainer: ComposeContainer, + composeContainer: ComposeContainer, private val skiaLayerAnalytics: SkiaLayerAnalytics, + private val transparent: Boolean, density: Density, layoutDirection: LayoutDirection, focusable: Boolean, compositionContext: CompositionContext -) : DesktopComposeSceneLayer() { +) : DesktopComposeSceneLayer(composeContainer, density, layoutDirection) { private val window get() = requireNotNull(composeContainer.window) - private val windowContainer get() = composeContainer.windowContainer private val windowContext = PlatformWindowContext().also { it.isWindowTransparent = true it.setContainerSize(windowContainer.sizeInPx) @@ -71,20 +64,21 @@ internal class WindowComposeSceneLayer( window, ).also { it.isAlwaysOnTop = true + it.isFocusable = focusable it.isUndecorated = true it.background = getTransparentWindowBackground( - isWindowTransparent = true, + isWindowTransparent = transparent, renderApi = composeContainer.renderApi ) } private val container = object : JLayeredPane() { override fun addNotify() { super.addNotify() - _mediator?.onComponentAttached() + mediator?.onComponentAttached() } }.also { it.layout = null - it.setTransparent(true) + it.setTransparent(transparent) dialog.contentPane = it } @@ -95,34 +89,12 @@ internal class WindowComposeSceneLayer( } } - private val dialogFocusListener = object : WindowFocusListener { - override fun windowGainedFocus(e: WindowEvent?) = Unit - override fun windowLostFocus(e: WindowEvent?) { - // Use this as trigger of outside click - outsidePointerCallback?.invoke(PointerEventType.Press) - outsidePointerCallback?.invoke(PointerEventType.Release) - } - } - - private var _mediator: ComposeSceneMediator? = null - private var outsidePointerCallback: ((eventType: PointerEventType) -> Unit)? = null - - override var density: Density = density - set(value) { - field = value - _mediator?.onChangeDensity(value) - } - - override var layoutDirection: LayoutDirection = layoutDirection - set(value) { - field = value - _mediator?.onChangeLayoutDirection(value) - } + override var mediator: ComposeSceneMediator? = null override var focusable: Boolean = focusable set(value) { field = value - // TODO: Pass it to mediator/scene + dialog.isFocusable = value } override var boundsInWindow: IntRect = IntRect.Zero @@ -131,19 +103,16 @@ internal class WindowComposeSceneLayer( setDialogBounds(value) } - override var compositionLocalContext: CompositionLocalContext? - get() = _mediator?.compositionLocalContext - set(value) { _mediator?.compositionLocalContext = value } - override var scrimColor: Color? = null init { - _mediator = ComposeSceneMediator( + mediator = ComposeSceneMediator( container = container, windowContext = windowContext, exceptionHandler = { composeContainer.exceptionHandler?.onException(it) ?: throw it }, + eventFilter = eventFilter, coroutineContext = compositionContext.effectCoroutineContext, skiaLayerComponentFactory = ::createSkiaLayerComponent, composeSceneFactory = ::createComposeScene, @@ -155,7 +124,6 @@ internal class WindowComposeSceneLayer( dialog.location = getDialogLocation(0, 0) dialog.size = windowContainer.size dialog.isVisible = true - dialog.addWindowFocusListener(dialogFocusListener) // Track window position in addition to [onChangeWindowPosition] because [windowContainer] // might be not the same as real [window]. @@ -165,40 +133,16 @@ internal class WindowComposeSceneLayer( } override fun close() { + super.close() composeContainer.detachLayer(this) - _mediator?.dispose() - _mediator = null + mediator?.dispose() + mediator = null window.removeComponentListener(windowPositionListener) - dialog.removeWindowFocusListener(dialogFocusListener) dialog.dispose() } - override fun setContent(content: @Composable () -> Unit) { - _mediator?.setContent(content) - } - - override fun setKeyEventListener( - onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, - onKeyEvent: ((KeyEvent) -> Boolean)? - ) { - _mediator?.setKeyEventListeners( - onPreviewKeyEvent = onPreviewKeyEvent ?: { false }, - onKeyEvent = onKeyEvent ?: { false } - ) - } - - override fun setOutsidePointerEventListener( - onOutsidePointerEvent: ((eventType: PointerEventType) -> Unit)? - ) { - outsidePointerCallback = onOutsidePointerEvent - } - - override fun calculateLocalPosition(positionInWindow: IntOffset): IntOffset { - return positionInWindow - } - override fun onChangeWindowPosition() { val scaledRectangle = boundsInWindow.toAwtRectangle(density) dialog.location = getDialogLocation(scaledRectangle.x, scaledRectangle.y) @@ -208,23 +152,37 @@ internal class WindowComposeSceneLayer( windowContext.setContainerSize(windowContainer.sizeInPx) // Update compose constrains based on main window size - _mediator?.sceneBoundsInPx = Rect( + mediator?.sceneBoundsInPx = Rect( offset = -boundsInWindow.topLeft.toOffset(), size = windowContainer.sizeInPx ) } - override fun onRenderOverlay(canvas: Canvas, width: Int, height: Int) { + override fun onLayersChange() { + // Force redraw because rendering depends on other layers + // see [onRenderOverlay] + dialog.repaint() + } + + override fun onRenderOverlay(canvas: Canvas, width: Int, height: Int, transparent: Boolean) { val scrimColor = scrimColor ?: return - val paint = Paint().apply { color = scrimColor }.asFrameworkPaint() - canvas.drawRect(org.jetbrains.skia.Rect.makeWH(width.toFloat(), height.toFloat()), paint) + val paint = Paint().apply { + color = scrimColor + blendMode = getDialogScrimBlendMode(transparent) + }.asFrameworkPaint() + canvas.drawRect(SkRect.makeWH(width.toFloat(), height.toFloat()), paint) } private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent { + val skikoView = OverlaySkikoViewDecorator(mediator) { canvas, width, height -> + composeContainer.layersAbove(this).forEach { + it.onRenderOverlay(canvas, width, height, transparent) + } + } return WindowSkiaLayerComponent( mediator = mediator, windowContext = windowContext, - skikoView = mediator, + skikoView = skikoView, skiaLayerAnalytics = skiaLayerAnalytics ) } @@ -255,22 +213,10 @@ internal class WindowComposeSceneLayer( val scaledRectangle = bounds.toAwtRectangle(density) dialog.location = getDialogLocation(scaledRectangle.x, scaledRectangle.y) dialog.setSize(scaledRectangle.width, scaledRectangle.height) - _mediator?.contentComponent?.setSize(scaledRectangle.width, scaledRectangle.height) - _mediator?.sceneBoundsInPx = Rect( + mediator?.contentComponent?.setSize(scaledRectangle.width, scaledRectangle.height) + mediator?.sceneBoundsInPx = Rect( offset = -bounds.topLeft.toOffset(), size = windowContainer.sizeInPx ) } } - -private fun IntRect.toAwtRectangle(density: Density): Rectangle { - val left = floor(left / density.density).toInt() - val top = floor(top / density.density).toInt() - val right = ceil(right / density.density).toInt() - val bottom = ceil(bottom / density.density).toInt() - val width = right - left - val height = bottom - top - return Rectangle( - left, top, width, height - ) -} diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/MultiLayerComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/MultiLayerComposeScene.skiko.kt index b594ea447a4d9..fd0132d8b56b8 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/MultiLayerComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/MultiLayerComposeScene.skiko.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.KeyEvent @@ -56,6 +55,7 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachReversed +import androidx.compose.ui.window.getDialogScrimBlendMode import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers @@ -530,20 +530,14 @@ private class MultiLayerComposeSceneImpl( invalidateIfNeeded() } - private val dialogScrimBlendMode - get() = if (composeSceneContext.platformContext.isWindowTransparent) { - // Use background alpha channel to respect transparent window shape. - BlendMode.SrcAtop - } else { - BlendMode.SrcOver - } - private val background: Modifier get() = scrimColor?.let { Modifier.drawBehind { drawRect( color = it, - blendMode = dialogScrimBlendMode + blendMode = getDialogScrimBlendMode( + composeSceneContext.platformContext.isWindowTransparent + ) ) } } ?: Modifier 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 index cb0c238f782a5..6fac30dca6358 100644 --- 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 @@ -21,6 +21,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent @@ -251,3 +252,11 @@ private fun rememberDialogMeasurePolicy( private fun KeyEvent.isDismissRequest() = type == KeyEventType.KeyDown && key == Key.Escape + +internal fun getDialogScrimBlendMode(isWindowTransparent: Boolean) = + if (isWindowTransparent) { + // Use background alpha channel to respect transparent window shape. + BlendMode.SrcAtop + } else { + BlendMode.SrcOver + }