All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.ui.focus.FocusInvalidationManager.kt Maven / Gradle / Ivy

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

package androidx.compose.ui.focus

import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.internal.checkPrecondition
import androidx.compose.ui.node.Nodes
import androidx.compose.ui.node.visitSelfAndChildren
import androidx.compose.ui.util.fastForEach

/**
 * The [FocusInvalidationManager] allows us to schedule focus related nodes for invalidation. These
 * nodes are invalidated after onApplyChanges. It does this by registering an onApplyChangesListener
 * when nodes are scheduled for invalidation.
 */
internal class FocusInvalidationManager(
    private val onRequestApplyChangesListener: (() -> Unit) -> Unit,
    private val invalidateOwnerFocusState: () -> Unit,
    private val rootFocusStateFetcher: () -> FocusState
) {
    private val focusTargetNodes = mutableListOf()
    private val focusEventNodes = mutableListOf()
    private val focusPropertiesNodes = mutableListOf()
    private val focusTargetsWithInvalidatedFocusEvents = mutableListOf()

    fun scheduleInvalidation(node: FocusTargetNode) {
        focusTargetNodes.scheduleInvalidation(node)
    }

    fun scheduleInvalidation(node: FocusEventModifierNode) {
        focusEventNodes.scheduleInvalidation(node)
    }

    fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
        focusPropertiesNodes.scheduleInvalidation(node)
    }

    fun hasPendingInvalidation(): Boolean {
        return focusTargetNodes.isNotEmpty() ||
            focusPropertiesNodes.isNotEmpty() ||
            focusEventNodes.isNotEmpty()
    }

    private fun  MutableList.scheduleInvalidation(node: T) {
        if (add(node)) {
            // If this is the first node scheduled for invalidation,
            // we set up a listener that runs after onApplyChanges.
            if (focusTargetNodes.size + focusEventNodes.size + focusPropertiesNodes.size == 1) {
                onRequestApplyChangesListener.invoke(::invalidateNodes)
            }
        }
    }

    private fun invalidateNodes() {
        if (!rootFocusStateFetcher().hasFocus) {
            // If root doesn't have focus, skip full invalidation and default to the Inactive state.
            focusEventNodes.fastForEach { it.onFocusEvent(Inactive) }
            focusTargetNodes.fastForEach { node ->
                if (node.isAttached && !node.isInitialized()) {
                    node.initializeFocusState(Inactive)
                }
            }
            focusTargetNodes.clear()
            focusEventNodes.clear()
            focusPropertiesNodes.clear()
            focusTargetsWithInvalidatedFocusEvents.clear()
            invalidateOwnerFocusState()
            return
        }

        // Process all the invalidated FocusProperties nodes.
        focusPropertiesNodes.fastForEach {
            // We don't need to invalidate a focus properties node if it was scheduled for
            // invalidation earlier in the composition but was then removed.
            if (!it.node.isAttached) return@fastForEach

            it.visitSelfAndChildren(Nodes.FocusTarget) { focusTarget ->
                focusTargetNodes.add(focusTarget)
            }
        }
        focusPropertiesNodes.clear()

        // Process all the focus events nodes.
        focusEventNodes.fastForEach { focusEventNode ->
            // When focus nodes are removed, the corresponding focus events are scheduled for
            // invalidation. If the focus event was also removed, we don't need to invalidate it.
            // We call onFocusEvent with the default value, just to make it easier for the user,
            // so that they don't have to keep track of whether they caused a focused item to be
            // removed (Which would cause it to lose focus).
            if (!focusEventNode.node.isAttached) {
                focusEventNode.onFocusEvent(Inactive)
                return@fastForEach
            }

            var requiresUpdate = true
            var aggregatedNode = false
            var focusTarget: FocusTargetNode? = null
            focusEventNode.visitSelfAndChildren(Nodes.FocusTarget) {

                // If there are multiple focus targets associated with this focus event node,
                // we need to calculate the aggregated state.
                if (focusTarget != null) {
                    aggregatedNode = true
                }

                focusTarget = it

                // If the associated focus node is already scheduled for invalidation, it will
                // send an onFocusEvent if the invalidation causes a focus state change.
                // However this onFocusEvent was invalidated, so we have to ensure that we call
                // onFocusEvent even if the focus state didn't change.
                if (it in focusTargetNodes) {
                    requiresUpdate = false
                    focusTargetsWithInvalidatedFocusEvents.add(it)
                    return@visitSelfAndChildren
                }
            }

            if (requiresUpdate) {
                focusEventNode.onFocusEvent(
                    if (aggregatedNode) {
                        focusEventNode.getFocusState()
                    } else {
                        focusTarget?.focusState ?: Inactive
                    }
                )
            }
        }
        focusEventNodes.clear()

        // Process all the focus target nodes.
        focusTargetNodes.fastForEach {
            // We don't need to invalidate the focus target if it was scheduled for invalidation
            // earlier in the composition but was then removed.
            if (!it.isAttached) return@fastForEach

            val preInvalidationState = it.focusState
            it.invalidateFocus()
            if (
                preInvalidationState != it.focusState ||
                    it in focusTargetsWithInvalidatedFocusEvents
            ) {
                it.dispatchFocusCallbacks()
            }
        }
        focusTargetNodes.clear()
        // Clear the set so we can reuse it
        focusTargetsWithInvalidatedFocusEvents.clear()

        invalidateOwnerFocusState()

        checkPrecondition(focusPropertiesNodes.isEmpty()) { "Unprocessed FocusProperties nodes" }
        checkPrecondition(focusEventNodes.isEmpty()) { "Unprocessed FocusEvent nodes" }
        checkPrecondition(focusTargetNodes.isEmpty()) { "Unprocessed FocusTarget nodes" }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy