OneDimensionalFocusSearch.kt

/*
 * Copyright 2021 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.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusStateImpl.Disabled
import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.node.ModifiedFocusNode
import androidx.compose.ui.util.fastForEach

private const val InvalidFocusDirection = "This function should only be used for 1-D focus search"
private const val NoActiveChild = "ActiveParent must have a focusedChild"
private const val NotYetAvailable = "Implement this after adding API to disable a node"

internal fun ModifiedFocusNode.oneDimensionalFocusSearch(
    direction: FocusDirection
): ModifiedFocusNode = when (direction) {
    Next -> forwardFocusSearch() ?: this
    Previous -> backwardFocusSearch()
    else -> error(InvalidFocusDirection)
}

private fun ModifiedFocusNode.forwardFocusSearch(): ModifiedFocusNode? = when (focusState) {
    ActiveParent -> {
        val focusedChild = focusedChild ?: error(NoActiveChild)
        focusedChild.forwardFocusSearch()?.let { return it }

        var currentItemIsAfterFocusedItem = false
        // TODO(b/192681045): Instead of fetching the children and then iterating on them, add a
        //  forEachFocusableChild function that does not allocate a list.
        focusableChildren().fastForEach {
            if (currentItemIsAfterFocusedItem) {
                return it
            }
            if (it == focusedChild) {
                currentItemIsAfterFocusedItem = true
            }
        }
        null // Couldn't find a focusable child after the current focused child.
    }
    Active, Captured -> focusableChildren().firstOrNull()
    Inactive -> this
    Disabled -> TODO(NotYetAvailable)
}

private fun ModifiedFocusNode.backwardFocusSearch(): ModifiedFocusNode = when (focusState) {
    ActiveParent -> {
        val focusedChild = focusedChild ?: error(NoActiveChild)
        when (focusedChild.focusState) {
            ActiveParent -> focusedChild.backwardFocusSearch()
            Active, Captured -> {
                var previousFocusedItem: ModifiedFocusNode? = null
                // TODO(b/192681045): Instead of fetching the children and then iterating on them, add a
                //  forEachFocusableChild() function that does not allocate a list.
                focusableChildren().fastForEach {
                    if (it == focusedChild) {
                        return previousFocusedItem?.backwardFocusSearch() ?: this
                    }
                    previousFocusedItem = it
                }
                error(NoActiveChild)
            }
            else -> error(NoActiveChild)
        }
    }
    // The BackwardFocusSearch Searches among siblings of the ActiveParent for a child that is
    // focused. So this function should never be called when this node is focused. If we reached
    // here, it indicates an initial focus state, so we run the same logic as if this node was
    // Inactive.
    Active, Captured, Inactive -> focusableChildren().lastOrNull()?.backwardFocusSearch() ?: this
    Disabled -> TODO(NotYetAvailable)
}