LayoutNodeDrawScope.kt

/*
 * Copyright 2020 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.node

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.unit.toSize

/**
 * [ContentDrawScope] implementation that extracts density and layout direction information
 * from the given NodeCoordinator
 */
@OptIn(ExperimentalComposeUiApi::class)
internal class LayoutNodeDrawScope(
    private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
) : DrawScope by canvasDrawScope, ContentDrawScope {

    // NOTE, currently a single ComponentDrawScope is shared across composables
    // which done to allocate a single set of Paint objects and re-use them across
    // draw calls for all composables.
    // As a result there could be thread safety concerns here for multi-threaded drawing
    // scenarios, generally a single ComponentDrawScope should be shared for a particular thread
    private var drawNode: DrawModifierNode? = null

    override fun drawContent() {
        drawIntoCanvas { canvas ->
            val drawNode = drawNode!!
            val nextDrawNode = drawNode.nextDrawNode()
            // NOTE(lmr): we only run performDraw directly on the node if the node's coordinator
            // is our own. This seems to work, but we should think about a cleaner way to dispatch
            // the draw pass as with the new modifier.node / coordinator structure this feels
            // somewhat error prone.
            if (nextDrawNode != null) {
                nextDrawNode.performDraw(canvas)
            } else {
                // TODO(lmr): this is needed in the case that the drawnode is also a measure node,
                //  but we should think about the right ways to handle this as this is very error
                //  prone i think
                val coordinator = drawNode.requireCoordinator(Nodes.Draw)
                val nextCoordinator = if (coordinator.tail === drawNode)
                    coordinator.wrapped!!
                else
                    coordinator
                nextCoordinator.performDraw(canvas)
            }
        }
    }

    // This is not thread safe
    fun DrawModifierNode.performDraw(canvas: Canvas) {
        val coordinator = requireCoordinator(Nodes.Draw)
        val size = coordinator.size.toSize()
        val drawScope = coordinator.layoutNode.mDrawScope
        drawScope.draw(canvas, size, coordinator, this)
    }

    internal fun draw(
        canvas: Canvas,
        size: Size,
        coordinator: NodeCoordinator,
        drawNode: DrawModifierNode,
    ) {
        val previousDrawNode = this.drawNode
        this.drawNode = drawNode
        canvasDrawScope.draw(
            coordinator,
            coordinator.layoutDirection,
            canvas,
            size
        ) {
            with(drawNode) {
                this@LayoutNodeDrawScope.draw()
            }
        }
        this.drawNode = previousDrawNode
    }
}

@OptIn(ExperimentalComposeUiApi::class)
private fun DelegatableNode.nextDrawNode(): DrawModifierNode? {
    val drawMask = Nodes.Draw.mask
    val measureMask = Nodes.Layout.mask
    val child = node.child ?: return null
    if (child.aggregateChildKindSet and drawMask == 0) return null
    var next: Modifier.Node? = child
    while (next != null) {
        if (next.kindSet and measureMask != 0) return null
        if (next.kindSet and drawMask != 0) {
            return next as DrawModifierNode
        }
        next = next.child
    }
    return null
}