diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.nonAndroid.kt index 5340e6772d33f..1c51a286f2a0a 100644 --- a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.nonAndroid.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.nonAndroid.kt @@ -19,6 +19,7 @@ package androidx.compose.ui.graphics +import androidx.compose.runtime.InternalComposeApi import androidx.compose.ui.InternalComposeUiApi import kotlin.jvm.JvmName import androidx.compose.ui.geometry.Offset @@ -35,12 +36,13 @@ import org.jetbrains.skia.Image import org.jetbrains.skia.Matrix44 import org.jetbrains.skia.MipmapMode import org.jetbrains.skia.SamplingMode +import org.jetbrains.skia.Surface import org.jetbrains.skia.impl.use /** * Convert the [org.jetbrains.skia.Canvas] instance into a Compose-compatible Canvas */ -fun SkCanvas.asComposeCanvas(): Canvas = SkiaBackedCanvas(this) +fun SkCanvas.asComposeCanvas(): Canvas = SkiaBackedCanvas().apply { internalSkiaCanvas = this@asComposeCanvas } /** * Provides access to the underlying [org.jetbrains.skia.Canvas] instance. @@ -62,9 +64,31 @@ val Canvas.skiaCanvas: SkCanvas val Canvas.nativeCanvas: SkCanvas get() = skiaCanvas -internal class SkiaBackedCanvas( - internal val internalSkiaCanvas: SkCanvas, -) : Canvas { +// Stub canvas instance used to keep the internal canvas parameter non-null during its +// scoped usage and prevent unnecessary byte code null checks from being generated +private val EmptyCanvas = Surface.makeNull(1,1).canvas + +/** + * Holder class that is used to issue scoped calls to a [Canvas] + * without having to allocate a SkiaBackedCanvas on each draw call. + */ +@InternalComposeApi +class SkiaCanvasHolder(skiaCanvas : SkCanvas? = null) { + internal val skiaBackedCanvas = SkiaBackedCanvas().apply { internalSkiaCanvas = skiaCanvas ?: EmptyCanvas } + + internal inline fun drawInto(targetCanvas: SkCanvas, crossinline block: Canvas.() -> Unit) { + val previousCanvas = skiaBackedCanvas.internalSkiaCanvas + skiaBackedCanvas.internalSkiaCanvas = targetCanvas + skiaBackedCanvas.block() + skiaBackedCanvas.internalSkiaCanvas = previousCanvas + } +} + +internal class SkiaBackedCanvas : Canvas { + + // Keep the internal canvas as a var prevent having to allocate an AndroidCanvas + // instance on each draw call + internal var internalSkiaCanvas: SkCanvas = EmptyCanvas override fun save() { internalSkiaCanvas.save() } diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt index c99d18c48e6d2..ad150f7f6a37f 100644 --- a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.nonAndroid.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.RoundRect import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName +import org.jetbrains.skia.Matrix33 as SkMatrix3 import org.jetbrains.skia.Path as SkPath import org.jetbrains.skia.PathDirection import org.jetbrains.skia.PathBuilder @@ -93,6 +94,11 @@ internal class SkiaBackedPath( */ internal var isSkiaPathObserved = false + // Temporary value holders to reuse an object (not part of a state): + private var radii: FloatArray? = null + + private var mMatrix: SkMatrix3? = null + private inline fun mutatePath(block: PathBuilder.() -> Unit) { synchronizeBuilderIfNeeded() pathBuilder.apply(block) @@ -301,42 +307,29 @@ internal class SkiaBackedPath( replaceWith = ReplaceWith("addRoundRect(roundRect)"), level = DeprecationLevel.HIDDEN ) - override fun addRoundRect(roundRect: RoundRect) = mutatePath { - addRRect( - roundRect.left, - roundRect.top, - roundRect.right, - roundRect.bottom, - floatArrayOf( - roundRect.topLeftCornerRadius.x, - roundRect.topLeftCornerRadius.y, - roundRect.topRightCornerRadius.x, - roundRect.topRightCornerRadius.y, - roundRect.bottomRightCornerRadius.x, - roundRect.bottomRightCornerRadius.y, - roundRect.bottomLeftCornerRadius.x, - roundRect.bottomLeftCornerRadius.y - ), - PathDirection.COUNTER_CLOCKWISE - ) - } + override fun addRoundRect(roundRect: RoundRect) = addRoundRect(roundRect) override fun addRoundRect(roundRect: RoundRect, direction: Path.Direction) = mutatePath { + if (radii == null) radii = FloatArray(8) + with(radii!!) { + this[0] = roundRect.topLeftCornerRadius.x + this[1] = roundRect.topLeftCornerRadius.y + + this[2] = roundRect.topRightCornerRadius.x + this[3] = roundRect.topRightCornerRadius.y + + this[4] = roundRect.bottomRightCornerRadius.x + this[5] = roundRect.bottomRightCornerRadius.y + + this[6] = roundRect.bottomLeftCornerRadius.x + this[7] = roundRect.bottomLeftCornerRadius.y + } addRRect( roundRect.left, roundRect.top, roundRect.right, roundRect.bottom, - floatArrayOf( - roundRect.topLeftCornerRadius.x, - roundRect.topLeftCornerRadius.y, - roundRect.topRightCornerRadius.x, - roundRect.topRightCornerRadius.y, - roundRect.bottomRightCornerRadius.x, - roundRect.bottomRightCornerRadius.y, - roundRect.bottomLeftCornerRadius.x, - roundRect.bottomLeftCornerRadius.y - ), + radii!!, direction.toSkiaPathDirection() ) } @@ -376,7 +369,9 @@ internal class SkiaBackedPath( } override fun transform(matrix: Matrix) = mutatePath { - transform(identityMatrix33().apply { setFrom(matrix) }) + if (mMatrix == null) mMatrix = identityMatrix33() + mMatrix!!.setFrom(matrix) + transform(mMatrix!!) } override fun getBounds(): Rect { diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphics.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphics.nonAndroid.kt index 7c430a7448a30..875e1cadfbabd 100644 --- a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphics.nonAndroid.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/SkikoGraphics.nonAndroid.kt @@ -291,6 +291,12 @@ private fun List.toSkiaGradient( ), ) +/** + * This overload avoids iterator allocation in comparison with its stdlib counterpart + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun List.toFloatArray(): FloatArray = FloatArray(size) { i -> this[i] } + private fun List.toColor4fArray(): Array = Array(size) { i -> val color = this[i] diff --git a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt index 4884f69ecdc6b..aa8fbd5d8111a 100644 --- a/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt +++ b/compose/ui/ui-skiko/src/nonAndroidMain/kotlin/androidx/compose/ui/graphics/layer/SkikoGraphicsLayer.nonAndroid.kt @@ -16,6 +16,7 @@ package androidx.compose.ui.graphics.layer +import androidx.compose.runtime.InternalComposeApi import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.isUnspecified @@ -26,8 +27,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.RenderEffect -import androidx.compose.ui.graphics.SkiaBackedCanvas -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.graphics.asSkiaColorFilter import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope @@ -68,6 +68,12 @@ internal class SkikoGraphicsLayer( private var cachedOutline: Outline? = null private var cachedClip: Boolean = false + // Temporary value holders to reuse an object (not part of a state): + private var radii: FloatArray? = null + + @OptIn(InternalComposeApi::class) + private val canvasHolder : SkiaCanvasHolder = SkiaCanvasHolder() + override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto set(value) { field = value @@ -204,23 +210,30 @@ internal class SkikoGraphicsLayer( outline.rect.bottom + dy, antiAlias = true ) - is Outline.Rounded -> renderNode.setClipRRect( - outline.roundRect.left + dx, - outline.roundRect.top + dy, - outline.roundRect.right + dx, - outline.roundRect.bottom + dy, - floatArrayOf( - outline.roundRect.topLeftCornerRadius.x, - outline.roundRect.topLeftCornerRadius.y, - outline.roundRect.topRightCornerRadius.x, - outline.roundRect.topRightCornerRadius.y, - outline.roundRect.bottomRightCornerRadius.x, - outline.roundRect.bottomRightCornerRadius.y, - outline.roundRect.bottomLeftCornerRadius.x, - outline.roundRect.bottomLeftCornerRadius.y - ), - antiAlias = true - ) + is Outline.Rounded -> { + if (radii == null) radii = FloatArray(8) + with(radii!!) { + this[0] = outline.roundRect.topLeftCornerRadius.x + this[1] = outline.roundRect.topLeftCornerRadius.y + + this[2] = outline.roundRect.topRightCornerRadius.x + this[3] = outline.roundRect.topRightCornerRadius.y + + this[4] = outline.roundRect.bottomRightCornerRadius.x + this[5] = outline.roundRect.bottomRightCornerRadius.y + + this[6] = outline.roundRect.bottomLeftCornerRadius.x + this[7] = outline.roundRect.bottomLeftCornerRadius.y + } + renderNode.setClipRRect( + outline.roundRect.left, + outline.roundRect.top, + outline.roundRect.right, + outline.roundRect.bottom, + radii!!, + antiAlias = true + ) + } is Outline.Generic -> renderNode.setClipPath( updatePathOutline(outline.path), antiAlias = true @@ -236,6 +249,7 @@ internal class SkikoGraphicsLayer( path }.materializeSkiaPath() + @OptIn(InternalComposeApi::class) override fun record( density: Density, layoutDirection: LayoutDirection, @@ -245,21 +259,23 @@ internal class SkikoGraphicsLayer( val renderNode = renderNode ?: return val recordingCanvas = renderNode.beginRecording() try { - val composeCanvas = recordingCanvas.asComposeCanvas() as SkiaBackedCanvas - if (outsetLeft > 0 || outsetTop > 0) { - composeCanvas.save() - composeCanvas.translate(outsetLeft.toFloat(), outsetTop.toFloat()) - } - pictureDrawScope.draw( - density = density, - layoutDirection = layoutDirection, - canvas = composeCanvas, - size = layer.size.toSize(), - graphicsLayer = layer, - block = block, - ) - if (outsetLeft > 0 || outsetTop > 0) { - composeCanvas.restore() + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(recordingCanvas) { + if (outsetLeft > 0 || outsetTop > 0) { + save() + translate(outsetLeft.toFloat(), outsetTop.toFloat()) + } + pictureDrawScope.draw( + density = density, + layoutDirection = layoutDirection, + canvas = this, + size = layer.size.toSize(), + graphicsLayer = layer, + block = block, + ) + if (outsetLeft > 0 || outsetTop > 0) { + restore() + } } } finally { renderNode.endRecording() diff --git a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt index 2923cb752090e..104f849d601d0 100644 --- a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt +++ b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skiko.kt @@ -17,13 +17,14 @@ package androidx.compose.ui.test import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.node.RootForTest @@ -227,6 +228,9 @@ open class SkikoComposeUiTest @InternalTestApi constructor( ) private val surface = Surface.makeRasterN32Premul(width, height) + + @OptIn(InternalComposeApi::class) + private val canvasHolder : SkiaCanvasHolder = SkiaCanvasHolder(surface.canvas) private val size = IntSize(width, height) @InternalComposeUiApi @@ -325,11 +329,13 @@ open class SkikoComposeUiTest @InternalTestApi constructor( * so a capture reflects the latest state. Draw is decoupled from idle, so producing * an up-to-date image is the capture's responsibility rather than the idle loop's. */ + @OptIn(InternalComposeApi::class) private fun redraw() = runOnUiThread { scene.measureAndLayout() with(surface.canvas) { clear(Color.TRANSPARENT) - scene.draw(asComposeCanvas()) + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(this) { scene.draw(this) } } } 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 344bb976970cf..279e81df41909 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 @@ -18,6 +18,7 @@ package androidx.compose.ui.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalContext +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.mutableStateSetOf import androidx.compose.ui.ComposeFeatureFlags import androidx.compose.ui.ComposeUiFlags @@ -32,7 +33,8 @@ 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.asComposeCanvas +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.graphics.toAwtImage import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.input.InputModeManager @@ -193,6 +195,8 @@ internal class ComposeSceneMediator( val renderApi by skiaLayerComponent::renderApi val semanticsOwners: Collection by semanticsOwnerManager::semanticsOwners + @OptIn(InternalComposeApi::class) + private val canvasHolder: SkiaCanvasHolder = SkiaCanvasHolder() /** * @see ComposeFeatureFlags.useInteropBlending */ @@ -739,13 +743,14 @@ internal class ComposeSceneMediator( interopContainer.postponingExecutingScheduledUpdates { canvas.withSceneOffset { with(sceneRenderingScope) { - scene.render(frameRecomposer, asComposeCanvas(), nanoTime) + scene.render(frameRecomposer, this@withSceneOffset, nanoTime) } } } } - private inline fun SkCanvas.withSceneOffset(crossinline block: SkCanvas.() -> Unit) { + @OptIn(InternalComposeApi::class) + private inline fun SkCanvas.withSceneOffset(crossinline block: Canvas.() -> Unit) { // Offset of scene relative to [container] val sceneBoundsOffset = sceneBoundsInPx?.topLeft ?: Offset.Zero // Offset of canvas relative to [container] @@ -756,7 +761,10 @@ internal class ComposeSceneMediator( val sceneOffset = sceneBoundsOffset - contentOffset save() translate(sceneOffset.x, sceneOffset.y) - block() + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(this){ + block(this@drawInto) + } restore() } @@ -960,7 +968,7 @@ internal class ComposeSceneMediator( target.drawScene(offsetX, offsetY, size, contentComponent.density) { fillBackground(contentComponent.background) if (!shouldPlaceInteropAbove) drawInterop(interopContainer.root) - drawCompose { canvas -> canvas.withSceneOffset { scene.draw(asComposeCanvas()) } } + drawCompose { canvas -> canvas.withSceneOffset { scene.draw(this) } } if (shouldPlaceInteropAbove) drawInterop(interopContainer.root) } } diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt index 7caf5c20f4212..98db9149bd96c 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt @@ -18,10 +18,11 @@ package androidx.compose.ui.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.LocalSystemTheme -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.navigationevent.UIKitNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.FrameRecomposer @@ -139,6 +140,9 @@ internal class ComposeContainer( private val focusedViewsList = FocusedViewsList() + @OptIn(InternalComposeApi::class) + private val canvasHolder = SkiaCanvasHolder() + val currentLifecycleState: Lifecycle.State get() = architectureComponentsOwner.lifecycle.currentState @@ -218,6 +222,7 @@ internal class ComposeContainer( systemThemeState.value = style.asComposeSystemTheme() } + @OptIn(InternalComposeApi::class) fun initializeComposeScene() { sceneJob = Job() val sceneCoroutineContext = coroutineContext + motionDurationScale + sceneJob @@ -230,7 +235,10 @@ internal class ComposeContainer( }, useSeparateRenderThreadWhenPossible = configuration.parallelRendering, render = { canvas, nanoTime -> - mediator?.render(canvas.asComposeCanvas(), nanoTime) + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(canvas) { + mediator?.render(this@drawInto, nanoTime) + } } ) metalView.canBeOpaque = configuration.opaque diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeLayersViewController.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeLayersViewController.ios.kt index f0eb37a112bc0..278f9ea5009cb 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeLayersViewController.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeLayersViewController.ios.kt @@ -16,8 +16,9 @@ package androidx.compose.ui.scene +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.withFrameNanos -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.uikit.addLayoutConstraintsToMatch import androidx.compose.ui.uikit.embedSubview @@ -87,6 +88,9 @@ internal class ComposeLayersViewController( ) } + @OptIn(InternalComposeApi::class) + private val canvasHolder = SkiaCanvasHolder() + init { coroutineContext.job.invokeOnCompletion { dispose() @@ -306,14 +310,16 @@ internal class ComposeLayersViewController( ) } + @OptIn(InternalComposeApi::class) private fun render(canvas: Canvas, nanoTime: Long) { - val composeCanvas = canvas.asComposeCanvas() - - // Some layers may be removed during rendering, because recomposition will happen in the - // process, so we need to make a temporary copy of the list - layersCache.withCopy { layers -> - layers.fastForEach { - it.render(composeCanvas, nanoTime) + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(canvas) { + // Some layers may be removed during rendering, because recomposition will happen in the + // process, so we need to make a temporary copy of the list + layersCache.withCopy { layers -> + layers.fastForEach { + it.render(this@drawInto, nanoTime) + } } } } diff --git a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt index 983f75f27ffc7..5e6de476d0e86 100644 --- a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt +++ b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/window/ComposeWindow.macos.kt @@ -19,8 +19,9 @@ package androidx.compose.ui.window import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.toComposeEvent import androidx.compose.ui.input.pointer.MacosCursor @@ -136,6 +137,10 @@ private class ComposeWindow( invalidateLayout = sceneRenderingScope::onSceneInvalidation, invalidateDraw = sceneRenderingScope::onSceneInvalidation, ) + + @OptIn(InternalComposeApi::class) + private val canvasHolder = SkiaCanvasHolder() + @OptIn(InternalComposeApi::class) private val renderDelegate = object : SkikoRenderDelegate { override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { val sizeInPx = IntSize(width, height) @@ -143,7 +148,10 @@ private class ComposeWindow( _windowInfo.containerDpSize = sizeInPx.toSize().toDpSize(scene.density) scene.size = sizeInPx // TODO: Move it out from onRender to avoid extra invalidation with(sceneRenderingScope) { - scene.render(frameRecomposer, canvas.asComposeCanvas(), nanoTime) + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(canvas) { + scene.render(frameRecomposer, this@drawInto, nanoTime) + } } } } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt index b47ed706426aa..c4522a18609b5 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ImageComposeScene.skiko.kt @@ -19,12 +19,13 @@ package androidx.compose.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateSetOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerButtons @@ -163,6 +164,9 @@ class ImageComposeScene @ExperimentalComposeUiApi constructor( containerDpSize = imageSize.toSize().toDpSize(density) } + @OptIn(InternalComposeApi::class) + private val canvasHolder = SkiaCanvasHolder(surface.canvas) + private val frameRecomposer = FrameRecomposer(coroutineContext) private val _platformContext = object : PlatformContext by PlatformContext.Empty(), @@ -290,11 +294,15 @@ class ImageComposeScene @ExperimentalComposeUiApi constructor( * Render the current content into an image. [nanoTime] will be used to drive all * animations in the content (or any other code, which uses [withFrameNanos] */ + @OptIn(InternalComposeApi::class) fun render(nanoTime: Long = 0): Image { surface.canvas.clear(Color.TRANSPARENT) frameRecomposer.performFrame(nanoTime) scene.measureAndLayout() - scene.draw(surface.canvas.asComposeCanvas()) + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(surface.canvas){ + scene.draw(this@drawInto) + } return surface.makeImageSnapshot() } diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt index 494c8ab0b2a9c..1e1ec84771e15 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.draganddrop.WebDragAndDropManager import androidx.compose.ui.events.EventTargetListener import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.SkiaCanvasHolder import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent @@ -379,10 +379,15 @@ internal class ComposeWindow( get() = configuration.isClearFocusOnMouseDownEnabled } + @InternalComposeApi + private val canvasHolder = SkiaCanvasHolder() private val skiaLayer: SkiaLayer = SkiaLayer().apply { renderDelegate = SkikoRenderDelegate { canvas, _, _, nanoTime -> - with(sceneRenderingScope) { - scene.render(frameRecomposer, canvas.asComposeCanvas(), nanoTime) + @Suppress("INVISIBLE_REFERENCE") + canvasHolder.drawInto(canvas) { + with(sceneRenderingScope) { + scene.render(frameRecomposer, this@drawInto, nanoTime) + } } } }