Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ abstract class DesktopGraphicsTest {
protected fun initCanvas(widthPx: Int, heightPx: Int): Canvas {
require(_surface == null)
_surface = Surface.makeRasterN32Premul(widthPx, heightPx)
return SkiaBackedCanvas(_surface!!.canvas)
return _surface!!.canvas.asComposeCanvas()
}

@After
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.jetbrains.skia.Matrix44
import org.jetbrains.skia.MipmapMode
import org.jetbrains.skia.Paint as SkPaint
import org.jetbrains.skia.SamplingMode
import org.jetbrains.skia.Surface
import org.jetbrains.skia.impl.use

@Deprecated(
Expand All @@ -46,13 +47,15 @@ internal actual fun ActualCanvas(image: ImageBitmap): Canvas {
require(!skiaBitmap.isImmutable) {
"Cannot draw on immutable ImageBitmap"
}
return SkiaBackedCanvas(SkCanvas(skiaBitmap))
return SkiaBackedCanvas().apply {
internalSkiaCanvas = SkCanvas(skiaBitmap)
}
}

/**
* 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.
Expand All @@ -75,6 +78,26 @@ val Canvas.skiaCanvas: SkCanvas
val Canvas.nativeCanvas: NativeCanvas
get() = skiaCanvas


// 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] from the framework equivalent
* canvas without having to allocate an object on each draw call
*/
class CanvasHolder {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: a new public-stable API that we'll have to maintain forever

@PublishedApi internal val skiaBackedCanvas = SkiaBackedCanvas()

inline fun drawInto(targetCanvas: SkCanvas, block: Canvas.() -> Unit) {
val previousCanvas = skiaBackedCanvas.internalSkiaCanvas
skiaBackedCanvas.internalSkiaCanvas = targetCanvas
skiaBackedCanvas.block()
skiaBackedCanvas.internalSkiaCanvas = previousCanvas
}
}

// This was added for internal usage from old render layers (another submodule),
// but wasn't properly marked as internal. Keep it as deprecated for some time to be safe.
@InternalComposeApi
Expand All @@ -86,9 +109,12 @@ var Canvas.alphaMultiplier: Float
get() = (this as SkiaBackedCanvas).alphaMultiplier
set(value) { (this as SkiaBackedCanvas).alphaMultiplier = value }

internal class SkiaBackedCanvas(
internal val internalSkiaCanvas: SkCanvas,
) : Canvas {
@PublishedApi
internal class SkiaBackedCanvas : Canvas {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't supposed to be exposed. Again, requirement for binary compatibility in a place where we're not ready to do it


// Keep the internal canvas as a var prevent having to allocate an AndroidCanvas
// instance on each draw call
@PublishedApi internal var internalSkiaCanvas: SkCanvas = EmptyCanvas
internal var alphaMultiplier: Float = 1.0f

private fun Paint.asSkiaPaintWithAppliedAlphaMultiplier(): SkPaint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import org.jetbrains.skia.Matrix33
import org.jetbrains.skia.Path as SkPath
import org.jetbrains.skia.PathDirection
import org.jetbrains.skia.PathBuilder
Expand Down Expand Up @@ -89,6 +90,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: Matrix33? = null

private inline fun mutatePath(block: PathBuilder.() -> Unit) {
synchronizeBuilderIfNeeded()
pathBuilder.apply(block)
Expand Down Expand Up @@ -298,41 +304,30 @@ internal class SkiaBackedPath(
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
)
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()
)
}
Expand Down Expand Up @@ -372,7 +367,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ private fun List<Color>.toColor4fArray(): Array<Color4f> =
Color4f(color.red, color.green, color.blue, color.alpha)
}

private fun List<Float>.toFloatArray(): FloatArray = FloatArray(size) { i -> this[i] }

private fun validateColorStops(colors: List<Color>, colorStops: List<Float>?) {
if (colorStops == null) {
if (colors.size < 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
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.asSkiaColorFilter
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
Expand Down Expand Up @@ -70,6 +70,8 @@ actual class GraphicsLayer internal constructor(
private var parentLayerUsages = 0
private val childDependenciesTracker = ChildLayerDependenciesTracker()

private val canvasHolder : CanvasHolder = CanvasHolder()

actual var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
set(value) {
if (field != value) {
Expand Down Expand Up @@ -337,10 +339,11 @@ actual class GraphicsLayer internal constructor(
val renderNode = renderNode ?: return
val recordingCanvas = renderNode.beginRecording()
try {
val composeCanvas = recordingCanvas.asComposeCanvas() as SkiaBackedCanvas
childDependenciesTracker.withTracking(
onDependencyRemoved = { it.onRemovedFromParentLayer() },
) { block(composeCanvas) }
canvasHolder.drawInto(recordingCanvas) {
childDependenciesTracker.withTracking(
onDependencyRemoved = { it.onRemovedFromParentLayer() },
) { block(this@drawInto as SkiaBackedCanvas) }
}
} finally {
renderNode.endRecording()
}
Expand Down Expand Up @@ -368,6 +371,9 @@ actual class GraphicsLayer internal constructor(
discardContentIfReleasedAndHaveNoParentLayerUsages()
}

// Temporary value holders to reuse an object (not part of a state):
private var radii: FloatArray? = null

@OptIn(InternalComposeUiApi::class)
private fun configureOutlineAndClip() {
if (!outlineDirty) return
Expand All @@ -386,23 +392,30 @@ actual class GraphicsLayer internal constructor(
tmpOutline.rect.bottom,
antiAlias = true
)
is Outline.Rounded -> renderNode.setClipRRect(
tmpOutline.roundRect.left,
tmpOutline.roundRect.top,
tmpOutline.roundRect.right,
tmpOutline.roundRect.bottom,
floatArrayOf(
tmpOutline.roundRect.topLeftCornerRadius.x,
tmpOutline.roundRect.topLeftCornerRadius.y,
tmpOutline.roundRect.topRightCornerRadius.x,
tmpOutline.roundRect.topRightCornerRadius.y,
tmpOutline.roundRect.bottomRightCornerRadius.x,
tmpOutline.roundRect.bottomRightCornerRadius.y,
tmpOutline.roundRect.bottomLeftCornerRadius.x,
tmpOutline.roundRect.bottomLeftCornerRadius.y
),
antiAlias = true
)
is Outline.Rounded -> {
if (radii == null) radii = FloatArray(8)
with(radii!!) {
this[0] = tmpOutline.roundRect.topLeftCornerRadius.x
this[1] = tmpOutline.roundRect.topLeftCornerRadius.y

this[2] = tmpOutline.roundRect.topRightCornerRadius.x
this[3] = tmpOutline.roundRect.topRightCornerRadius.y

this[4] = tmpOutline.roundRect.bottomRightCornerRadius.x
this[5] = tmpOutline.roundRect.bottomRightCornerRadius.y

this[6] = tmpOutline.roundRect.bottomLeftCornerRadius.x
this[7] = tmpOutline.roundRect.bottomLeftCornerRadius.y
}
renderNode.setClipRRect(
tmpOutline.roundRect.left,
tmpOutline.roundRect.top,
tmpOutline.roundRect.right,
tmpOutline.roundRect.bottom,
radii!!,
antiAlias = true
)
}
is Outline.Generic -> renderNode.setClipPath(tmpOutline.path.materializeSkiaPath(), antiAlias = true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ 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.CanvasHolder
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.node.RootForTest
Expand Down Expand Up @@ -227,6 +227,9 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
)

private val surface = Surface.makeRasterN32Premul(width, height)

private val canvasHolder : CanvasHolder = CanvasHolder()

private val size = IntSize(width, height)

@InternalComposeUiApi
Expand Down Expand Up @@ -322,9 +325,10 @@ open class SkikoComposeUiTest @InternalTestApi constructor(
*/
private fun redraw() = runOnUiThread {
scene.measureAndLayout()
with(surface.canvas) {
clear(Color.TRANSPARENT)
scene.draw(asComposeCanvas())
val skCanvas = surface.canvas
skCanvas.clear(Color.TRANSPARENT)
canvasHolder.drawInto(skCanvas) {
scene.draw(this)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,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.CanvasHolder
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
import androidx.compose.ui.input.key.internal
Expand Down Expand Up @@ -182,6 +183,8 @@ internal class ComposeSceneMediator(
val windowHandle by skiaLayerComponent::windowHandle
val renderApi by skiaLayerComponent::renderApi
val semanticsOwners: Collection<SemanticsOwner> by semanticsOwnerManager::semanticsOwners

private val canvasHolder: CanvasHolder = CanvasHolder()

/**
* @see ComposeFeatureFlags.useInteropBlending
Expand Down Expand Up @@ -727,13 +730,13 @@ 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) {
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]
Expand All @@ -744,7 +747,7 @@ internal class ComposeSceneMediator(
val sceneOffset = sceneBoundsOffset - contentOffset
save()
translate(sceneOffset.x, sceneOffset.y)
block()
canvasHolder.drawInto(this, block)
restore()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.LocalSystemTheme
import androidx.compose.ui.SystemTheme
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.navigationevent.UIKitNavigationEventInput
import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner
import androidx.compose.ui.platform.FrameRecomposer
Expand Down Expand Up @@ -120,6 +120,7 @@ internal class ComposeContainer(
private val systemThemeState: MutableState<SystemTheme> = mutableStateOf(SystemTheme.Unknown)

private val focusedViewsList = FocusedViewsList()
private val canvasHolder = CanvasHolder()

init {
if (configuration.enforceStrictPlistSanityCheck) {
Expand Down Expand Up @@ -205,7 +206,9 @@ internal class ComposeContainer(
},
useSeparateRenderThreadWhenPossible = configuration.parallelRendering,
render = { canvas, nanoTime ->
mediator?.render(canvas.asComposeCanvas(), nanoTime)
canvasHolder.drawInto(canvas) {
mediator?.render(this, nanoTime)
}
}
)
metalView.canBeOpaque = configuration.opaque
Expand Down
Loading