| /* |
| * Copyright 2019 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:OptIn( |
| InternalComposeApi::class, |
| ) |
| package androidx.compose.runtime |
| |
| import androidx.compose.runtime.Composer.Companion.equals |
| import androidx.compose.runtime.collection.IdentityArrayMap |
| import androidx.compose.runtime.collection.IdentityArraySet |
| import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap |
| import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentHashMapOf |
| import androidx.compose.runtime.snapshots.currentSnapshot |
| import androidx.compose.runtime.snapshots.fastForEach |
| import androidx.compose.runtime.snapshots.fastForEachIndexed |
| import androidx.compose.runtime.snapshots.fastMap |
| import androidx.compose.runtime.snapshots.fastToSet |
| import androidx.compose.runtime.tooling.CompositionData |
| import androidx.compose.runtime.tooling.LocalInspectionTables |
| import kotlin.coroutines.CoroutineContext |
| |
| internal typealias Change = ( |
| applier: Applier<*>, |
| slots: SlotWriter, |
| rememberManager: RememberManager |
| ) -> Unit |
| |
| private class GroupInfo( |
| /** |
| * The current location of the slot relative to the start location of the pending slot changes |
| */ |
| var slotIndex: Int, |
| |
| /** |
| * The current location of the first node relative the start location of the pending node |
| * changes |
| */ |
| var nodeIndex: Int, |
| |
| /** |
| * The current number of nodes the group contains after changes have been applied |
| */ |
| var nodeCount: Int |
| ) |
| |
| /** |
| * An interface used during [ControlledComposition.applyChanges] and [Composition.dispose] to |
| * track when [RememberObserver] instances and leave the composition an also allows recording |
| * [SideEffect] calls. |
| */ |
| internal interface RememberManager { |
| /** |
| * The [RememberObserver] is being remembered by a slot in the slot table. |
| */ |
| fun remembering(instance: RememberObserver) |
| |
| /** |
| * The [RememberObserver] is being forgotten by a slot in the slot table. |
| */ |
| fun forgetting(instance: RememberObserver) |
| |
| /** |
| * The [effect] should be called when changes are being applied but after the remember/forget |
| * notifications are sent. |
| */ |
| fun sideEffect(effect: () -> Unit) |
| } |
| |
| /** |
| * Pending starts when the key is different than expected indicating that the structure of the tree |
| * changed. It is used to determine how to update the nodes and the slot table when changes to the |
| * structure of the tree is detected. |
| */ |
| private class Pending( |
| val keyInfos: MutableList<KeyInfo>, |
| val startIndex: Int |
| ) { |
| var groupIndex: Int = 0 |
| |
| init { |
| require(startIndex >= 0) { "Invalid start index" } |
| } |
| |
| private val usedKeys = mutableListOf<KeyInfo>() |
| private val groupInfos = run { |
| var runningNodeIndex = 0 |
| val result = hashMapOf<Int, GroupInfo>() |
| for (index in 0 until keyInfos.size) { |
| val keyInfo = keyInfos[index] |
| @OptIn(InternalComposeApi::class) |
| result[keyInfo.location] = GroupInfo(index, runningNodeIndex, keyInfo.nodes) |
| @OptIn(InternalComposeApi::class) |
| runningNodeIndex += keyInfo.nodes |
| } |
| result |
| } |
| |
| /** |
| * A multi-map of keys from the previous composition. The keys can be retrieved in the order |
| * they were generated by the previous composition. |
| */ |
| val keyMap by lazy { |
| multiMap<Any, KeyInfo>().also { |
| for (index in 0 until keyInfos.size) { |
| val keyInfo = keyInfos[index] |
| @Suppress("ReplacePutWithAssignment") |
| it.put(keyInfo.joinedKey, keyInfo) |
| } |
| } |
| } |
| |
| /** |
| * Get the next key information for the given key. |
| */ |
| fun getNext(key: Int, dataKey: Any?): KeyInfo? { |
| val joinedKey: Any = if (dataKey != null) JoinedKey(key, dataKey) else key |
| return keyMap.pop(joinedKey) |
| } |
| |
| /** |
| * Record that this key info was generated. |
| */ |
| fun recordUsed(keyInfo: KeyInfo) = usedKeys.add(keyInfo) |
| |
| val used: List<KeyInfo> get() = usedKeys |
| |
| // TODO(chuckj): This is a correct but expensive implementation (worst cases of O(N^2)). Rework |
| // to O(N) |
| fun registerMoveSlot(from: Int, to: Int) { |
| if (from > to) { |
| groupInfos.values.forEach { group -> |
| val position = group.slotIndex |
| if (position == from) group.slotIndex = to |
| else if (position in to until from) group.slotIndex = position + 1 |
| } |
| } else if (to > from) { |
| groupInfos.values.forEach { group -> |
| val position = group.slotIndex |
| if (position == from) group.slotIndex = to |
| else if (position in (from + 1) until to) group.slotIndex = position - 1 |
| } |
| } |
| } |
| |
| fun registerMoveNode(from: Int, to: Int, count: Int) { |
| if (from > to) { |
| groupInfos.values.forEach { group -> |
| val position = group.nodeIndex |
| if (position in from until from + count) group.nodeIndex = to + (position - from) |
| else if (position in to until from) group.nodeIndex = position + count |
| } |
| } else if (to > from) { |
| groupInfos.values.forEach { group -> |
| val position = group.nodeIndex |
| if (position in from until from + count) group.nodeIndex = to + (position - from) |
| else if (position in (from + 1) until to) group.nodeIndex = position - count |
| } |
| } |
| } |
| |
| @OptIn(InternalComposeApi::class) |
| fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) { |
| groupInfos[keyInfo.location] = GroupInfo(-1, insertIndex, 0) |
| } |
| |
| fun updateNodeCount(group: Int, newCount: Int): Boolean { |
| val groupInfo = groupInfos[group] |
| if (groupInfo != null) { |
| val index = groupInfo.nodeIndex |
| val difference = newCount - groupInfo.nodeCount |
| groupInfo.nodeCount = newCount |
| if (difference != 0) { |
| groupInfos.values.forEach { childGroupInfo -> |
| if (childGroupInfo.nodeIndex >= index && childGroupInfo != groupInfo) { |
| val newIndex = childGroupInfo.nodeIndex + difference |
| if (newIndex >= 0) |
| childGroupInfo.nodeIndex = newIndex |
| } |
| } |
| } |
| return true |
| } |
| return false |
| } |
| |
| @OptIn(InternalComposeApi::class) |
| fun slotPositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.location]?.slotIndex ?: -1 |
| |
| @OptIn(InternalComposeApi::class) |
| fun nodePositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.location]?.nodeIndex ?: -1 |
| |
| @OptIn(InternalComposeApi::class) |
| fun updatedNodeCountOf(keyInfo: KeyInfo) = |
| groupInfos[keyInfo.location]?.nodeCount ?: keyInfo.nodes |
| } |
| |
| private class Invalidation( |
| /** |
| * The recompose scope being invalidate |
| */ |
| val scope: RecomposeScopeImpl, |
| |
| /** |
| * The index of the group in the slot table being invalidated. |
| */ |
| val location: Int, |
| |
| /** |
| * The instances invalidating the scope. If this is `null` or empty then the scope is |
| * unconditionally invalid. If it contains instances it is only invalid if at least on of the |
| * instances is changed. This is used to track `DerivedState<*>` changes and only treat the |
| * scope as invalid if the instance has changed. |
| */ |
| var instances: IdentityArraySet<Any>? |
| ) { |
| fun isInvalid(): Boolean = scope.isInvalidFor(instances) |
| } |
| |
| /** |
| * Internal compose compiler plugin API that is used to update the function the composer will |
| * call to recompose a recomposition scope. This should not be used or called directly. |
| */ |
| @ComposeCompilerApi |
| interface ScopeUpdateScope { |
| /** |
| * Called by generated code to update the recomposition scope with the function to call |
| * recompose the scope. This is called by code generated by the compose compiler plugin and |
| * should not be called directly. |
| */ |
| fun updateScope(block: (Composer, Int) -> Unit) |
| } |
| |
| internal enum class InvalidationResult { |
| /** |
| * The invalidation was ignored because the associated recompose scope is no longer part of the |
| * composition or has yet to be entered in the composition. This could occur for invalidations |
| * called on scopes that are no longer part of composition or if the scope was invalidated |
| * before [ControlledComposition.applyChanges] was called that will enter the scope into the |
| * composition. |
| */ |
| IGNORED, |
| |
| /** |
| * The composition is not currently composing and the invalidation was recorded for a future |
| * composition. A recomposition requested to be scheduled. |
| */ |
| SCHEDULED, |
| |
| /** |
| * The composition that owns the recompose scope is actively composing but the scope has |
| * already been composed or is in the process of composing. The invalidation is treated as |
| * SCHEDULED above. |
| */ |
| DEFERRED, |
| |
| /** |
| * The composition that owns the recompose scope is actively composing and the invalidated |
| * scope has not been composed yet but will be recomposed before the composition completes. A |
| * new recomposition was not scheduled for this invalidation. |
| */ |
| IMMINENT |
| } |
| |
| /** |
| * An instance to hold a value provided by [CompositionLocalProvider] and is created by the |
| * [ProvidableCompositionLocal.provides] infixed operator. If [canOverride] is `false`, the |
| * provided value will not overwrite a potentially already existing value in the scope. |
| */ |
| class ProvidedValue<T> internal constructor( |
| val compositionLocal: CompositionLocal<T>, |
| val value: T, |
| val canOverride: Boolean |
| ) |
| |
| /** |
| * 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 typealias CompositionLocalMap = PersistentMap<CompositionLocal<Any?>, State<Any?>> |
| |
| internal inline fun CompositionLocalMap.mutate( |
| mutator: (MutableMap<CompositionLocal<Any?>, State<Any?>>) -> Unit |
| ): CompositionLocalMap = builder().apply(mutator).build() |
| |
| @Suppress("UNCHECKED_CAST") |
| internal fun <T> CompositionLocalMap.contains(key: CompositionLocal<T>) = |
| this.containsKey(key as CompositionLocal<Any?>) |
| |
| @Suppress("UNCHECKED_CAST") |
| internal fun <T> CompositionLocalMap.getValueOf(key: CompositionLocal<T>) = |
| this[key as CompositionLocal<Any?>]?.value as T |
| |
| @Composable |
| private fun compositionLocalMapOf( |
| values: Array<out ProvidedValue<*>>, |
| parentScope: CompositionLocalMap |
| ): CompositionLocalMap { |
| val result: CompositionLocalMap = persistentHashMapOf() |
| 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) |
| } |
| } |
| } |
| } |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * An instance used to track the identity of the movable content. Using a holder object allows |
| * creating unique movable content instances from the same instance of a lambda. This avoids |
| * using the identity of a lambda instance as it can be merged into a singleton or merged by later |
| * rewritings and using its identity might lead to unpredictable results that might change from the |
| * debug and release builds. |
| */ |
| @InternalComposeApi |
| class MovableContent<P>(val content: @Composable (parameter: P) -> Unit) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * A reference to the movable content state prior to changes being applied. |
| */ |
| @InternalComposeApi |
| class MovableContentStateReference internal constructor( |
| internal val content: MovableContent<Any?>, |
| internal val parameter: Any?, |
| internal val composition: ControlledComposition, |
| internal val slotTable: SlotTable, |
| internal val anchor: Anchor, |
| internal val invalidations: List<Pair<RecomposeScopeImpl, IdentityArraySet<Any>?>>, |
| internal val locals: CompositionLocalMap |
| ) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * A reference to the state of a [MovableContent] after changes have being applied. This is the |
| * state that was removed from the `from` composition during [ControlledComposition.applyChanges] |
| * and before it is inserted during [ControlledComposition.insertMovableContent]. |
| */ |
| @InternalComposeApi |
| class MovableContentState internal constructor( |
| internal val slotTable: SlotTable |
| ) |
| |
| /** |
| * Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by |
| * code generation helpers. It is highly recommended that direct calls these be avoided as the |
| * runtime assumes that the calls are generated by the compiler and contain only a minimum amount |
| * of state validation. |
| */ |
| sealed interface Composer { |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Changes calculated and recorded during composition and are sent to [applier] which makes |
| * the physical changes to the node tree implied by a composition. |
| * |
| * Composition has two discrete phases, 1) calculate and record changes and 2) making the |
| * changes via the [applier]. While a [Composable] functions is executing, none of the |
| * [applier] methods are called. The recorded changes are sent to the [applier] all at once |
| * after all [Composable] functions have completed. |
| */ |
| @ComposeCompilerApi |
| val applier: Applier<*> |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Reflects that a new part of the composition is being created, that is, the composition |
| * will insert new nodes into the resulting tree. |
| */ |
| @ComposeCompilerApi |
| val inserting: Boolean |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Reflects whether the [Composable] function can skip. Even if a [Composable] function is |
| * called with the same parameters it might still need to run because, for example, a new |
| * value was provided for a [CompositionLocal] created by [staticCompositionLocalOf]. |
| */ |
| @ComposeCompilerApi |
| val skipping: Boolean |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Reflects whether the default parameter block of a [Composable] function is valid. This is |
| * `false` if a [State] object read in the [startDefaults] group was modified since the last |
| * time the [Composable] function was run. |
| */ |
| @ComposeCompilerApi |
| val defaultsInvalid: Boolean |
| |
| /** |
| * A Compose internal property. DO NOT call directly. Use [currentRecomposeScope] instead. |
| * |
| * The invalidation current invalidation scope. An new invalidation scope is created whenever |
| * [startRestartGroup] is called. when this scope's [RecomposeScope.invalidate] is called |
| * then lambda supplied to [endRestartGroup]'s [ScopeUpdateScope] will be scheduled to be |
| * run. |
| */ |
| @InternalComposeApi |
| val recomposeScope: RecomposeScope? |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Return an object that can be used to uniquely identity of the current recomposition scope. |
| * This identity will be the same even if the recompose scope instance changes. |
| * |
| * This is used internally by tooling track composable function invocations. |
| */ |
| @ComposeCompilerApi |
| val recomposeScopeIdentity: Any? |
| |
| /** |
| * A Compose internal property. DO NOT call directly. Use [currentCompositeKeyHash] instead. |
| * |
| * This a hash value used to coordinate map externally stored state to the composition. For |
| * example, this is used by saved instance state to preserve state across activity lifetime |
| * boundaries. |
| * |
| * This value is not likely to be unique but is not guaranteed unique. There are known cases, |
| * such as for loops without a [key], where the runtime does not have enough information to |
| * make the compound key hash unique. |
| */ |
| @InternalComposeApi |
| val compoundKeyHash: Int |
| |
| // Groups |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Start a replacable group. A replacable group is a group that cannot be moved during |
| * execution and can only either inserted, removed, or replaced. For example, the group |
| * created by most control flow constructs such as an `if` statement are replacable groups. |
| * |
| * @param key A compiler generated key based on the source location of the call. |
| */ |
| @ComposeCompilerApi |
| fun startReplaceableGroup(key: Int) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called at the end of a replacable group. |
| * |
| * @see startRestartGroup |
| */ |
| @ComposeCompilerApi |
| fun endReplaceableGroup() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Start a movable group. A movable group is one that can be moved based on the value of |
| * [dataKey] which is typically supplied by the [key][androidx.compose.runtime.key] pseudo |
| * compiler function. |
| * |
| * A movable group implements the semantics of [key][androidx.compose.runtime.key] which allows |
| * the state and nodes generated by a loop to move with the composition implied by the key |
| * passed to [key][androidx.compose.runtime.key]. |
| |
| * @param key A compiler generated key based on the source location of the call. |
| */ |
| @ComposeCompilerApi |
| fun startMovableGroup(key: Int, dataKey: Any?) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called at the end of a movable group. |
| * |
| * @see startMovableGroup |
| */ |
| @ComposeCompilerApi |
| fun endMovableGroup() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called to start the group that calculates the default parameters of a [Composable] function. |
| * |
| * This method is called near the beginning of a [Composable] function with default |
| * parameters and surrounds the remembered values or [Composable] calls necessary to produce |
| * the default parameters. For example, for `model: Model = remember { DefaultModel() }` the |
| * call to [remember] is called inside a [startDefaults] group. |
| */ |
| @ComposeCompilerApi |
| fun startDefaults() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called at the end of defaults group. |
| * |
| * @see startDefaults |
| */ |
| @ComposeCompilerApi |
| fun endDefaults() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called to record a group for a [Composable] function and starts a group that can be |
| * recomposed on demand based on the lambda passed to |
| * [updateScope][ScopeUpdateScope.updateScope] when [endRestartGroup] is called |
| * |
| * @param key A compiler generated key based on the source location of the call. |
| * @return the instance of the composer to use for the rest of the function. |
| */ |
| @ComposeCompilerApi |
| fun startRestartGroup(key: Int): Composer |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called to end a restart group. |
| */ |
| @ComposeCompilerApi |
| fun endRestartGroup(): ScopeUpdateScope? |
| |
| /** |
| * A Compose internal API. DO NOT call directly. |
| * |
| * Request movable content be inserted at the current location. This will schedule with the |
| * root composition parent a call to [insertMovableContent] with the correct |
| * [MovableContentState] if one was released in another part of composition. |
| */ |
| @InternalComposeApi |
| fun insertMovableContent(value: MovableContent<*>, parameter: Any?) |
| |
| /** |
| * A Compose internal API. DO NOT call directly. |
| * |
| * Perform a late composition that adds to the current late apply that will insert the given |
| * references to [MovableContent] into the composition. If a [MovableContent] is paired |
| * then this is a request to move a released [MovableContent] from a different location or |
| * from a different composition. If it is not paired (i.e. the `second` |
| * [MovableContentStateReference] is `null`) then new state for the [MovableContent] is |
| * inserted into the composition. |
| */ |
| @InternalComposeApi |
| fun insertMovableContentReferences( |
| references: List<Pair<MovableContentStateReference, MovableContentStateReference?>> |
| ) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Record the source information string for a group. This must be immediately called after the |
| * start of a group. |
| * |
| * @param sourceInformation An string value to that provides the compose tools enough |
| * information to calculate the source location of calls to composable functions. |
| */ |
| fun sourceInformation(sourceInformation: String) |
| |
| /** |
| * A compose compiler plugin API. DO NOT call directly. |
| * |
| * Record a source information marker. This marker can be used in place of a group that would |
| * have contained the information but was elided as the compiler plugin determined the group |
| * was not necessary such as when a function is marked with [ReadOnlyComposable]. |
| * |
| * @param key A compiler generated key based on the source location of the call. |
| * @param sourceInformation An string value to that provides the compose tools enough |
| * information to calculate the source location of calls to composable functions. |
| * |
| */ |
| fun sourceInformationMarkerStart(key: Int, sourceInformation: String) |
| |
| /** |
| * A compose compiler plugin API. DO NOT call directly. |
| * |
| * Record the end of the marked source information range. |
| */ |
| fun sourceInformationMarkerEnd() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Skips the composer to the end of the current group. This generated by the compiler to when |
| * the body of a [Composable] function can be skipped typically because the parameters to the |
| * function are equal to the values passed to it in the previous composition. |
| */ |
| @ComposeCompilerApi |
| fun skipToGroupEnd() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Deactivates the content to the end of the group by treating content as if it was deleted and |
| * replaces all slot table entries for calls to [cache] to be [Empty]. This must be called as |
| * the first call for a group. |
| */ |
| @ComposeCompilerApi |
| fun deactivateToEndGroup(changed: Boolean) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Skips the current group. This called by the compiler to indicate that the current group |
| * can be skipped, for example, this is generated to skip the [startDefaults] group the |
| * default group is was not invalidated. |
| */ |
| @ComposeCompilerApi |
| fun skipCurrentGroup() |
| |
| // Nodes |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Start a group that tracks a the code that will create or update a node that is generated |
| * as part of the tree implied by the composition. |
| */ |
| @ComposeCompilerApi |
| fun startNode() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Start a group that tracks a the code that will create or update a node that is generated |
| * as part of the tree implied by the composition. A reusable node can be reused in a |
| * reusable group even if the group key is changed. |
| */ |
| @ComposeCompilerApi |
| fun startReusableNode() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Report the [factory] that will be used to create the node that will be generated into the |
| * tree implied by the composition. This will only be called if [inserting] is is `true`. |
| * |
| * @param factory a factory function that will generate a node that will eventually be |
| * supplied to [applier] though [Applier.insertBottomUp] and [Applier.insertTopDown]. |
| */ |
| @ComposeCompilerApi |
| fun <T> createNode(factory: () -> T) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Report that the node is still being used. This will be called in the same location as the |
| * corresponding [createNode] when [inserting] is `false`. |
| */ |
| @ComposeCompilerApi |
| fun useNode() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called at the end of a node group. |
| */ |
| @ComposeCompilerApi |
| fun endNode() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Start a reuse group. Unlike a movable group, in a reuse group if the [dataKey] changes |
| * the composition shifts into a reusing state cause the composer to act like it is |
| * inserting (e.g. [cache] acts as if all values are invalid, [changed] always returns |
| * true, etc.) even though it is recomposing until it encounters a reusable node. If the |
| * node is reusable it temporarily shifts into recomposition for the node and then shifts |
| * back to reusing for the children. If a non-reusable node is generated the composer |
| * shifts to inserting for the node and all of its children. |
| * |
| * @param key An compiler generated key based on the source location of the call. |
| * @param dataKey A key provided by the [ReusableContent] composable function that is used to |
| * determine if the composition shifts into a reusing state for this group. |
| */ |
| @ComposeCompilerApi |
| fun startReusableGroup(key: Int, dataKey: Any?) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Called at the end of a reusable group. |
| */ |
| @ComposeCompilerApi |
| fun endReusableGroup() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Temporarily disable reusing if it is enabled. |
| */ |
| @ComposeCompilerApi |
| fun disableReusing() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Reenable reusing if it was previously enabled before the last call to [disableReusing]. |
| */ |
| @ComposeCompilerApi |
| fun enableReusing() |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Return a marker for the current group that can be used in a call to [endToMarker]. |
| */ |
| @ComposeCompilerApi |
| val currentMarker: Int |
| |
| /** |
| * Compose compiler plugin API. DO NOT call directly. |
| * |
| * Ends all the groups up to but not including the group that is the parent group when |
| * [currentMarker] was called to produce [marker]. All groups ended must have been started with |
| * either [startReplaceableGroup] or [startMovableGroup]. Ending other groups can cause the |
| * state of the composer to become inconsistent. |
| */ |
| @ComposeCompilerApi |
| fun endToMarker(marker: Int) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Schedule [block] to called with [value]. This is intended to update the node generated by |
| * [createNode] to changes discovered by composition. |
| * |
| * @param value the new value to be set into some property of the node. |
| * @param block the block that sets the some property of the node to [value]. |
| */ |
| @ComposeCompilerApi |
| fun <V, T> apply(value: V, block: T.(V) -> Unit) |
| |
| // State |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Produce an object that will compare equal an iff [left] and [right] compare equal to |
| * some [left] and [right] of a previous call to [joinKey]. This is used by [key] to handle |
| * multiple parameters. Since the previous composition stored [left] and [right] in a "join |
| * key" object this call is used to return the previous value without an allocation instead |
| * of blindly creating a new value that will be immediately discarded. |
| * |
| * @param left the first part of a a joined key. |
| * @param right the second part of a joined key. |
| * @return an object that will compare equal to a value previously returned by [joinKey] iff |
| * [left] and [right] compare equal to the [left] and [right] passed to the previous call. |
| */ |
| @ComposeCompilerApi |
| fun joinKey(left: Any?, right: Any?): Any |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Remember a value into the composition state. This is a primitive method used to implement |
| * [remember]. |
| * |
| * @return [Composer.Empty] when [inserting] is `true` or the value passed to |
| * [updateRememberedValue] |
| * from the previous composition. |
| * |
| * @see cache |
| */ |
| @ComposeCompilerApi |
| fun rememberedValue(): Any? |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Update the remembered value correspond to the previous call to [rememberedValue]. The |
| * [value] will be returned by [rememberedValue] for the next composition. |
| */ |
| @ComposeCompilerApi |
| fun updateRememberedValue(value: Any?) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Any?): Boolean |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Boolean): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Char): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Byte): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Short): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Int): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Float): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Long): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition. This is used, |
| * for example, to check parameter values to determine if they have changed. |
| * |
| * This overload is provided to avoid boxing [value] to compare with a potentially boxed |
| * version of [value] in the composition state. |
| * |
| * @param value the value to check |
| * @return `true` if the value if [equals] of the previous value returns `false` when passed |
| * [value]. |
| */ |
| @ComposeCompilerApi |
| fun changed(value: Double): Boolean = changed(value) |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Check [value] is different than the value used in the previous composition using `===` |
| * instead of `==` equality. This is used, for example, to check parameter values to determine |
| * if they have changed for values that use value equality but, for correct behavior, the |
| * composer needs reference equality. |
| * |
| * @param value the value to check |
| * @return `true` if the value is === equal to the previous value and returns `false` when |
| * [value] is different. |
| */ |
| @ComposeCompilerApi |
| fun changedInstance(value: Any?): Boolean = changed(value) |
| |
| // Scopes |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Mark [scope] as used. [endReplaceableGroup] will return `null` unless [recordUsed] is |
| * called on the corresponding [scope]. This is called implicitly when [State] objects are |
| * read during composition is called when [currentRecomposeScope] is called in the |
| * [Composable] function. |
| */ |
| @InternalComposeApi |
| fun recordUsed(scope: RecomposeScope) |
| |
| // Internal API |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Record a function to call when changes to the corresponding tree are applied to the |
| * [applier]. This is used to implement [SideEffect]. |
| * |
| * @param effect a lambda to invoke after the changes calculated up to this point have been |
| * applied. |
| */ |
| @InternalComposeApi |
| fun recordSideEffect(effect: () -> Unit) |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Return the [CompositionLocal] value associated with [key]. This is the primitive function |
| * used to implement [CompositionLocal.current]. |
| * |
| * @param key the [CompositionLocal] value to be retrieved. |
| */ |
| @InternalComposeApi |
| fun <T> consume(key: CompositionLocal<T>): T |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Provide the given values for the associated [CompositionLocal] keys. This is the primitive |
| * function used to implement [CompositionLocalProvider]. |
| * |
| * @param values an array of value to provider key pairs. |
| */ |
| @InternalComposeApi |
| fun startProviders(values: Array<out ProvidedValue<*>>) |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * End the provider group. |
| * |
| * @see startProviders |
| */ |
| @InternalComposeApi |
| fun endProviders() |
| |
| /** |
| * A tooling API function. DO NOT call directly. |
| * |
| * The data stored for the composition. This is used by Compose tools, such as the preview and |
| * the inspector, to display or interpret the result of composition. |
| */ |
| val compositionData: CompositionData |
| |
| /** |
| * A tooling API function. DO NOT call directly. |
| * |
| * Called by the inspector to inform the composer that it should collect additional |
| * information about call parameters. By default, only collect parameter information for |
| * scopes that are [recordUsed] has been called on. If [collectParameterInformation] is called |
| * it will attempt to collect all calls even if the runtime doesn't need them. |
| * |
| * WARNING: calling this will result in a significant number of additional allocations that are |
| * typically avoided. |
| */ |
| fun collectParameterInformation() |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Build a composition context that can be used to created a subcomposition. A composition |
| * reference is used to communicate information from this composition to the subcompositions |
| * such as the all the [CompositionLocal]s provided at the point the reference is created. |
| */ |
| @InternalComposeApi |
| fun buildContext(): CompositionContext |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * The coroutine context for the composition. This is used, for example, to implement |
| * [LaunchedEffect]. This context is managed by the [Recomposer]. |
| */ |
| @InternalComposeApi |
| val applyCoroutineContext: CoroutineContext |
| @TestOnly |
| get |
| |
| /** |
| * The composition that is used to control this composer. |
| */ |
| val composition: ControlledComposition |
| @TestOnly get |
| |
| fun disableSourceInformation() |
| |
| companion object { |
| /** |
| * A special value used to represent no value was stored (e.g. an empty slot). This is |
| * returned, for example by [Composer.rememberedValue] while it is [Composer.inserting] |
| * is `true`. |
| */ |
| val Empty = object { |
| override fun toString() = "Empty" |
| } |
| |
| /** |
| * Internal API for specifying a tracer used for instrumenting frequent |
| * operations, e.g. recompositions. |
| */ |
| @InternalComposeTracingApi |
| fun setTracer(tracer: CompositionTracer) { |
| compositionTracer = tracer |
| } |
| } |
| } |
| |
| /** |
| * A Compose compiler plugin API. DO NOT call directly. |
| * |
| * Cache, that is remember, a value in the composition data of a composition. This is used to |
| * implement [remember] and used by the compiler plugin to generate more efficient calls to |
| * [remember] when it determines these optimizations are safe. |
| */ |
| @ComposeCompilerApi |
| inline fun <T> Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T { |
| @Suppress("UNCHECKED_CAST") |
| return rememberedValue().let { |
| if (invalid || it === Composer.Empty) { |
| val value = block() |
| updateRememberedValue(value) |
| value |
| } else it |
| } as T |
| } |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Records source information that can be used for tooling to determine the source location of |
| * the corresponding composable function. By default, this function is declared as having no |
| * side-effects. It is safe for code shrinking tools (such as R8 or ProGuard) to remove it. |
| */ |
| @ComposeCompilerApi |
| fun sourceInformation(composer: Composer, sourceInformation: String) { |
| composer.sourceInformation(sourceInformation) |
| } |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Records the start of a source information marker that can be used for tooling to determine the |
| * source location of the corresponding composable function that otherwise don't require tracking |
| * information such as [ReadOnlyComposable] functions. By default, this function is declared as |
| * having no side-effects. It is safe for code shrinking tools (such as R8 or ProGuard) to remove |
| * it. |
| * |
| * Important that both [sourceInformationMarkerStart] and [sourceInformationMarkerEnd] are removed |
| * together or both kept. Removing only one will cause incorrect runtime behavior. |
| */ |
| @ComposeCompilerApi |
| fun sourceInformationMarkerStart(composer: Composer, key: Int, sourceInformation: String) { |
| composer.sourceInformationMarkerStart(key, sourceInformation) |
| } |
| |
| /** |
| * Internal tracing API. |
| * |
| * Should be called without thread synchronization with occasional information loss. |
| */ |
| @InternalComposeTracingApi |
| interface CompositionTracer { |
| fun traceEventStart(key: Int, dirty1: Int, dirty2: Int, info: String): Unit |
| fun traceEventEnd(): Unit |
| fun isTraceInProgress(): Boolean |
| } |
| |
| @OptIn(InternalComposeTracingApi::class) |
| private var compositionTracer: CompositionTracer? = null |
| |
| /** |
| * Internal tracing API. |
| * |
| * Should be called without thread synchronization with occasional information loss. |
| */ |
| @OptIn(InternalComposeTracingApi::class) |
| @ComposeCompilerApi |
| fun isTraceInProgress(): Boolean = compositionTracer.let { it != null && it.isTraceInProgress() } |
| |
| @OptIn(InternalComposeTracingApi::class) |
| @ComposeCompilerApi |
| @Deprecated( |
| message = "Use the overload with \$dirty metadata instead", |
| ReplaceWith("traceEventStart(key, dirty1, dirty2, info)"), |
| DeprecationLevel.HIDDEN |
| ) |
| fun traceEventStart(key: Int, info: String): Unit = traceEventStart(key, -1, -1, info) |
| |
| /** |
| * Internal tracing API. |
| * |
| * Should be called without thread synchronization with occasional information loss. |
| * |
| * @param dirty1 $dirty metadata: forced-recomposition and function parameters 1..10 if present |
| * @param dirty2 $dirty2 metadata: forced-recomposition and function parameters 11..20 if present |
| */ |
| @OptIn(InternalComposeTracingApi::class) |
| @ComposeCompilerApi |
| fun traceEventStart(key: Int, dirty1: Int, dirty2: Int, info: String) { |
| compositionTracer?.traceEventStart(key, dirty1, dirty2, info) |
| } |
| |
| /** |
| * Internal tracing API. |
| * |
| * Should be called without thread synchronization with occasional information loss. |
| */ |
| @OptIn(InternalComposeTracingApi::class) |
| @ComposeCompilerApi |
| fun traceEventEnd() { |
| compositionTracer?.traceEventEnd() |
| } |
| |
| /** |
| * A Compose internal function. DO NOT call directly. |
| * |
| * Records the end of a source information marker that can be used for tooling to determine the |
| * source location of the corresponding composable function that otherwise don't require tracking |
| * information such as [ReadOnlyComposable] functions. By default, this function is declared as |
| * having no side-effects. It is safe for code shrinking tools (such as R8 or ProGuard) to remove |
| * it. |
| * |
| * Important that both [sourceInformationMarkerStart] and [sourceInformationMarkerEnd] are removed |
| * together or both kept. Removing only one will cause incorrect runtime behavior. |
| */ |
| @ComposeCompilerApi |
| fun sourceInformationMarkerEnd(composer: Composer) { |
| composer.sourceInformationMarkerEnd() |
| } |
| |
| /** |
| * Implementation of a composer for a mutable tree. |
| */ |
| internal class ComposerImpl( |
| /** |
| * An adapter that applies changes to the tree using the Applier abstraction. |
| */ |
| override val applier: Applier<*>, |
| |
| /** |
| * Parent of this composition; a [Recomposer] for root-level compositions. |
| */ |
| private val parentContext: CompositionContext, |
| |
| /** |
| * The slot table to use to store composition data |
| */ |
| private val slotTable: SlotTable, |
| |
| private val abandonSet: MutableSet<RememberObserver>, |
| |
| private var changes: MutableList<Change>, |
| |
| private var lateChanges: MutableList<Change>, |
| |
| /** |
| * The composition that owns this composer |
| */ |
| override val composition: ControlledComposition |
| ) : Composer { |
| private val pendingStack = Stack<Pending?>() |
| private var pending: Pending? = null |
| private var nodeIndex: Int = 0 |
| private var nodeIndexStack = IntStack() |
| private var groupNodeCount: Int = 0 |
| private var groupNodeCountStack = IntStack() |
| private var nodeCountOverrides: IntArray? = null |
| private var nodeCountVirtualOverrides: HashMap<Int, Int>? = null |
| private var forceRecomposeScopes = false |
| private var forciblyRecompose = false |
| private var nodeExpected = false |
| private val invalidations: MutableList<Invalidation> = mutableListOf() |
| private val entersStack = IntStack() |
| private var parentProvider: CompositionLocalMap = persistentHashMapOf() |
| private val providerUpdates = HashMap<Int, CompositionLocalMap>() |
| private var providersInvalid = false |
| private val providersInvalidStack = IntStack() |
| private var reusing = false |
| private var reusingGroup = -1 |
| private var childrenComposing: Int = 0 |
| private var snapshot = currentSnapshot() |
| private var compositionToken: Int = 0 |
| private var sourceInformationEnabled = true |
| |
| private val invalidateStack = Stack<RecomposeScopeImpl>() |
| |
| internal var isComposing = false |
| private set |
| internal var isDisposed = false |
| private set |
| internal val areChildrenComposing get() = childrenComposing > 0 |
| |
| internal val hasPendingChanges: Boolean get() = changes.isNotEmpty() |
| |
| private var reader: SlotReader = slotTable.openReader().also { it.close() } |
| |
| internal var insertTable = SlotTable() |
| |
| private var writer: SlotWriter = insertTable.openWriter().also { it.close() } |
| private var writerHasAProvider = false |
| private var providerCache: CompositionLocalMap? = null |
| internal var deferredChanges: MutableList<Change>? = null |
| |
| private var insertAnchor: Anchor = insertTable.read { it.anchor(0) } |
| private val insertFixups = mutableListOf<Change>() |
| |
| override val applyCoroutineContext: CoroutineContext |
| @TestOnly get() = parentContext.effectCoroutineContext |
| |
| /** |
| * Inserts a "Replaceable Group" starting marker in the slot table at the current execution |
| * position. A Replaceable Group is a group which cannot be moved between its siblings, but |
| * can be removed or inserted. These groups are inserted by the compiler around branches of |
| * conditional logic in Composable functions such as if expressions, when expressions, early |
| * returns, and null-coalescing operators. |
| * |
| * A call to [startReplaceableGroup] must be matched with a corresponding call to |
| * [endReplaceableGroup]. |
| * |
| * Warning: This is expected to be executed by the compiler only and should not be called |
| * directly from source code. Call this API at your own risk. |
| * |
| * @param key The source-location-based key for the group. Expected to be unique among its |
| * siblings. |
| * |
| * @see [endReplaceableGroup] |
| * @see [startMovableGroup] |
| * @see [startRestartGroup] |
| */ |
| @ComposeCompilerApi |
| override fun startReplaceableGroup(key: Int) = start(key, null, false, null) |
| |
| /** |
| * Indicates the end of a "Replaceable Group" at the current execution position. A |
| * Replaceable Group is a group which cannot be moved between its siblings, but |
| * can be removed or inserted. These groups are inserted by the compiler around branches of |
| * conditional logic in Composable functions such as if expressions, when expressions, early |
| * returns, and null-coalescing operators. |
| * |
| * Warning: This is expected to be executed by the compiler only and should not be called |
| * directly from source code. Call this API at your own risk. |
| * |
| * @see [startReplaceableGroup] |
| */ |
| @ComposeCompilerApi |
| override fun endReplaceableGroup() = endGroup() |
| |
| /** |
| * |
| * Warning: This is expected to be executed by the compiler only and should not be called |
| * directly from source code. Call this API at your own risk. |
| * |
| */ |
| @ComposeCompilerApi |
| @Suppress("unused") |
| override fun startDefaults() = start(defaultsKey, null, false, null) |
| |
| /** |
| * |
| * Warning: This is expected to be executed by the compiler only and should not be called |
| * directly from source code. Call this API at your own risk. |
| * |
| * @see [startReplaceableGroup] |
| */ |
| @ComposeCompilerApi |
| @Suppress("unused") |
| override fun endDefaults() { |
| endGroup() |
| val scope = currentRecomposeScope |
| if (scope != null && scope.used) { |
| scope.defaultsInScope = true |
| } |
| } |
| |
| @ComposeCompilerApi |
| @Suppress("unused") |
| override val defaultsInvalid: Boolean |
| get() { |
| return providersInvalid || currentRecomposeScope?.defaultsInvalid == true |
| } |
| |
| /** |
| * Inserts a "Movable Group" starting marker in the slot table at the current execution |
| * position. A Movable Group is a group which can be moved or reordered between its siblings |
| * and retain slot table state, in addition to being removed or inserted. Movable Groups |
| * are more expensive than other groups because when they are encountered with a mismatched |
| * key in the slot table, they must be held on to temporarily until the entire parent group |
| * finishes execution in case it moved to a later position in the group. Movable groups are |
| * only inserted by the compiler as a result of calls to [key]. |
| * |
| * A call to [startMovableGroup] must be matched with a corresponding call to [endMovableGroup]. |
| * |
| * Warning: This is expected to be executed by the compiler only and should not be called |
| * directly from source code. Call this API at your own risk. |
| * |
| * @param key The source-location-based key for the group. Expected to be unique among its |
| * siblings. |
| * |
| * @param dataKey Additional identifying information to compound with [key]. If there are |
| * multiple values, this is expected to be compounded together with [joinKey]. Whatever value |
| * is passed in here is expected to have a meaningful [equals] and [hashCode] implementation. |
| * |
| * @see [endMovableGroup] |
| * @see [key] |
| * @see [joinKey] |
| * @see [startReplaceableGroup] |
| * @see [startRestartGroup] |
| */ |
| @ComposeCompilerApi |
| override fun startMovableGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null) |
| |
| /** |
| * Indicates the end of a "Movable Group" at the current execution position. A Movable Group is |
| * a group which can be moved or reordered between its siblings and retain slot table state, |
| * in addition to being removed or inserted. These groups are only valid when they are |
| * inserted as direct children of Container Groups. Movable Groups are more expensive than |
| * other groups because when they are encountered with a mismatched key in the slot table, |
| * they must be held on to temporarily until the entire parent group finishes execution in |
| * case it moved to a later position in the group. Movable groups are only inserted by the |
| * compiler as a result of calls to [key]. |
| * |
| * Warning: This is expected to be executed by the compiler only and should not be called |
| * directly from source code. Call this API at your own risk. |
| * |
| * @see [startMovableGroup] |
| */ |
| @ComposeCompilerApi |
| override fun endMovableGroup() = endGroup() |
| |
| /** |
| * Start the composition. This should be called, and only be called, as the first group in |
| * the composition. |
| */ |
| @OptIn(InternalComposeApi::class) |
| private fun startRoot() { |
| reader = slotTable.openReader() |
| startGroup(rootKey) |
| |
| // parent reference management |
| parentContext.startComposing() |
| parentProvider = parentContext.getCompositionLocalScope() |
| providersInvalidStack.push(providersInvalid.asInt()) |
| providersInvalid = changed(parentProvider) |
| providerCache = null |
| if (!forceRecomposeScopes) { |
| forceRecomposeScopes = parentContext.collectingParameterInformation |
| } |
| resolveCompositionLocal(LocalInspectionTables, parentProvider)?.let { |
| it.add(slotTable) |
| parentContext.recordInspectionTable(it) |
| } |
| startGroup(parentContext.compoundHashKey) |
| } |
| |
| /** |
| * End the composition. This should be called, and only be called, to end the first group in |
| * the composition. |
| */ |
| @OptIn(InternalComposeApi::class) |
| private fun endRoot() { |
| endGroup() |
| parentContext.doneComposing() |
| endGroup() |
| recordEndRoot() |
| finalizeCompose() |
| reader.close() |
| forciblyRecompose = false |
| } |
| |
| /** |
| * Discard a pending composition because an error was encountered during composition |
| */ |
| @OptIn(InternalComposeApi::class) |
| private fun abortRoot() { |
| cleanUpCompose() |
| pendingStack.clear() |
| nodeIndexStack.clear() |
| groupNodeCountStack.clear() |
| entersStack.clear() |
| providersInvalidStack.clear() |
| providerUpdates.clear() |
| if (!reader.closed) { |
| reader.close() |
| } |
| if (!writer.closed) { |
| writer.close() |
| } |
| createFreshInsertTable() |
| compoundKeyHash = 0 |
| childrenComposing = 0 |
| nodeExpected = false |
| inserting = false |
| reusing = false |
| isComposing = false |
| forciblyRecompose = false |
| } |
| |
| internal fun changesApplied() { |
| providerUpdates.clear() |
| } |
| |
| /** |
| * True if the composition is currently scheduling nodes to be inserted into the tree. During |
| * first composition this is always true. During recomposition this is true when new nodes |
| * are being scheduled to be added to the tree. |
| */ |
| @ComposeCompilerApi |
| override var inserting: Boolean = false |
| private set |
| |
| /** |
| * True if the composition should be checking if the composable functions can be skipped. |
| */ |
| @ComposeCompilerApi |
| override val skipping: Boolean |
| get() { |
| return !inserting && !reusing && |
| !providersInvalid && |
| currentRecomposeScope?.requiresRecompose == false && |
| !forciblyRecompose |
| } |
| |
| /** |
| * Returns the hash of the compound key calculated as a combination of the keys of all the |
| * currently started groups via [startGroup]. |
| */ |
| @InternalComposeApi |
| override var compoundKeyHash: Int = 0 |
| private set |
| |
| /** |
| * Start collecting parameter information. This enables the tools API to always be able to |
| * determine the parameter values of composable calls. |
| */ |
| override fun collectParameterInformation() { |
| forceRecomposeScopes = true |
| } |
| |
| @OptIn(InternalComposeApi::class) |
| internal fun dispose() { |
| trace("Compose:Composer.dispose") { |
| parentContext.unregisterComposer(this) |
| invalidateStack.clear() |
| invalidations.clear() |
| changes.clear() |
| providerUpdates.clear() |
| applier.clear() |
| isDisposed = true |
| } |
| } |
| |
| internal fun forceRecomposeScopes(): Boolean { |
| return if (!forceRecomposeScopes) { |
| forceRecomposeScopes = true |
| forciblyRecompose = true |
| true |
| } else { |
| false |
| } |
| } |
| |
| /** |
| * Start a group with the given key. During recomposition if the currently expected group does |
| * not match the given key a group the groups emitted in the same parent group are inspected |
| * to determine if one of them has this key and that group the first such group is moved |
| * (along with any nodes emitted by the group) to the current position and composition |
| * continues. If no group with this key is found, then the composition shifts into insert |
| * mode and new nodes are added at the current position. |
| * |
| * @param key The key for the group |
| */ |
| private fun startGroup(key: Int) = start(key, null, false, null) |
| |
| private fun startGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null) |
| |
| /** |
| * End the current group. |
| */ |
| private fun endGroup() = end(isNode = false) |
| |
| @OptIn(InternalComposeApi::class) |
| private fun skipGroup() { |
| groupNodeCount += reader.skipGroup() |
| } |
| |
| /** |
| * Start emitting a node. It is required that [createNode] is called after [startNode]. |
| * Similar to [startGroup], if, during recomposition, the current node does not have the |
| * provided key a node with that key is scanned for and moved into the current position if |
| * found, if no such node is found the composition switches into insert mode and a the node |
| * is scheduled to be inserted at the current location. |
| */ |
| override fun startNode() { |
| val key = if (inserting) nodeKey |
| else if (reusing) |
| if (reader.groupKey == nodeKey) nodeKeyReplace else nodeKey |
| else if (reader.groupKey == nodeKeyReplace) nodeKeyReplace |
| else nodeKey |
| start(key, null, true, null) |
| nodeExpected = true |
| } |
| |
| override fun startReusableNode() { |
| start(nodeKey, null, true, null) |
| nodeExpected = true |
| } |
| |
| /** |
| * Schedule a node to be created and inserted at the current location. This is only valid to |
| * call when the composer is inserting. |
| */ |
| @Suppress("UNUSED") |
| override fun <T> createNode(factory: () -> T) { |
| validateNodeExpected() |
| runtimeCheck(inserting) { "createNode() can only be called when inserting" } |
| val insertIndex = nodeIndexStack.peek() |
| val groupAnchor = writer.anchor(writer.parent) |
| groupNodeCount++ |
| recordFixup { applier, slots, _ -> |
| @Suppress("UNCHECKED_CAST") |
| val node = factory() |
| slots.updateNode(groupAnchor, node) |
| @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T> |
| nodeApplier.insertTopDown(insertIndex, node) |
| applier.down(node) |
| } |
| recordInsertUpFixup { applier, slots, _ -> |
| @Suppress("UNCHECKED_CAST") |
| val nodeToInsert = slots.node(groupAnchor) |
| applier.up() |
| @Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<Any?> |
| nodeApplier.insertBottomUp(insertIndex, nodeToInsert) |
| } |
| } |
| |
| /** |
| * Mark the node that was created by [createNode] as used by composition. |
| */ |
| @OptIn(InternalComposeApi::class) |
| override fun useNode() { |
| validateNodeExpected() |
| runtimeCheck(!inserting) { "useNode() called while inserting" } |
| recordDown(reader.node) |
| } |
| |
| /** |
| * Called to end the node group. |
| */ |
| override fun endNode() = end(isNode = true) |
| |
| override fun startReusableGroup(key: Int, dataKey: Any?) { |
| if (reader.groupKey == key && reader.groupAux != dataKey && reusingGroup < 0) { |
| // Starting to reuse nodes |
| reusingGroup = reader.currentGroup |
| reusing = true |
| } |
| start(key, null, false, dataKey) |
| } |
| |
| override fun endReusableGroup() { |
| if (reusing && reader.parent == reusingGroup) { |
| reusingGroup = -1 |
| reusing = false |
| } |
| end(isNode = false) |
| } |
| |
| override fun disableReusing() { |
| reusing = false |
| } |
| |
| override fun enableReusing() { |
| reusing = reusingGroup >= 0 |
| } |
| |
| override val currentMarker: Int |
| get() = if (inserting) -writer.parent else reader.parent |
| |
| override fun endToMarker(marker: Int) { |
| if (marker < 0) { |
| val writerLocation = -marker |
| while (writer.parent > writerLocation) end(false) |
| } else { |
| while (reader.parent > marker) end(false) |
| } |
| } |
| |
| /** |
| * Schedule a change to be applied to a node's property. This change will be applied to the |
| * node that is the current node in the tree which was either created by [createNode]. |
| */ |
| override fun <V, T> apply(value: V, block: T.(V) -> Unit) { |
| val operation: Change = { applier, _, _ -> |
| @Suppress("UNCHECKED_CAST") |
| (applier.current as T).block(value) |
| } |
| if (inserting) recordFixup(operation) |
| else recordApplierOperation(operation) |
| } |
| |
| /** |
| * Create a composed key that can be used in calls to [startGroup] or [startNode]. This will |
| * use the key stored at the current location in the slot table to avoid allocating a new key. |
| */ |
| @ComposeCompilerApi |
| @OptIn(InternalComposeApi::class) |
| override fun joinKey(left: Any?, right: Any?): Any = |
| getKey(reader.groupObjectKey, left, right) ?: JoinedKey(left, right) |
| |
| /** |
| * Return the next value in the slot table and advance the current location. |
| */ |
| @PublishedApi |
| @OptIn(InternalComposeApi::class) |
| internal fun nextSlot(): Any? = if (inserting) { |
| validateNodeNotExpected() |
| Composer.Empty |
| } else reader.next().let { if (reusing) Composer.Empty else it } |
| |
| /** |
| * Determine if the current slot table value is equal to the given value, if true, the value |
| * is scheduled to be skipped during [ControlledComposition.applyChanges] and [changes] return |
| * false; otherwise [ControlledComposition.applyChanges] will update the slot table to [value]. |
| * In either case the composer's slot table is advanced. |
| * |
| * @param value the value to be compared. |
| */ |
| @ComposeCompilerApi |
| override fun changed(value: Any?): Boolean { |
| return if (nextSlot() != value) { |
| updateValue(value) |
| true |
| } else { |
| false |
| } |
| } |
| |
| @ComposeCompilerApi |
| override fun changedInstance(value: Any?): Boolean { |
| return if (nextSlot() !== value) { |
| updateValue(value) |
| true |
| } else { |
| false |
| } |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Char): Boolean { |
| val next = nextSlot() |
| if (next is Char) { |
| val nextPrimitive: Char = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Byte): Boolean { |
| val next = nextSlot() |
| if (next is Byte) { |
| val nextPrimitive: Byte = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Short): Boolean { |
| val next = nextSlot() |
| if (next is Short) { |
| val nextPrimitive: Short = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Boolean): Boolean { |
| val next = nextSlot() |
| if (next is Boolean) { |
| val nextPrimitive: Boolean = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Float): Boolean { |
| val next = nextSlot() |
| if (next is Float) { |
| val nextPrimitive: Float = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Long): Boolean { |
| val next = nextSlot() |
| if (next is Long) { |
| val nextPrimitive: Long = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Double): Boolean { |
| val next = nextSlot() |
| if (next is Double) { |
| val nextPrimitive: Double = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| @ComposeCompilerApi |
| override fun changed(value: Int): Boolean { |
| val next = nextSlot() |
| if (next is Int) { |
| val nextPrimitive: Int = next |
| if (value == nextPrimitive) return false |
| } |
| updateValue(value) |
| return true |
| } |
| |
| /** |
| * Cache a value in the composition. During initial composition [block] is called to produce the |
| * value that is then stored in the slot table. During recomposition, if [invalid] is false |
| * the value is obtained from the slot table and [block] is not invoked. If [invalid] is |
| * false a new value is produced by calling [block] and the slot table is updated to contain |
| * the new value. |
| */ |
| @ComposeCompilerApi |
| inline fun <T> cache(invalid: Boolean, block: () -> T): T { |
| var result = nextSlot() |
| if (result === Composer.Empty || invalid) { |
| val value = block() |
| updateValue(value) |
| result = value |
| } |
| |
| @Suppress("UNCHECKED_CAST") |
| return result as T |
| } |
| |
| /** |
| * Schedule the current value in the slot table to be updated to [value]. |
| * |
| * @param value the value to schedule to be written to the slot table. |
| */ |
| @PublishedApi |
| @OptIn(InternalComposeApi::class) |
| internal fun updateValue(value: Any?) { |
| if (inserting) { |
| writer.update(value) |
| if (value is RememberObserver) { |
| record { _, _, rememberManager -> rememberManager.remembering(value) } |
| abandonSet.add(value) |
| } |
| } else { |
| val groupSlotIndex = reader.groupSlotIndex - 1 |
| if (value is RememberObserver) { |
| abandonSet.add(value) |
| } |
| recordSlotTableOperation(forParent = true) { _, slots, rememberManager -> |
| if (value is RememberObserver) { |
| rememberManager.remembering(value) |
| } |
| when (val previous = slots.set(groupSlotIndex, value)) { |
| is RememberObserver -> |
| rememberManager.forgetting(previous) |
| is RecomposeScopeImpl -> { |
| val composition = previous.composition |
| if (composition != null) { |
| previous.release() |
| composition.pendingInvalidScopes = true |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Schedule the current value in the slot table to be updated to [value]. |
| * |
| * @param value the value to schedule to be written to the slot table. |
| */ |
| @PublishedApi |
| @OptIn(InternalComposeApi::class) |
| internal fun updateCachedValue(value: Any?) { |
| updateValue(value) |
| } |
| |
| override val compositionData: CompositionData get() = slotTable |
| |
| /** |
| * Schedule a side effect to run when we apply composition changes. |
| */ |
| override fun recordSideEffect(effect: () -> Unit) { |
| record { _, _, rememberManager -> rememberManager.sideEffect(effect) } |
| } |
| |
| /** |
| * Return the current [CompositionLocal] scope which was provided by a parent group. |
| */ |
| private fun currentCompositionLocalScope(group: Int? = null): CompositionLocalMap { |
| if (group == null) |
| providerCache?.let { return it } |
| if (inserting && writerHasAProvider) { |
| var current = writer.parent |
| while (current > 0) { |
| if (writer.groupKey(current) == compositionLocalMapKey && |
| writer.groupObjectKey(current) == compositionLocalMap |
| ) { |
| @Suppress("UNCHECKED_CAST") |
| val providers = writer.groupAux(current) as CompositionLocalMap |
| providerCache = providers |
| return providers |
| } |
| current = writer.parent(current) |
| } |
| } |
| if (reader.size > 0) { |
| var current = group ?: reader.parent |
| while (current > 0) { |
| if (reader.groupKey(current) == compositionLocalMapKey && |
| reader.groupObjectKey(current) == compositionLocalMap |
| ) { |
| @Suppress("UNCHECKED_CAST") |
| val providers = providerUpdates[current] |
| ?: reader.groupAux(current) as CompositionLocalMap |
| providerCache = providers |
| return providers |
| } |
| current = reader.parent(current) |
| } |
| } |
| providerCache = parentProvider |
| return parentProvider |
| } |
| |
| /** |
| * Update (or create) the slots to record the providers. The providers maps are first the |
| * scope followed by the map used to augment the parent scope. Both are needed to detect |
| * inserts, updates and deletes to the providers. |
| */ |
| private fun updateProviderMapGroup( |
| parentScope: CompositionLocalMap, |
| currentProviders: CompositionLocalMap |
| ): CompositionLocalMap { |
| val providerScope = parentScope.mutate { it.putAll(currentProviders) } |
| startGroup(providerMapsKey, providerMaps) |
| changed(providerScope) |
| changed(currentProviders) |
| endGroup() |
| return providerScope |
| } |
| |
| @InternalComposeApi |
| override fun startProviders(values: Array<out ProvidedValue<*>>) { |
| val parentScope = currentCompositionLocalScope() |
| startGroup(providerKey, provider) |
| // The group is needed here because compositionLocalMapOf() might change the number or |
| // kind of slots consumed depending on the content of values to remember, for example, the |
| // value holders used last time. |
| startGroup(providerValuesKey, providerValues) |
| val currentProviders = invokeComposableForResult(this) { |
| compositionLocalMapOf(values, parentScope) |
| } |
| endGroup() |
| val providers: CompositionLocalMap |
| val invalid: Boolean |
| if (inserting) { |
| providers = updateProviderMapGroup(parentScope, currentProviders) |
| invalid = false |
| writerHasAProvider = true |
| } else { |
| @Suppress("UNCHECKED_CAST") |
| val oldScope = reader.groupGet(0) as CompositionLocalMap |
| |
| @Suppress("UNCHECKED_CAST") |
| val oldValues = reader.groupGet(1) as CompositionLocalMap |
| |
| // skipping is true iff parentScope has not changed. |
| if (!skipping || oldValues != currentProviders) { |
| providers = updateProviderMapGroup(parentScope, currentProviders) |
| |
| // Compare against the old scope as currentProviders might have modified the scope |
| // back to the previous value. This could happen, for example, if currentProviders |
| // and parentScope have a key in common and the oldScope had the same value as |
| // currentProviders for that key. If the scope has not changed, because these |
| // providers obscure a change in the parent as described above, re-enable skipping |
| // for the child region. |
| invalid = providers != oldScope |
| } else { |
| // Nothing has changed |
| skipGroup() |
| providers = oldScope |
| invalid = false |
| } |
| } |
| |
| if (invalid && !inserting) { |
| providerUpdates[reader.currentGroup] = providers |
| } |
| providersInvalidStack.push(providersInvalid.asInt()) |
| providersInvalid = invalid |
| providerCache = providers |
| start(compositionLocalMapKey, compositionLocalMap, false, providers) |
| } |
| |
| @InternalComposeApi |
| override fun endProviders() { |
| endGroup() |
| endGroup() |
| providersInvalid = providersInvalidStack.pop().asBool() |
| providerCache = null |
| } |
| |
| @InternalComposeApi |
| override fun <T> consume(key: CompositionLocal<T>): T = |
| resolveCompositionLocal(key, currentCompositionLocalScope()) |
| |
| /** |
| * Create or use a memoized [CompositionContext] instance at this position in the slot table. |
| */ |
| override fun buildContext(): CompositionContext { |
| startGroup(referenceKey, reference) |
| if (inserting) |
| writer.markGroup() |
| |
| var holder = nextSlot() as? CompositionContextHolder |
| if (holder == null) { |
| holder = CompositionContextHolder( |
| CompositionContextImpl( |
| compoundKeyHash, |
| forceRecomposeScopes |
| ) |
| ) |
| updateValue(holder) |
| } |
| holder.ref.updateCompositionLocalScope(currentCompositionLocalScope()) |
| endGroup() |
| |
| return holder.ref |
| } |
| |
| private fun <T> resolveCompositionLocal( |
| key: CompositionLocal<T>, |
| scope: CompositionLocalMap |
| ): T = if (scope.contains(key)) { |
| scope.getValueOf(key) |
| } else { |
| key.defaultValueHolder.value |
| } |
| |
| /** |
| * The number of changes that have been scheduled to be applied during |
| * [ControlledComposition.applyChanges]. |
| * |
| * Slot table movement (skipping groups and nodes) will be coalesced so this number is |
| * possibly less than the total changes detected. |
| */ |
| internal val changeCount get() = changes.size |
| |
| internal val currentRecomposeScope: RecomposeScopeImpl? |
| get() = invalidateStack.let { |
| if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null |
| } |
| |
| private fun ensureWriter() { |
| if (writer.closed) { |
| writer = insertTable.openWriter() |
| // Append to the end of the table |
| writer.skipToGroupEnd() |
| writerHasAProvider = false |
| providerCache = null |
| } |
| } |
| |
| private fun createFreshInsertTable() { |
| runtimeCheck(writer.closed) |
| insertTable = SlotTable() |
| writer = insertTable.openWriter().also { it.close() } |
| } |
| |
| /** |
| * Start the reader group updating the data of the group if necessary |
| */ |
| private fun startReaderGroup(isNode: Boolean, data: Any?) { |
| if (isNode) { |
| reader.startNode() |
| } else { |
| if (data != null && reader.groupAux !== data) { |
| recordSlotTableOperation { _, slots, _ -> |
| slots.updateAux(data) |
| } |
| } |
| reader.startGroup() |
| } |
| } |
| |
| private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) { |
| validateNodeNotExpected() |
| |
| updateCompoundKeyWhenWeEnterGroup(key, objectKey, data) |
| |
| // Check for the insert fast path. If we are already inserting (creating nodes) then |
| // there is no need to track insert, deletes and moves with a pending changes object. |
| if (inserting) { |
| reader.beginEmpty() |
| val startIndex = writer.currentGroup |
| when { |
| isNode -> writer.startNode(Composer.Empty) |
| data != null -> writer.startData(key, objectKey ?: Composer.Empty, data) |
| else -> writer.startGroup(key, objectKey ?: Composer.Empty) |
| } |
| pending?.let { pending -> |
| val insertKeyInfo = KeyInfo( |
| key = key, |
| objectKey = -1, |
| location = insertedGroupVirtualIndex(startIndex), |
| nodes = -1, |
| index = 0 |
| ) |
| pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex) |
| pending.recordUsed(insertKeyInfo) |
| } |
| enterGroup(isNode, null) |
| return |
| } |
| |
| if (pending == null) { |
| val slotKey = reader.groupKey |
| if (slotKey == key && objectKey == reader.groupObjectKey) { |
| // The group is the same as what was generated last time. |
| startReaderGroup(isNode, data) |
| } else { |
| pending = Pending( |
| reader.extractKeys(), |
| nodeIndex |
| ) |
| } |
| } |
| |
| val pending = pending |
| var newPending: Pending? = null |
| if (pending != null) { |
| // Check to see if the key was generated last time from the keys collected above. |
| val keyInfo = pending.getNext(key, objectKey) |
| if (keyInfo != null) { |
| // This group was generated last time, use it. |
| pending.recordUsed(keyInfo) |
| |
| // Move the slot table to the location where the information about this group is |
| // stored. The slot information will move once the changes are applied so moving the |
| // current of the slot table is sufficient. |
| val location = keyInfo.location |
| |
| // Determine what index this group is in. This is used for inserting nodes into the |
| // group. |
| nodeIndex = pending.nodePositionOf(keyInfo) + pending.startIndex |
| |
| // Determine how to move the slot group to the correct position. |
| val relativePosition = pending.slotPositionOf(keyInfo) |
| val currentRelativePosition = relativePosition - pending.groupIndex |
| pending.registerMoveSlot(relativePosition, pending.groupIndex) |
| recordReaderMoving(location) |
| reader.reposition(location) |
| if (currentRelativePosition > 0) { |
| // The slot group must be moved, record the move to be performed during apply. |
| recordSlotEditingOperation { _, slots, _ -> |
| slots.moveGroup(currentRelativePosition) |
| } |
| } |
| startReaderGroup(isNode, data) |
| } else { |
| // The group is new, go into insert mode. All child groups will written to the |
| // insertTable until the group is complete which will schedule the groups to be |
| // inserted into in the table. |
| reader.beginEmpty() |
| inserting = true |
| providerCache = null |
| ensureWriter() |
| writer.beginInsert() |
| val startIndex = writer.currentGroup |
| when { |
| isNode -> writer.startNode(Composer.Empty) |
| data != null -> writer.startData(key, objectKey ?: Composer.Empty, data) |
| else -> writer.startGroup(key, objectKey ?: Composer.Empty) |
| } |
| insertAnchor = writer.anchor(startIndex) |
| val insertKeyInfo = KeyInfo( |
| key = key, |
| objectKey = -1, |
| location = insertedGroupVirtualIndex(startIndex), |
| nodes = -1, |
| index = 0 |
| ) |
| pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex) |
| pending.recordUsed(insertKeyInfo) |
| newPending = Pending( |
| mutableListOf(), |
| if (isNode) 0 else nodeIndex |
| ) |
| } |
| } |
| |
| enterGroup(isNode, newPending) |
| } |
| |
| private fun enterGroup(isNode: Boolean, newPending: Pending?) { |
| // When entering a group all the information about the parent should be saved, to be |
| // restored when end() is called, and all the tracking counters set to initial state for the |
| // group. |
| pendingStack.push(pending) |
| this.pending = newPending |
| this.nodeIndexStack.push(nodeIndex) |
| if (isNode) nodeIndex = 0 |
| this.groupNodeCountStack.push(groupNodeCount) |
| groupNodeCount = 0 |
| } |
| |
| private fun exitGroup(expectedNodeCount: Int, inserting: Boolean) { |
| // Restore the parent's state updating them if they have changed based on changes in the |
| // children. For example, if a group generates nodes then the number of generated nodes will |
| // increment the node index and the group's node count. If the parent is tracking structural |
| // changes in pending then restore that too. |
| val previousPending = pendingStack.pop() |
| if (previousPending != null && !inserting) { |
| previousPending.groupIndex++ |
| } |
| this.pending = previousPending |
| this.nodeIndex = nodeIndexStack.pop() + expectedNodeCount |
| this.groupNodeCount = this.groupNodeCountStack.pop() + expectedNodeCount |
| } |
| |
| private fun end(isNode: Boolean) { |
| // All the changes to the group (or node) have been recorded. All new nodes have been |
| // inserted but it has yet to determine which need to be removed or moved. Note that the |
| // changes are relative to the first change in the list of nodes that are changing. |
| |
| if (inserting) { |
| val parent = writer.parent |
| updateCompoundKeyWhenWeExitGroup( |
| writer.groupKey(parent), |
| writer.groupObjectKey(parent), |
| writer.groupAux(parent) |
| ) |
| } else { |
| val parent = reader.parent |
| updateCompoundKeyWhenWeExitGroup( |
| reader.groupKey(parent), |
| reader.groupObjectKey(parent), |
| reader.groupAux(parent) |
| ) |
| } |
| var expectedNodeCount = groupNodeCount |
| val pending = pending |
| if (pending != null && pending.keyInfos.size > 0) { |
| // previous contains the list of keys as they were generated in the previous composition |
| val previous = pending.keyInfos |
| |
| // current contains the list of keys in the order they need to be in the new composition |
| val current = pending.used |
| |
| // usedKeys contains the keys that were used in the new composition, therefore if a key |
| // doesn't exist in this set, it needs to be removed. |
| val usedKeys = current.fastToSet() |
| |
| val placedKeys = mutableSetOf<KeyInfo>() |
| var currentIndex = 0 |
| val currentEnd = current.size |
| var previousIndex = 0 |
| val previousEnd = previous.size |
| |
| // Traverse the list of changes to determine startNode movement |
| var nodeOffset = 0 |
| while (previousIndex < previousEnd) { |
| val previousInfo = previous[previousIndex] |
| if (!usedKeys.contains(previousInfo)) { |
| // If the key info was not used the group was deleted, remove the nodes in the |
| // group |
| val deleteOffset = pending.nodePositionOf(previousInfo) |
| recordRemoveNode(deleteOffset + pending.startIndex, previousInfo.nodes) |
| pending.updateNodeCount(previousInfo.location, 0) |
| recordReaderMoving(previousInfo.location) |
| reader.reposition(previousInfo.location) |
| recordDelete() |
| reader.skipGroup() |
| |
| // Remove any invalidations pending for the group being removed. These are no |
| // longer part of the composition. The group being composed is one after the |
| // start of the group. |
| invalidations.removeRange( |
| previousInfo.location, |
| previousInfo.location + reader.groupSize(previousInfo.location) |
| ) |
| previousIndex++ |
| continue |
| } |
| |
| if (previousInfo in placedKeys) { |
| // If the group was already placed in the correct location, skip it. |
| previousIndex++ |
| continue |
| } |
| |
| if (currentIndex < currentEnd) { |
| // At this point current should match previous unless the group is new or was |
| // moved. |
| val currentInfo = current[currentIndex] |
| if (currentInfo !== previousInfo) { |
| val nodePosition = pending.nodePositionOf(currentInfo) |
| placedKeys.add(currentInfo) |
| if (nodePosition != nodeOffset) { |
| val updatedCount = pending.updatedNodeCountOf(currentInfo) |
| recordMoveNode( |
| nodePosition + pending.startIndex, |
| nodeOffset + pending.startIndex, updatedCount |
| ) |
| pending.registerMoveNode(nodePosition, nodeOffset, updatedCount) |
| } // else the nodes are already in the correct position |
| } else { |
| // The correct nodes are in the right location |
| previousIndex++ |
| } |
| currentIndex++ |
| nodeOffset += pending.updatedNodeCountOf(currentInfo) |
| } |
| } |
| |
| // If there are any current nodes left they where inserted into the right location |
| // when the group began so the rest are ignored. |
| realizeMovement() |
| |
| // We have now processed the entire list so move the slot table to the end of the list |
| // by moving to the last key and skipping it. |
| if (previous.size > 0) { |
| recordReaderMoving(reader.groupEnd) |
| reader.skipToGroupEnd() |
| } |
| } |
| |
| // Detect removing nodes at the end. No pending is created in this case we just have more |
| // nodes in the previous composition than we expect (i.e. we are not yet at an end) |
| val removeIndex = nodeIndex |
| while (!reader.isGroupEnd) { |
| val startSlot = reader.currentGroup |
| recordDelete() |
| val nodesToRemove = reader.skipGroup() |
| recordRemoveNode(removeIndex, nodesToRemove) |
| invalidations.removeRange(startSlot, reader.currentGroup) |
| } |
| |
| val inserting = inserting |
| if (inserting) { |
| if (isNode) { |
| registerInsertUpFixup() |
| expectedNodeCount = 1 |
| } |
| reader.endEmpty() |
| val parentGroup = writer.parent |
| writer.endGroup() |
| if (!reader.inEmpty) { |
| val virtualIndex = insertedGroupVirtualIndex(parentGroup) |
| writer.endInsert() |
| writer.close() |
| recordInsert(insertAnchor) |
| this.inserting = false |
| if (!slotTable.isEmpty) { |
| updateNodeCount(virtualIndex, 0) |
| updateNodeCountOverrides(virtualIndex, expectedNodeCount) |
| } |
| } |
| } else { |
| if (isNode) recordUp() |
| recordEndGroup() |
| val parentGroup = reader.parent |
| val parentNodeCount = updatedNodeCount(parentGroup) |
| if (expectedNodeCount != parentNodeCount) { |
| updateNodeCountOverrides(parentGroup, expectedNodeCount) |
| } |
| if (isNode) { |
| expectedNodeCount = 1 |
| } |
| reader.endGroup() |
| realizeMovement() |
| } |
| |
| exitGroup(expectedNodeCount, inserting) |
| } |
| |
| /** |
| * Recompose any invalidate child groups of the current parent group. This should be called |
| * after the group is started but on or before the first child group. It is intended to be |
| * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children |
| * are invalid it will call [skipReaderToGroupEnd]. |
| */ |
| private fun recomposeToGroupEnd() { |
| val wasComposing = isComposing |
| isComposing = true |
| var recomposed = false |
| |
| val parent = reader.parent |
| val end = parent + reader.groupSize(parent) |
| val recomposeIndex = nodeIndex |
| val recomposeCompoundKey = compoundKeyHash |
| val oldGroupNodeCount = groupNodeCount |
| var oldGroup = parent |
| |
| var firstInRange = invalidations.firstInRange(reader.currentGroup, end) |
| while (firstInRange != null) { |
| val location = firstInRange.location |
| |
| invalidations.removeLocation(location) |
| |
| if (firstInRange.isInvalid()) { |
| recomposed = true |
| |
| reader.reposition(location) |
| val newGroup = reader.currentGroup |
| // Record the changes to the applier location |
| recordUpsAndDowns(oldGroup, newGroup, parent) |
| oldGroup = newGroup |
| |
| // Calculate the node index (the distance index in the node this groups nodes are |
| // located in the parent node). |
| nodeIndex = nodeIndexOf( |
| location, |
| newGroup, |
| parent, |
| recomposeIndex |
| ) |
| |
| // Calculate the compound hash code (a semi-unique code for every group in the |
| // composition used to restore saved state). |
| compoundKeyHash = compoundKeyOf( |
| reader.parent(newGroup), |
| parent, |
| recomposeCompoundKey |
| ) |
| |
| // We have moved so the cached lookup of the provider is invalid |
| providerCache = null |
| |
| // Invoke the scope's composition function |
| firstInRange.scope.compose(this) |
| |
| // We could have moved out of a provider so the provider cache is invalid. |
| providerCache = null |
| |
| // Restore the parent of the reader to the previous parent |
| reader.restoreParent(parent) |
| } else { |
| // If the invalidation is not used restore the reads that were removed when the |
| // the invalidation was recorded. This happens, for example, when on of a derived |
| // state's dependencies changed but the derived state itself was not changed. |
| invalidateStack.push(firstInRange.scope) |
| firstInRange.scope.rereadTrackedInstances() |
| invalidateStack.pop() |
| } |
| |
| // Using slots.current here ensures composition always walks forward even if a component |
| // before the current composition is invalidated when performing this composition. Any |
| // such components will be considered invalid for the next composition. Skipping them |
| // prevents potential infinite recomposes at the cost of potentially missing a compose |
| // as well as simplifies the apply as it always modifies the slot table in a forward |
| // direction. |
| firstInRange = invalidations.firstInRange(reader.currentGroup, end) |
| } |
| |
| if (recomposed) { |
| recordUpsAndDowns(oldGroup, parent, parent) |
| reader.skipToGroupEnd() |
| val parentGroupNodes = updatedNodeCount(parent) |
| nodeIndex = recomposeIndex + parentGroupNodes |
| groupNodeCount = oldGroupNodeCount + parentGroupNodes |
| } else { |
| // No recompositions were requested in the range, skip it. |
| skipReaderToGroupEnd() |
| } |
| compoundKeyHash = recomposeCompoundKey |
| |
| isComposing = wasComposing |
| } |
| |
| /** |
| * The index in the insertTable overlap with indexes the slotTable so the group index used to |
| * track newly inserted groups is set to be negative offset from -2. This reserves -1 as the |
| * root index which is the parent value returned by the root groups of the slot table. |
| * |
| * This function will also restore a virtual index to its index in the insertTable which is |
| * not needed here but could be useful for debugging. |
| */ |
| private fun insertedGroupVirtualIndex(index: Int) = -2 - index |
| |
| /** |
| * As operations to insert and remove nodes are recorded, the number of nodes that will be in |
| * the group after changes are applied is maintained in a side overrides table. This method |
| * updates that count and then updates any parent groups that include the nodes this group |
| * emits. |
| */ |
| private fun updateNodeCountOverrides(group: Int, newCount: Int) { |
| // The value of group can be negative which indicates it is tracking an inserted group |
| // instead of an existing group. The index is a virtual index calculated by |
| // insertedGroupVirtualIndex which corresponds to the location of the groups to insert in |
| // the insertTable. |
| val currentCount = updatedNodeCount(group) |
| if (currentCount != newCount) { |
| // Update the overrides |
| val delta = newCount - currentCount |
| var current = group |
| |
| var minPending = pendingStack.size - 1 |
| while (current != -1) { |
| val newCurrentNodes = updatedNodeCount(current) + delta |
| updateNodeCount(current, newCurrentNodes) |
| for (pendingIndex in minPending downTo 0) { |
| val pending = pendingStack.peek(pendingIndex) |
| if (pending != null && pending.updateNodeCount(current, newCurrentNodes)) { |
| minPending = pendingIndex - 1 |
| break |
| } |
| } |
| @Suppress("LiftReturnOrAssignment") |
| if (current < 0) { |
| current = reader.parent |
| } else { |
| if (reader.isNode(current)) break |
| current = reader.parent(current) |
| } |
| } |
| } |
| } |
| |
| /** |
| * Calculates the node index (the index in the child list of a node will appear in the |
| * resulting tree) for [group]. Passing in [recomposeGroup] and its node index in |
| * [recomposeIndex] allows the calculation to exit early if there is no node group between |
| * [group] and [recomposeGroup]. |
| */ |
| private fun nodeIndexOf( |
| groupLocation: Int, |
| group: Int, |
| recomposeGroup: Int, |
| recomposeIndex: Int |
| ): Int { |
| // Find the anchor group which is either the recomposeGroup or the first parent node |
| var anchorGroup = reader.parent(group) |
| while (anchorGroup != recomposeGroup) { |
| if (reader.isNode(anchorGroup)) break |
| anchorGroup = reader.parent(anchorGroup) |
| } |
| |
| var index = if (reader.isNode(anchorGroup)) 0 else recomposeIndex |
| |
| // An early out if the group and anchor are the same |
| if (anchorGroup == group) return index |
| |
| // Walk down from the anc ghor group counting nodes of siblings in front of this group |
| var current = anchorGroup |
| val nodeIndexLimit = index + (updatedNodeCount(anchorGroup) - reader.nodeCount(group)) |
| loop@ while (index < nodeIndexLimit) { |
| if (current == groupLocation) break |
| current++ |
| while (current < groupLocation) { |
| val end = current + reader.groupSize(current) |
| if (groupLocation < end) continue@loop |
| index += updatedNodeCount(current) |
| current = end |
| } |
| break |
| } |
| return index |
| } |
| |
| private fun updatedNodeCount(group: Int): Int { |
| if (group < 0) return nodeCountVirtualOverrides?.let { it[group] } ?: 0 |
| val nodeCounts = nodeCountOverrides |
| if (nodeCounts != null) { |
| val override = nodeCounts[group] |
| if (override >= 0) return override |
| } |
| return reader.nodeCount(group) |
| } |
| |
| private fun updateNodeCount(group: Int, count: Int) { |
| if (updatedNodeCount(group) != count) { |
| if (group < 0) { |
| val virtualCounts = nodeCountVirtualOverrides ?: run { |
| val newCounts = HashMap<Int, Int>() |
| nodeCountVirtualOverrides = newCounts |
| newCounts |
| } |
| virtualCounts[group] = count |
| } else { |
| val nodeCounts = nodeCountOverrides ?: run { |
| val newCounts = IntArray(reader.size) |
| newCounts.fill(-1) |
| nodeCountOverrides = newCounts |
| newCounts |
| } |
| nodeCounts[group] = count |
| } |
| } |
| } |
| |
| private fun clearUpdatedNodeCounts() { |
| nodeCountOverrides = null |
| nodeCountVirtualOverrides = null |
| } |
| |
| /** |
| * Records the operations necessary to move the applier the node affected by the previous |
| * group to the new group. |
| */ |
| private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) { |
| val reader = reader |
| val nearestCommonRoot = reader.nearestCommonRootOf( |
| oldGroup, |
| newGroup, |
| commonRoot |
| ) |
| |
| // Record ups for the nodes between oldGroup and nearestCommonRoot |
| var current = oldGroup |
| while (current > 0 && current != nearestCommonRoot) { |
| if (reader.isNode(current)) recordUp() |
| current = reader.parent(current) |
| } |
| |
| // Record downs from nearestCommonRoot to newGroup |
| doRecordDownsFor(newGroup, nearestCommonRoot) |
| } |
| |
| private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) { |
| if (group > 0 && group != nearestCommonRoot) { |
| doRecordDownsFor(reader.parent(group), nearestCommonRoot) |
| if (reader.isNode(group)) recordDown(reader.nodeAt(group)) |
| } |
| } |
| |
| /** |
| * Calculate the compound key (a semi-unique key produced for every group in the composition) |
| * for [group]. Passing in the [recomposeGroup] and [recomposeKey] allows this method to exit |
| * early. |
| */ |
| private fun compoundKeyOf(group: Int, recomposeGroup: Int, recomposeKey: Int): Int { |
| return if (group == recomposeGroup) recomposeKey else run { |
| val groupKey = reader.groupCompoundKeyPart(group) |
| if (groupKey == movableContentKey) |
| groupKey |
| else |
| ( |
| compoundKeyOf( |
| reader.parent(group), |
| recomposeGroup, |
| recomposeKey |
| ) rol 3 |
| ) xor groupKey |
| } |
| } |
| |
| private fun SlotReader.groupCompoundKeyPart(group: Int) = |
| if (hasObjectKey(group)) { |
| groupObjectKey(group)?.let { |
| when (it) { |
| is Enum<*> -> it.ordinal |
| is MovableContent<*> -> movableContentKey |
| else -> it.hashCode() |
| } |
| } ?: 0 |
| } else groupKey(group).let { |
| if (it == reuseKey) groupAux(group)?.let { aux -> |
| if (aux == Composer.Empty) it else aux.hashCode() |
| } ?: it else it |
| } |
| |
| internal fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean { |
| val anchor = scope.anchor ?: return false |
| val location = anchor.toIndexFor(slotTable) |
| if (isComposing && location >= reader.currentGroup) { |
| // if we are invalidating a scope that is going to be traversed during this |
| // composition. |
| invalidations.insertIfMissing(location, scope, instance) |
| return true |
| } |
| return false |
| } |
| |
| @TestOnly |
| internal fun parentKey(): Int { |
| return if (inserting) { |
| writer.groupKey(writer.parent) |
| } else { |
| reader.groupKey(reader.parent) |
| } |
| } |
| |
| /** |
| * Skip a group. Skips the group at the current location. This is only valid to call if the |
| * composition is not inserting. |
| */ |
| @ComposeCompilerApi |
| override fun skipCurrentGroup() { |
| if (invalidations.isEmpty()) { |
| skipGroup() |
| } else { |
| val reader = reader |
| val key = reader.groupKey |
| val dataKey = reader.groupObjectKey |
| val aux = reader.groupAux |
| updateCompoundKeyWhenWeEnterGroup(key, dataKey, aux) |
| startReaderGroup(reader.isNode, null) |
| recomposeToGroupEnd() |
| reader.endGroup() |
| updateCompoundKeyWhenWeExitGroup(key, dataKey, aux) |
| } |
| } |
| |
| private fun skipReaderToGroupEnd() { |
| groupNodeCount = reader.parentNodes |
| reader.skipToGroupEnd() |
| } |
| |
| /** |
| * Skip to the end of the group opened by [startGroup]. |
| */ |
| @ComposeCompilerApi |
| override fun skipToGroupEnd() { |
| runtimeCheck(groupNodeCount == 0) { |
| "No nodes can be emitted before calling skipAndEndGroup" |
| } |
| currentRecomposeScope?.scopeSkipped() |
| if (invalidations.isEmpty()) { |
| skipReaderToGroupEnd() |
| } else { |
| recomposeToGroupEnd() |
| } |
| } |
| |
| @ComposeCompilerApi |
| override fun deactivateToEndGroup(changed: Boolean) { |
| runtimeCheck(groupNodeCount == 0) { |
| "No nodes can be emitted before calling dactivateToEndGroup" |
| } |
| if (!inserting) { |
| if (!changed) { |
| skipReaderToGroupEnd() |
| return |
| } |
| val start = reader.currentGroup |
| val end = reader.currentEnd |
| for (group in start until end) { |
| reader.forEachData(group) { index, data -> |
| when (data) { |
| is RememberObserver -> { |
| reader.reposition(group) |
| recordSlotTableOperation { _, slots, rememberManager -> |
| runtimeCheck(data == slots.slot(group, index)) { |
| "Slot table is out of sync" |
| } |
| rememberManager.forgetting(data) |
| slots.set(index, Composer.Empty) |
| } |
| } |
| is RecomposeScopeImpl -> { |
| val composition = data.composition |
| if (composition != null) { |
| composition.pendingInvalidScopes = true |
| data.release() |
| } |
| reader.reposition(group) |
| recordSlotTableOperation { _, slots, _ -> |
| runtimeCheck(data == slots.slot(group, index)) { |
| "Slot table is out of sync" |
| } |
| slots.set(index, Composer.Empty) |
| } |
| } |
| } |
| } |
| } |
| invalidations.removeRange(start, end) |
| reader.reposition(start) |
| reader.skipToGroupEnd() |
| } |
| } |
| |
| /** |
| * Start a restart group. A restart group creates a recompose scope and sets it as the current |
| * recompose scope of the composition. If the recompose scope is invalidated then this group |
| * will be recomposed. A recompose scope can be invalidated by calling invalidate on the object |
| * returned by [androidx.compose.runtime.currentRecomposeScope]. |
| */ |
| @ComposeCompilerApi |
| override fun startRestartGroup(key: Int): Composer { |
| start(key, null, false, null) |
| addRecomposeScope() |
| return this |
| } |
| |
| private fun addRecomposeScope() { |
| if (inserting) { |
| val scope = RecomposeScopeImpl(composition as CompositionImpl) |
| invalidateStack.push(scope) |
| updateValue(scope) |
| scope.start(compositionToken) |
| } else { |
| val invalidation = invalidations.removeLocation(reader.parent) |
| val slot = reader.next() |
| val scope = if (slot == Composer.Empty) { |
| // This code is executed when a previously deactivate region is becomes active |
| // again. See Composer.deactivateToEndGroup() |
| val newScope = RecomposeScopeImpl(composition as CompositionImpl) |
| updateValue(newScope) |
| newScope |
| } else slot as RecomposeScopeImpl |
| scope.requiresRecompose = invalidation != null |
| invalidateStack.push(scope) |
| scope.start(compositionToken) |
| } |
| } |
| |
| /** |
| * End a restart group. If the recompose scope was marked used during composition then a |
| * [ScopeUpdateScope] is returned that allows attaching a lambda that will produce the same |
| * composition as was produced by this group (including calling [startRestartGroup] and |
| * [endRestartGroup]). |
| */ |
| @ComposeCompilerApi |
| override fun endRestartGroup(): ScopeUpdateScope? { |
| // This allows for the invalidate stack to be out of sync since this might be called during |
| // exception stack unwinding that might have not called the doneJoin/endRestartGroup in the |
| // the correct order. |
| val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop() |
| else null |
| scope?.requiresRecompose = false |
| scope?.end(compositionToken)?.let { |
| record { _, _, _ -> it(composition) } |
| } |
| val result = if (scope != null && |
| !scope.skipped && |
| (scope.used || forceRecomposeScopes) |
| ) { |
| if (scope.anchor == null) { |
| scope.anchor = if (inserting) { |
| writer.anchor(writer.parent) |
| } else { |
| reader.anchor(reader.parent) |
| } |
| } |
| scope.defaultsInvalid = false |
| scope |
| } else { |
| null |
| } |
| end(isNode = false) |
| return result |
| } |
| |
| @InternalComposeApi |
| override fun insertMovableContent(value: MovableContent<*>, parameter: Any?) { |
| @Suppress("UNCHECKED_CAST") |
| invokeMovableContentLambda( |
| value as MovableContent<Any?>, |
| currentCompositionLocalScope(), |
| parameter, |
| force = false |
| ) |
| } |
| |
| private fun invokeMovableContentLambda( |
| content: MovableContent<Any?>, |
| locals: CompositionLocalMap, |
| parameter: Any?, |
| force: Boolean |
| ) { |
| // Start the movable content group |
| startMovableGroup(movableContentKey, content) |
| changed(parameter) |
| |
| // All movable content has a compound hash value rooted at the content itself so the hash |
| // value doesn't change as the content moves in the tree. |
| val savedCompoundKeyHash = compoundKeyHash |
| |
| try { |
| compoundKeyHash = movableContentKey |
| |
| if (inserting) writer.markGroup() |
| |
| // Capture the local providers at the point of the invocation. This allows detecting |
| // changes to the locals as the value moves well as enables finding the correct providers |
| // when applying late changes which might be very complicated otherwise. |
| val providersChanged = if (inserting) false else reader.groupAux != locals |
| if (providersChanged) providerUpdates[reader.currentGroup] = locals |
| start(compositionLocalMapKey, compositionLocalMap, false, locals) |
| |
| // Either insert a place-holder to be inserted later (either created new or moved from |
| // another location) or (re)compose the movable content. This is forced if a new value |
| // needs to be created as a late change. |
| if (inserting && !force) { |
| writerHasAProvider = true |
| providerCache = null |
| |
| // Create an anchor to the movable group |
| val anchor = writer.anchor(writer.parent(writer.parent)) |
| val reference = MovableContentStateReference( |
| content, |
| parameter, |
| composition, |
| insertTable, |
| anchor, |
| emptyList(), |
| currentCompositionLocalScope() |
| ) |
| parentContext.insertMovableContent(reference) |
| } else { |
| val savedProvidersInvalid = providersInvalid |
| providersInvalid = providersChanged |
| invokeComposable(this, { content.content(parameter) }) |
| providersInvalid = savedProvidersInvalid |
| } |
| } finally { |
| // Restore the state back to what is expected by the caller. |
| endGroup() |
| compoundKeyHash = savedCompoundKeyHash |
| endMovableGroup() |
| } |
| } |
| |
| @InternalComposeApi |
| override fun insertMovableContentReferences( |
| references: List<Pair<MovableContentStateReference, MovableContentStateReference?>> |
| ) { |
| var completed = false |
| try { |
| insertMovableContentGuarded(references) |
| completed = true |
| } finally { |
| if (completed) { |
| cleanUpCompose() |
| } else { |
| // if we finished with error, cleanup more aggressively |
| abortRoot() |
| } |
| } |
| } |
| |
| private fun insertMovableContentGuarded( |
| references: List<Pair<MovableContentStateReference, MovableContentStateReference?>> |
| ) { |
| fun positionToParentOf(slots: SlotWriter, applier: Applier<Any?>, index: Int) { |
| while (!slots.indexInParent(index)) { |
| slots.skipToGroupEnd() |
| if (slots.isNode(slots.parent)) applier.up() |
| slots.endGroup() |
| } |
| } |
| |
| fun currentNodeIndex(slots: SlotWriter): Int { |
| val original = slots.currentGroup |
| |
| // Find parent node |
| var current = slots.parent |
| while (current >= 0 && !slots.isNode(current)) { |
| current = slots.parent(current) |
| } |
| |
| var index = 0 |
| current++ |
| while (current < original) { |
| if (slots.indexInGroup(original, current)) { |
| if (slots.isNode(current)) index = 0 |
| current++ |
| } else { |
| index += if (slots.isNode(current)) 1 else slots.nodeCount(current) |
| current += slots.groupSize(current) |
| } |
| } |
| return index |
| } |
| |
| fun positionToInsert(slots: SlotWriter, anchor: Anchor, applier: Applier<Any?>): Int { |
| val destination = slots.anchorIndex(anchor) |
| runtimeCheck(slots.currentGroup < destination) |
| positionToParentOf(slots, applier, destination) |
| var nodeIndex = currentNodeIndex(slots) |
| while (slots.currentGroup < destination) { |
| when { |
| slots.indexInCurrentGroup(destination) -> { |
| if (slots.isNode) { |
| applier.down(slots.node(slots.currentGroup)) |
| nodeIndex = 0 |
| } |
| slots.startGroup() |
| } |
| else -> nodeIndex += slots.skipGroup() |
| } |
| } |
| |
| runtimeCheck(slots.currentGroup == destination) |
| return nodeIndex |
| } |
| |
| withChanges(lateChanges) { |
| record(resetSlotsInstance) |
| references.fastForEach { (to, from) -> |
| val anchor = to.anchor |
| val location = to.slotTable.anchorIndex(anchor) |
| var effectiveNodeIndex = 0 |
| realizeUps() |
| // Insert content at the anchor point |
| record { applier, slots, _ -> |
| @Suppress("UNCHECKED_CAST") |
| applier as Applier<Any?> |
| effectiveNodeIndex = positionToInsert(slots, anchor, applier) |
| } |
| if (from == null) { |
| val toSlotTable = to.slotTable |
| if (toSlotTable == insertTable) { |
| // We are going to compose reading the insert table which will also |
| // perform an insert. This would then cause both a reader and a writer to |
| // be created simultaneously which will throw an exception. To prevent |
| // that we release the old insert table and replace it with a fresh one. |
| // This allows us to read from the old table and write to the new table. |
| |
| // This occurs when the placeholder version of movable content was inserted |
| // but no content was available to move so we now need to create the |
| // content. |
| |
| createFreshInsertTable() |
| } |
| to.slotTable.read { reader -> |
| reader.reposition(location) |
| writersReaderDelta = location |
| val offsetChanges = mutableListOf<Change>() |
| recomposeMovableContent { |
| withChanges(offsetChanges) { |
| withReader(reader) { |
| invokeMovableContentLambda( |
| to.content, |
| to.locals, |
| to.parameter, |
| force = true |
| ) |
| } |
| } |
| } |
| if (offsetChanges.isNotEmpty()) { |
| record { applier, slots, rememberManager -> |
| val offsetApplier = if (effectiveNodeIndex > 0) |
| OffsetApplier(applier, effectiveNodeIndex) else applier |
| offsetChanges.fastForEach { change -> |
| change(offsetApplier, slots, rememberManager) |
| } |
| } |
| } |
| } |
| } else { |
| // If the state was already removed from the from table then it will have a |
| // state recorded in the recomposer, retrieve that now if we can. If not the |
| // state is still in its original location, recompose over it there. |
| val resolvedState = parentContext.movableContentStateResolve(from) |
| val fromTable = resolvedState?.slotTable ?: from.slotTable |
| val fromAnchor = resolvedState?.slotTable?.anchor(0) ?: from.anchor |
| val nodesToInsert = fromTable.collectNodesFrom(fromAnchor) |
| |
| // Insert nodes if necessary |
| if (nodesToInsert.isNotEmpty()) { |
| record { applier, _, _ -> |
| val base = effectiveNodeIndex |
| @Suppress("UNCHECKED_CAST") |
| nodesToInsert.fastForEachIndexed { i, node -> |
| applier as Applier<Any?> |
| applier.insertBottomUp(base + i, node) |
| applier.insertTopDown(base + i, node) |
| } |
| } |
| if (to.slotTable == slotTable) { |
| // Inserting the content into the current slot table then we need to |
| // update the virtual node counts. Otherwise, we are inserting into |
| // a new slot table which is being created, not updated, so the virtual |
| // node counts do not need to be updated. |
| val group = slotTable.anchorIndex(anchor) |
| updateNodeCount( |
| group, |
| updatedNodeCount(group) + nodesToInsert.size |
| ) |
| } |
| } |
| |
| // Copy the slot table into the anchor location |
| record { _, slots, _ -> |
| val state = resolvedState ?: parentContext.movableContentStateResolve(from) |
| ?: composeRuntimeError("Could not resolve state for movable content") |
| |
| // The slot table contains the movable content group plus the group |
| // containing the movable content's table which then contains the actual |
| // state to be inserted. The state is at index 2 in the table (for the |
| // two groups) and is inserted into the provider group at offset 1 from the |
| // current location. |
| val anchors = slots.moveIntoGroupFrom(1, state.slotTable, 2) |
| |
| // For all the anchors that moved, if the anchor is tracking a recompose |
| // scope, update it to reference its new composer. |
| if (anchors.isNotEmpty()) { |
| val toComposition = to.composition as CompositionImpl |
| anchors.fastForEach { anchor -> |
| // The recompose scope is always at slot 0 of a restart group. |
| val recomposeScope = slots.slot(anchor, 0) as? RecomposeScopeImpl |
| // Check for null as the anchor might not be for a recompose scope |
| recomposeScope?.adoptedBy(toComposition) |
| } |
| } |
| } |
| |
| fromTable.read { reader -> |
| withReader(reader) { |
| val newLocation = fromTable.anchorIndex(fromAnchor) |
| reader.reposition(newLocation) |
| writersReaderDelta = newLocation |
| val offsetChanges = mutableListOf<Change>() |
| |
| withChanges(offsetChanges) { |
| recomposeMovableContent( |
| from = from.composition, |
| to = to.composition, |
| reader.currentGroup, |
| invalidations = from.invalidations |
| ) { |
| invokeMovableContentLambda( |
| to.content, |
| to.locals, |
| to.parameter, |
| force = true |
| ) |
| } |
| } |
| if (offsetChanges.isNotEmpty()) { |
| record { applier, slots, rememberManager -> |
| val offsetApplier = if (effectiveNodeIndex > 0) |
| OffsetApplier(applier, effectiveNodeIndex) else applier |
| offsetChanges.fastForEach { change -> |
| change(offsetApplier, slots, rememberManager) |
| } |
| } |
| } |
| } |
| } |
| } |
| record(skipToGroupEndInstance) |
| } |
| record { applier, slots, _ -> |
| @Suppress("UNCHECKED_CAST") |
| applier as Applier<Any?> |
| positionToParentOf(slots, applier, 0) |
| slots.endGroup() |
| } |
| writersReaderDelta = 0 |
| } |
| } |
| |
| private inline fun <R> withChanges(newChanges: MutableList<Change>, block: () -> R): R { |
| val savedChanges = changes |
| try { |
| changes = newChanges |
| return block() |
| } finally { |
| changes = savedChanges |
| } |
| } |
| |
| private inline fun <R> withReader(reader: SlotReader, block: () -> R): R { |
| val savedReader = this.reader |
| val savedCountOverrides = nodeCountOverrides |
| nodeCountOverrides = null |
| try { |
| this.reader = reader |
| return block() |
| } finally { |
| this.reader = savedReader |
| nodeCountOverrides = savedCountOverrides |
| } |
| } |
| |
| private fun <R> recomposeMovableContent( |
| from: ControlledComposition? = null, |
| to: ControlledComposition? = null, |
| index: Int? = null, |
| invalidations: List<Pair<RecomposeScopeImpl, IdentityArraySet<Any>?>> = emptyList(), |
| block: () -> R |
| ): R { |
| val savedImplicitRootStart = this.implicitRootStart |
| val savedIsComposing = isComposing |
| val savedNodeIndex = nodeIndex |
| try { |
| implicitRootStart = false |
| isComposing = true |
| nodeIndex = 0 |
| invalidations.fastForEach { (scope, instances) -> |
| if (instances != null) { |
| instances.fastForEach { instance -> |
| tryImminentInvalidation(scope, instance) |
| } |
| } else { |
| tryImminentInvalidation(scope, null) |
| } |
| } |
| return from?.delegateInvalidations(to, index ?: -1, block) ?: block() |
| } finally { |
| implicitRootStart = savedImplicitRootStart |
| isComposing = savedIsComposing |
| nodeIndex = savedNodeIndex |
| } |
| } |
| |
| @ComposeCompilerApi |
| override fun sourceInformation(sourceInformation: String) { |
| if (inserting && sourceInformationEnabled) { |
| writer.insertAux(sourceInformation) |
| } |
| } |
| |
| @ComposeCompilerApi |
| override fun sourceInformationMarkerStart(key: Int, sourceInformation: String) { |
| if (sourceInformationEnabled) |
| start(key, objectKey = null, isNode = false, data = sourceInformation) |
| } |
| |
| @ComposeCompilerApi |
| override fun sourceInformationMarkerEnd() { |
| if (sourceInformationEnabled) |
| end(isNode = false) |
| } |
| |
| override fun disableSourceInformation() { |
| sourceInformationEnabled = false |
| } |
| |
| /** |
| * Synchronously compose the initial composition of [content]. This collects all the changes |
| * which must be applied by [ControlledComposition.applyChanges] to build the tree implied by |
| * [content]. |
| */ |
| internal fun composeContent( |
| invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>, |
| content: @Composable () -> Unit |
| ) { |
| runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" } |
| doCompose(invalidationsRequested, content) |
| } |
| |
| internal fun prepareCompose(block: () -> Unit) { |
| runtimeCheck(!isComposing) { "Preparing a composition while composing is not supported" } |
| isComposing = true |
| try { |
| block() |
| } finally { |
| isComposing = false |
| } |
| } |
| |
| /** |
| * Synchronously recompose all invalidated groups. This collects the changes which must be |
| * applied by [ControlledComposition.applyChanges] to have an effect. |
| */ |
| internal fun recompose( |
| invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?> |
| ): Boolean { |
| runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" } |
| // even if invalidationsRequested is empty we still need to recompose if the Composer has |
| // some invalidations scheduled already. it can happen when during some parent composition |
| // there were a change for a state which was used by the child composition. such changes |
| // will be tracked and added into `invalidations` list. |
| if ( |
| invalidationsRequested.isNotEmpty() || |
| invalidations.isNotEmpty() || |
| forciblyRecompose |
| ) { |
| doCompose(invalidationsRequested, null) |
| return changes.isNotEmpty() |
| } |
| return false |
| } |
| |
| private fun doCompose( |
| invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>, |
| content: (@Composable () -> Unit)? |
| ) { |
| runtimeCheck(!isComposing) { "Reentrant composition is not supported" } |
| trace("Compose:recompose") { |
| snapshot = currentSnapshot() |
| compositionToken = snapshot.id |
| providerUpdates.clear() |
| invalidationsRequested.forEach { scope, set -> |
| val location = scope.anchor?.location ?: return |
| invalidations.add(Invalidation(scope, location, set)) |
| } |
| invalidations.sortBy { it.location } |
| nodeIndex = 0 |
| var complete = false |
| isComposing = true |
| try { |
| startRoot() |
| |
| // vv Experimental for forced |
| @Suppress("UNCHECKED_CAST") |
| val savedContent = nextSlot() |
| if (savedContent !== content && content != null) { |
| updateValue(content as Any?) |
| } |
| // ^^ Experimental for forced |
| |
| // Ignore reads of derivedStateOf recalculations |
| observeDerivedStateRecalculations( |
| start = { |
| childrenComposing++ |
| }, |
| done = { |
| childrenComposing-- |
| }, |
| ) { |
| if (content != null) { |
| startGroup(invocationKey, invocation) |
| invokeComposable(this, content) |
| endGroup() |
| } else if ( |
| (forciblyRecompose || providersInvalid) && |
| savedContent != null && |
| savedContent != Composer.Empty |
| ) { |
| startGroup(invocationKey, invocation) |
| @Suppress("UNCHECKED_CAST") |
| invokeComposable(this, savedContent as @Composable () -> Unit) |
| endGroup() |
| } else { |
| skipCurrentGroup() |
| } |
| } |
| endRoot() |
| complete = true |
| } finally { |
| isComposing = false |
| invalidations.clear() |
| if (!complete) abortRoot() |
| } |
| } |
| } |
| |
| val hasInvalidations get() = invalidations.isNotEmpty() |
| |
| private val SlotReader.node get() = node(parent) |
| |
| private fun SlotReader.nodeAt(index: Int) = node(index) |
| |
| private fun validateNodeExpected() { |
| runtimeCheck(nodeExpected) { |
| "A call to createNode(), emitNode() or useNode() expected was not expected" |
| } |
| nodeExpected = false |
| } |
| |
| private fun validateNodeNotExpected() { |
| runtimeCheck(!nodeExpected) { "A call to createNode(), emitNode() or useNode() expected" } |
| } |
| |
| /** |
| * Add a raw change to the change list. Once [record] is called, the operation is realized |
| * into the change list. The helper routines below reduce the number of operations that must |
| * be realized to change the previous tree to the new tree as well as update the slot table |
| * to prepare for the next composition. |
| */ |
| private fun record(change: Change) { |
| changes.add(change) |
| } |
| |
| /** |
| * Record a change ensuring, when it is applied, that the applier is focused on the current |
| * node. |
| */ |
| private fun recordApplierOperation(change: Change) { |
| realizeUps() |
| realizeDowns() |
| record(change) |
| } |
| |
| /** |
| * Record a change that will insert, remove or move a slot table group. This ensures the slot |
| * table is prepared for the change by ensuring the parent group is started and then ended |
| * as the group is left. |
| */ |
| private fun recordSlotEditingOperation(change: Change) { |
| realizeOperationLocation() |
| recordSlotEditing() |
| record(change) |
| } |
| |
| /** |
| * Record a change ensuring, when it is applied, the write matches the current slot in the |
| * reader. |
| */ |
| private fun recordSlotTableOperation(forParent: Boolean = false, change: Change) { |
| realizeOperationLocation(forParent) |
| record(change) |
| } |
| |
| // Navigation of the node tree is performed by recording all the locations of the nodes as |
| // they are traversed by the reader and recording them in the downNodes array. When the node |
| // navigation is realized all the downs in the down nodes is played to the applier. |
| // |
| // If an up is recorded before the corresponding down is realized then it is simply removed |
| // from the downNodes stack. |
| |
| private var pendingUps = 0 |
| private var downNodes = Stack<Any?>() |
| |
| private fun realizeUps() { |
| val count = pendingUps |
| if (count > 0) { |
| pendingUps = 0 |
| record { applier, _, _ -> repeat(count) { applier.up() } } |
| } |
| } |
| |
| private fun realizeDowns(nodes: Array<Any?>) { |
| record { applier, _, _ -> |
| for (index in nodes.indices) { |
| @Suppress("UNCHECKED_CAST") |
| val nodeApplier = applier as Applier<Any?> |
| nodeApplier.down(nodes[index]) |
| } |
| } |
| } |
| |
| private fun realizeDowns() { |
| if (downNodes.isNotEmpty()) { |
| @Suppress("UNCHECKED_CAST") |
| realizeDowns(downNodes.toArray()) |
| downNodes.clear() |
| } |
| } |
| |
| private fun recordDown(node: Any?) { |
| @Suppress("UNCHECKED_CAST") |
| downNodes.push(node) |
| } |
| |
| private fun recordUp() { |
| if (downNodes.isNotEmpty()) { |
| downNodes.pop() |
| } else { |
| pendingUps++ |
| } |
| } |
| |
| // Navigating the writer slot is performed relatively as the location of a group in the writer |
| // might be different than it is in the reader as groups can be inserted, deleted, or moved. |
| // |
| // writersReaderDelta tracks the difference between reader's current slot the current of |
| // the writer must be before the recorded change is applied. Moving the writer to a location |
| // is performed by advancing the writer the same the number of slots traversed by the reader |
| // since the last write change. This works transparently for inserts. For deletes the number |
| // of nodes deleted needs to be added to writersReaderDelta. When slots move the delta is |
| // updated as if the move has already taken place. The delta is updated again once the group |
| // begin edited is complete. |
| // |
| // The SlotTable requires that the group that contains any moves, inserts or removes must have |
| // the group that contains the moved, inserted or removed groups be started with a startGroup |
| // and terminated with a endGroup so the effects of the inserts, deletes, and moves can be |
| // recorded correctly in its internal data structures. The startedGroups stack maintains the |
| // groups that must be closed before we can move past the started group. |
| |
| /** |
| * The skew or delta between where the writer will be and where the reader is now. This can |
| * be thought of as the unrealized distance the writer must move to match the current slot in |
| * the reader. When an operation affects the slot table the writer location must be realized |
| * by moving the writer slot table the unrealized distance. |
| */ |
| private var writersReaderDelta = 0 |
| |
| /** |
| * Record whether any groups were stared. If no groups were started then the root group |
| * doesn't need to be started or ended either. |
| */ |
| private var startedGroup = false |
| |
| /** |
| * During late change calculation the group start/end is handled by [insertMovableContentReferences] |
| * directly instead of requiring implicit starts/end groups to be inserted. |
| */ |
| private var implicitRootStart = true |
| |
| /** |
| * A stack of the location of the groups that were started. |
| */ |
| private val startedGroups = IntStack() |
| |
| private fun realizeOperationLocation(forParent: Boolean = false) { |
| val location = if (forParent) reader.parent else reader.currentGroup |
| val distance = location - writersReaderDelta |
| runtimeCheck(distance >= 0) { |
| "Tried to seek backward" |
| } |
| if (distance > 0) { |
| record { _, slots, _ -> slots.advanceBy(distance) } |
| writersReaderDelta = location |
| } |
| } |
| |
| private fun recordInsert(anchor: Anchor) { |
| if (insertFixups.isEmpty()) { |
| val insertTable = insertTable |
| recordSlotEditingOperation { _, slots, _ -> |
| slots.beginInsert() |
| slots.moveFrom(insertTable, anchor.toIndexFor(insertTable)) |
| slots.endInsert() |
| } |
| } else { |
| val fixups = insertFixups.toMutableList() |
| insertFixups.clear() |
| realizeUps() |
| realizeDowns() |
| val insertTable = insertTable |
| recordSlotEditingOperation { applier, slots, rememberManager -> |
| insertTable.write { writer -> |
| fixups.fastForEach { fixup -> |
| fixup(applier, writer, rememberManager) |
| } |
| } |
| slots.beginInsert() |
| slots.moveFrom(insertTable, anchor.toIndexFor(insertTable)) |
| slots.endInsert() |
| } |
| } |
| } |
| |
| private fun recordFixup(change: Change) { |
| insertFixups.add(change) |
| } |
| |
| private val insertUpFixups = Stack<Change>() |
| |
| private fun recordInsertUpFixup(change: Change) { |
| insertUpFixups.push(change) |
| } |
| |
| private fun registerInsertUpFixup() { |
| insertFixups.add(insertUpFixups.pop()) |
| } |
| |
| /** |
| * When a group is removed the reader will move but the writer will not so to ensure both the |
| * writer and reader are tracking the same slot we advance the [writersReaderDelta] to |
| * account for the removal. |
| */ |
| private fun recordDelete() { |
| // It is import that the movable content is reported first so it can be removed before the |
| // group itself is removed. |
| reportFreeMovableContent(reader.currentGroup) |
| recordSlotEditingOperation(change = removeCurrentGroupInstance) |
| writersReaderDelta += reader.groupSize |
| } |
| |
| /** |
| * Report any movable content that the group contains as being removed and ready to be moved. |
| * Returns true if the group itself was removed. |
| * |
| * Returns the number of nodes left in place which is used to calculate the node index of |
| * any nested calls. |
| * |
| * @param groupBeingRemoved The group that is being removed from the table or 0 if the entire |
| * table is being removed. |
| */ |
| private fun reportFreeMovableContent(groupBeingRemoved: Int) { |
| |
| fun reportGroup(group: Int, needsNodeDelete: Boolean, nodeIndex: Int): Int { |
| return if (reader.hasMark(group)) { |
| // If the group has a mark then it is either a movable content group or a |
| // composition context group |
| val key = reader.groupKey(group) |
| val objectKey = reader.groupObjectKey(group) |
| if (key == movableContentKey && objectKey is MovableContent<*>) { |
| // If the group is a movable content block schedule it to be removed and report |
| // that it is free to be moved to the parentContext. Nested movable content is |
| // recomposed if necessary once the group has been claimed by another insert. |
| // If the nested movable content ends up being removed this is reported during |
| // that recomposition so there is no need to look at child movable content here. |
| @Suppress("UNCHECKED_CAST") |
| val movableContent = objectKey as MovableContent<Any?> |
| val parameter = reader.groupGet(group, 0) |
| val anchor = reader.anchor(group) |
| val end = group + reader.groupSize(group) |
| val invalidations = this.invalidations.filterToRange(group, end).fastMap { |
| it.scope to it.instances |
| } |
| val reference = MovableContentStateReference( |
| movableContent, |
| parameter, |
| composition, |
| slotTable, |
| anchor, |
| invalidations, |
| currentCompositionLocalScope(group) |
| ) |
| parentContext.deletedMovableContent(reference) |
| recordSlotEditing() |
| record { _, slots, _ -> releaseMovableGroupAtCurrent(reference, slots) } |
| if (needsNodeDelete) { |
| realizeMovement() |
| realizeUps() |
| realizeDowns() |
| val nodeCount = if (reader.isNode(group)) 1 else reader.nodeCount(group) |
| if (nodeCount > 0) { |
| recordRemoveNode(nodeIndex, nodeCount) |
| } |
| 0 // These nodes were deleted |
| } else reader.nodeCount(group) |
| } else if (key == referenceKey && objectKey == reference) { |
| // Group is a composition context reference. As this is being removed assume |
| // all movable groups in the composition that have this context will also be |
| // released whe the compositions are disposed. |
| val contextHolder = reader.groupGet(group, 0) as? CompositionContextHolder |
| if (contextHolder != null) { |
| // The contextHolder can be EMPTY in cases wher the content has been |
| // deactivated. Content is deactivated if the content is just being |
| // held onto for recycling and is not otherwise active. In this case |
| // the composers we are likely to find here have already been disposed. |
| val compositionContext = contextHolder.ref |
| compositionContext.composers.forEach { composer -> |
| composer.reportAllMovableContent() |
| } |
| } |
| reader.nodeCount(group) |
| } else reader.nodeCount(group) |
| } else if (reader.containsMark(group)) { |
| // Traverse the group freeing the child movable content. This group is known to |
| // have at least one child that contains movable content because the group is |
| // marked as containing a mark. |
| val size = reader.groupSize(group) |
| val end = group + size |
| var current = group + 1 |
| var runningNodeCount = 0 |
| while (current < end) { |
| // A tree is not disassembled when it is removed, the root nodes of the |
| // sub-trees are removed, therefore, if we enter a node that contains movable |
| // content, the nodes should be removed so some future composition can |
| // re-insert them at a new location. Otherwise the applier will attempt to |
| // insert a node that already has a parent. If there is no node between the |
| // group removed and this group then the nodes will be removed by normal |
| // recomposition. |
| val isNode = reader.isNode(current) |
| if (isNode) { |
| realizeMovement() |
| recordDown(reader.node(current)) |
| } |
| runningNodeCount += reportGroup( |
| group = current, |
| needsNodeDelete = isNode || needsNodeDelete, |
| nodeIndex = if (isNode) 0 else nodeIndex + runningNodeCount |
| ) |
| if (isNode) { |
| realizeMovement() |
| recordUp() |
| } |
| current += reader.groupSize(current) |
| } |
| runningNodeCount |
| } else reader.nodeCount(group) |
| } |
| reportGroup(groupBeingRemoved, needsNodeDelete = false, nodeIndex = 0) |
| realizeMovement() |
| } |
| |
| /** |
| * Release the reference the movable group stored in [slots] to the recomposer for to be used |
| * to insert to insert to other locations. |
| */ |
| private fun releaseMovableGroupAtCurrent( |
| reference: MovableContentStateReference, |
| slots: SlotWriter |
| ) { |
| val slotTable = SlotTable() |
| |
| // Write a table that as if it was written by a calling |
| // invokeMovableContentLambda because this might be removed from the |
| // composition before the new composition can be composed to receive it. When |
| // the new composition receives the state it must recompose over the state by |
| // calling invokeMovableContentLambda. |
| slotTable.write { writer -> |
| writer.beginInsert() |
| |
| // This is the prefix created by invokeMovableContentLambda |
| writer.startGroup(movableContentKey, reference.content) |
| writer.markGroup() |
| writer.update(reference.parameter) |
| |
| // Move the content into current location |
| slots.moveTo(reference.anchor, 1, writer) |
| |
| // skip the group that was just inserted. |
| writer.skipGroup() |
| |
| // End the group that represents the call to invokeMovableContentLambda |
| writer.endGroup() |
| |
| writer.endInsert() |
| } |
| val state = MovableContentState(slotTable) |
| parentContext.movableContentStateReleased(reference, state) |
| } |
| |
| /** |
| * Called during composition to report all the content of the composition will be released |
| * as this composition is to be disposed. |
| */ |
| private fun reportAllMovableContent() { |
| if (slotTable.containsMark()) { |
| val changes = mutableListOf<Change>() |
| deferredChanges = changes |
| slotTable.read { reader -> |
| this.reader = reader |
| withChanges(changes) { |
| reportFreeMovableContent(0) |
| realizeUps() |
| if (startedGroup) { |
| record(skipToGroupEndInstance) |
| recordEndRoot() |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Called when reader current is moved directly, such as when a group moves, to [location]. |
| */ |
| private fun recordReaderMoving(location: Int) { |
| val distance = reader.currentGroup - writersReaderDelta |
| |
| // Ensure the next skip will account for the distance we have already travelled. |
| writersReaderDelta = location - distance |
| } |
| |
| private fun recordSlotEditing() { |
| // During initial composition (when the slot table is empty), no group needs |
| // to be started. |
| if (reader.size > 0) { |
| val reader = reader |
| val location = reader.parent |
| |
| if (startedGroups.peekOr(invalidGroupLocation) != location) { |
| if (!startedGroup && implicitRootStart) { |
| // We need to ensure the root group is started. |
| recordSlotTableOperation(change = startRootGroup) |
| startedGroup = true |
| } |
| if (location > 0) { |
| val anchor = reader.anchor(location) |
| startedGroups.push(location) |
| recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) } |
| } |
| } |
| } |
| } |
| |
| private fun recordEndGroup() { |
| val location = reader.parent |
| val currentStartedGroup = startedGroups.peekOr(-1) |
| runtimeCheck(currentStartedGroup <= location) { "Missed recording an endGroup" } |
| if (startedGroups.peekOr(-1) == location) { |
| startedGroups.pop() |
| recordSlotTableOperation(change = endGroupInstance) |
| } |
| } |
| |
| private fun recordEndRoot() { |
| if (startedGroup) { |
| recordSlotTableOperation(change = endGroupInstance) |
| startedGroup = false |
| } |
| } |
| |
| private fun finalizeCompose() { |
| realizeUps() |
| runtimeCheck(pendingStack.isEmpty()) { "Start/end imbalance" } |
| runtimeCheck(startedGroups.isEmpty()) { "Missed recording an endGroup()" } |
| cleanUpCompose() |
| } |
| |
| private fun cleanUpCompose() { |
| pending = null |
| nodeIndex = 0 |
| groupNodeCount = 0 |
| writersReaderDelta = 0 |
| compoundKeyHash = 0 |
| nodeExpected = false |
| startedGroup = false |
| startedGroups.clear() |
| invalidateStack.clear() |
| clearUpdatedNodeCounts() |
| } |
| |
| internal fun verifyConsistent() { |
| insertTable.verifyWellFormed() |
| } |
| |
| private var previousRemove = -1 |
| private var previousMoveFrom = -1 |
| private var previousMoveTo = -1 |
| private var previousCount = 0 |
| |
| private fun recordRemoveNode(nodeIndex: Int, count: Int) { |
| if (count > 0) { |
| runtimeCheck(nodeIndex >= 0) { "Invalid remove index $nodeIndex" } |
| if (previousRemove == nodeIndex) previousCount += count |
| else { |
| realizeMovement() |
| previousRemove = nodeIndex |
| previousCount = count |
| } |
| } |
| } |
| |
| private fun recordMoveNode(from: Int, to: Int, count: Int) { |
| if (count > 0) { |
| if (previousCount > 0 && previousMoveFrom == from - previousCount && |
| previousMoveTo == to - previousCount |
| ) { |
| previousCount += count |
| } else { |
| realizeMovement() |
| previousMoveFrom = from |
| previousMoveTo = to |
| previousCount = count |
| } |
| } |
| } |
| |
| private fun realizeMovement() { |
| val count = previousCount |
| previousCount = 0 |
| if (count > 0) { |
| if (previousRemove >= 0) { |
| val removeIndex = previousRemove |
| previousRemove = -1 |
| recordApplierOperation { applier, _, _ -> applier.remove(removeIndex, count) } |
| } else { |
| val from = previousMoveFrom |
| previousMoveFrom = -1 |
| val to = previousMoveTo |
| previousMoveTo = -1 |
| recordApplierOperation { applier, _, _ -> applier.move(from, to, count) } |
| } |
| } |
| } |
| |
| /** |
| * A holder that will dispose of its [CompositionContext] when it leaves the composition |
| * that will not have its reference made visible to user code. |
| */ |
| // This warning becomes an error if its advice is followed since Composer needs its type param |
| @Suppress("RemoveRedundantQualifierName") |
| private class CompositionContextHolder( |
| val ref: ComposerImpl.CompositionContextImpl |
| ) : RememberObserver { |
| override fun onRemembered() { } |
| override fun onAbandoned() { |
| ref.dispose() |
| } |
| override fun onForgotten() { |
| ref.dispose() |
| } |
| } |
| |
| private inner class CompositionContextImpl( |
| override val compoundHashKey: Int, |
| override val collectingParameterInformation: Boolean |
| ) : CompositionContext() { |
| var inspectionTables: MutableSet<MutableSet<CompositionData>>? = null |
| val composers = mutableSetOf<ComposerImpl>() |
| |
| fun dispose() { |
| if (composers.isNotEmpty()) { |
| inspectionTables?.let { |
| for (composer in composers) { |
| for (table in it) |
| table.remove(composer.slotTable) |
| } |
| } |
| composers.clear() |
| } |
| } |
| |
| override fun registerComposer(composer: Composer) { |
| super.registerComposer(composer as ComposerImpl) |
| composers.add(composer) |
| } |
| |
| override fun unregisterComposer(composer: Composer) { |
| inspectionTables?.forEach { it.remove((composer as ComposerImpl).slotTable) } |
| composers.remove(composer) |
| } |
| |
| override fun registerComposition(composition: ControlledComposition) { |
| parentContext.registerComposition(composition) |
| } |
| |
| override fun unregisterComposition(composition: ControlledComposition) { |
| parentContext.unregisterComposition(composition) |
| } |
| |
| override val effectCoroutineContext: CoroutineContext |
| get() = parentContext.effectCoroutineContext |
| |
| @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") |
| @OptIn(ExperimentalComposeApi::class) |
| @get:OptIn(ExperimentalComposeApi::class) |
| override val recomposeCoroutineContext: CoroutineContext |
| get() = composition.recomposeCoroutineContext |
| |
| override fun composeInitial( |
| composition: ControlledComposition, |
| content: @Composable () -> Unit |
| ) { |
| parentContext.composeInitial(composition, content) |
| } |
| |
| override fun invalidate(composition: ControlledComposition) { |
| // Invalidate ourselves with our parent before we invalidate a child composer. |
| // This ensures that when we are scheduling recompositions, parents always |
| // recompose before their children just in case a recomposition in the parent |
| // would also cause other recomposition in the child. |
| // If the parent ends up having no real invalidations to process we will skip work |
| // for that composer along a fast path later. |
| // This invalidation process could be made more efficient as it's currently N^2 with |
| // subcomposition meta-tree depth thanks to the double recursive parent walk |
| // performed here, but we currently assume a low N. |
| parentContext.invalidate(this@ComposerImpl.composition) |
| parentContext.invalidate(composition) |
| } |
| |
| override fun invalidateScope(scope: RecomposeScopeImpl) { |
| parentContext.invalidateScope(scope) |
| } |
| |
| // This is snapshot state not because we need it to be observable, but because |
| // we need changes made to it in composition to be visible for the rest of the current |
| // composition and not become visible outside of the composition process until composition |
| // succeeds. |
| private var compositionLocalScope by mutableStateOf<CompositionLocalMap>( |
| persistentHashMapOf() |
| ) |
| |
| override fun getCompositionLocalScope(): CompositionLocalMap = compositionLocalScope |
| |
| fun updateCompositionLocalScope(scope: CompositionLocalMap) { |
| compositionLocalScope = scope |
| } |
| |
| override fun recordInspectionTable(table: MutableSet<CompositionData>) { |
| ( |
| inspectionTables ?: HashSet<MutableSet<CompositionData>>().also { |
| inspectionTables = it |
| } |
| ).add(table) |
| } |
| |
| override fun startComposing() { |
| childrenComposing++ |
| } |
| |
| override fun doneComposing() { |
| childrenComposing-- |
| } |
| |
| override fun insertMovableContent(reference: MovableContentStateReference) { |
| parentContext.insertMovableContent(reference) |
| } |
| |
| override fun deletedMovableContent(reference: MovableContentStateReference) { |
| parentContext.deletedMovableContent(reference) |
| } |
| |
| override fun movableContentStateResolve( |
| reference: MovableContentStateReference |
| ): MovableContentState? = parentContext.movableContentStateResolve(reference) |
| |
| override fun movableContentStateReleased( |
| reference: MovableContentStateReference, |
| data: MovableContentState |
| ) { |
| parentContext.movableContentStateReleased(reference, data) |
| } |
| } |
| |
| private fun updateCompoundKeyWhenWeEnterGroup(groupKey: Int, dataKey: Any?, data: Any?) { |
| if (dataKey == null) |
| if (data != null && groupKey == reuseKey && data != Composer.Empty) |
| updateCompoundKeyWhenWeEnterGroupKeyHash(data.hashCode()) |
| else |
| updateCompoundKeyWhenWeEnterGroupKeyHash(groupKey) |
| else if (dataKey is Enum<*>) |
| updateCompoundKeyWhenWeEnterGroupKeyHash(dataKey.ordinal) |
| else |
| updateCompoundKeyWhenWeEnterGroupKeyHash(dataKey.hashCode()) |
| } |
| |
| private fun updateCompoundKeyWhenWeEnterGroupKeyHash(keyHash: Int) { |
| compoundKeyHash = (compoundKeyHash rol 3) xor keyHash |
| } |
| |
| private fun updateCompoundKeyWhenWeExitGroup(groupKey: Int, dataKey: Any?, data: Any?) { |
| if (dataKey == null) |
| if (data != null && groupKey == reuseKey && data != Composer.Empty) |
| updateCompoundKeyWhenWeExitGroupKeyHash(data.hashCode()) |
| else |
| updateCompoundKeyWhenWeExitGroupKeyHash(groupKey) |
| else if (dataKey is Enum<*>) |
| updateCompoundKeyWhenWeExitGroupKeyHash(dataKey.ordinal) |
| else |
| updateCompoundKeyWhenWeExitGroupKeyHash(dataKey.hashCode()) |
| } |
| |
| private fun updateCompoundKeyWhenWeExitGroupKeyHash(groupKey: Int) { |
| compoundKeyHash = (compoundKeyHash xor groupKey.hashCode()) ror 3 |
| } |
| |
| override val recomposeScope: RecomposeScope? get() = currentRecomposeScope |
| override val recomposeScopeIdentity: Any? get() = currentRecomposeScope?.anchor |
| override fun rememberedValue(): Any? = nextSlot() |
| override fun updateRememberedValue(value: Any?) = updateValue(value) |
| override fun recordUsed(scope: RecomposeScope) { (scope as? RecomposeScopeImpl)?.used = true } |
| } |
| |
| /** |
| * A helper receiver scope class used by [ComposeNode] to help write code to initialized and update a |
| * node. |
| * |
| * @see ComposeNode |
| */ |
| @kotlin.jvm.JvmInline |
| value class Updater<T> constructor( |
| @PublishedApi internal val composer: Composer |
| ) { |
| /** |
| * Set the value property of the emitted node. |
| * |
| * Schedules [block] to be run when the node is first created or when [value] is different |
| * than the previous composition. |
| * |
| * @see update |
| */ |
| @Suppress("NOTHING_TO_INLINE") // Inlining the compare has noticeable impact |
| inline fun set( |
| value: Int, |
| noinline block: T.(value: Int) -> Unit |
| ) = with(composer) { |
| if (inserting || rememberedValue() != value) { |
| updateRememberedValue(value) |
| composer.apply(value, block) |
| } |
| } |
| |
| /** |
| * Set the value property of the emitted node. |
| * |
| * Schedules [block] to be run when the node is first created or when [value] is different |
| * than the previous composition. |
| * |
| * @see update |
| */ |
| fun <V> set( |
| value: V, |
| block: T.(value: V) -> Unit |
| ) = with(composer) { |
| if (inserting || rememberedValue() != value) { |
| updateRememberedValue(value) |
| composer.apply(value, block) |
| } |
| } |
| |
| /** |
| * Update the value of a property of the emitted node. |
| * |
| * Schedules [block] to be run when [value] is different than the previous composition. It is |
| * different than [set] in that it does not run when the node is created. This is used when |
| * initial value set by the [ComposeNode] in the constructor callback already has the correct value. |
| * For example, use [update} when [value] is passed into of the classes constructor |
| * parameters. |
| * |
| * @see set |
| */ |
| @Suppress("NOTHING_TO_INLINE") // Inlining the compare has noticeable impact |
| inline fun update( |
| value: Int, |
| noinline block: T.(value: Int) -> Unit |
| ) = with(composer) { |
| val inserting = inserting |
| if (inserting || rememberedValue() != value) { |
| updateRememberedValue(value) |
| if (!inserting) apply(value, block) |
| } |
| } |
| |
| /** |
| * Update the value of a property of the emitted node. |
| * |
| * Schedules [block] to be run when [value] is different than the previous composition. It is |
| * different than [set] in that it does not run when the node is created. This is used when |
| * initial value set by the [ComposeNode] in the constructor callback already has the correct value. |
| * For example, use [update} when [value] is passed into of the classes constructor |
| * parameters. |
| * |
| * @see set |
| */ |
| fun <V> update( |
| value: V, |
| block: T.(value: V) -> Unit |
| ) = with(composer) { |
| val inserting = inserting |
| if (inserting || rememberedValue() != value) { |
| updateRememberedValue(value) |
| if (!inserting) apply(value, block) |
| } |
| } |
| |
| /** |
| * Initialize emitted node. |
| * |
| * Schedule [block] to be executed after the node is created. |
| * |
| * This is only executed once. The can be used to call a method or set a value on a node |
| * instance that is required to be set after one or more other properties have been set. |
| * |
| * @see reconcile |
| */ |
| fun init(block: T.() -> Unit) { |
| if (composer.inserting) composer.apply<Unit, T>(Unit) { |
| block() |
| } |
| } |
| |
| /** |
| * Reconcile the node to the current state. |
| * |
| * This is used when [set] and [update] are insufficient to update the state of the node |
| * based on changes passed to the function calling [ComposeNode]. |
| * |
| * Schedules [block] to execute. As this unconditionally schedules [block] to executed it |
| * might be executed unnecessarily as no effort is taken to ensure it only executes when the |
| * values [block] captures have changed. It is highly recommended that [set] and [update] be |
| * used instead as they will only schedule their blocks to executed when the value passed to |
| * them has changed. |
| */ |
| @Suppress("MemberVisibilityCanBePrivate") |
| fun reconcile(block: T.() -> Unit) { |
| composer.apply<Unit, T>(Unit) { |
| this.block() |
| } |
| } |
| } |
| |
| @kotlin.jvm.JvmInline |
| value class SkippableUpdater<T> constructor( |
| @PublishedApi internal val composer: Composer |
| ) { |
| inline fun update(block: Updater<T>.() -> Unit) { |
| composer.startReplaceableGroup(0x1e65194f) |
| Updater<T>(composer).block() |
| composer.endReplaceableGroup() |
| } |
| } |
| |
| internal fun SlotWriter.removeCurrentGroup(rememberManager: RememberManager) { |
| // Notify the lifecycle manager of any observers leaving the slot table |
| // The notification order should ensure that listeners are notified of leaving |
| // in opposite order that they are notified of entering. |
| |
| // To ensure this order, we call `enters` as a pre-order traversal |
| // of the group tree, and then call `leaves` in the inverse order. |
| |
| for (slot in groupSlots()) { |
| when (slot) { |
| is RememberObserver -> { |
| rememberManager.forgetting(slot) |
| } |
| is RecomposeScopeImpl -> { |
| val composition = slot.composition |
| if (composition != null) { |
| composition.pendingInvalidScopes = true |
| slot.release() |
| } |
| } |
| } |
| } |
| |
| removeGroup() |
| } |
| |
| // Mutable list |
| private fun <K, V> multiMap() = HashMap<K, LinkedHashSet<V>>() |
| |
| private fun <K, V> HashMap<K, LinkedHashSet<V>>.put(key: K, value: V) = getOrPut(key) { |
| LinkedHashSet() |
| }.add(value) |
| |
| private fun <K, V> HashMap<K, LinkedHashSet<V>>.remove(key: K, value: V) = |
| get(key)?.let { |
| it.remove(value) |
| if (it.isEmpty()) remove(key) |
| } |
| |
| private fun <K, V> HashMap<K, LinkedHashSet<V>>.pop(key: K) = get(key)?.firstOrNull()?.also { |
| remove(key, it) |
| } |
| |
| private fun getKey(value: Any?, left: Any?, right: Any?): Any? = (value as? JoinedKey)?.let { |
| if (it.left == left && it.right == right) value |
| else getKey(it.left, left, right) ?: getKey( |
| it.right, |
| left, |
| right |
| ) |
| } |
| |
| // Invalidation helpers |
| private fun MutableList<Invalidation>.findLocation(location: Int): Int { |
| var low = 0 |
| var high = size - 1 |
| |
| while (low <= high) { |
| val mid = (low + high).ushr(1) // safe from overflows |
| val midVal = get(mid) |
| val cmp = midVal.location.compareTo(location) |
| |
| when { |
| cmp < 0 -> low = mid + 1 |
| cmp > 0 -> high = mid - 1 |
| else -> return mid // key found |
| } |
| } |
| return -(low + 1) // key not found |
| } |
| |
| private fun MutableList<Invalidation>.findInsertLocation(location: Int): Int = |
| findLocation(location).let { if (it < 0) -(it + 1) else it } |
| |
| private fun MutableList<Invalidation>.insertIfMissing( |
| location: Int, |
| scope: RecomposeScopeImpl, |
| instance: Any? |
| ) { |
| val index = findLocation(location) |
| if (index < 0) { |
| add( |
| -(index + 1), |
| Invalidation( |
| scope, |
| location, |
| instance?.let { i -> |
| IdentityArraySet<Any>().also { it.add(i) } |
| } |
| ) |
| ) |
| } else { |
| if (instance == null) { |
| get(index).instances = null |
| } else { |
| get(index).instances?.add(instance) |
| } |
| } |
| } |
| |
| private fun MutableList<Invalidation>.firstInRange(start: Int, end: Int): Invalidation? { |
| val index = findInsertLocation(start) |
| if (index < size) { |
| val firstInvalidation = get(index) |
| if (firstInvalidation.location < end) return firstInvalidation |
| } |
| return null |
| } |
| |
| private fun MutableList<Invalidation>.removeLocation(location: Int): Invalidation? { |
| val index = findLocation(location) |
| return if (index >= 0) removeAt(index) else null |
| } |
| |
| private fun MutableList<Invalidation>.removeRange(start: Int, end: Int) { |
| val index = findInsertLocation(start) |
| while (index < size) { |
| val validation = get(index) |
| if (validation.location < end) removeAt(index) |
| else break |
| } |
| } |
| |
| private fun MutableList<Invalidation>.filterToRange( |
| start: Int, |
| end: Int |
| ): MutableList<Invalidation> { |
| val result = mutableListOf<Invalidation>() |
| var index = findInsertLocation(start) |
| while (index < size) { |
| val invalidation = get(index) |
| if (invalidation.location < end) result.add(invalidation) |
| else break |
| index++ |
| } |
| return result |
| } |
| |
| private fun Boolean.asInt() = if (this) 1 else 0 |
| private fun Int.asBool() = this != 0 |
| |
| private fun SlotTable.collectNodesFrom(anchor: Anchor): List<Any?> { |
| val result = mutableListOf<Any?>() |
| read { reader -> |
| val index = anchorIndex(anchor) |
| fun collectFromGroup(group: Int) { |
| if (reader.isNode(group)) { |
| result.add(reader.node(group)) |
| } else { |
| var current = group + 1 |
| val end = group + reader.groupSize(group) |
| while (current < end) { |
| collectFromGroup(current) |
| current += reader.groupSize(current) |
| } |
| } |
| } |
| collectFromGroup(index) |
| } |
| return result |
| } |
| |
| private fun SlotReader.distanceFrom(index: Int, root: Int): Int { |
| var count = 0 |
| var current = index |
| while (current > 0 && current != root) { |
| current = parent(current) |
| count++ |
| } |
| return count |
| } |
| |
| // find the nearest common root |
| private fun SlotReader.nearestCommonRootOf(a: Int, b: Int, common: Int): Int { |
| // Early outs, to avoid calling distanceFrom in trivial cases |
| if (a == b) return a // A group is the nearest common root of itself |
| if (a == common || b == common) return common // If either is common then common is nearest |
| if (parent(a) == b) return b // if b is a's parent b is the nearest common root |
| if (parent(b) == a) return a // if a is b's parent a is the nearest common root |
| if (parent(a) == parent(b)) return parent(a) // if a an b share a parent it is common |
| |
| // Find the nearest using distance from common |
| var currentA = a |
| var currentB = b |
| val aDistance = distanceFrom(a, common) |
| val bDistance = distanceFrom(b, common) |
| repeat(aDistance - bDistance) { currentA = parent(currentA) } |
| repeat(bDistance - aDistance) { currentB = parent(currentB) } |
| |
| // Both ca and cb are now the same distance from a known common root, |
| // therefore, the first parent that is the same is the lowest common root. |
| while (currentA != currentB) { |
| currentA = parent(currentA) |
| currentB = parent(currentB) |
| } |
| |
| // ca == cb so it doesn't matter which is returned |
| return currentA |
| } |
| |
| private val removeCurrentGroupInstance: Change = { _, slots, rememberManager -> |
| slots.removeCurrentGroup(rememberManager) |
| } |
| |
| private val skipToGroupEndInstance: Change = { _, slots, _ -> slots.skipToGroupEnd() } |
| |
| private val endGroupInstance: Change = { _, slots, _ -> slots.endGroup() } |
| |
| private val startRootGroup: Change = { _, slots, _ -> slots.ensureStarted(0) } |
| |
| private val resetSlotsInstance: Change = { _, slots, _ -> slots.reset() } |
| |
| private val KeyInfo.joinedKey: Any get() = if (objectKey != null) JoinedKey(key, objectKey) else key |
| |
| /* |
| * Integer keys are arbitrary values in the biload range. The do not need to be unique as if |
| * there is a chance they will collide with a compiler generated key they are paired with a |
| * OpaqueKey to ensure they are unique. |
| */ |
| |
| // rootKey doesn't need a corresponding OpaqueKey as it never has sibling nodes and will always |
| // a unique key. |
| private const val rootKey = 100 |
| |
| // An arbitrary key value for a node. |
| private const val nodeKey = 125 |
| |
| // An arbitrary key value for a node used to force the node to be replaced. |
| private const val nodeKeyReplace = 126 |
| |
| // An arbitrary key value for a node used to force the node to be replaced. |
| private const val defaultsKey = -127 |
| |
| @PublishedApi |
| internal const val invocationKey = 200 |
| |
| @PublishedApi |
| internal val invocation: Any = OpaqueKey("provider") |
| |
| @PublishedApi |
| internal const val providerKey = 201 |
| |
| @PublishedApi |
| internal val provider: Any = OpaqueKey("provider") |
| |
| @PublishedApi |
| internal const val compositionLocalMapKey = 202 |
| |
| @PublishedApi |
| internal val compositionLocalMap: Any = OpaqueKey("compositionLocalMap") |
| |
| @PublishedApi |
| internal const val providerValuesKey = 203 |
| |
| @PublishedApi |
| internal val providerValues: Any = OpaqueKey("providerValues") |
| |
| @PublishedApi |
| internal const val providerMapsKey = 204 |
| |
| @PublishedApi |
| internal val providerMaps: Any = OpaqueKey("providers") |
| |
| @PublishedApi |
| internal const val referenceKey = 206 |
| |
| @PublishedApi |
| internal val reference: Any = OpaqueKey("reference") |
| |
| @PublishedApi |
| internal const val reuseKey = 207 |
| |
| private const val invalidGroupLocation = -2 |
| |
| internal class ComposeRuntimeError(override val message: String) : IllegalStateException() |
| |
| internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> Any) { |
| if (!value) { |
| val message = lazyMessage() |
| composeRuntimeError(message.toString()) |
| } |
| } |
| |
| internal fun runtimeCheck(value: Boolean) = runtimeCheck(value) { "Check failed" } |
| |
| internal fun composeRuntimeError(message: String): Nothing { |
| throw ComposeRuntimeError( |
| "Compose Runtime internal error. Unexpected or incorrect use of the Compose " + |
| "internal runtime API ($message). Please report to Google or use " + |
| "https://goo.gle/compose-feedback" |
| ) |
| } |