blob: 40c1527e6a8d74b566603333438bf98b49dec066 [file]
/*
* Copyright 2025 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)
@file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
package androidx.compose.runtime
import androidx.collection.MutableIntIntMap
import androidx.collection.MutableIntObjectMap
import androidx.collection.MutableScatterMap
import androidx.collection.MutableScatterSet
import androidx.collection.ScatterSet
import androidx.collection.mutableScatterSetOf
import androidx.compose.runtime.collection.MultiValueMap
import androidx.compose.runtime.collection.ScopeMap
import androidx.compose.runtime.composer.GroupInfo
import androidx.compose.runtime.composer.GroupKind
import androidx.compose.runtime.composer.RememberManager
import androidx.compose.runtime.composer.gapbuffer.GapAnchor
import androidx.compose.runtime.composer.gapbuffer.KeyInfo
import androidx.compose.runtime.composer.gapbuffer.SlotReader
import androidx.compose.runtime.composer.gapbuffer.SlotTable
import androidx.compose.runtime.composer.gapbuffer.SlotWriter
import androidx.compose.runtime.composer.gapbuffer.asGapAnchor
import androidx.compose.runtime.composer.gapbuffer.asGapBufferSlotTable
import androidx.compose.runtime.composer.gapbuffer.changelist.ChangeList
import androidx.compose.runtime.composer.gapbuffer.changelist.ComposerChangeListWriter
import androidx.compose.runtime.composer.gapbuffer.changelist.FixupList
import androidx.compose.runtime.composer.gapbuffer.changelist.asGapBufferChangeList
import androidx.compose.runtime.composer.gapbuffer.compositionGroupOf
import androidx.compose.runtime.internal.IntRef
import androidx.compose.runtime.internal.invokeComposable
import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
import androidx.compose.runtime.internal.trace
import androidx.compose.runtime.snapshots.currentSnapshot
import androidx.compose.runtime.snapshots.fastForEach
import androidx.compose.runtime.snapshots.fastToSet
import androidx.compose.runtime.tooling.ComposeStackTrace
import androidx.compose.runtime.tooling.ComposeStackTraceFrame
import androidx.compose.runtime.tooling.ComposeToolingApi
import androidx.compose.runtime.tooling.CompositionData
import androidx.compose.runtime.tooling.CompositionErrorContextImpl
import androidx.compose.runtime.tooling.CompositionGroup
import androidx.compose.runtime.tooling.CompositionInstance
import androidx.compose.runtime.tooling.LocalCompositionErrorContext
import androidx.compose.runtime.tooling.LocalInspectionTables
import androidx.compose.runtime.tooling.attachComposeStackTrace
import androidx.compose.runtime.tooling.buildTrace
import androidx.compose.runtime.tooling.findLocation
import androidx.compose.runtime.tooling.findSubcompositionContextGroup
import androidx.compose.runtime.tooling.traceForGroup
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* 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 GapPending(val keyInfos: MutableList<KeyInfo>, val startIndex: Int) {
var groupIndex: Int = 0
init {
requirePrecondition(startIndex >= 0) { "Invalid start index" }
}
private val usedKeys = mutableListOf<KeyInfo>()
private val groupInfos = run {
var runningNodeIndex = 0
val result = MutableIntObjectMap<GroupInfo>()
for (index in 0 until keyInfos.size) {
val keyInfo = keyInfos[index]
result[keyInfo.location] = GroupInfo(index, runningNodeIndex, keyInfo.nodes)
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>(keyInfos.size).also {
for (index in 0 until keyInfos.size) {
val keyInfo = keyInfos[index]
it.add(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.removeFirst(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.forEachValue { 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.forEachValue { 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.forEachValue { 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.forEachValue { 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.forEachValue { 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. */
var 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.
*
* Can contain a [ScatterSet] of instances, single instance or null.
*/
var instances: Any?,
) {
fun isInvalid(): Boolean = scope.isInvalidFor(instances)
}
/** Implementation of a composer for a mutable tree. */
@OptIn(ExperimentalComposeRuntimeApi::class)
internal class GapComposer(
/** 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: Changes,
private var lateChanges: Changes,
private val observerHolder: CompositionObserverHolder,
/** The composition that owns this composer */
override val composition: CompositionImpl,
) : InternalComposer() {
private val pendingStack = Stack<GapPending?>()
private var pending: GapPending? = null
private var nodeIndex: Int = 0
private var groupNodeCount: Int = 0
private var rGroupIndex: Int = 0
private val parentStateStack = IntStack()
private var nodeCountOverrides: IntArray? = null
private var nodeCountVirtualOverrides: MutableIntIntMap? = 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 rootProvider: PersistentCompositionLocalMap = persistentCompositionLocalHashMapOf()
private var providerUpdates: MutableIntObjectMap<PersistentCompositionLocalMap>? = null
private var providersInvalid = false
private val providersInvalidStack = IntStack()
private var reusing = false
private var reusingGroup = -1
private var childrenComposing: Int = 0
private var parentComposing = false
private var compositionToken: Int = 0
override var sourceMarkersEnabled =
parentContext.collectingSourceInformation || parentContext.collectingCallByInformation
private val derivedStateObserver =
object : DerivedStateObserver {
override fun start(derivedState: DerivedState<*>) {
childrenComposing++
}
override fun done(derivedState: DerivedState<*>) {
childrenComposing--
}
}
private val invalidateStack = Stack<RecomposeScopeImpl>()
override var isComposing = false
private set
internal var isDisposed = false
private set
override val areChildrenComposing
get() = childrenComposing > 0
override val hasPendingChanges: Boolean
get() = changes.isNotEmpty()
internal var reader: SlotReader = slotTable.openReader().also { it.close() }
internal var insertTable =
SlotTable().apply {
if (parentContext.collectingSourceInformation) collectSourceInformation()
if (parentContext.collectingCallByInformation) collectCalledByInformation()
}
private var writer: SlotWriter = insertTable.openWriter().also { it.close(true) }
private var writerHasAProvider = false
private var providerCache: PersistentCompositionLocalMap? = null
override var deferredChanges: ChangeList? = null
private val changeListWriter = ComposerChangeListWriter(this, changes.asGapBufferChangeList())
private var insertAnchor: GapAnchor = insertTable.read { it.anchor(0) }
private var insertFixups = FixupList()
private var pausable: Boolean = false
private var shouldPauseCallback: ShouldPauseCallback? = null
override val errorContext: CompositionErrorContextImpl? = CompositionErrorContextImpl(this)
get() = if (parentContext.stackTraceEnabled) field else null
override val applyCoroutineContext: CoroutineContext =
parentContext.effectCoroutineContext + (errorContext ?: EmptyCoroutineContext)
/**
* 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: Versions of the compiler that generate calls to this function also contain subtle
* bug that does not generate a group around a loop containing code that just creates composable
* lambdas (AnimatedContent from androidx.compose.animation, for example) which makes replacing
* the group unsafe and the this must treat this like a movable group. [startReplaceGroup] was
* added that will replace the group as described above and is only called by versions of the
* compiler that correctly generate code around loops that create lambdas.
*
* 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, GroupKind.Group, 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()
/** See [Composer.startReplaceGroup] */
@ComposeCompilerApi
override fun startReplaceGroup(key: Int) {
val pending = pending
if (pending != null) {
start(key, null, GroupKind.Group, null)
return
}
validateNodeNotExpected()
updateCompositeKeyWhenWeEnterGroup(key, rGroupIndex, null, null)
rGroupIndex++
val reader = reader
if (inserting) {
reader.beginEmpty()
writer.startGroup(key, Composer.Empty)
enterGroup(false, null)
return
}
val slotKey = reader.groupKey
if (slotKey == key && !reader.hasObjectKey) {
reader.startGroup()
enterGroup(false, null)
return
}
if (!reader.isGroupEnd) {
// Delete the group that was not expected
val removeIndex = nodeIndex
val startSlot = reader.currentGroup
recordDelete()
val nodesToRemove = reader.skipGroup()
changeListWriter.removeNode(removeIndex, nodesToRemove)
invalidations.removeRange(startSlot, reader.currentGroup)
}
// Insert the new group
reader.beginEmpty()
inserting = true
providerCache = null
ensureWriter()
val writer = writer
writer.beginInsert()
val startIndex = writer.currentGroup
writer.startGroup(key, Composer.Empty)
insertAnchor = writer.anchor(startIndex)
enterGroup(false, null)
}
/** See [Composer.endReplaceGroup] */
@ComposeCompilerApi override fun endReplaceGroup() = 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, GroupKind.Group, 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 !skipping || 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, GroupKind.Group, 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() {
rGroupIndex = 0
reader = slotTable.openReader()
startGroup(rootKey)
// parent reference management
parentContext.startComposing()
parentComposing = true
val parentProvider = parentContext.getCompositionLocalScope()
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = changed(parentProvider)
providerCache = null
// Inform observer if one is defined
if (!forceRecomposeScopes) {
forceRecomposeScopes = parentContext.collectingParameterInformation
}
// Propagate collecting source information
if (!sourceMarkersEnabled) {
sourceMarkersEnabled = parentContext.collectingSourceInformation
}
rootProvider =
if (sourceMarkersEnabled) {
@Suppress("UNCHECKED_CAST") // ProvidableCompositionLocal to CompositionLocal
parentProvider.putValue(
LocalCompositionErrorContext as CompositionLocal<Any?>,
StaticValueHolder(errorContext),
)
} else {
parentProvider
}
rootProvider.read(LocalInspectionTables)?.let {
it.add(compositionData)
parentContext.recordInspectionTable(it)
}
startGroup(parentContext.compositeKeyHashCode.hashCode())
}
/**
* 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()
parentComposing = false
parentContext.doneComposing()
endGroup()
changeListWriter.endRoot()
finalizeCompose()
reader.close()
forciblyRecompose = false
providersInvalid = providersInvalidStack.pop().asBool()
}
/** Discard a pending composition because an error was encountered during composition */
@OptIn(InternalComposeApi::class)
private fun abortRoot() {
if (parentComposing) {
parentComposing = false
parentContext.doneComposing()
}
cleanUpCompose()
pendingStack.clear()
parentStateStack.clear()
entersStack.clear()
providersInvalidStack.clear()
providerUpdates = null
insertFixups.clear()
compositeKeyHashCode = CompositeKeyHashCode(0)
childrenComposing = 0
nodeExpected = false
inserting = false
reusing = false
isComposing = false
forciblyRecompose = false
reusingGroup = -1
if (!reader.closed) {
reader.close()
}
if (!writer.closed) {
// We cannot just close the insert table as the state of the table is uncertain
// here and writer.close() might throw.
forceFreshInsertTable()
}
}
override fun changesApplied() {
providerUpdates = null
}
/**
* 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 composite key calculated as a combination of the keys of all the
* currently started groups via [startGroup].
*/
@InternalComposeApi
override var compositeKeyHashCode: CompositeKeyHashCode = EmptyCompositeKeyHashCode
private set
/**
* Start collecting parameter information and line number information. This enables the tools
* API to always be able to determine the parameter values of composable calls as well as the
* source location of calls.
*/
override fun collectParameterInformation() {
forceRecomposeScopes = true
sourceMarkersEnabled = true
slotTable.collectSourceInformation()
insertTable.collectSourceInformation()
writer.updateToTableMaps()
}
override fun scheduleFrameEndCallback(action: () -> Unit): CancellationHandle {
return parentContext.scheduleFrameEndCallback(action)
}
@OptIn(InternalComposeApi::class)
override fun dispose() {
trace("Compose:Composer.dispose") {
parentContext.unregisterComposer(this)
deactivate()
applier.clear()
isDisposed = true
}
}
override fun deactivate() {
invalidateStack.clear()
invalidations.clear()
changes.clear()
providerUpdates = null
}
override 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, GroupKind.Group, null)
private fun startGroup(key: Int, dataKey: Any?) = start(key, dataKey, GroupKind.Group, 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() {
start(nodeKey, null, GroupKind.Node, null)
nodeExpected = true
}
override fun startReusableNode() {
start(nodeKey, null, GroupKind.ReusableNode, 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 = parentStateStack.peek()
val groupAnchor = writer.anchor(writer.parent)
groupNodeCount++
insertFixups.createAndInsertNode(factory, insertIndex, groupAnchor)
}
/** 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" }
val node = reader.node
changeListWriter.moveDown(node)
if (reusing && node is ComposeNodeLifecycleCallback) {
changeListWriter.useNode(node)
}
}
/** Called to end the node group. */
override fun endNode() = end(isNode = true)
override fun startReusableGroup(key: Int, dataKey: Any?) {
if (!inserting) {
if (reader.groupKey == key && reader.groupAux != dataKey && reusingGroup < 0) {
// Starting to reuse nodes
reusingGroup = reader.currentGroup
reusing = true
}
}
start(key, null, GroupKind.Group, 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 fun startReuseFromRoot() {
// 0 is the root group index
reusingGroup = 0
reusing = true
}
override fun endReuseFromRoot() {
// 0 is the root group index
requirePrecondition(!isComposing && reusingGroup == 0) {
"Cannot disable reuse from root if it was caused by other groups"
}
reusingGroup = -1
reusing = false
}
override val currentMarker: Int
get() = if (inserting) -writer.parent else reader.parent
override fun endToMarker(marker: Int) {
if (marker < 0) {
// If the marker is negative then the marker is for the writer
val writerLocation = -marker
val writer = writer
while (true) {
val parent = writer.parent
if (parent <= writerLocation) break
end(writer.isNode(parent))
}
} else {
// If the marker is positive then the marker is for the reader. However, if we are
// inserting then we need to close the inserting groups first.
if (inserting) {
// We might be inserting, we need to close all the groups until we are no longer
// inserting.
val writer = writer
while (inserting) {
end(writer.isNode(writer.parent))
}
}
val reader = reader
while (true) {
val parent = reader.parent
if (parent <= marker) break
end(reader.isNode(parent))
}
}
}
/**
* 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) {
if (inserting) {
insertFixups.updateNode(value, block)
} else {
changeListWriter.updateNode(value, block)
}
}
/**
* 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 && it !is ReusableRememberObserverHolder) Composer.Empty else it
}
@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun nextSlotForCache(): Any? {
return if (inserting) {
validateNodeNotExpected()
Composer.Empty
} else
reader.next().let {
if (reusing && it !is ReusableRememberObserverHolder) Composer.Empty
else if (it is RememberObserverHolder) it.wrapped 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 = nextSlotForCache()
if (result === Composer.Empty || invalid) {
val value = block()
updateCachedValue(value)
result = value
}
@Suppress("UNCHECKED_CAST")
return result as T
}
private fun updateSlot(value: Any?) {
nextSlot()
updateValue(value)
}
/**
* 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)
} else {
if (reader.hadNext) {
// We need to update the slot we just read so which is is one previous to the
// current group slot index.
val groupSlotIndex = reader.groupSlotIndex - 1
if (changeListWriter.pastParent) {
// The reader is after the first child of the group so we cannot reposition the
// writer to the parent to update it as this will cause the writer to navigate
// backward which violates the single pass, forward walking nature of update.
// Using an anchored updated allows to to violate this principle just for
// updating slots as this is required if the update occurs after the writer has
// been moved past the parent.
changeListWriter.updateAnchoredValue(
value,
reader.anchor(reader.parent),
groupSlotIndex,
)
} else {
// No children have been seen yet so we are still in a position where we can
// directly update the parent.
changeListWriter.updateValue(value, groupSlotIndex)
}
} else {
// This uses an anchor for the same reason as `updateAnchoredValue` uses and anchor,
// the writer might have advanced past the parent and we need to go back and update
// the parent. As this is likely to never occur in an empty group, we don't bother
// checking if the reader has moved so we don't need an anchored and un-anchored
// version of the same function.
changeListWriter.appendValue(reader.anchor(reader.parent), value)
}
}
}
/**
* 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?) {
val toStore =
if (value is RememberObserver) {
val holder = GapRememberObserverHolder(value, rememberObserverGroupIndex())
if (inserting) {
changeListWriter.remember(holder)
}
abandonSet.add(value)
holder
} else value
updateValue(toStore)
}
private fun rememberObserverGroupIndex() = rGroupIndex - 1
private var _compositionData: CompositionData? = null
override val compositionData: CompositionData
get() {
val data = _compositionData
if (data == null) {
val newData = GapCompositionDataImpl(composition)
_compositionData = newData
return newData
}
return data
}
/** Schedule a side effect to run when we apply composition changes. */
override fun recordSideEffect(effect: () -> Unit) {
changeListWriter.sideEffect(effect)
}
private fun currentCompositionLocalScope(): PersistentCompositionLocalMap {
providerCache?.let {
return it
}
return currentCompositionLocalScope(reader.parent)
}
override val currentCompositionLocalMap: CompositionLocalMap
get() = currentCompositionLocalScope()
/** Return the current [CompositionLocal] scope which was provided by a parent group. */
private fun currentCompositionLocalScope(group: Int): PersistentCompositionLocalMap {
if (inserting && writerHasAProvider) {
var current = writer.parent
while (current > 0) {
if (
writer.groupKey(current) == compositionLocalMapKey &&
writer.groupObjectKey(current) == compositionLocalMap
) {
val providers = writer.groupAux(current) as PersistentCompositionLocalMap
providerCache = providers
return providers
}
current = writer.parent(current)
}
}
if (reader.size > 0) {
var current = group
while (current > 0) {
if (
reader.groupKey(current) == compositionLocalMapKey &&
reader.groupObjectKey(current) == compositionLocalMap
) {
val providers =
providerUpdates?.get(current)
?: reader.groupAux(current) as PersistentCompositionLocalMap
providerCache = providers
return providers
}
current = reader.parent(current)
}
}
providerCache = rootProvider
return rootProvider
}
/**
* 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: PersistentCompositionLocalMap,
currentProviders: PersistentCompositionLocalMap,
): PersistentCompositionLocalMap {
val providerScope = parentScope.mutate { it.putAll(currentProviders) }
startGroup(providerMapsKey, providerMaps)
updateSlot(providerScope)
updateSlot(currentProviders)
endGroup()
return providerScope
}
@InternalComposeApi
@Suppress("UNCHECKED_CAST")
override fun startProvider(value: ProvidedValue<*>) {
val parentScope = currentCompositionLocalScope()
startGroup(providerKey, provider)
val oldState =
rememberedValue().let { if (it == Composer.Empty) null else it as ValueHolder<Any?> }
val local = value.compositionLocal as CompositionLocal<Any?>
val state = local.updatedStateOf(value as ProvidedValue<Any?>, oldState)
val change = state != oldState
if (change) {
updateRememberedValue(state)
}
val providers: PersistentCompositionLocalMap
val invalid: Boolean
if (inserting) {
providers =
if (value.canOverride || !parentScope.contains(local)) {
parentScope.putValue(local, state)
} else {
parentScope
}
invalid = false
writerHasAProvider = true
} else {
val oldScope = reader.groupAux(reader.currentGroup) as PersistentCompositionLocalMap
providers =
when {
(!skipping || change) && (value.canOverride || !parentScope.contains(local)) ->
parentScope.putValue(local, state)
!change && !providersInvalid -> oldScope
providersInvalid -> parentScope
else -> oldScope
}
invalid = reusing || oldScope !== providers
}
if (invalid && !inserting) {
recordProviderUpdate(providers)
}
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = invalid
providerCache = providers
start(compositionLocalMapKey, compositionLocalMap, GroupKind.Group, providers)
}
private fun recordProviderUpdate(providers: PersistentCompositionLocalMap) {
val providerUpdates =
providerUpdates
?: run {
val newProviderUpdates = MutableIntObjectMap<PersistentCompositionLocalMap>()
this.providerUpdates = newProviderUpdates
newProviderUpdates
}
providerUpdates[reader.currentGroup] = providers
}
@InternalComposeApi
override fun endProvider() {
endGroup()
endGroup()
providersInvalid = providersInvalidStack.pop().asBool()
providerCache = null
}
@InternalComposeApi
override fun startProviders(values: Array<out ProvidedValue<*>>) {
val parentScope = currentCompositionLocalScope()
startGroup(providerKey, provider)
val providers: PersistentCompositionLocalMap
val invalid: Boolean
if (inserting) {
val currentProviders = updateCompositionMap(values, parentScope)
providers = updateProviderMapGroup(parentScope, currentProviders)
invalid = false
writerHasAProvider = true
} else {
val oldScope = reader.groupGet(0) as PersistentCompositionLocalMap
val oldValues = reader.groupGet(1) as PersistentCompositionLocalMap
val currentProviders = updateCompositionMap(values, parentScope, oldValues)
// skipping is true iff parentScope has not changed.
if (!skipping || reusing || 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 = reusing || providers != oldScope
} else {
// Nothing has changed
skipGroup()
providers = oldScope
invalid = false
}
}
if (invalid && !inserting) {
recordProviderUpdate(providers)
}
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = invalid
providerCache = providers
start(compositionLocalMapKey, compositionLocalMap, GroupKind.Group, providers)
}
@InternalComposeApi
override fun endProviders() {
endGroup()
endGroup()
providersInvalid = providersInvalidStack.pop().asBool()
providerCache = null
}
@InternalComposeApi
override fun <T> consume(key: CompositionLocal<T>): T = currentCompositionLocalScope().read(key)
/**
* 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 observerHolder = nextSlot() as? RememberObserverHolder
if (observerHolder == null) {
observerHolder =
ReusableGapRememberObserverHolder(
CompositionContextHolder(
CompositionContextImpl(
this@GapComposer.compositeKeyHashCode,
forceRecomposeScopes,
sourceMarkersEnabled,
composition.observerHolder,
)
),
afterGroupIndex = -1,
)
updateValue(observerHolder)
}
val holder = observerHolder.wrapped as CompositionContextHolder
holder.ref.updateCompositionLocalScope(currentCompositionLocalScope())
endGroup()
return holder.ref
}
override 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)
forceFreshInsertTable()
}
private fun forceFreshInsertTable() {
insertTable =
SlotTable().apply {
if (sourceMarkersEnabled) collectSourceInformation()
if (parentContext.collectingCallByInformation) collectCalledByInformation()
}
writer = insertTable.openWriter().also { it.close(true) }
}
/** 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) {
changeListWriter.updateAuxData(data)
}
reader.startGroup()
}
}
private fun start(key: Int, objectKey: Any?, kind: GroupKind, data: Any?) {
validateNodeNotExpected()
updateCompositeKeyWhenWeEnterGroup(key, rGroupIndex, objectKey, data)
if (objectKey == null) rGroupIndex++
// 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.
val isNode = kind.isNode
if (inserting) {
reader.beginEmpty()
val startIndex = writer.currentGroup
when {
isNode -> writer.startNode(key, 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
}
val forceReplace = !kind.isReusable && reusing
if (pending == null) {
val slotKey = reader.groupKey
if (!forceReplace && slotKey == key && objectKey == reader.groupObjectKey) {
// The group is the same as what was generated last time.
startReaderGroup(isNode, data)
} else {
pending = GapPending(reader.extractKeys(), nodeIndex)
}
}
val pending = pending
var newPending: GapPending? = 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 (!forceReplace && 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)
changeListWriter.moveReaderRelativeTo(location)
reader.reposition(location)
if (currentRelativePosition > 0) {
// The slot group must be moved, record the move to be performed during apply.
changeListWriter.moveCurrentGroup(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(key, 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 = GapPending(mutableListOf(), if (isNode) 0 else nodeIndex)
}
}
enterGroup(isNode, newPending)
}
private fun enterGroup(isNode: Boolean, newPending: GapPending?) {
// 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.parentStateStack.push(groupNodeCount)
this.parentStateStack.push(rGroupIndex)
this.parentStateStack.push(nodeIndex)
if (isNode) nodeIndex = 0
groupNodeCount = 0
rGroupIndex = 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 = parentStateStack.pop() + expectedNodeCount
this.rGroupIndex = parentStateStack.pop()
this.groupNodeCount = parentStateStack.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.
// The rGroupIndex for parent is two pack from the current stack top which has already been
// incremented past this group needs to be offset by one.
val rGroupIndex = parentStateStack.peek2() - 1
if (inserting) {
val parent = writer.parent
updateCompositeKeyWhenWeExitGroup(
writer.groupKey(parent),
rGroupIndex,
writer.groupObjectKey(parent),
writer.groupAux(parent),
)
} else {
val parent = reader.parent
updateCompositeKeyWhenWeExitGroup(
reader.groupKey(parent),
rGroupIndex,
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 = mutableScatterSetOf<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)
changeListWriter.removeNode(
nodeIndex = deleteOffset + pending.startIndex,
count = previousInfo.nodes,
)
pending.updateNodeCount(previousInfo.location, 0)
changeListWriter.moveReaderRelativeTo(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)
changeListWriter.moveNode(
from = nodePosition + pending.startIndex,
to = nodeOffset + pending.startIndex,
count = 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.
changeListWriter.endNodeMovement()
// 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) {
changeListWriter.moveReaderRelativeTo(reader.groupEnd)
reader.skipToGroupEnd()
}
}
val inserting = inserting
if (!inserting) {
// Detect when slots were not used. This happens when a `remember` was removed at the
// end of a group. Due to code generation issues (b/346821372) this may also see
// remembers that were removed prior to the children being called so this must be done
// before the children are deleted to ensure that the `RememberEventDispatcher` receives
// the `leaving()` call in the correct order so the `onForgotten` is dispatched in the
// correct order for the values being removed.
val remainingSlots = reader.remainingSlots
if (remainingSlots > 0) {
changeListWriter.trimValues(remainingSlots)
}
}
// 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()
changeListWriter.removeNode(removeIndex, nodesToRemove)
invalidations.removeRange(startSlot, reader.currentGroup)
}
if (inserting) {
if (isNode) {
insertFixups.endNodeInsert()
expectedNodeCount = 1
}
reader.endEmpty()
val parentGroup = writer.parent
writer.endGroup()
if (!reader.inEmpty) {
val virtualIndex = insertedGroupVirtualIndex(parentGroup)
writer.endInsert()
writer.close(true)
recordInsert(insertAnchor)
this.inserting = false
if (!slotTable.isEmpty) {
updateNodeCount(virtualIndex, 0)
updateNodeCountOverrides(virtualIndex, expectedNodeCount)
}
}
} else {
if (isNode) changeListWriter.moveUp()
changeListWriter.endCurrentGroup()
val parentGroup = reader.parent
val parentNodeCount = updatedNodeCount(parentGroup)
if (expectedNodeCount != parentNodeCount) {
updateNodeCountOverrides(parentGroup, expectedNodeCount)
}
if (isNode) {
expectedNodeCount = 1
}
reader.endGroup()
changeListWriter.endNodeMovement()
}
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 recomposeCompositeKey = this@GapComposer.compositeKeyHashCode
val oldGroupNodeCount = groupNodeCount
val oldRGroupIndex = rGroupIndex
var oldGroup = parent
var firstInRange = invalidations.firstInRange(reader.currentGroup, end)
while (firstInRange != null) {
val location = firstInRange.location
val scope = firstInRange.scope
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 current rGroupIndex for this node, storing any parent rGroup
// indexes we needed into the rGroup IntList
rGroupIndex = rGroupIndexOf(newGroup)
// Calculate the composite hash code (a semi-unique code for every group in the
// composition used to restore saved state).
val newParent = reader.parent(newGroup)
this@GapComposer.compositeKeyHashCode =
compositeKeyOf(newParent, parent, recomposeCompositeKey)
// We have moved so the cached lookup of the provider is invalid
providerCache = null
// Invoke the function with the same parameters as the last composition (which
// were captured in the lambda set into the scope).
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(scope)
val observer = observerHolder.current()
if (observer != null) {
try {
observer.onScopeEnter(scope)
scope.rereadTrackedInstances()
} finally {
observer.onScopeExit(scope)
}
} else {
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
rGroupIndex = oldRGroupIndex
} else {
// No recompositions were requested in the range, skip it.
skipReaderToGroupEnd()
// No need to restore the parent state for nodeIndex, groupNodeCount and
// rGroupIndex as they are going to be restored immediately by the endGroup
}
this@GapComposer.compositeKeyHashCode = recomposeCompositeKey
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 anchor 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 += if (reader.isNode(current)) 1 else updatedNodeCount(current)
current = end
}
break
}
return index
}
private fun rGroupIndexOf(group: Int): Int {
var result = 0
val parent = reader.parent(group)
var child = parent + 1
while (child < group) {
if (!reader.hasObjectKey(child)) result++
child += reader.groupSize(child)
}
return result
}
private fun updatedNodeCount(group: Int): Int {
if (group < 0)
return nodeCountVirtualOverrides?.let { if (it.contains(group)) it[group] else 0 } ?: 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 = MutableIntIntMap()
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)) changeListWriter.moveUp()
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)) changeListWriter.moveDown(reader.nodeAt(group))
}
}
/**
* Calculate the composite 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 compositeKeyOf(
group: Int,
recomposeGroup: Int,
recomposeKey: CompositeKeyHashCode,
): CompositeKeyHashCode {
// The general form of a group's compositeKey can be solved by recursively evaluating:
// compositeKey(group) = ((compositeKey(parent(group)) rol 3)
// xor compositeKeyPart(group) rol 3) xor effectiveRGroupIndex
//
// To solve this without recursion, first expand the terms:
// compositeKey(group) = (compositeKey(parent(group)) rol 6)
// xor (compositeKeyPart(group) rol 3)
// xor effectiveRGroupIndex
//
// Then rewrite this as an iterative XOR sum, where n represents the distance from the
// starting node and takes the range 0 <= n < depth(group) and g - n represents the n-th
// parent of g, and all terms are XOR-ed together:
//
// [compositeKeyPart(g - n) rol (6n + 3)] xor [rGroupIndexOf(g - n) rol (6n)]
//
// Because compositeKey(g - n) is known when (g - n) == recomposeGroup, we can terminate
// early and substitute that iteration's terms with recomposeKey rol (6n).
var keyRot = 3
var rgiRot = 0
var result = CompositeKeyHashCode(0)
var parent = group
while (parent >= 0) {
if (parent == recomposeGroup) {
result = result.bottomUpCompoundWith(recomposeKey, rgiRot)
return result
}
val groupKey = reader.groupCompositeKeyPart(parent)
if (groupKey == movableContentKey) {
result = result.bottomUpCompoundWith(groupKey, rgiRot)
return result
}
val effectiveRGroupIndex = if (reader.hasObjectKey(parent)) 0 else rGroupIndexOf(parent)
result =
result
.bottomUpCompoundWith(groupKey, keyRot)
.bottomUpCompoundWith(effectiveRGroupIndex, rgiRot)
keyRot = (keyRot + 6) % CompositeKeyHashSizeBits
rgiRot = (rgiRot + 6) % CompositeKeyHashSizeBits
parent = reader.parent(parent)
}
return result
}
private fun SlotReader.groupCompositeKeyPart(group: Int): 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
}
override fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean {
val anchor = scope.anchor ?: return false
val slotTable = reader.table
val location = anchor.asGapAnchor().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
override 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
val rGroupIndex = rGroupIndex
updateCompositeKeyWhenWeEnterGroup(key, rGroupIndex, dataKey, aux)
startReaderGroup(reader.isNode, null)
recomposeToGroupEnd()
reader.endGroup()
updateCompositeKeyWhenWeExitGroup(key, rGroupIndex, dataKey, aux)
}
}
private fun skipReaderToGroupEnd() {
groupNodeCount = reader.parentNodes
reader.skipToGroupEnd()
}
@ComposeCompilerApi
override fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean {
// We only want to pause when we are not resuming and only when inserting new content or
// when reusing content. This 0 bit of `flags` is only 1 if this function was restarted by
// the restart lambda. The other bits of this flags are currently all 0's and are reserved
// for future use.
if (((flags and 1) == 0) && (inserting || reusing)) {
val callback = shouldPauseCallback ?: return true
val scope = currentRecomposeScope ?: return true
val pausing = callback.shouldPause()
if (pausing && !scope.resuming) {
scope.used = true
// Force the composer back into the reusing state when this scope restarts.
scope.reusing = reusing
scope.paused = true
// Remember a place-holder object to ensure all remembers are sent in the correct
// order. The remember manager will record the remember callback for the resumed
// content into a place-holder to ensure that, when the remember callbacks are
// dispatched, the callbacks for the resumed content are dispatched in the same
// order they would have been had the content not paused.
changeListWriter.rememberPausingScope(scope)
parentContext.reportPausedScope(scope)
return false
}
return true
}
// Otherwise we should execute the function if the parameters have changed or when
// skipping is disabled.
return parametersChanged || !skipping
}
/** 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"
}
// This can be called when inserting is true and `shouldExecute` returns false.
// When `inserting` the writer is already at the end of the group so we don't need to
// move the writer.
if (!inserting) {
currentRecomposeScope?.scopeSkipped()
if (invalidations.isEmpty()) {
skipReaderToGroupEnd()
} else {
recomposeToGroupEnd()
}
}
}
@ComposeCompilerApi
override fun deactivateToEndGroup(changed: Boolean) {
runtimeCheck(groupNodeCount == 0) {
"No nodes can be emitted before calling deactivateToEndGroup"
}
if (!inserting) {
if (!changed) {
skipReaderToGroupEnd()
return
}
val start = reader.currentGroup
val end = reader.currentEnd
changeListWriter.deactivateCurrentGroup()
invalidations.removeRange(start, end)
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 {
startReplaceGroup(key)
addRecomposeScope()
return this
}
private fun addRecomposeScope() {
if (inserting) {
val scope = RecomposeScopeImpl(composition as CompositionImpl)
invalidateStack.push(scope)
updateValue(scope)
enterRecomposeScope(scope)
} 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 ||
scope.forcedRecompose.also { forced ->
if (forced) scope.forcedRecompose = false
}
invalidateStack.push(scope)
enterRecomposeScope(scope)
if (scope.paused) {
scope.paused = false
scope.resuming = true
changeListWriter.startResumingScope(scope)
if (!reusing && scope.reusing) {
reusing = true
reusingGroup = reader.parent
scope.resetReusing = true
}
}
}
}
private fun enterRecomposeScope(scope: RecomposeScopeImpl) {
scope.start(compositionToken)
observerHolder.current()?.onScopeEnter(scope)
}
/**
* 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
if (scope != null) {
scope.requiresRecompose = false
exitRecomposeScope(scope)?.let { changeListWriter.endCompositionScope(it, composition) }
if (scope.resuming) {
scope.resuming = false
changeListWriter.endResumingScope(scope)
scope.reusing = false
if (scope.resetReusing) {
scope.resetReusing = false
if (reusingGroup == reader.parent) {
reusing = false
reusingGroup = -1
}
}
}
}
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
}
private fun exitRecomposeScope(scope: RecomposeScopeImpl): ((Composition) -> Unit)? {
observerHolder.current()?.onScopeExit(scope)
return scope.end(compositionToken)
}
@InternalComposeApi
override fun insertMovableContent(value: MovableContent<*>, parameter: Any?) {
@Suppress("UNCHECKED_CAST")
invokeMovableContentLambda(
value as MovableContent<Any?>,
currentCompositionLocalScope(),
parameter,
force = false,
)
}
@OptIn(ExperimentalComposeApi::class)
private fun invokeMovableContentLambda(
content: MovableContent<Any?>,
locals: PersistentCompositionLocalMap,
parameter: Any?,
force: Boolean,
) {
// Start the movable content group
startMovableGroup(movableContentKey, content)
updateSlot(parameter)
// All movable content has a composite hash value rooted at the content itself so the hash
// value doesn't change as the content moves in the tree.
val savedCompositeKeyHash = compositeKeyHashCode
try {
compositeKeyHashCode = CompositeKeyHashCode(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) recordProviderUpdate(locals)
start(compositionLocalMapKey, compositionLocalMap, GroupKind.Group, locals)
providerCache = null
// 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
// 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(),
null,
)
parentContext.insertMovableContent(reference)
} else {
val savedProvidersInvalid = providersInvalid
providersInvalid = providersChanged
invokeComposable(this, { content.content(parameter) })
providersInvalid = savedProvidersInvalid
}
} catch (e: Throwable) {
throw e.attachComposeStackTrace { currentStackTrace() }
} finally {
// Restore the state back to what is expected by the caller.
endGroup()
providerCache = null
compositeKeyHashCode = savedCompositeKeyHash
endMovableGroup()
}
}
@InternalComposeApi
override fun insertMovableContentReferences(
references: List<Pair<MovableContentStateReference, MovableContentStateReference?>>
) {
trace("Compose:insertMovableContent") {
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?>>
) {
changeListWriter.withChangeList(lateChanges.asGapBufferChangeList()) {
changeListWriter.resetSlots()
references.fastForEach { (to, from) ->
val anchor = to.anchor.asGapAnchor()
val toSlotTable = to.slotStorage.asGapBufferSlotTable()
val location = toSlotTable.anchorIndex(anchor)
val effectiveNodeIndex = IntRef()
// Insert content at the anchor point
changeListWriter.determineMovableContentNodeIndex(effectiveNodeIndex, anchor)
if (from == null) {
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()
}
toSlotTable.read { reader ->
reader.reposition(location)
changeListWriter.moveReaderToAbsolute(location)
val offsetChanges = ChangeList()
recomposeMovableContent {
changeListWriter.withChangeList(offsetChanges) {
withReader(reader) {
changeListWriter.withoutImplicitRootStart {
invokeMovableContentLambda(
to.content,
to.locals,
to.parameter,
force = true,
)
}
}
}
}
changeListWriter.includeOperationsIn(
other = offsetChanges,
effectiveNodeIndex = effectiveNodeIndex,
)
}
} 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 resolvedSlotTable = resolvedState?.slotStorage?.asGapBufferSlotTable()
val fromTable = resolvedSlotTable ?: from.slotStorage.asGapBufferSlotTable()
val fromAnchor = (resolvedSlotTable?.anchor(0) ?: from.anchor).asGapAnchor()
val nodesToInsert = fromTable.collectNodesFrom(fromAnchor)
// Insert nodes if necessary
if (nodesToInsert.isNotEmpty()) {
changeListWriter.copyNodesToNewAnchorLocation(
nodesToInsert,
effectiveNodeIndex,
)
if (toSlotTable == 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
changeListWriter.copySlotTableToAnchorLocation(
resolvedState = resolvedState,
parentContext = parentContext,
from = from,
to = to,
)
fromTable.read { reader ->
withReader(reader) {
val newLocation = fromTable.anchorIndex(fromAnchor.asGapAnchor())
reader.reposition(newLocation)
changeListWriter.moveReaderToAbsolute(newLocation)
val offsetChanges = ChangeList()
changeListWriter.withChangeList(offsetChanges) {
changeListWriter.withoutImplicitRootStart {
recomposeMovableContent(
from = from.composition,
to = to.composition,
reader.currentGroup,
invalidations = from.invalidations,
) {
invokeMovableContentLambda(
to.content,
to.locals,
to.parameter,
force = true,
)
}
}
}
changeListWriter.includeOperationsIn(
other = offsetChanges,
effectiveNodeIndex = effectiveNodeIndex,
)
}
}
}
changeListWriter.skipToEndOfCurrentGroup()
}
changeListWriter.endMovableContentPlacement()
changeListWriter.moveReaderToAbsolute(0)
}
}
private inline fun <R> withReader(reader: SlotReader, block: () -> R): R {
val savedReader = this.reader
val savedCountOverrides = nodeCountOverrides
val savedProviderUpdates = providerUpdates
nodeCountOverrides = null
providerUpdates = null
try {
this.reader = reader
return block()
} finally {
this.reader = savedReader
nodeCountOverrides = savedCountOverrides
providerUpdates = savedProviderUpdates
}
}
private fun <R> recomposeMovableContent(
from: ControlledComposition? = null,
to: ControlledComposition? = null,
index: Int? = null,
invalidations: List<Pair<RecomposeScopeImpl, Any?>> = emptyList(),
block: () -> R,
): R {
val savedIsComposing = isComposing
val savedNodeIndex = nodeIndex
try {
isComposing = true
nodeIndex = 0
invalidations.fastForEach { (scope, instances) ->
if (instances != null) {
tryImminentInvalidation(scope, instances)
} else {
tryImminentInvalidation(scope, null)
}
}
return from?.delegateInvalidations(to, index ?: -1, block) ?: block()
} finally {
isComposing = savedIsComposing
nodeIndex = savedNodeIndex
}
}
@ComposeCompilerApi
override fun sourceInformation(sourceInformation: String) {
if (inserting && sourceMarkersEnabled) {
writer.recordGroupSourceInformation(sourceInformation)
}
}
@ComposeCompilerApi
override fun sourceInformationMarkerStart(key: Int, sourceInformation: String) {
if (inserting && sourceMarkersEnabled) {
writer.recordGrouplessCallSourceInformationStart(key, sourceInformation)
}
}
@ComposeCompilerApi
override fun sourceInformationMarkerEnd() {
if (inserting && sourceMarkersEnabled) {
writer.recordGrouplessCallSourceInformationEnd()
}
}
override fun disableSourceInformation() {
sourceMarkersEnabled = false
}
@OptIn(ComposeToolingApi::class)
override fun stackTraceForValue(value: Any?): ComposeStackTrace {
val stackTrace =
slotTable
.findLocation { it === value || (it as? RememberObserverHolder)?.wrapped === value }
?.let { (groupIndex, dataIndex) ->
stackTraceForGroup(groupIndex, dataIndex) + parentStackTrace()
} ?: emptyList()
return ComposeStackTrace(stackTrace, sourceMarkersEnabled)
}
@OptIn(ComposeToolingApi::class)
private fun currentStackTrace(): ComposeStackTrace? =
if (parentContext.stackTraceEnabled) {
ComposeStackTrace(
buildList {
addAll(writer.buildTrace())
addAll(reader.buildTrace())
addAll(parentStackTrace())
},
sourceMarkersEnabled,
)
} else {
null
}
private fun stackTraceForGroup(group: Int, dataOffset: Int?): List<ComposeStackTraceFrame> =
slotTable.read { it.traceForGroup(group, dataOffset) }
override fun parentStackTrace(): List<ComposeStackTraceFrame> {
val parentComposition = parentContext.composition as? CompositionImpl ?: return emptyList()
val position =
parentComposition.slotStorage
.asGapBufferSlotTable()
.findSubcompositionContextGroup(parentContext)
return if (position != null) {
parentComposition.slotStorage.asGapBufferSlotTable().read { reader ->
reader.traceForGroup(position, 0)
} + parentComposition.composer.parentStackTrace()
} else {
emptyList()
}
}
/**
* 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].
*/
override fun composeContent(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
content: @Composable () -> Unit,
shouldPause: ShouldPauseCallback?,
) {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
this.shouldPauseCallback = shouldPause
try {
doCompose(invalidationsRequested, content)
} finally {
this.shouldPauseCallback = null
}
}
override 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.
*/
override fun recompose(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
shouldPause: ShouldPauseCallback?,
): 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.size > 0 || invalidations.isNotEmpty() || forciblyRecompose) {
shouldPauseCallback = shouldPause
try {
doCompose(invalidationsRequested, null)
} finally {
shouldPauseCallback = null
}
return changes.isNotEmpty()
}
return false
}
override fun updateComposerInvalidations(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>
) {
// Update any invalidations that have may have moved since they were added, removing any
// that are no longer in the slot table.
for (i in invalidations.lastIndex downTo 0) {
val invalidation = invalidations[i]
val anchor = invalidation.scope.anchor?.asGapAnchor()
if (anchor != null && anchor.valid) {
if (invalidation.location != anchor.location)
invalidation.location = anchor.location
} else {
invalidations.removeAt(i)
}
}
// Add the requested invalidations
invalidationsRequested.map.forEach { scope, instances ->
scope as RecomposeScopeImpl
val location = scope.anchor?.asGapAnchor()?.location ?: return@forEach
invalidations.add(
Invalidation(scope, location, instances.takeUnless { it === ScopeInvalidated })
)
}
// Ensure the invalidations are in sorted order.
invalidations.sortWith(InvalidationLocationAscending)
}
private fun doCompose(
invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
content: (@Composable () -> Unit)?,
) {
runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
val observer = observerHolder.current()
trace("Compose:recompose") {
compositionToken = currentSnapshot().snapshotId.hashCode()
providerUpdates = null
updateComposerInvalidations(invalidationsRequested)
nodeIndex = 0
var complete = false
isComposing = true
observer?.onBeginComposition(composition)
try {
startRoot()
// vv Experimental for forced
val savedContent = nextSlot()
if (savedContent !== content && content != null) {
updateValue(content as Any?)
}
// ^^ Experimental for forced
// Ignore reads of derivedStateOf recalculations
observeDerivedStateRecalculations(derivedStateObserver) {
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
} catch (e: Throwable) {
throw e.attachComposeStackTrace { currentStackTrace() }
} finally {
observer?.onEndComposition(composition)
isComposing = false
invalidations.clear()
if (!complete) abortRoot()
createFreshInsertTable()
}
}
}
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" }
}
private fun recordInsert(anchor: GapAnchor) {
if (insertFixups.isEmpty()) {
changeListWriter.insertSlots(anchor, insertTable)
} else {
changeListWriter.insertSlots(anchor, insertTable, insertFixups)
insertFixups = FixupList()
}
}
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)
changeListWriter.removeCurrentGroup()
}
/**
* 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 createMovableContentReferenceForGroup(
group: Int,
nestedStates: List<MovableContentStateReference>?,
): MovableContentStateReference {
@Suppress("UNCHECKED_CAST")
val movableContent = reader.groupObjectKey(group) as MovableContent<Any?>
val parameter = reader.groupGet(group, 0)
val anchor = reader.anchor(group)
val end = group + reader.groupSize(group)
val invalidations = mutableListOf<Pair<RecomposeScopeImpl, Any?>>()
this.invalidations.forEachInRange(group, end) {
invalidations += it.scope to it.instances
}
val reference =
MovableContentStateReference(
movableContent,
parameter,
composition,
slotTable,
anchor,
invalidations,
currentCompositionLocalScope(group),
nestedStates,
)
return reference
}
fun movableContentReferenceFor(group: Int): MovableContentStateReference? {
val key = reader.groupKey(group)
val objectKey = reader.groupObjectKey(group)
return if (key == movableContentKey && objectKey is MovableContent<*>) {
val nestedStates =
if (reader.containsMark(group)) {
val nestedStates = mutableListOf<MovableContentStateReference>()
fun traverseGroups(group: Int) {
val size = reader.groupSize(group)
val end = group + size
var current = group + 1
while (current < end) {
if (reader.hasMark(current)) {
movableContentReferenceFor(current)?.let {
nestedStates.add(it)
}
} else if (reader.containsMark(current)) traverseGroups(current)
current += reader.groupSize(current)
}
}
traverseGroups(group)
nestedStates.takeIf { it.isNotEmpty() }
} else null
createMovableContentReferenceForGroup(group, nestedStates)
} else null
}
fun reportGroup(group: Int, needsNodeDelete: Boolean, nodeIndex: Int): Int {
val reader = reader
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.
// reportMovableContentForGroup(group)
// reportMovableContentAt(group)
val reference = movableContentReferenceFor(group)
if (reference != null) {
parentContext.deletedMovableContent(reference)
changeListWriter.recordSlotEditing()
changeListWriter.releaseMovableGroupAtCurrent(
composition,
parentContext,
reference,
)
}
if (needsNodeDelete && group != groupBeingRemoved) {
changeListWriter.endNodeMovementAndDeleteNode(nodeIndex, group)
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 when the compositions are disposed.
val observerHolder = reader.groupGet(group, 0) as? RememberObserverHolder
val contextHolder = observerHolder?.wrapped as? CompositionContextHolder
if (contextHolder != null) {
// The contextHolder can be EMPTY in cases where 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()
// Mark the composition as being removed so it will not be recomposed
// this turn.
parentContext.reportRemovedComposition(composer.composition)
}
}
reader.nodeCount(group)
} else if (reader.isNode(group)) 1 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) {
changeListWriter.endNodeMovement()
changeListWriter.moveDown(reader.node(current))
}
runningNodeCount +=
reportGroup(
group = current,
needsNodeDelete = isNode || needsNodeDelete,
nodeIndex = if (isNode) 0 else nodeIndex + runningNodeCount,
)
if (isNode) {
changeListWriter.endNodeMovement()
changeListWriter.moveUp()
}
current += reader.groupSize(current)
}
if (reader.isNode(group)) 1 else runningNodeCount
} else if (reader.isNode(group)) 1 else reader.nodeCount(group)
}
// If the group that is being deleted is a node we need to remove any children that
// are moved.
val rootIsNode = reader.isNode(groupBeingRemoved)
if (rootIsNode) {
changeListWriter.endNodeMovement()
changeListWriter.moveDown(reader.node(groupBeingRemoved))
}
reportGroup(groupBeingRemoved, needsNodeDelete = rootIsNode, nodeIndex = 0)
changeListWriter.endNodeMovement()
if (rootIsNode) {
changeListWriter.moveUp()
}
}
/**
* 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()) {
composition.updateMovingInvalidations()
val changes = ChangeList()
deferredChanges = changes
slotTable.read { reader ->
this.reader = reader
changeListWriter.withChangeList(changes) {
reportFreeMovableContent(0)
changeListWriter.releaseMovableContent()
}
}
}
}
private fun finalizeCompose() {
changeListWriter.finalizeComposition()
runtimeCheck(pendingStack.isEmpty()) { "Start/end imbalance" }
cleanUpCompose()
}
private fun cleanUpCompose() {
pending = null
nodeIndex = 0
groupNodeCount = 0
compositeKeyHashCode = EmptyCompositeKeyHashCode
nodeExpected = false
changeListWriter.resetTransientState()
invalidateStack.clear()
clearUpdatedNodeCounts()
}
override fun verifyConsistent() {
insertTable.verifyWellFormed()
}
/**
* A holder that will dispose of its [CompositionContext] when it leaves the composition that
* will not have its reference made visible to user code.
*/
internal class CompositionContextHolder(val ref: CompositionContextImpl) : RememberObserver {
override fun onRemembered() {}
override fun onAbandoned() {
ref.dispose()
}
override fun onForgotten() {
ref.dispose()
}
}
@OptIn(ExperimentalComposeRuntimeApi::class)
internal inner class CompositionContextImpl(
override val compositeKeyHashCode: CompositeKeyHashCode,
override val collectingParameterInformation: Boolean,
override val collectingSourceInformation: Boolean,
override val observerHolder: CompositionObserverHolder?,
) : CompositionContext() {
var inspectionTables: MutableSet<MutableSet<CompositionData>>? = null
val composers = mutableScatterSetOf<GapComposer>()
override val collectingCallByInformation: Boolean
get() = parentContext.collectingCallByInformation
override val stackTraceEnabled: Boolean
get() = parentContext.stackTraceEnabled
fun dispose() {
if (composers.isNotEmpty()) {
inspectionTables?.let {
composers.forEach { composer ->
for (table in it) table.remove(composer.compositionData)
}
}
composers.clear()
}
}
override fun registerComposer(composer: Composer) {
super.registerComposer(composer as GapComposer)
composers.add(composer)
}
override fun unregisterComposer(composer: Composer) {
inspectionTables?.forEach { it.remove((composer as GapComposer).compositionData) }
if (composer is GapComposer) {
composers.remove(composer)
}
}
override fun registerComposition(composition: ControlledComposition) {
parentContext.registerComposition(composition)
}
override fun unregisterComposition(composition: ControlledComposition) {
parentContext.unregisterComposition(composition)
}
override fun reportPausedScope(scope: RecomposeScopeImpl) {
parentContext.reportPausedScope(scope)
}
override val effectCoroutineContext: CoroutineContext
get() = parentContext.effectCoroutineContext
override fun composeInitial(
composition: ControlledComposition,
content: @Composable () -> Unit,
) {
parentContext.composeInitial(composition, content)
}
override fun composeInitialPaused(
composition: ControlledComposition,
shouldPause: ShouldPauseCallback,
content: @Composable () -> Unit,
): ScatterSet<RecomposeScopeImpl> =
parentContext.composeInitialPaused(composition, shouldPause, content)
override fun recomposePaused(
composition: ControlledComposition,
shouldPause: ShouldPauseCallback,
invalidScopes: ScatterSet<RecomposeScopeImpl>,
): ScatterSet<RecomposeScopeImpl> =
parentContext.recomposePaused(composition, shouldPause, invalidScopes)
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@GapComposer.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<PersistentCompositionLocalMap>(
persistentCompositionLocalHashMapOf(),
referentialEqualityPolicy(),
)
override fun getCompositionLocalScope(): PersistentCompositionLocalMap =
compositionLocalScope
fun updateCompositionLocalScope(scope: PersistentCompositionLocalMap) {
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,
applier: Applier<*>,
) {
parentContext.movableContentStateReleased(reference, data, applier)
}
override fun reportRemovedComposition(composition: ControlledComposition) {
parentContext.reportRemovedComposition(composition)
}
override val composition: Composition
get() = this@GapComposer.composition
override fun scheduleFrameEndCallback(action: () -> Unit): CancellationHandle {
return parentContext.scheduleFrameEndCallback(action)
}
}
private inline fun updateCompositeKeyWhenWeEnterGroup(
groupKey: Int,
rGroupIndex: Int,
dataKey: Any?,
data: Any?,
) {
if (dataKey == null)
if (data != null && groupKey == reuseKey && data != Composer.Empty)
updateCompositeKeyWhenWeEnterGroupKeyHash(data.hashCode(), rGroupIndex)
else updateCompositeKeyWhenWeEnterGroupKeyHash(groupKey, rGroupIndex)
else if (dataKey is Enum<*>) updateCompositeKeyWhenWeEnterGroupKeyHash(dataKey.ordinal, 0)
else updateCompositeKeyWhenWeEnterGroupKeyHash(dataKey.hashCode(), 0)
}
private inline fun updateCompositeKeyWhenWeEnterGroupKeyHash(groupKey: Int, rGroupIndex: Int) {
compositeKeyHashCode =
compositeKeyHashCode.compoundWith(groupKey, 3).compoundWith(rGroupIndex, 3)
}
private inline fun updateCompositeKeyWhenWeExitGroup(
groupKey: Int,
rGroupIndex: Int,
dataKey: Any?,
data: Any?,
) {
if (dataKey == null)
if (data != null && groupKey == reuseKey && data != Composer.Empty)
updateCompositeKeyWhenWeExitGroupKeyHash(data.hashCode(), rGroupIndex)
else updateCompositeKeyWhenWeExitGroupKeyHash(groupKey, rGroupIndex)
else if (dataKey is Enum<*>) updateCompositeKeyWhenWeExitGroupKeyHash(dataKey.ordinal, 0)
else updateCompositeKeyWhenWeExitGroupKeyHash(dataKey.hashCode(), 0)
}
private inline fun updateCompositeKeyWhenWeExitGroupKeyHash(groupKey: Int, rGroupIndex: Int) {
compositeKeyHashCode =
compositeKeyHashCode.unCompoundWith(rGroupIndex, 3).unCompoundWith(groupKey, 3)
}
// This is only used in tests to ensure the stacks do not silently leak.
override fun stacksSize(): Int {
return entersStack.size +
invalidateStack.size +
providersInvalidStack.size +
pendingStack.size +
parentStateStack.size
}
override val recomposeScope: RecomposeScope?
get() = currentRecomposeScope
override val recomposeScopeIdentity: Any?
get() = currentRecomposeScope?.anchor
override fun rememberedValue(): Any? = nextSlotForCache()
override fun updateRememberedValue(value: Any?) = updateCachedValue(value)
override fun recordUsed(scope: RecomposeScope) {
(scope as? RecomposeScopeImpl)?.used = true
}
}
internal open class GapRememberObserverHolder(
override var wrapped: RememberObserver,
var afterGroupIndex: Int,
) : RememberObserverHolder
internal class ReusableGapRememberObserverHolder(wrapped: RememberObserver, afterGroupIndex: Int) :
GapRememberObserverHolder(wrapped, afterGroupIndex), ReusableRememberObserverHolder
internal fun RememberObserverHolder.asGapRememberObserverHolder() =
this as? GapRememberObserverHolder ?: composeRuntimeError("Inconsistent composition")
internal fun ReusableGapRememberObserverHolder.asGapRememberObserverHolder() =
this as? ReusableGapRememberObserverHolder ?: composeRuntimeError("Inconsistent composition")
internal class GapCompositionDataImpl(val composition: Composition) :
CompositionData, CompositionInstance {
private val slotTable
get() = (composition as CompositionImpl).slotStorage.asGapBufferSlotTable()
override val compositionGroups: Iterable<CompositionGroup>
get() = slotTable.compositionGroups
override val isEmpty: Boolean
get() = slotTable.isEmpty
override fun find(identityToFind: Any): CompositionGroup? = slotTable.find(identityToFind)
override fun hashCode(): Int = composition.hashCode() * 31
override fun equals(other: Any?): Boolean =
other is GapCompositionDataImpl && composition == other.composition
override val parent: CompositionInstance?
get() = composition.parent?.let { GapCompositionDataImpl(it) }
override val data: CompositionData
get() = this
override fun findContextGroup(): CompositionGroup? {
val parentSlotTable = composition.parent?.slotTable?.asGapBufferSlotTable() ?: return null
val context = composition.context ?: return null
return parentSlotTable.findSubcompositionContextGroup(context)?.let {
parentSlotTable.compositionGroupOf(it)
}
}
private val Composition.slotTable
get() = (this as? CompositionImpl)?.slotStorage
private val Composition.context
get() = (this as? CompositionImpl)?.parent
private val Composition.parent
get() = context?.composition
}
internal fun SlotWriter.deactivateCurrentGroup(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.
forAllDataInRememberOrder(currentGroup) { slotIndex, data ->
when (data) {
is ComposeNodeLifecycleCallback -> {
rememberManager.deactivating(data)
}
is ReusableRememberObserverHolder -> {
// do nothing, the value should be preserved on reuse
}
is RememberObserverHolder -> {
removeData(slotIndex, data)
rememberManager.forgetting(data)
}
is RecomposeScopeImpl -> {
removeData(slotIndex, data)
data.release()
}
}
}
}
private fun SlotWriter.removeData(index: Int, data: Any?) {
val result = clear(index)
runtimeCheck(data === result) { "Slot table is out of sync (expected $data, got $result)" }
}
private fun <K : Any, V : Any> multiMap(initialCapacity: Int) =
MultiValueMap<K, V>(MutableScatterMap(initialCapacity))
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 List<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 List<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,
// Only derived state instance is important for composition
instance.takeIf { it is DerivedState<*> },
),
)
} else {
val invalidation = get(index)
// Only derived state instance is important for composition
if (instance is DerivedState<*>) {
when (val oldInstance = invalidation.instances) {
null -> {
invalidation.instances = instance
}
is MutableScatterSet<*> -> {
@Suppress("UNCHECKED_CAST")
oldInstance as MutableScatterSet<Any?>
oldInstance.add(instance)
}
else -> {
invalidation.instances = mutableScatterSetOf(oldInstance, instance)
}
}
} else {
invalidation.instances = null
}
}
}
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 inline fun List<Invalidation>.forEachInRange(
start: Int,
end: Int,
block: (Invalidation) -> Unit,
) {
var index = findInsertLocation(start)
while (index < size) {
val invalidation = get(index)
if (invalidation.location >= end) break
block(invalidation)
index++
}
}
private fun Boolean.asInt() = if (this) 1 else 0
private fun Int.asBool() = this != 0
private fun SlotTable.collectNodesFrom(anchor: GapAnchor): 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 KeyInfo.joinedKey: Any
get() = if (objectKey != null) JoinedKey(key, objectKey) else key
private val InvalidationLocationAscending =
Comparator<Invalidation> { i1, i2 -> i1.location.compareTo(i2.location) }
/*
* 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.
internal const val rootKey = 100
// An arbitrary key value for a node.
internal const val nodeKey = 125