CompositionLocalMap.kt

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

import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap
import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf

/**
 * A read-only, immutable snapshot of the [CompositionLocals][CompositionLocal] that are set at a
 * specific position in the composition hierarchy.
 */
sealed interface CompositionLocalMap {
    /**
     * Returns the value of the provided [composition local][key] at the position in the composition
     * hierarchy represented by this [CompositionLocalMap] instance. If the provided [key]
     * is not set at this point in the hierarchy, its default value will be used.
     *
     * For [non-static CompositionLocals][compositionLocalOf], this function will return the latest
     * value of the CompositionLocal, which might change over time across the same instance of the
     * CompositionLocalMap. Reads done in this way are not tracked in the snapshot system.
     *
     * For [static CompositionLocals][staticCompositionLocalOf], this function returns the value
     * at the time of creation of the CompositionLocalMap. When a static CompositionLocal is
     * reassigned, the entire composition hierarchy is recomposed and a new CompositionLocalMap is
     * created with the updated value of the static CompositionLocal.
     */
    operator fun <T> get(key: CompositionLocal<T>): T

    companion object {
        /**
         * An empty [CompositionLocalMap] instance which contains no keys or values.
         */
        val Empty: CompositionLocalMap = persistentCompositionLocalHashMapOf()
    }
}

/**
 * A [CompositionLocal] map is is an immutable map that maps [CompositionLocal] keys to a provider
 * of their current value. It is used to represent the combined scope of all provided
 * [CompositionLocal]s.
 */
internal interface PersistentCompositionLocalMap :
    PersistentMap<CompositionLocal<Any?>, State<Any?>>,
    CompositionLocalMap {

    // Override the builder APIs so that we can create new PersistentMaps that retain the type
    // information of PersistentCompositionLocalMap. If we use the built-in implementation, we'll
    // get back a PersistentMap<CompositionLocal<Any?>, State<Any?>> instead of a
    // PersistentCompositionLocalMap
    override fun builder(): Builder

    interface Builder : PersistentMap.Builder<CompositionLocal<Any?>, State<Any?>> {
        override fun build(): PersistentCompositionLocalMap
    }
}

internal inline fun PersistentCompositionLocalMap.mutate(
    mutator: (MutableMap<CompositionLocal<Any?>, State<Any?>>) -> Unit
): PersistentCompositionLocalMap = builder().apply(mutator).build()

@Suppress("UNCHECKED_CAST")
internal fun <T> PersistentCompositionLocalMap.contains(key: CompositionLocal<T>) =
    this.containsKey(key as CompositionLocal<Any?>)

@Suppress("UNCHECKED_CAST")
internal fun <T> PersistentCompositionLocalMap.getValueOf(key: CompositionLocal<T>) =
    this[key as CompositionLocal<Any?>]?.value as T

internal fun <T> PersistentCompositionLocalMap.read(
    key: CompositionLocal<T>
): T = if (contains(key)) {
    getValueOf(key)
} else {
    key.defaultValueHolder.value
}

@Composable
internal fun compositionLocalMapOf(
    values: Array<out ProvidedValue<*>>,
    parentScope: PersistentCompositionLocalMap
): PersistentCompositionLocalMap {
    val result: PersistentCompositionLocalMap = persistentCompositionLocalHashMapOf()
    return result.mutate {
        for (provided in values) {
            if (provided.canOverride || !parentScope.contains(provided.compositionLocal)) {
                @Suppress("UNCHECKED_CAST")
                it[provided.compositionLocal as CompositionLocal<Any?>] =
                    provided.compositionLocal.provided(provided.value)
            }
        }
    }
}