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

package androidx.compose.ui.input

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.ProvidableModifierLocal
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.platform.debugInspectorInfo

/**
 * Represents a component that handles scroll events, so that other components in the hierarchy
 * can adjust their behaviour.
 * @See [provideScrollContainerInfo] and [consumeScrollContainerInfo]
 */
interface ScrollContainerInfo {
    /** @return whether this component handles horizontal scroll events */
    fun canScrollHorizontally(): Boolean
    /** @return whether this component handles vertical scroll events */
    fun canScrollVertically(): Boolean
}

/** @return whether this container handles either horizontal or vertical scroll events */
fun ScrollContainerInfo.canScroll() = canScrollVertically() || canScrollHorizontally()

/**
 * A modifier to query whether there are any parents in the hierarchy that handle scroll events.
 * The [ScrollContainerInfo] provided in [consumer] will recursively look for ancestors if the
 * nearest parent does not handle scroll events in the queried direction.
 * This can be used to delay UI changes in cases where a pointer event may later become a scroll,
 * cancelling any existing press or other gesture.
 *
 * @sample androidx.compose.ui.samples.ScrollableContainerSample
 */
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.consumeScrollContainerInfo(consumer: (ScrollContainerInfo?) -> Unit): Modifier =
    modifierLocalConsumer {
        consumer(ModifierLocalScrollContainerInfo.current)
    }

/**
 * A Modifier that indicates that this component handles scroll events. Use
 * [consumeScrollContainerInfo] to query whether there is a parent in the hierarchy that is
 * a [ScrollContainerInfo].
 */
fun Modifier.provideScrollContainerInfo(scrollContainerInfo: ScrollContainerInfo): Modifier =
    composed(
        inspectorInfo = debugInspectorInfo {
            name = "provideScrollContainerInfo"
            value = scrollContainerInfo
        }) {
    remember(scrollContainerInfo) {
        ScrollContainerInfoModifierLocal(scrollContainerInfo)
    }
}

/**
 * ModifierLocal to propagate ScrollableContainer throughout the hierarchy.
 * This Modifier will recursively check for ancestor ScrollableContainers,
 * if the current ScrollableContainer does not handle scroll events in a particular direction.
 */
private class ScrollContainerInfoModifierLocal(
    private val scrollContainerInfo: ScrollContainerInfo,
) : ScrollContainerInfo, ModifierLocalProvider<ScrollContainerInfo?>, ModifierLocalConsumer {

    private var parent: ScrollContainerInfo? by mutableStateOf(null)

    override val key: ProvidableModifierLocal<ScrollContainerInfo?> =
        ModifierLocalScrollContainerInfo
    override val value: ScrollContainerInfoModifierLocal = this

    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
        parent = ModifierLocalScrollContainerInfo.current
    }

    override fun canScrollHorizontally(): Boolean {
        return scrollContainerInfo.canScrollHorizontally() ||
            parent?.canScrollHorizontally() == true
    }

    override fun canScrollVertically(): Boolean {
        return scrollContainerInfo.canScrollVertically() || parent?.canScrollVertically() == true
    }
}

internal val ModifierLocalScrollContainerInfo = modifierLocalOf<ScrollContainerInfo?> {
    null
}