diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index b6f326b9aca5d..433734d2d921d 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -3134,6 +3134,7 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext { public fun calculateLocalPosition-MK-Hz9U (J)J public fun calculatePositionInWindow-MK-Hz9U (J)J public abstract fun getInputModeManager ()Landroidx/compose/ui/input/InputModeManager; + public fun getMeasureDrawLayerBounds ()Z public fun getParentFocusManager ()Landroidx/compose/ui/focus/FocusManager; public fun getRootForTestListener ()Landroidx/compose/ui/platform/PlatformContext$RootForTestListener; public fun getSemanticsOwnerListener ()Landroidx/compose/ui/platform/PlatformContext$SemanticsOwnerListener; diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt index f7d4ecee1489e..934abf68f918b 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt @@ -22,6 +22,13 @@ import androidx.compose.ui.window.Popup internal enum class LayerType { OnSameCanvas, OnComponent, + + /** + * TODO known issues: + * - [Rendering issues on Linux](https://github.com/JetBrains/compose-multiplatform/issues/4437) + * - [Blinking when showing](https://github.com/JetBrains/compose-multiplatform/issues/4475) + * - [Resizing the parent window clips the dialog](https://github.com/JetBrains/compose-multiplatform/issues/4484) + */ OnWindow; companion object { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt index e30313c4ea670..6156f7443414e 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt @@ -101,6 +101,11 @@ internal class ComposeSceneMediator( private var exceptionHandler: WindowExceptionHandler?, private val eventListener: AwtEventListener = OnlyValidPrimaryMouseButtonFilter, + /** + * @see PlatformContext.measureDrawLayerBounds + */ + private val measureDrawLayerBounds: Boolean = false, + val coroutineContext: CoroutineContext, skiaLayerComponentFactory: (ComposeSceneMediator) -> SkiaLayerComponent, @@ -625,6 +630,7 @@ internal class ComposeSceneMediator( override fun calculateLocalPosition(positionInWindow: Offset): Offset = windowContext.calculateLocalPosition(container, positionInWindow) + override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration() override val textInputService: PlatformTextInputService = this@ComposeSceneMediator.textInputService 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 22ebd9359adaf..5cb0c5f65e43a 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 @@ -24,14 +24,21 @@ import androidx.compose.ui.awt.AwtEventListeners import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter import androidx.compose.ui.awt.toAwtRectangle import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.skiko.RecordDrawRectSkikoViewDecorator 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.roundToIntRect import androidx.compose.ui.util.fastForEachReversed +import java.awt.Rectangle import java.awt.event.KeyEvent import java.awt.event.MouseEvent import javax.swing.SwingUtilities +import kotlin.math.max +import kotlin.math.min import org.jetbrains.skia.Canvas +import org.jetbrains.skiko.SkikoView /** * Represents an abstract class for a desktop Compose scene layer. @@ -49,11 +56,25 @@ internal abstract class DesktopComposeSceneLayer( protected val eventListener get() = AwtEventListeners( OnlyValidPrimaryMouseButtonFilter, DetectEventOutsideLayer(), + boundsEventFilter, FocusableLayerEventFilter() ) + private val boundsEventFilter = BoundsEventFilter( + bounds = Rectangle(windowContainer.size) + ) protected abstract val mediator: ComposeSceneMediator? + /** + * Bounds of real drawings based on previous renders. + */ + protected var drawBounds = IntRect.Zero + + /** + * The maximum amount to inflate the [drawBounds] comparing to [boundsInWindow]. + */ + private var maxDrawInflate = IntRect.Zero + private var outsidePointerCallback: ((eventType: PointerEventType) -> Unit)? = null private var isClosed = false @@ -69,6 +90,13 @@ internal abstract class DesktopComposeSceneLayer( mediator?.onChangeLayoutDirection(value) } + // It shouldn't be used for setting canvas size - it will crop drawings outside + override var boundsInWindow: IntRect = IntRect.Zero + set(value) { + field = value + boundsEventFilter.bounds = value.toAwtRectangle(density) + } + final override var compositionLocalContext: CompositionLocalContext? get() = mediator?.compositionLocalContext set(value) { mediator?.compositionLocalContext = value } @@ -101,6 +129,20 @@ internal abstract class DesktopComposeSceneLayer( override fun calculateLocalPosition(positionInWindow: IntOffset) = positionInWindow // [ComposeScene] is equal to [windowContainer] for the layer. + protected fun recordDrawBounds(skikoView: SkikoView) = + RecordDrawRectSkikoViewDecorator(skikoView) { canvasBoundsInPx -> + val currentCanvasOffset = drawBounds.topLeft + val drawBoundsInWindow = canvasBoundsInPx.roundToIntRect().translate(currentCanvasOffset) + maxDrawInflate = maxInflate(boundsInWindow, drawBoundsInWindow, maxDrawInflate) + drawBounds = IntRect( + left = boundsInWindow.left - maxDrawInflate.left, + top = boundsInWindow.top - maxDrawInflate.top, + right = boundsInWindow.right + maxDrawInflate.right, + bottom = boundsInWindow.bottom + maxDrawInflate.bottom + ) + onUpdateBounds() + } + /** * Called when the focus of the window containing main Compose view has changed. */ @@ -125,6 +167,12 @@ internal abstract class DesktopComposeSceneLayer( open fun onLayersChange() { } + /** + * Called when bounds of the layer has been updated. + */ + open fun onUpdateBounds() { + } + /** * Renders an overlay on the canvas. * @@ -185,7 +233,44 @@ internal abstract class DesktopComposeSceneLayer( override fun onMouseEvent(event: MouseEvent): Boolean = !noFocusableLayersAbove override fun onKeyEvent(event: KeyEvent): Boolean = !focusable || !noFocusableLayersAbove } + + private inner class BoundsEventFilter( + var bounds: Rectangle, + ) : AwtEventListener { + private val MouseEvent.isInBounds: Boolean + get() { + val localPoint = if (component != windowContainer) { + SwingUtilities.convertPoint(component, point, windowContainer) + } else { + point + } + return bounds.contains(localPoint) + } + + override fun onMouseEvent(event: MouseEvent): Boolean { + when (event.id) { + // Do not filter motion events + MouseEvent.MOUSE_MOVED, + MouseEvent.MOUSE_ENTERED, + MouseEvent.MOUSE_EXITED, + MouseEvent.MOUSE_DRAGGED -> return false + } + return if (event.isInBounds) { + false + } else { + onMouseEventOutside(event) + true + } + } + } } private fun MouseEvent.isMainAction() = button == MouseEvent.BUTTON1 + +private fun maxInflate(baseBounds: IntRect, currentBounds: IntRect, maxInflate: IntRect) = IntRect( + left = max(baseBounds.left - currentBounds.left, maxInflate.left), + top = max(baseBounds.top - currentBounds.top, maxInflate.top), + right = max(currentBounds.right - baseBounds.right, maxInflate.right), + bottom = max(currentBounds.bottom - baseBounds.bottom, maxInflate.bottom) +) 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 65a7643cb3492..0e38a525d2e51 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 @@ -19,14 +19,16 @@ package androidx.compose.ui.scene import androidx.compose.runtime.CompositionContext import androidx.compose.ui.awt.toAwtColor import androidx.compose.ui.awt.toAwtRectangle +import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Color 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.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.window.density +import androidx.compose.ui.window.sizeInPx import java.awt.Dimension import java.awt.Graphics import java.awt.event.MouseAdapter @@ -52,9 +54,7 @@ internal class SwingComposeSceneLayer( override fun addNotify() { super.addNotify() mediator?.onComponentAttached() - _boundsInWindow?.let { - mediator?.contentComponent?.bounds = it.toAwtRectangle(density) - } + onUpdateBounds() } override fun paint(g: Graphics) { @@ -71,10 +71,6 @@ internal class SwingComposeSceneLayer( it.background = Color.Transparent.toAwtColor() it.size = Dimension(windowContainer.width, windowContainer.height) it.addMouseListener(backgroundMouseListener) - - // TODO: Currently it works only with offscreen rendering - // TODO: Do not clip this from main scene if layersContainer == main container - windowContainer.add(it, JLayeredPane.POPUP_LAYER, 0) } private var containerSize = IntSize.Zero @@ -82,9 +78,7 @@ internal class SwingComposeSceneLayer( 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() } } @@ -97,18 +91,6 @@ internal class SwingComposeSceneLayer( container.isFocusable = value } - private var _boundsInWindow: IntRect? = null - override var boundsInWindow: IntRect - get() = _boundsInWindow ?: IntRect.Zero - set(value) { - _boundsInWindow = value - val localBounds = SwingUtilities.convertRectangle( - /* source = */ windowContainer, - /* aRectangle = */ value.toAwtRectangle(container.density), - /* destination = */ container) - mediator?.contentComponent?.bounds = localBounds - } - override var scrimColor: Color? = null set(value) { field = value @@ -117,6 +99,8 @@ internal class SwingComposeSceneLayer( } init { + val boundsInPx = windowContainer.sizeInPx.toRect() + drawBounds = boundsInPx.roundToIntRect() mediator = ComposeSceneMediator( container = container, windowContext = composeContainer.windowContext, @@ -124,6 +108,7 @@ internal class SwingComposeSceneLayer( composeContainer.exceptionHandler?.onException(it) ?: throw it }, eventListener = eventListener, + measureDrawLayerBounds = true, coroutineContext = compositionContext.effectCoroutineContext, skiaLayerComponentFactory = ::createSkiaLayerComponent, composeSceneFactory = ::createComposeScene, @@ -131,6 +116,11 @@ internal class SwingComposeSceneLayer( it.onChangeWindowTransparency(true) it.contentComponent.size = container.size } + + // TODO: Currently it works only with offscreen rendering + // TODO: Do not clip this from main scene if layersContainer == main container + windowContainer.add(container, JLayeredPane.POPUP_LAYER, 0) + composeContainer.attachLayer(this) } @@ -149,10 +139,20 @@ internal class SwingComposeSceneLayer( containerSize = IntSize(windowContainer.width, windowContainer.height) } + override fun onUpdateBounds() { + val scaledRectangle = drawBounds.toAwtRectangle(density) + val localBounds = SwingUtilities.convertRectangle( + /* source = */ windowContainer, + /* aRectangle = */ scaledRectangle, + /* destination = */ container) + mediator?.contentComponent?.bounds = localBounds + } + private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent { + val skikoView = recordDrawBounds(mediator) return SwingSkiaLayerComponent( mediator = mediator, - skikoView = mediator, + skikoView = skikoView, skiaLayerAnalytics = skiaLayerAnalytics ) } 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 f324008a48f07..300399fffdac5 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 @@ -30,8 +30,8 @@ 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.IntRect import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.toOffset import androidx.compose.ui.window.density import androidx.compose.ui.window.getDialogScrimBlendMode @@ -97,15 +97,11 @@ internal class WindowComposeSceneLayer( dialog.isFocusable = value } - override var boundsInWindow: IntRect = IntRect.Zero - set(value) { - field = value - setDialogBounds(value) - } - override var scrimColor: Color? = null init { + val boundsInPx = windowContainer.sizeInPx.toRect() + drawBounds = boundsInPx.roundToIntRect() mediator = ComposeSceneMediator( container = container, windowContext = windowContext, @@ -113,16 +109,17 @@ internal class WindowComposeSceneLayer( composeContainer.exceptionHandler?.onException(it) ?: throw it }, eventListener = eventListener, + measureDrawLayerBounds = true, coroutineContext = compositionContext.effectCoroutineContext, skiaLayerComponentFactory = ::createSkiaLayerComponent, composeSceneFactory = ::createComposeScene, ).also { it.onChangeWindowTransparency(true) - it.sceneBoundsInPx = windowContainer.sizeInPx.toRect() + it.sceneBoundsInPx = boundsInPx it.contentComponent.size = windowContainer.size } - dialog.location = getDialogLocation(0, 0) - dialog.size = windowContainer.size + onUpdateBounds() + dialog.isVisible = true // Track window position in addition to [onChangeWindowPosition] because [windowContainer] @@ -144,8 +141,8 @@ internal class WindowComposeSceneLayer( } override fun onChangeWindowPosition() { - val scaledRectangle = boundsInWindow.toAwtRectangle(density) - dialog.location = getDialogLocation(scaledRectangle.x, scaledRectangle.y) + val scaledRectangle = drawBounds.toAwtRectangle(density) + setDialogLocation(scaledRectangle.x, scaledRectangle.y) } override fun onChangeWindowSize() { @@ -153,7 +150,7 @@ internal class WindowComposeSceneLayer( // Update compose constrains based on main window size mediator?.sceneBoundsInPx = Rect( - offset = -boundsInWindow.topLeft.toOffset(), + offset = -drawBounds.topLeft.toOffset(), size = windowContainer.sizeInPx ) } @@ -164,6 +161,17 @@ internal class WindowComposeSceneLayer( dialog.repaint() } + override fun onUpdateBounds() { + val scaledRectangle = drawBounds.toAwtRectangle(density) + setDialogLocation(scaledRectangle.x, scaledRectangle.y) + dialog.setSize(scaledRectangle.width, scaledRectangle.height) + mediator?.contentComponent?.setSize(scaledRectangle.width, scaledRectangle.height) + mediator?.sceneBoundsInPx = Rect( + offset = -drawBounds.topLeft.toOffset(), + size = windowContainer.sizeInPx + ) + } + override fun onRenderOverlay(canvas: Canvas, width: Int, height: Int, transparent: Boolean) { val scrimColor = scrimColor ?: return val paint = Paint().apply { @@ -174,7 +182,9 @@ internal class WindowComposeSceneLayer( } private fun createSkiaLayerComponent(mediator: ComposeSceneMediator): SkiaLayerComponent { - val skikoView = OverlaySkikoViewDecorator(mediator) { canvas, width, height -> + val skikoView = OverlaySkikoViewDecorator( + recordDrawBounds(mediator) + ) { canvas, width, height -> composeContainer.layersAbove(this).forEach { it.onRenderOverlay(canvas, width, height, transparent) } @@ -201,22 +211,14 @@ internal class WindowComposeSceneLayer( ) } - private fun getDialogLocation(x: Int, y: Int): Point { + private fun setDialogLocation(x: Int, y: Int) { + if (!windowContainer.isShowing) { + return + } val locationOnScreen = windowContainer.locationOnScreen - return Point( + dialog.location = Point( locationOnScreen.x + x, locationOnScreen.y + y ) } - - private fun setDialogBounds(bounds: IntRect) { - 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( - offset = -bounds.topLeft.toOffset(), - size = windowContainer.sizeInPx - ) - } } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index 28e15504001bd..fb06553c89a17 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt @@ -364,12 +364,13 @@ internal class RootNodeOwner( drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit ) = RenderNodeLayer( - Snapshot.withoutReadObservation { + density = Snapshot.withoutReadObservation { // density is a mutable state that is observed whenever layer is created. the layer // is updated manually on draw, so not observing the density changes here helps with // performance in layout. density }, + measureDrawBounds = platformContext.measureDrawLayerBounds, invalidateParentLayer = { invalidateParentLayer() snapshotInvalidationTracker.requestDraw() diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index e69de242f7ac0..bddc6b841a98f 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.InputModeManager import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.node.LayoutNode +import androidx.compose.ui.node.OwnedLayer import androidx.compose.ui.node.Owner import androidx.compose.ui.node.RootForTest import androidx.compose.ui.scene.ComposeScene @@ -76,6 +77,14 @@ interface PlatformContext { fun calculateLocalPosition(positionInWindow: Offset): Offset = positionInWindow + /** + * Determines if [OwnedLayer] should measure bounds for all drawings. + * It's required to determine bounds of any graphics even if it was drawn out of measured + * layout bounds (for example shadows). It might be used to resize platform views based on + * such bounds. + */ + val measureDrawLayerBounds: Boolean get() = false + val viewConfiguration: ViewConfiguration get() = EmptyViewConfiguration val inputModeManager: InputModeManager val textInputService: PlatformTextInputService get() = EmptyPlatformTextInputService diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.skiko.kt index 82d5e4e30a1e1..87c3cdb9eafeb 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.skiko.kt @@ -16,19 +16,53 @@ package androidx.compose.ui.platform -import androidx.compose.ui.geometry.* +import org.jetbrains.skia.Rect as SkRect +import androidx.compose.ui.geometry.MutableRect +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.* +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.DefaultCameraDistance +import androidx.compose.ui.graphics.DefaultShadowColor +import androidx.compose.ui.graphics.Fields +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.RenderEffect +import androidx.compose.ui.graphics.ReusableGraphicsLayerScope +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.alphaMultiplier +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.asSkiaPath +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.toSkiaRRect +import androidx.compose.ui.graphics.toSkiaRect import androidx.compose.ui.node.OwnedLayer -import androidx.compose.ui.unit.* +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize import kotlin.math.abs import kotlin.math.max -import org.jetbrains.skia.* +import org.jetbrains.skia.ClipMode +import org.jetbrains.skia.Picture +import org.jetbrains.skia.PictureRecorder +import org.jetbrains.skia.Point3 +import org.jetbrains.skia.RTreeFactory +import org.jetbrains.skia.ShadowUtils internal class RenderNodeLayer( private var density: Density, + measureDrawBounds: Boolean, private val invalidateParentLayer: () -> Unit, private val drawBlock: (Canvas) -> Unit, private val onDestroy: () -> Unit = {} @@ -45,6 +79,8 @@ internal class RenderNodeLayer( } private val pictureRecorder = PictureRecorder() + // Use factory for BBoxHierarchy to track real bounds of drawn content + private val bbhFactory = if (measureDrawBounds) RTreeFactory() else null private var picture: Picture? = null private var isDestroyed = false @@ -205,21 +241,8 @@ internal class RenderNodeLayer( if (picture == null) { val bounds = size.toSize().toRect() val pictureCanvas = pictureRecorder.beginRecording( - // The goal with selecting the size of the rectangle here is to avoid limiting the - // drawable area as much as possible. - // Due to https://partnerissuetracker.corp.google.com/issues/324465764 we have to - // leave room for scale between the values we specify here and Float.MAX_VALUE. - // The maximum possible scale that can be applied to the canvas will be - // Float.MAX_VALUE divided by the largest value below. - // 2^30 was chosen because it's big enough, leaves quite a lot of room between it - // and Float.MAX_VALUE, and also lets the width and height fit into int32 (just in - // case). - org.jetbrains.skia.Rect.makeLTRB( - l = -(1 shl 30).toFloat(), - t = -(1 shl 30).toFloat(), - r = ((1 shl 30)-1).toFloat(), - b = ((1 shl 30)-1).toFloat() - ) + bounds = if (clip) bounds.toSkiaRect() else PICTURE_BOUNDS, + bbh = if (clip) null else bbhFactory ) performDrawLayer(pictureCanvas.asComposeCanvas(), bounds) picture = pictureRecorder.finishRecordingAsPicture() @@ -330,3 +353,21 @@ internal class RenderNodeLayer( // Copy from Android's frameworks/base/libs/hwui/utils/MathUtils.h private const val NON_ZERO_EPSILON = 0.001f private inline fun Float.isZero(): Boolean = abs(this) <= NON_ZERO_EPSILON + +// The goal with selecting the size of the rectangle here is to avoid limiting the +// drawable area as much as possible. +// Due to https://partnerissuetracker.corp.google.com/issues/324465764 we have to +// leave room for scale between the values we specify here and Float.MAX_VALUE. +// The maximum possible scale that can be applied to the canvas will be +// Float.MAX_VALUE divided by the largest value below. +// 2^30 was chosen because it's big enough, leaves quite a lot of room between it +// and Float.MAX_VALUE, and also lets the width and height fit into int32 (just in +// case). +private const val PICTURE_MIN_VALUE = -(1 shl 30).toFloat() +private const val PICTURE_MAX_VALUE = ((1 shl 30)-1).toFloat() +private val PICTURE_BOUNDS = SkRect.makeLTRB( + l = PICTURE_MIN_VALUE, + t = PICTURE_MIN_VALUE, + r = PICTURE_MAX_VALUE, + b = PICTURE_MAX_VALUE +) diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/skiko/RecordDrawRectSkikoViewDecorator.desktop.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/skiko/RecordDrawRectSkikoViewDecorator.desktop.kt new file mode 100644 index 0000000000000..749815f86e688 --- /dev/null +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/skiko/RecordDrawRectSkikoViewDecorator.desktop.kt @@ -0,0 +1,89 @@ +/* + * 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.skiko + +import org.jetbrains.skia.Rect as SkRect +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.toComposeRect +import org.jetbrains.skia.Canvas +import org.jetbrains.skia.Picture +import org.jetbrains.skia.PictureRecorder +import org.jetbrains.skia.RTreeFactory +import org.jetbrains.skiko.SkikoView + +internal class RecordDrawRectSkikoViewDecorator( + private val decorated: SkikoView, + private val onDrawRectChange: (Rect) -> Unit +) : SkikoView by decorated { + private val pictureRecorder = PictureRecorder() + private val bbhFactory = RTreeFactory() + private var drawRect = Rect.Zero + private set(value) { + if (value != field) { + field = value + onDrawRectChange(value) + } + } + + fun close() { + pictureRecorder.close() + bbhFactory.close() + } + + override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { + drawRect = canvas.recordCullRect { + decorated.onRender(it, width, height, nanoTime) + }?.toComposeRect() ?: Rect.Zero + } + + private inline fun Canvas.recordCullRect( + block: (Canvas) -> Unit + ): SkRect? { + val pictureCanvas = pictureRecorder.beginRecording(PICTURE_BOUNDS, bbhFactory) + pictureCanvas.translate(MEASURE_OFFSET, MEASURE_OFFSET) + block(pictureCanvas) + val picture = pictureRecorder.finishRecordingAsPicture() + try { + save() + translate(-MEASURE_OFFSET, -MEASURE_OFFSET) + drawPicture(picture, null, null) + restore() + return if (!picture.cullRect.isEmpty) { + picture.cullRect.offset(-MEASURE_OFFSET, -MEASURE_OFFSET) + } else { + // It means that there ware no drawings. + // Applying our offset is incorrect in this case. + null + } + } finally { + picture.close() + } + } +} + +/** + * Skia cannot return negative values in [Picture.cullRect], + * so temporary applying some offset is required to get right measurement in negative area. + */ +private const val MEASURE_OFFSET = (1 shl 14).toFloat() + +private val PICTURE_BOUNDS = SkRect.makeLTRB( + l = Float.MIN_VALUE, + t = Float.MIN_VALUE, + r = Float.MAX_VALUE, + b = Float.MAX_VALUE +) diff --git a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/RenderNodeLayerTest.kt b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/RenderNodeLayerTest.kt index dd36424dbcacd..a106c17d0a02d 100644 --- a/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/RenderNodeLayerTest.kt +++ b/compose/ui/ui/src/skikoTest/kotlin/androidx/compose/ui/platform/RenderNodeLayerTest.kt @@ -466,6 +466,7 @@ class RenderNodeLayerTest { private fun TestRenderNodeLayer() = RenderNodeLayer( Density(1f, 1f), + measureDrawBounds = false, invalidateParentLayer = {}, drawBlock = {} ) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 18add9d96f590..63f710fe8c966 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.interop.LocalUIKitInteropContainer import androidx.compose.ui.interop.LocalUIKitInteropContext import androidx.compose.ui.interop.UIKitInteropContainer import androidx.compose.ui.interop.UIKitInteropContext -import androidx.compose.ui.interop.UIKitInteropTransaction import androidx.compose.ui.node.TrackInteropContainer import androidx.compose.ui.platform.AccessibilityMediator import androidx.compose.ui.platform.AccessibilitySyncOptions @@ -71,11 +70,10 @@ import androidx.compose.ui.window.InteractionUIView import androidx.compose.ui.window.KeyboardEventHandler import androidx.compose.ui.window.KeyboardVisibilityListenerImpl import androidx.compose.ui.window.RenderingUIView +import androidx.compose.ui.window.SkikoRenderDelegate import androidx.compose.ui.window.UITouchesEventPhase import kotlin.coroutines.CoroutineContext -import kotlin.math.floor import kotlin.math.roundToInt -import kotlin.math.roundToLong import kotlinx.cinterop.CValue import kotlinx.cinterop.ObjCAction import kotlinx.cinterop.readValue @@ -94,7 +92,6 @@ import platform.CoreGraphics.CGSize import platform.Foundation.NSNotification import platform.Foundation.NSNotificationCenter import platform.Foundation.NSSelectorFromString -import platform.Foundation.NSTimeInterval import platform.QuartzCore.CATransaction import platform.UIKit.NSLayoutConstraint import platform.UIKit.UIEvent @@ -114,7 +111,7 @@ internal sealed interface SceneLayout { object Undefined : SceneLayout object UseConstraintsToFillContainer : SceneLayout class UseConstraintsToCenter(val size: CValue) : SceneLayout - class Bounds(val rect: IntRect) : SceneLayout + class Bounds(val renderBounds: IntRect, val interactionBounds: IntRect) : SceneLayout } /** @@ -175,19 +172,21 @@ private class SemanticsOwnerListenerImpl( } private class RenderingUIViewDelegateImpl( - private val interopContext: UIKitInteropContext, - private val getBoundsInPx: () -> IntRect, - private val scene: ComposeScene -) : RenderingUIView.Delegate { - override fun retrieveInteropTransaction(): UIKitInteropTransaction = - interopContext.retrieve() - - override fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) { - val composeCanvas = canvas.asComposeCanvas() - val topLeft = getBoundsInPx().topLeft.toOffset() - composeCanvas.translate(-topLeft.x, -topLeft.y) - scene.render(composeCanvas, targetTimestamp.toNanoSeconds()) - composeCanvas.translate(topLeft.x, topLeft.y) + private val scene: ComposeScene, + private val sceneOffset: () -> Offset, +) : SkikoRenderDelegate { + override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { + canvas.withSceneOffset { + scene.render(asComposeCanvas(), nanoTime) + } + } + + private inline fun Canvas.withSceneOffset(block: Canvas.() -> Unit) { + val sceneOffset = sceneOffset() + save() + translate(sceneOffset.x, sceneOffset.y) + block() + restore() } } @@ -225,8 +224,12 @@ internal class ComposeSceneMediator( private val configuration: ComposeUIViewControllerConfiguration, private val focusStack: FocusStack?, private val windowContext: PlatformWindowContext, + /** + * @see PlatformContext.measureDrawLayerBounds + */ + private val measureDrawLayerBounds: Boolean = false, val coroutineContext: CoroutineContext, - private val renderingUIViewFactory: (RenderingUIView.Delegate) -> RenderingUIView, + private val renderingUIViewFactory: (UIKitInteropContext, SkikoRenderDelegate) -> RenderingUIView, composeSceneFactory: ( invalidate: () -> Unit, platformContext: PlatformContext, @@ -269,7 +272,7 @@ internal class ComposeSceneMediator( private val focusManager get() = scene.focusManager private val renderingView by lazy { - renderingUIViewFactory(renderDelegate) + renderingUIViewFactory(interopContext, renderDelegate) } /** @@ -282,6 +285,11 @@ internal class ComposeSceneMediator( */ private val interopViewContainer = UIKitInteropContainer() + private val interactionBounds: IntRect get() { + val boundsLayout = _layout as? SceneLayout.Bounds + return boundsLayout?.interactionBounds ?: renderingViewBoundsInPx + } + private val interactionView by lazy { InteractionUIView( keyboardEventHandler = keyboardEventHandler, @@ -290,9 +298,11 @@ internal class ComposeSceneMediator( val needHighFrequencyPolling = count > 0 renderingView.redrawer.needsProactiveDisplayLink = needHighFrequencyPolling }, - checkBounds = { dpPoint: DpOffset -> - val point = dpPoint.toOffset(container.systemDensity) - getBoundsInPx().contains(point.round()) + inBounds = { point -> + val positionInContainer = point.useContents { + asDpOffset().toOffset(container.systemDensity).round() + } + interactionBounds.contains(positionInContainer) } ) } @@ -412,9 +422,8 @@ internal class ComposeSceneMediator( private val renderDelegate by lazy { RenderingUIViewDelegateImpl( - interopContext = interopContext, - getBoundsInPx = ::getBoundsInPx, - scene = scene + scene = scene, + sceneOffset = { -renderingViewBoundsInPx.topLeft.toOffset() } ) } @@ -461,6 +470,7 @@ internal class ComposeSceneMediator( NSLayoutConstraint.activateConstraints( getConstraintsToFillParent(interactionView, rootView) ) + // FIXME: interactionView might be smaller than renderingView (shadows etc) interactionView.addSubview(renderingView) } @@ -546,7 +556,7 @@ internal class ComposeSceneMediator( val density = container.systemDensity.density renderingView.translatesAutoresizingMaskIntoConstraints = true renderingView.setFrame( - with(value.rect) { + with(value.renderBounds) { CGRectMake( x = left.toDouble() / density, y = top.toDouble() / density, @@ -598,10 +608,8 @@ internal class ComposeSceneMediator( ) } - fun getBoundsInDp(): DpRect = renderingView.frame.useContents { this.asDpRect() } - - fun getBoundsInPx(): IntRect = with(container.systemDensity) { - getBoundsInDp().toRect().roundToIntRect() + private val renderingViewBoundsInPx: IntRect get() = with(container.systemDensity) { + renderingView.frame.useContents { asDpRect().toRect().roundToIntRect() } } fun viewWillTransitionToSize( @@ -708,11 +716,12 @@ internal class ComposeSceneMediator( override fun calculateLocalPosition(positionInWindow: Offset): Offset = windowContext.calculateLocalPosition(container, positionInWindow) - override val viewConfiguration = this@ComposeSceneMediator.viewConfiguration + override val measureDrawLayerBounds get() = this@ComposeSceneMediator.measureDrawLayerBounds + override val viewConfiguration get() = this@ComposeSceneMediator.viewConfiguration override val inputModeManager = DefaultInputModeManager(InputMode.Touch) - override val textInputService = this@ComposeSceneMediator.uiKitTextInputService - override val textToolbar = this@ComposeSceneMediator.uiKitTextInputService - override val semanticsOwnerListener = this@ComposeSceneMediator.semanticsOwnerListener + override val textInputService get() = this@ComposeSceneMediator.uiKitTextInputService + override val textToolbar get() = this@ComposeSceneMediator.uiKitTextInputService + override val semanticsOwnerListener get() = this@ComposeSceneMediator.semanticsOwnerListener } } @@ -779,14 +788,3 @@ private fun UITouch.offsetInView(view: UIView, density: Float): Offset = locationInView(view).useContents { Offset(x.toFloat() * density, y.toFloat() * density) } - -private fun NSTimeInterval.toNanoSeconds(): Long { - // The calculation is split in two instead of - // `(targetTimestamp * 1e9).toLong()` - // to avoid losing precision for fractional part - val integral = floor(this) - val fractional = this - integral - val secondsToNanos = 1_000_000_000L - val nanos = integral.roundToLong() * secondsToNanos + (fractional * 1e9).roundToLong() - return nanos -} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt index dc651e848ff20..2a19ab9a3d09f 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt @@ -22,8 +22,10 @@ import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.interop.UIKitInteropContext import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformWindowContext +import androidx.compose.ui.skiko.RecordDrawRectSkikoViewDecorator import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration import androidx.compose.ui.uikit.toUIColor import androidx.compose.ui.unit.Density @@ -32,12 +34,16 @@ import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.asDpOffset import androidx.compose.ui.unit.round +import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.toOffset import androidx.compose.ui.window.ComposeContainer import androidx.compose.ui.window.FocusStack import androidx.compose.ui.window.ProvideContainerCompositionLocals import androidx.compose.ui.window.RenderingUIView +import androidx.compose.ui.window.SkikoRenderDelegate import kotlin.coroutines.CoroutineContext +import kotlin.math.max +import kotlin.math.min import kotlinx.cinterop.CValue import kotlinx.cinterop.readValue import kotlinx.cinterop.useContents @@ -66,7 +72,6 @@ internal class UIViewComposeSceneLayer( private val backgroundView: UIView = object : UIView( frame = CGRectZero.readValue() ) { - private var previousSuccessHitTestTimestamp: Double? = null private fun touchStartedOutside(withEvent: UIEvent?) { @@ -94,10 +99,10 @@ internal class UIViewComposeSceneLayer( } override fun hitTest(point: CValue, withEvent: UIEvent?): UIView? { - if ( - mediator.hitTestInteractionView(point, withEvent) == null && - super.hitTest(point, withEvent) == this - ) { + val positionInWindow = point.useContents { asDpOffset().toOffset(density).round() } + val inBounds = mediator.hitTestInteractionView(point, withEvent) != null && + boundsInWindow.contains(positionInWindow) // canvas might be bigger than logical bounds + if (!inBounds && super.hitTest(point, withEvent) == this) { touchStartedOutside(withEvent) if (focusable) { // Focusable layers don't pass touches through, even if it's out of bounds. @@ -114,12 +119,23 @@ internal class UIViewComposeSceneLayer( configuration = configuration, focusStack = focusStack, windowContext = windowContext, + measureDrawLayerBounds = true, coroutineContext = compositionContext.effectCoroutineContext, renderingUIViewFactory = ::createSkikoUIView, composeSceneFactory = ::createComposeScene ) } + /** + * Bounds of real drawings based on previous renders. + */ + private var drawBounds = IntRect.Zero + + /** + * The maximum amount to inflate the [drawBounds] comparing to [boundsInWindow]. + */ + private var maxDrawInflate = IntRect.Zero + init { backgroundView.translatesAutoresizingMaskIntoConstraints = false rootView.addSubview(backgroundView) @@ -129,8 +145,11 @@ internal class UIViewComposeSceneLayer( composeContainer.attachLayer(this) } - private fun createSkikoUIView(renderDelegate: RenderingUIView.Delegate): RenderingUIView = - RenderingUIView(renderDelegate = renderDelegate).apply { + private fun createSkikoUIView(interopContext: UIKitInteropContext, renderDelegate: SkikoRenderDelegate): RenderingUIView = + RenderingUIView( + interopContext = interopContext, + renderDelegate = recordDrawBounds(renderDelegate) + ).apply { opaque = false } @@ -149,19 +168,14 @@ internal class UIViewComposeSceneLayer( override var density by mediator::density override var layoutDirection by mediator::layoutDirection - - override var boundsInWindow: IntRect - get() = mediator.getBoundsInPx() + override var boundsInWindow: IntRect = IntRect.Zero set(value) { - mediator.setLayout( - SceneLayout.Bounds(rect = value) - ) + field = value + updateBounds() } - override var compositionLocalContext: CompositionLocalContext? by mediator::compositionLocalContext override var scrimColor: Color? = null - get() = field set(value) { field = value backgroundView.setBackgroundColor(value?.toUIColor()) @@ -198,6 +212,29 @@ internal class UIViewComposeSceneLayer( return positionInWindow } + private fun recordDrawBounds(renderDelegate: SkikoRenderDelegate) = + RecordDrawRectSkikoViewDecorator(renderDelegate) { canvasBoundsInPx -> + val currentCanvasOffset = drawBounds.topLeft + val drawBoundsInWindow = canvasBoundsInPx.roundToIntRect().translate(currentCanvasOffset) + maxDrawInflate = maxInflate(boundsInWindow, drawBoundsInWindow, maxDrawInflate) + drawBounds = IntRect( + left = boundsInWindow.left - maxDrawInflate.left, + top = boundsInWindow.top - maxDrawInflate.top, + right = boundsInWindow.right + maxDrawInflate.right, + bottom = boundsInWindow.bottom + maxDrawInflate.bottom + ) + updateBounds() + } + + private fun updateBounds() { + mediator.setLayout( + SceneLayout.Bounds( + renderBounds = drawBounds, + interactionBounds = boundsInWindow + ) + ) + } + fun viewDidAppear(animated: Boolean) { mediator.viewDidAppear(animated) } @@ -220,5 +257,11 @@ internal class UIViewComposeSceneLayer( ) { mediator.viewWillTransitionToSize(targetSize, coordinator) } - } + +private fun maxInflate(baseBounds: IntRect, currentBounds: IntRect, maxInflate: IntRect) = IntRect( + left = max(baseBounds.left - currentBounds.left, maxInflate.left), + top = max(baseBounds.top - currentBounds.top, maxInflate.top), + right = max(currentBounds.right - baseBounds.right, maxInflate.right), + bottom = max(currentBounds.bottom - baseBounds.bottom, maxInflate.bottom) +) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index c3488cac52a82..391554e562cba 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.LocalSystemTheme import androidx.compose.ui.SystemTheme import androidx.compose.ui.interop.LocalUIViewController +import androidx.compose.ui.interop.UIKitInteropContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformWindowContext @@ -287,8 +288,8 @@ internal class ComposeContainer( ComposeSceneContextImpl(platformContext) @OptIn(ExperimentalComposeApi::class) - private fun createSkikoUIView(renderRelegate: RenderingUIView.Delegate): RenderingUIView = - RenderingUIView(renderDelegate = renderRelegate).apply { + private fun createSkikoUIView(interopContext: UIKitInteropContext, renderRelegate: SkikoRenderDelegate): RenderingUIView = + RenderingUIView(interopContext, renderRelegate).apply { opaque = configuration.opaque } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/InteractionUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/InteractionUIView.uikit.kt index a639d7433c123..af4b717dbb7b4 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/InteractionUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/InteractionUIView.uikit.kt @@ -44,7 +44,7 @@ internal class InteractionUIView( private var keyboardEventHandler: KeyboardEventHandler, private var touchesDelegate: Delegate, private var updateTouchesCount: (count: Int) -> Unit, - private var checkBounds: (point: DpOffset) -> Boolean, + private var inBounds: (CValue) -> Boolean, ) : UIView(CGRectZero.readValue()) { interface Delegate { @@ -83,8 +83,7 @@ internal class InteractionUIView( * https://developer.apple.com/documentation/uikit/uiview/1622533-point */ override fun pointInside(point: CValue, withEvent: UIEvent?): Boolean { - val pointOffset = point.useContents { this.asDpOffset() } - return checkBounds(pointOffset) && touchesDelegate.pointInside(point, withEvent) + return inBounds(point) && touchesDelegate.pointInside(point, withEvent) } override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) { @@ -135,7 +134,7 @@ internal class InteractionUIView( override fun onTouchesEvent(view: UIView, event: UIEvent, phase: UITouchesEventPhase) {} } updateTouchesCount = {} - checkBounds = { false } + inBounds = { false } keyboardEventHandler = object: KeyboardEventHandler { override fun onKeyboardEvent(event: SkikoKeyboardEvent) {} } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt index 1c12b7f79d34e..63c40da81b01e 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/RenderingUIView.uikit.kt @@ -19,9 +19,14 @@ package androidx.compose.ui.window import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.interop.UIKitInteropContext import androidx.compose.ui.interop.UIKitInteropTransaction +import kotlin.math.floor +import kotlin.math.roundToLong import kotlinx.cinterop.* import org.jetbrains.skia.Canvas +import org.jetbrains.skiko.SkikoUIView +import org.jetbrains.skiko.SkikoView import platform.CoreGraphics.* import platform.Foundation.* import platform.Metal.MTLCreateSystemDefaultDevice @@ -30,8 +35,11 @@ import platform.Metal.MTLPixelFormatBGRA8Unorm import platform.QuartzCore.CAMetalLayer import platform.UIKit.* +internal typealias SkikoRenderDelegate = SkikoView + internal class RenderingUIView( - private val renderDelegate: Delegate, + private val interopContext: UIKitInteropContext, + private val renderDelegate: SkikoRenderDelegate, ) : UIView( frame = CGRectMake( x = 0.0, @@ -40,12 +48,6 @@ internal class RenderingUIView( height = 1.0 ) ) { - - interface Delegate { - fun retrieveInteropTransaction(): UIKitInteropTransaction - fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) - } - companion object : UIViewMeta() { override fun layerClass() = CAMetalLayer } @@ -58,15 +60,17 @@ internal class RenderingUIView( MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Metal is not supported on this system") private val metalLayer: CAMetalLayer get() = layer as CAMetalLayer + private var _width: CGFloat = 0.0 + private var _height: CGFloat = 0.0 internal val redrawer: MetalRedrawer = MetalRedrawer( metalLayer, callbacks = object : MetalRedrawerCallbacks { override fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) { - renderDelegate.render(canvas, targetTimestamp) + renderDelegate.onRender(canvas, _width.toInt(), _height.toInt(), targetTimestamp.toNanoSeconds()) } override fun retrieveInteropTransaction(): UIKitInteropTransaction = - renderDelegate.retrieveInteropTransaction() + interopContext.retrieve() } ) @@ -123,8 +127,9 @@ internal class RenderingUIView( if (window == null || CGRectIsEmpty(bounds)) { return } - val scaledSize = bounds.useContents { - CGSizeMake(size.width * contentScaleFactor, size.height * contentScaleFactor) + bounds.useContents { + _width = size.width * contentScaleFactor + _height = size.height * contentScaleFactor } // If drawableSize is zero in any dimension it means that it's a first layout @@ -134,7 +139,7 @@ internal class RenderingUIView( width == 0.0 || height == 0.0 } - metalLayer.drawableSize = scaledSize + metalLayer.drawableSize = CGSizeMake(_width, _height) if (needsSynchronousDraw) { redrawer.drawSynchronously() @@ -142,5 +147,15 @@ internal class RenderingUIView( } override fun canBecomeFirstResponder() = false +} +private fun NSTimeInterval.toNanoSeconds(): Long { + // The calculation is split in two instead of + // `(targetTimestamp * 1e9).toLong()` + // to avoid losing precision for fractional part + val integral = floor(this) + val fractional = this - integral + val secondsToNanos = 1_000_000_000L + val nanos = integral.roundToLong() * secondsToNanos + (fractional * 1e9).roundToLong() + return nanos }