NodeKind.kt

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

@file:Suppress("DEPRECATION", "NOTHING_TO_INLINE")

package androidx.compose.ui.node

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.focus.FocusEventModifier
import androidx.compose.ui.focus.FocusEventModifierNode
import androidx.compose.ui.focus.FocusOrderModifier
import androidx.compose.ui.focus.FocusProperties
import androidx.compose.ui.focus.FocusPropertiesModifierNode
import androidx.compose.ui.focus.FocusTargetModifierNode
import androidx.compose.ui.input.key.KeyInputModifierNode
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.input.rotary.RotaryInputModifierNode
import androidx.compose.ui.layout.IntermediateLayoutModifierNode
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.OnGloballyPositionedModifier
import androidx.compose.ui.layout.OnPlacedModifier
import androidx.compose.ui.layout.OnRemeasuredModifier
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalNode
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.semantics.SemanticsModifier

@JvmInline
internal value class NodeKind<T>(val mask: Int) {
    inline infix fun or(other: NodeKind<*>): Int = mask or other.mask
    inline infix fun or(other: Int): Int = mask or other
}

internal inline infix fun Int.or(other: NodeKind<*>): Int = this or other.mask

// For a given NodeCoordinator, the "LayoutAware" nodes that it is concerned with should include
// its own measureNode if the measureNode happens to implement LayoutAware. If the measureNode
// implements any other node interfaces, such as draw, those should be visited by the coordinator
// below them.
@OptIn(ExperimentalComposeUiApi::class)
internal val NodeKind<*>.includeSelfInTraversal: Boolean
    get() = mask and Nodes.LayoutAware.mask != 0

// Note that these don't inherit from Modifier.Node to allow for a single Modifier.Node
// instance to implement multiple Node interfaces

@OptIn(ExperimentalComposeUiApi::class)
internal object Nodes {
    @JvmStatic
    inline val Any get() = NodeKind<Modifier.Node>(0b1 shl 0)
    @JvmStatic
    inline val Layout get() = NodeKind<LayoutModifierNode>(0b1 shl 1)
    @JvmStatic
    inline val Draw get() = NodeKind<DrawModifierNode>(0b1 shl 2)
    @JvmStatic
    inline val Semantics get() = NodeKind<SemanticsModifierNode>(0b1 shl 3)
    @JvmStatic
    inline val PointerInput get() = NodeKind<PointerInputModifierNode>(0b1 shl 4)
    @JvmStatic
    inline val Locals get() = NodeKind<ModifierLocalNode>(0b1 shl 5)
    @JvmStatic
    inline val ParentData get() = NodeKind<ParentDataModifierNode>(0b1 shl 6)
    @JvmStatic
    inline val LayoutAware get() = NodeKind<LayoutAwareModifierNode>(0b1 shl 7)
    @JvmStatic
    inline val GlobalPositionAware get() = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 8)
    @JvmStatic
    inline val IntermediateMeasure get() = NodeKind<IntermediateLayoutModifierNode>(0b1 shl 9)
    @JvmStatic
    inline val FocusTarget get() = NodeKind<FocusTargetModifierNode>(0b1 shl 10)
    @JvmStatic
    inline val FocusProperties get() = NodeKind<FocusPropertiesModifierNode>(0b1 shl 11)
    @JvmStatic
    inline val FocusEvent get() = NodeKind<FocusEventModifierNode>(0b1 shl 12)
    @JvmStatic
    inline val KeyInput get() = NodeKind<KeyInputModifierNode>(0b1 shl 13)
    @JvmStatic
    inline val RotaryInput get() = NodeKind<RotaryInputModifierNode>(0b1 shl 14)
    @JvmStatic
    inline val CompositionLocalConsumer
        get() = NodeKind<CompositionLocalConsumerModifierNode>(0b1 shl 15)
    // ...
}

@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(element: Modifier.Element): Int {
    var mask = Nodes.Any.mask
    if (element is LayoutModifier) {
        mask = mask or Nodes.Layout
    }
    if (element is DrawModifier) {
        mask = mask or Nodes.Draw
    }
    if (element is SemanticsModifier) {
        mask = mask or Nodes.Semantics
    }
    if (element is PointerInputModifier) {
        mask = mask or Nodes.PointerInput
    }
    if (
        element is ModifierLocalConsumer ||
        element is ModifierLocalProvider<*>
    ) {
        mask = mask or Nodes.Locals
    }
    if (element is FocusEventModifier) {
        mask = mask or Nodes.FocusEvent
    }
    if (element is FocusOrderModifier) {
        mask = mask or Nodes.FocusProperties
    }
    if (element is OnGloballyPositionedModifier) {
        mask = mask or Nodes.GlobalPositionAware
    }
    if (element is ParentDataModifier) {
        mask = mask or Nodes.ParentData
    }
    if (
        element is OnPlacedModifier ||
        element is OnRemeasuredModifier
    ) {
        mask = mask or Nodes.LayoutAware
    }
    return mask
}

@OptIn(ExperimentalComposeUiApi::class)
internal fun calculateNodeKindSetFrom(node: Modifier.Node): Int {
    var mask = Nodes.Any.mask
    if (node is LayoutModifierNode) {
        mask = mask or Nodes.Layout
    }
    if (node is DrawModifierNode) {
        mask = mask or Nodes.Draw
    }
    if (node is SemanticsModifierNode) {
        mask = mask or Nodes.Semantics
    }
    if (node is PointerInputModifierNode) {
        mask = mask or Nodes.PointerInput
    }
    if (node is ModifierLocalNode) {
        mask = mask or Nodes.Locals
    }
    if (node is ParentDataModifierNode) {
        mask = mask or Nodes.ParentData
    }
    if (node is LayoutAwareModifierNode) {
        mask = mask or Nodes.LayoutAware
    }
    if (node is GlobalPositionAwareModifierNode) {
        mask = mask or Nodes.GlobalPositionAware
    }
    if (node is IntermediateLayoutModifierNode) {
        mask = mask or Nodes.IntermediateMeasure
    }
    if (node is FocusTargetModifierNode) {
        mask = mask or Nodes.FocusTarget
    }
    if (node is FocusPropertiesModifierNode) {
        mask = mask or Nodes.FocusProperties
    }
    if (node is FocusEventModifierNode) {
        mask = mask or Nodes.FocusEvent
    }
    if (node is KeyInputModifierNode) {
        mask = mask or Nodes.KeyInput
    }
    if (node is RotaryInputModifierNode) {
        mask = mask or Nodes.RotaryInput
    }
    if (node is CompositionLocalConsumerModifierNode) {
        mask = mask or Nodes.CompositionLocalConsumer
    }
    return mask
}

private const val Updated = 0
private const val Inserted = 1
private const val Removed = 2

@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateRemovedNode(node: Modifier.Node) = autoInvalidateNode(node, Removed)

@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateInsertedNode(node: Modifier.Node) = autoInvalidateNode(node, Inserted)

@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)

@OptIn(ExperimentalComposeUiApi::class)
private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
    check(node.isAttached)
    if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
        node.invalidateMeasurements()
        if (phase == Removed) {
            val coordinator = node.requireCoordinator(Nodes.Layout)
            coordinator.onRelease()
        }
    }
    if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
        node.requireLayoutNode().invalidateMeasurements()
    }
    if (node.isKind(Nodes.Draw) && node is DrawModifierNode) {
        node.invalidateDraw()
    }
    if (node.isKind(Nodes.Semantics) && node is SemanticsModifierNode) {
        node.invalidateSemantics()
    }
    if (node.isKind(Nodes.ParentData) && node is ParentDataModifierNode) {
        node.invalidateParentData()
    }
    if (node.isKind(Nodes.FocusTarget) && node is FocusTargetModifierNode) {
        when (phase) {
            // when we previously had focus target modifier on a node and then this modifier
            // is removed we need to notify the focus tree about so the focus state is reset.
            Removed -> node.onReset()
            else -> node.requireOwner().focusOwner.scheduleInvalidation(node)
        }
    }
    if (
        node.isKind(Nodes.FocusProperties) &&
        node is FocusPropertiesModifierNode &&
        node.specifiesCanFocusProperty()
    ) {
        when (phase) {
            Removed -> node.scheduleInvalidationOfAssociatedFocusTargets()
            else -> node.requireOwner().focusOwner.scheduleInvalidation(node)
        }
    }
    if (node.isKind(Nodes.FocusEvent) && node is FocusEventModifierNode && phase != Removed) {
        node.requireOwner().focusOwner.scheduleInvalidation(node)
    }
}

private fun FocusPropertiesModifierNode.scheduleInvalidationOfAssociatedFocusTargets() {
    visitChildren(Nodes.FocusTarget) {
        // Schedule invalidation for the focus target,
        // which will cause it to recalculate focus properties.
        requireOwner().focusOwner.scheduleInvalidation(it)
    }
}

/**
 * This function checks if the FocusProperties node has set the canFocus [FocusProperties.canFocus]
 * property.
 *
 * We use a singleton CanFocusChecker to prevent extra allocations, and in doing so, we assume that
 * there won't be multiple concurrent calls of this function. This is not an issue since this is
 * called from the main thread, but if this changes in the future, replace the
 * [CanFocusChecker.reset] call with a new [FocusProperties] object for every invocation.
 */
private fun FocusPropertiesModifierNode.specifiesCanFocusProperty(): Boolean {
    CanFocusChecker.reset()
    modifyFocusProperties(CanFocusChecker)
    return CanFocusChecker.isCanFocusSet()
}

private object CanFocusChecker : FocusProperties {
    private var canFocusValue: Boolean? = null
    override var canFocus: Boolean
        get() = checkNotNull(canFocusValue)
        set(value) { canFocusValue = value }
    fun isCanFocusSet(): Boolean = canFocusValue != null
    fun reset() { canFocusValue = null }
}