/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(
InternalComposeApi::class,
ExperimentalComposeApi::class,
ComposeCompilerApi::class
)
package androidx.compose.runtime
import androidx.compose.runtime.tooling.InspectionTables
import kotlinx.collections.immutable.PersistentMap
import kotlinx.collections.immutable.persistentHashMapOf
import kotlin.coroutines.CoroutineContext
internal typealias Change<N> = (
applier: Applier<N>,
slots: SlotWriter,
rememberManager: RememberManager
) -> Unit
private class GroupInfo(
/**
* The current location of the slot relative to the start location of the pending slot changes
*/
var slotIndex: Int,
/**
* The current location of the first node relative the start location of the pending node
* changes
*/
var nodeIndex: Int,
/**
* The current number of nodes the group contains after changes have been applied
*/
var nodeCount: Int
)
internal interface RememberManager {
fun entering(@Suppress("DEPRECATION") instance: CompositionLifecycleObserver)
fun leaving(@Suppress("DEPRECATION") instance: CompositionLifecycleObserver)
fun remembering(instance: RememberObserver)
fun forgetting(instance: RememberObserver)
fun sideEffect(effect: () -> Unit)
}
/**
* Pending starts when the key is different than expected indicating that the structure of the tree
* changed. It is used to determine how to update the nodes and the slot table when changes to the
* structure of the tree is detected.
*/
private class Pending(
val keyInfos: MutableList<KeyInfo>,
val startIndex: Int
) {
var groupIndex: Int = 0
init {
require(startIndex >= 0) { "Invalid start index" }
}
private val usedKeys = mutableListOf<KeyInfo>()
private val groupInfos = run {
var runningNodeIndex = 0
val result = hashMapOf<Int, GroupInfo>()
for (index in 0 until keyInfos.size) {
val keyInfo = keyInfos[index]
@OptIn(InternalComposeApi::class)
result[keyInfo.location] = GroupInfo(index, runningNodeIndex, keyInfo.nodes)
@OptIn(InternalComposeApi::class)
runningNodeIndex += keyInfo.nodes
}
result
}
/**
* A multi-map of keys from the previous composition. The keys can be retrieved in the order
* they were generated by the previous composition.
*/
val keyMap by lazy {
multiMap<Any, KeyInfo>().also {
for (index in 0 until keyInfos.size) {
val keyInfo = keyInfos[index]
@Suppress("ReplacePutWithAssignment")
it.put(keyInfo.joinedKey, keyInfo)
}
}
}
/**
* Get the next key information for the given key.
*/
fun getNext(key: Int, dataKey: Any?): KeyInfo? {
val joinedKey: Any = if (dataKey != null) JoinedKey(key, dataKey) else key
return keyMap.pop(joinedKey)
}
/**
* Record that this key info was generated.
*/
fun recordUsed(keyInfo: KeyInfo) = usedKeys.add(keyInfo)
val used: List<KeyInfo> get() = usedKeys
// TODO(chuckj): This is a correct but expensive implementation (worst cases of O(N^2)). Rework
// to O(N)
fun registerMoveSlot(from: Int, to: Int) {
if (from > to) {
groupInfos.values.forEach { group ->
val position = group.slotIndex
if (position == from) group.slotIndex = to
else if (position in to until from) group.slotIndex = position + 1
}
} else if (to > from) {
groupInfos.values.forEach { group ->
val position = group.slotIndex
if (position == from) group.slotIndex = to
else if (position in (from + 1) until to) group.slotIndex = position - 1
}
}
}
fun registerMoveNode(from: Int, to: Int, count: Int) {
if (from > to) {
groupInfos.values.forEach { group ->
val position = group.nodeIndex
if (position in from until from + count) group.nodeIndex = to + (position - from)
else if (position in to until from) group.nodeIndex = position + count
}
} else if (to > from) {
groupInfos.values.forEach { group ->
val position = group.nodeIndex
if (position in from until from + count) group.nodeIndex = to + (position - from)
else if (position in (from + 1) until to) group.nodeIndex = position - count
}
}
}
@OptIn(InternalComposeApi::class)
fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) {
groupInfos[keyInfo.location] = GroupInfo(-1, insertIndex, 0)
}
fun updateNodeCount(group: Int, newCount: Int): Boolean {
val groupInfo = groupInfos[group]
if (groupInfo != null) {
val index = groupInfo.nodeIndex
val difference = newCount - groupInfo.nodeCount
groupInfo.nodeCount = newCount
if (difference != 0) {
groupInfos.values.forEach { childGroupInfo ->
if (childGroupInfo.nodeIndex >= index && childGroupInfo != groupInfo)
childGroupInfo.nodeIndex += difference
}
}
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(
val scope: RecomposeScopeImpl,
var location: Int
)
/**
* Internal compose compiler plugin API that is used to update the function the composer will
* call to recompose a recomposition scope. This should not be used or called directly.
*/
@ComposeCompilerApi
interface ScopeUpdateScope {
/**
* Called by generated code to update the recomposition scope with the function to call
* recompose the scope. This is called by code generated by the compose compiler plugin and
* should not be called directly.
*/
fun updateScope(block: (Composer<*>, Int) -> Unit)
}
internal enum class InvalidationResult {
/**
* The invalidation was ignored because the associated recompose scope is no longer part of the
* composition or has yet to be entered in the composition. This could occur for invalidations
* called on scopes that are no longer part of composition or if the scope was invalidated
* before the applyChanges() was called that will enter the scope into the composition.
*/
IGNORED,
/**
* The composition is not currently composing and the invalidation was recorded for a future
* composition. A recomposition requested to be scheduled.
*/
SCHEDULED,
/**
* The composition that owns the recompose scope is actively composing but the scope has
* already been composed or is in the process of composing. The invalidation is treated as
* SCHEDULED above.
*/
DEFERRED,
/**
* The composition that owns the recompose scope is actively composing and the invalidated
* scope has not been composed yet but will be recomposed before the composition completes. A
* new recomposition was not scheduled for this invalidation.
*/
IMMINENT
}
/**
* An instance to hold a value provided by [Providers] and is created by the
* [ProvidableAmbient.provides] infixed operator. If [canOverride] is `false`, the
* provided value will not overwrite a potentially already existing value in the scope.
*/
class ProvidedValue<T> internal constructor(
val ambient: Ambient<T>,
val value: T,
val canOverride: Boolean
)
/**
* An ambient map is is an immutable map that maps ambient keys to a provider of their current
* value. It is used to represent the combined scope of all provided ambients.
*/
internal typealias AmbientMap = PersistentMap<Ambient<Any?>, State<Any?>>
internal inline fun AmbientMap.mutate(
mutator: (MutableMap<Ambient<Any?>, State<Any?>>) -> Unit
): AmbientMap = builder().apply(mutator).build()
@Suppress("UNCHECKED_CAST")
internal fun <T> AmbientMap.contains(key: Ambient<T>) = this.containsKey(key as Ambient<Any?>)
@Suppress("UNCHECKED_CAST")
internal fun <T> AmbientMap.getValueOf(key: Ambient<T>) = this[key as Ambient<Any?>]?.value as T
@Composable
private fun ambientMapOf(values: Array<out ProvidedValue<*>>, parentScope: AmbientMap): AmbientMap {
val result: AmbientMap = persistentHashMapOf()
return result.mutate {
for (provided in values) {
if (provided.canOverride || !parentScope.contains(provided.ambient)) {
@Suppress("UNCHECKED_CAST")
it[provided.ambient as Ambient<Any?>] = provided.ambient.provided(provided.value)
}
}
}
}
/**
* Implementation of a composer for mutable tree.
*/
class Composer<N>(
/**
* An adapter that applies changes to the tree using the Applier abstraction.
*/
@PublishedApi internal val applier: Applier<N>,
/**
* Parent of this composition; a [Recomposer] for root-level compositions.
*/
private val parentReference: CompositionReference
) {
private val slotTable: SlotTable = SlotTable()
private val changes = mutableListOf<Change<N>>()
private val lifecycleObservers = HashMap<
CompositionLifecycleObserverHolder,
CompositionLifecycleObserverHolder
>()
private val abandonSet = HashSet<RememberObserver>()
private val pendingStack = Stack<Pending?>()
private var pending: Pending? = null
private var nodeIndex: Int = 0
private var nodeIndexStack = IntStack()
private var groupNodeCount: Int = 0
private var groupNodeCountStack = IntStack()
private var nodeCountOverrides: IntArray? = null
private var nodeCountVirtualOverrides: HashMap<Int, Int>? = null
private var collectKeySources = false
private var collectParameterInformation = false
private var nodeExpected = false
private val observations: MutableList<Any> = mutableListOf()
private val observationsProcessed: MutableList<Any> = mutableListOf()
private val invalidations: MutableList<Invalidation> = mutableListOf()
internal var pendingInvalidScopes = false
private val entersStack = IntStack()
private var parentProvider: AmbientMap = persistentHashMapOf()
private val providerUpdates = HashMap<Int, AmbientMap>()
private var providersInvalid = false
private val providersInvalidStack = IntStack()
private var childrenComposing: Int = 0
private val invalidateStack = Stack<RecomposeScopeImpl>()
internal var isComposing = false
private set
internal var isDisposed = false
private set
private var reader: SlotReader = slotTable.openReader().also { it.close() }
internal val insertTable = SlotTable()
private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
private var hasProvider = false
private var insertAnchor: Anchor = insertTable.read { it.anchor(0) }
private val insertFixups = mutableListOf<Change<N>>()
val applyCoroutineContext: CoroutineContext
@TestOnly get() = parentReference.effectCoroutineContext
/**
* Inserts a "Replaceable Group" starting marker in the slot table at the current execution
* position. A Replaceable Group is a group which cannot be moved between its siblings, but
* can be removed or inserted. These groups are inserted by the compiler around branches of
* conditional logic in Composable functions such as if expressions, when expressions, early
* returns, and null-coalescing operators.
*
* A call to [startReplaceableGroup] must be matched with a corresponding call to
* [endReplaceableGroup].
*
* Warning: This is expected to be executed by the compiler only and should not be called
* directly from source code. Call this API at your own risk.
*
* @param key The source-location-based key for the group. Expected to be unique among its
* siblings.
*
* @see [endReplaceableGroup]
* @see [startMovableGroup]
* @see [startRestartGroup]
*/
@ComposeCompilerApi
fun startReplaceableGroup(key: Int) = start(key, null, false, null)
@ComposeCompilerApi
fun startReplaceableGroup(key: Int, sourceInformation: String?) =
start(key, null, false, sourceInformation)
/**
* 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
fun endReplaceableGroup() = endGroup()
/**
*
* Warning: This is expected to be executed by the compiler only and should not be called
* directly from source code. Call this API at your own risk.
*
*/
@ComposeCompilerApi
@Suppress("unused")
fun startDefaults() = start(0, null, false, null)
/**
*
* Warning: This is expected to be executed by the compiler only and should not be called
* directly from source code. Call this API at your own risk.
*
* @see [startReplaceableGroup]
*/
@ComposeCompilerApi
@Suppress("unused")
fun endDefaults() {
endGroup()
val scope = currentRecomposeScope
if (scope != null && scope.used) {
scope.defaultsInScope = true
}
}
@ComposeCompilerApi
@Suppress("unused")
val defaultsInvalid: Boolean
get() {
return providersInvalid || currentRecomposeScope?.defaultsInvalid == true
}
/**
* Inserts a "Movable Group" starting marker in the slot table at the current execution
* position. A Movable Group is a group which can be moved or reordered between its siblings
* and retain slot table state, in addition to being removed or inserted. Movable Groups
* are more expensive than other groups because when they are encountered with a mismatched
* key in the slot table, they must be held on to temporarily until the entire parent group
* finishes execution in case it moved to a later position in the group. Movable groups are
* only inserted by the compiler as a result of calls to [key].
*
* A call to [startMovableGroup] must be matched with a corresponding call to [endMovableGroup].
*
* Warning: This is expected to be executed by the compiler only and should not be called
* directly from source code. Call this API at your own risk.
*
* @param key The source-location-based key for the group. Expected to be unique among its
* siblings.
*
* @param dataKey Additional identifying information to compound with [key]. If there are
* multiple values, this is expected to be compounded together with [joinKey]. Whatever value
* is passed in here is expected to have a meaningful [equals] and [hashCode] implementation.
*
* @see [endMovableGroup]
* @see [key]
* @see [joinKey]
* @see [startReplaceableGroup]
* @see [startRestartGroup]
*/
@ComposeCompilerApi
fun startMovableGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null)
@ComposeCompilerApi
fun startMovableGroup(key: Int, dataKey: Any?, sourceInformation: String?) =
start(key, dataKey, false, sourceInformation)
/**
* 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
fun endMovableGroup() = endGroup()
/**
* Start the composition. This should be called, and only be called, as the first group in
* the composition.
*/
@OptIn(InternalComposeApi::class)
private fun startRoot() {
reader = slotTable.openReader()
startGroup(rootKey)
// parent reference management
parentReference.startComposing()
parentProvider = parentReference.getAmbientScope()
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = changed(parentProvider)
collectKeySources = parentReference.collectingKeySources
collectParameterInformation = parentReference.collectingParameterInformation
resolveAmbient(InspectionTables, parentProvider)?.let {
it.add(slotTable)
parentReference.recordInspectionTable(it)
}
startGroup(parentReference.compoundHashKey)
}
/**
* End the composition. This should be called, and only be called, to end the first group in
* the composition.
*/
@OptIn(InternalComposeApi::class)
private fun endRoot() {
endGroup()
parentReference.doneComposing()
endGroup()
recordEndRoot()
finalizeCompose()
reader.close()
}
/**
* Discard a pending composition because an error was encountered during composition
*/
@OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
private fun abortRoot() {
cleanUpCompose()
pendingStack.clear()
nodeIndexStack.clear()
groupNodeCountStack.clear()
entersStack.clear()
providersInvalidStack.clear()
invalidateStack.clear()
reader.close()
currentCompoundKeyHash = 0
childrenComposing = 0
nodeExpected = false
isComposing = false
}
/**
* 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.
*/
@PublishedApi
internal var inserting: Boolean = false
private set
/**
* True if the composition should be checking if the composable functions can be skipped.
*/
@ComposeCompilerApi
val skipping: Boolean get() {
return !inserting &&
!providersInvalid &&
currentRecomposeScope?.requiresRecompose == false
}
/**
* Returns the hash of the compound key calculated as a combination of the keys of all the
* currently started groups via [startGroup].
*/
@ExperimentalComposeApi
var currentCompoundKeyHash: Int = 0
private set
/**
* Start collecting key source information. This enables enables the tool API to be able to
* determine the source location of where groups and nodes are created.
*/
@InternalComposeApi
fun collectKeySourceInformation() {
collectKeySources = true
}
/**
* Start collecting parameter information. This enables the tools API to always be able to
* determine the parameter values of composable calls.
*/
@InternalComposeApi
fun collectParameterInformation() {
collectParameterInformation = true
}
/**
* Record that [value] was read from. If [recordWriteOf] or [recordModificationsOf] is called
* with [value] then the corresponding [currentRecomposeScope] is invalidated.
*
* This should only be called when this composition is actively composing.
*/
@InternalComposeApi
fun recordReadOf(value: Any) {
if (childrenComposing == 0) {
currentRecomposeScope?.let {
it.used = true
observations.insertIfMissing(value, it)
}
}
}
/**
* Record that [value] was written to during composition. This invalidates all scopes that were
* current when [recordReadOf] was called with [value] yet to be composed. If a scope has
* already been composed a request is made to the recomposer to recompose the composition.
*
* This should only be called when this composition is actively composing.
*/
@InternalComposeApi
fun recordWriteOf(value: Any) {
observations.forEachScopeOf(value) { scope ->
if (scope.invalidateForResult() == InvalidationResult.IMMINENT) {
// If we process this during recordWriteOf, ignore it when recording modifications
observationsProcessed.insertIfMissing(value, scope)
}
}
}
/**
* Throw a diagnostic exception if the internal tracking tables are inconsistent.
*/
@InternalComposeApi
fun verifyConsistent() {
if (!isComposing) {
slotTable.verifyWellFormed()
insertTable.verifyWellFormed()
validateRecomposeScopeAnchors(slotTable)
}
}
private fun validateRecomposeScopeAnchors(slotTable: SlotTable) {
val scopes = slotTable.slots.mapNotNull { it as? RecomposeScopeImpl }
for (scope in scopes) {
scope.anchor?.let { anchor ->
check(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
val dataIndex = slotTable.slots.indexOf(scope)
"Misaligned anchor $anchor in scope $scope encountered, scope found at " +
"$dataIndex"
}
}
}
}
/**
* Record that the objects in [values] have been modified. This invalidates any recomposes
* scopes that were current when [recordReadOf] was called with an instance in [values].
*
* This should only be calle when this composition is not actively composing.
*/
@InternalComposeApi
fun recordModificationsOf(values: Set<Any>) {
var invalidated: HashSet<RecomposeScopeImpl>? = null
for (value in values) {
observations.forEachScopeOf(value) { scope ->
if (!observationsProcessed.removeValueScope(value, scope) &&
scope.invalidateForResult() != InvalidationResult.IGNORED
) {
(
invalidated ?: (
HashSet<RecomposeScopeImpl>().also {
invalidated = it
}
)
).add(scope)
}
}
}
invalidated?.let {
observations.removeValueIf { _, scope -> scope in it }
}
}
/**
* Helper for collecting remember observers for later strictly ordered dispatch.
*
* This includes support for the deprecated [CompositionLifecycleObserver] which should be
* removed with it.
*/
private class RememberEventDispatcher(
private val lifecycleObservers: MutableMap<CompositionLifecycleObserverHolder,
CompositionLifecycleObserverHolder>,
private val abandoning: MutableSet<RememberObserver>
) : RememberManager {
private val enters = mutableSetOf<CompositionLifecycleObserverHolder>()
private val leaves = mutableSetOf<CompositionLifecycleObserverHolder>()
private val remembering = mutableListOf<RememberObserver>()
private val forgetting = mutableListOf<RememberObserver>()
private val sideEffects = mutableListOf<() -> Unit>()
override fun entering(@Suppress("DEPRECATION") instance: CompositionLifecycleObserver) {
val holder = CompositionLifecycleObserverHolder(instance)
lifecycleObservers.getOrPut(holder) {
enters.add(holder)
holder
}.apply { count++ }
}
override fun leaving(@Suppress("DEPRECATION") instance: CompositionLifecycleObserver) {
val holder = CompositionLifecycleObserverHolder(instance)
val left = lifecycleObservers[holder]?.let {
if (--it.count == 0) {
leaves.add(it)
it
} else null
}
if (left != null) lifecycleObservers.remove(left)
}
override fun remembering(instance: RememberObserver) {
forgetting.lastIndexOf(instance).let { index ->
if (index >= 0) {
forgetting.removeAt(index)
abandoning.remove(instance)
} else {
remembering.add(instance)
}
}
}
override fun forgetting(instance: RememberObserver) {
remembering.lastIndexOf(instance).let { index ->
if (index >= 0) {
remembering.removeAt(index)
abandoning.remove(instance)
} else {
forgetting.add(instance)
}
}
}
override fun sideEffect(effect: () -> Unit) {
sideEffects += effect
}
fun dispatchLifecycleObservers() {
// Send lifecycle leaves
if (leaves.isNotEmpty()) {
for (holder in leaves.reversed()) {
// The count of the holder might be greater than 0 here as it might leave one
// part of the composition and reappear in another. Only send a leave if the
// count is still 0 after all changes have been applied.
if (holder.count == 0) {
holder.instance.onLeave()
lifecycleObservers.remove(holder)
}
}
}
// Send forgets
if (forgetting.isNotEmpty()) {
for (instance in forgetting.reversed()) {
if (instance !in abandoning)
instance.onForgotten()
}
}
// Send lifecycle enters
if (enters.isNotEmpty()) {
for (holder in enters) {
holder.instance.onEnter()
}
}
// Send remembers
if (remembering.isNotEmpty()) {
for (instance in remembering) {
abandoning.remove(instance)
instance.onRemembered()
}
}
}
fun dispatchSideEffects() {
if (sideEffects.isNotEmpty()) {
for (sideEffect in sideEffects) {
sideEffect()
}
sideEffects.clear()
}
}
fun dispatchAbandons() {
if (abandoning.isNotEmpty()) {
val iterator = abandoning.iterator()
while (iterator.hasNext()) {
val instance = iterator.next()
iterator.remove()
instance.onAbandoned()
}
}
}
}
/**
* Apply the changes to the tree that were collected during the last composition.
*/
@InternalComposeApi
@OptIn(ExperimentalComposeApi::class)
fun applyChanges() {
trace("Compose:applyChanges") {
invalidateStack.clear()
val invalidationAnchors = slotTable.read { reader ->
invalidations.map { reader.anchor(it.location) to it }
}
val manager = RememberEventDispatcher(lifecycleObservers, abandonSet)
try {
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
val applier = applier
changes.forEach { change ->
change(applier, slots, manager)
}
changes.clear()
}
applier.onEndChanges()
providerUpdates.clear()
@Suppress("ReplaceManualRangeWithIndicesCalls") // Avoids allocation of an iterator
for (index in 0 until invalidationAnchors.size) {
val (anchor, invalidation) = invalidationAnchors[index]
if (anchor.valid) {
invalidation.location = anchor.toIndexFor(slotTable)
} else {
invalidations.remove(invalidation)
}
}
// Side effects run after lifecycle observers so that any remembered objects
// that implement CompositionLifecycleObserver receive onEnter before a side effect
// that captured it and operates on it can run.
manager.dispatchLifecycleObservers()
manager.dispatchSideEffects()
if (pendingInvalidScopes) {
pendingInvalidScopes = false
observations.removeValueIf { _, scope -> !scope.valid }
}
} finally {
manager.dispatchAbandons()
}
}
}
@ExperimentalComposeApi
@OptIn(InternalComposeApi::class)
internal fun dispose() {
trace("Compose:Composer.dispose") {
parentReference.unregisterComposer(this)
invalidateStack.clear()
invalidations.clear()
changes.clear()
applier.clear()
if (slotTable.groupsSize > 0) {
val manager = RememberEventDispatcher(lifecycleObservers, abandonSet)
slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}
providerUpdates.clear()
applier.clear()
manager.dispatchLifecycleObservers()
} else {
applier.clear()
}
isDisposed = true
}
}
/**
* Start a group with the given key. During recomposition if the currently expected group does
* not match the given key a group the groups emitted in the same parent group are inspected
* to determine if one of them has this key and that group the first such group is moved
* (along with any nodes emitted by the group) to the current position and composition
* continues. If no group with this key is found, then the composition shifts into insert
* mode and new nodes are added at the current position.
*
* @param key The key for the group
*/
private fun startGroup(key: Int) = start(key, null, false, null)
private fun startGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null)
/**
* End the current group.
*/
internal 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.
*/
@PublishedApi
internal fun startNode() {
start(nodeKey, null, true, null)
nodeExpected = true
}
/**
* Schedule a node to be created and inserted at the current location. This is only valid to
* call when the composer is inserting.
*/
@Suppress("UNUSED")
@OptIn(ExperimentalComposeApi::class)
@PublishedApi
internal fun <T> createNode(factory: () -> T) {
validateNodeExpected()
check(inserting) { "createNode() can only be called when inserting" }
val insertIndex = nodeIndexStack.peek()
val groupAnchor = writer.anchor(writer.parent)
groupNodeCount++
recordFixup { applier, slots, _ ->
@Suppress("UNCHECKED_CAST")
val node = factory() as N
slots.updateNode(groupAnchor, node)
applier.insertTopDown(insertIndex, node)
applier.down(node)
}
recordInsertUpFixup { applier, slots, _ ->
@Suppress("UNCHECKED_CAST")
val nodeToInsert = slots.node(groupAnchor) as N
applier.up()
applier.insertBottomUp(insertIndex, nodeToInsert)
}
}
/**
* Mark the node that was created by [createNode] as used by composition.
*/
@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun useNode() {
validateNodeExpected()
check(!inserting) { "useNode() called while inserting" }
recordDown(reader.node)
}
/**
* Called to end the node group.
*/
@PublishedApi
internal fun endNode() = end(isNode = true)
/**
* 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].
*/
@OptIn(ExperimentalComposeApi::class)
@PublishedApi
internal fun <V, T> apply(value: V, block: T.(V) -> Unit) {
val operation: Change<N> = { applier, _, _ ->
@Suppress("UNCHECKED_CAST")
(applier.current as T).block(value)
}
if (inserting) recordFixup(operation)
else recordApplierOperation(operation)
}
/**
* Create a composed key that can be used in calls to [startGroup] or [startNode]. This will
* use the key stored at the current location in the slot table to avoid allocating a new key.
*/
@ComposeCompilerApi
@OptIn(InternalComposeApi::class)
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()
EMPTY
} else reader.next()
/**
* Determine if the current slot table value is equal to the given value, if true, the value
* is scheduled to be skipped during [applyChanges] and [changes] return false; otherwise
* [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
fun changed(value: Any?): Boolean {
return if (nextSlot() != value) {
updateValue(value)
true
} else {
false
}
}
@ComposeCompilerApi
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
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
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
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
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
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
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
fun changed(value: Int): Boolean {
val next = nextSlot()
if (next is Int) {
val nextPrimitive: Int = next
if (value == nextPrimitive) return false
}
updateValue(value)
return true
}
/**
* Cache a value in the composition. During initial composition [block] is called to produce the
* value that is then stored in the slot table. During recomposition, if [invalid] is false
* the value is obtained from the slot table and [block] is not invoked. If [invalid] is
* false a new value is produced by calling [block] and the slot table is updated to contain
* the new value.
*/
@ComposeCompilerApi
inline fun <T> cache(invalid: Boolean, block: () -> T): T {
var result = nextSlot()
if (result === EMPTY || invalid) {
val value = block()
updateValue(value)
result = value
}
@Suppress("UNCHECKED_CAST")
return result as T
}
/**
* Schedule the current value in the slot table to be updated to [value].
*
* @param value the value to schedule to be written to the slot table.
*/
@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun updateValue(value: Any?) {
if (inserting) {
writer.update(value)
@Suppress("DEPRECATION")
if (value is CompositionLifecycleObserver) {
record { _, _, lifecycleManager -> lifecycleManager.entering(value) }
}
if (value is RememberObserver) {
record { _, _, rememberManager -> rememberManager.remembering(value) }
}
} else {
val groupSlotIndex = reader.groupSlotIndex - 1
recordSlotTableOperation(forParent = true) { _, slots, rememberManager ->
@Suppress("DEPRECATION")
if (value is CompositionLifecycleObserver)
rememberManager.entering(value)
if (value is RememberObserver) {
abandonSet.add(value)
rememberManager.remembering(value)
}
@Suppress("DEPRECATION")
when (val previous = slots.set(groupSlotIndex, value)) {
is CompositionLifecycleObserver ->
rememberManager.leaving(previous)
is RememberObserver ->
rememberManager.forgetting(previous)
is RecomposeScopeImpl -> {
if (previous.composer != null) {
previous.composer = null
pendingInvalidScopes = true
}
}
}
}
}
}
/**
* Schedule the current value in the slot table to be updated to [value].
*
* @param value the value to schedule to be written to the slot table.
*/
@PublishedApi
@OptIn(InternalComposeApi::class)
internal fun updateCachedValue(value: Any?) {
if (inserting && value is RememberObserver) {
abandonSet.add(value)
}
updateValue(value)
}
@InternalComposeApi
val compositionData: CompositionData get() = slotTable
/**
* Schedule a side effect to run when we apply composition changes.
*/
internal fun recordSideEffect(effect: () -> Unit) {
record { _, _, rememberManager -> rememberManager.sideEffect(effect) }
}
/**
* Return the current ambient scope which was provided by a parent group.
*/
private fun currentAmbientScope(): AmbientMap {
if (inserting && hasProvider) {
var current = writer.parent
while (current > 0) {
if (writer.groupKey(current) == ambientMapKey &&
writer.groupObjectKey(current) == ambientMap
) {
@Suppress("UNCHECKED_CAST")
return writer.groupAux(current) as AmbientMap
}
current = writer.parent(current)
}
}
if (slotTable.groupsSize > 0) {
var current = reader.parent
while (current > 0) {
if (reader.groupKey(current) == ambientMapKey &&
reader.groupObjectKey(current) == ambientMap
) {
@Suppress("UNCHECKED_CAST")
return providerUpdates[current] ?: reader.groupAux(current) as AmbientMap
}
current = reader.parent(current)
}
}
return parentProvider
}
/**
* Return the ambient scope for the location provided. If this is while the composer is
* composing then this is a query from a sub-composition that is being recomposed by this
* compose which might be inserting the sub-composition. In that case the current scope
* is the correct scope.
*/
private fun ambientScopeAt(location: Int): AmbientMap {
if (isComposing) {
// The sub-composer is being composed as part of a nested composition then use the
// current ambient scope as the one in the slot table might be out of date.
return currentAmbientScope()
}
if (location >= 0) {
slotTable.read { reader ->
var current = location
while (current > 0) {
if (reader.groupKey(current) == ambientMapKey &&
reader.groupObjectKey(current) == ambientMap
) {
@Suppress("UNCHECKED_CAST")
return providerUpdates[current] ?: reader.groupAux(current) as AmbientMap
}
current = reader.parent(current)
}
}
}
return parentProvider
}
/**
* Update (or create) the slots to record the providers. The providers maps are first the
* scope followed by the map used to augment the parent scope. Both are needed to detect
* inserts, updates and deletes to the providers.
*/
private fun updateProviderMapGroup(
parentScope: AmbientMap,
currentProviders: AmbientMap
): AmbientMap {
val providerScope = parentScope.mutate { it.putAll(currentProviders) }
startGroup(providerMapsKey, providerMaps)
changed(providerScope)
changed(currentProviders)
endGroup()
return providerScope
}
internal fun startProviders(values: Array<out ProvidedValue<*>>) {
val parentScope = currentAmbientScope()
startGroup(providerKey, provider)
// The group is needed here because ambientMapOf() might change the number or kind of
// slots consumed depending on the content of values to remember, for example, the value
// holders used last time.
startGroup(providerValuesKey, providerValues)
val currentProviders = invokeComposableForResult(this) { ambientMapOf(values, parentScope) }
endGroup()
val providers: AmbientMap
val invalid: Boolean
if (inserting) {
providers = updateProviderMapGroup(parentScope, currentProviders)
invalid = false
hasProvider = true
} else {
@Suppress("UNCHECKED_CAST")
val oldScope = reader.groupGet(0) as AmbientMap
@Suppress("UNCHECKED_CAST")
val oldValues = reader.groupGet(1) as AmbientMap
// skipping is true iff parentScope has not changed.
if (!skipping || oldValues != currentProviders) {
providers = updateProviderMapGroup(parentScope, currentProviders)
// Compare against the old scope as currentProviders might have modified the scope
// back to the previous value. This could happen, for example, if currentProviders
// and parentScope have a key in common and the oldScope had the same value as
// currentProviders for that key. If the scope has not changed, because these
// providers obscure a change in the parent as described above, re-enable skipping
// for the child region.
invalid = providers != oldScope
} else {
// Nothing has changed
skipGroup()
providers = oldScope
invalid = false
}
}
if (invalid && !inserting) {
providerUpdates[reader.currentGroup] = providers
}
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = invalid
start(ambientMapKey, ambientMap, false, providers)
}
internal fun endProviders() {
endGroup()
endGroup()
providersInvalid = providersInvalidStack.pop().asBool()
}
@PublishedApi
internal fun <T> consume(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
/**
* Create or use a memoized `CompositionReference` instance at this position in the slot table.
*/
internal fun buildReference(): CompositionReference {
startGroup(referenceKey, reference)
var ref = nextSlot() as? CompositionReferenceHolder<*>
if (ref == null) {
val scope = invalidateStack.peek()
scope.used = true
ref = CompositionReferenceHolder(
CompositionReferenceImpl(
scope,
currentCompoundKeyHash,
collectKeySources,
collectParameterInformation
)
)
updateValue(ref)
}
endGroup()
return ref.ref
}
private fun <T> resolveAmbient(key: Ambient<T>, scope: AmbientMap): T =
if (scope.contains(key)) scope.getValueOf(key) else parentReference.getAmbient(key)
internal fun <T> parentAmbient(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
private fun <T> parentAmbient(key: Ambient<T>, location: Int): T =
resolveAmbient(key, ambientScopeAt(location))
/**
* The number of changes that have been scheduled to be applied during [applyChanges].
*
* Slot table movement (skipping groups and nodes) will be coalesced so this number is
* possibly less than the total changes detected.
*/
internal val changeCount get() = changes.size
internal val currentRecomposeScope: RecomposeScopeImpl?
get() = invalidateStack.let {
if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null
}
private fun ensureWriter() {
if (writer.closed) {
writer = insertTable.openWriter()
// Append to the end of the table
writer.skipToGroupEnd()
hasProvider = false
}
}
/**
* Start the reader group updating the data of the group if necessary
*/
private fun startReaderGroup(isNode: Boolean, data: Any?) {
if (isNode) {
reader.startNode()
} else {
if (data != null && reader.groupAux !== data) {
recordSlotTableOperation { _, slots, _ ->
slots.updateAux(data)
}
}
reader.startGroup()
}
}
private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
validateNodeNotExpected()
updateCompoundKeyWhenWeEnterGroup(key, objectKey)
// Check for the insert fast path. If we are already inserting (creating nodes) then
// there is no need to track insert, deletes and moves with a pending changes object.
if (inserting) {
reader.beginEmpty()
val startIndex = writer.currentGroup
if (collectKeySources)
recordSourceKeyInfo(key)
when {
isNode -> writer.startNode(EMPTY)
data != null -> writer.startData(key, objectKey ?: EMPTY, data)
else -> writer.startGroup(key, objectKey ?: EMPTY)
}
pending?.let { pending ->
val insertKeyInfo = KeyInfo(
key = key,
objectKey = -1,
location = insertedGroupVirtualIndex(startIndex),
nodes = -1,
index = 0
)
pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex)
pending.recordUsed(insertKeyInfo)
}
enterGroup(isNode, null)
return
}
if (pending == null) {
val slotKey = reader.groupKey
if (slotKey == key && objectKey == reader.groupObjectKey) {
// The group is the same as what was generated last time.
startReaderGroup(isNode, data)
} else {
pending = Pending(
reader.extractKeys(),
nodeIndex
)
}
}
val pending = pending
var newPending: Pending? = null
if (pending != null) {
// Check to see if the key was generated last time from the keys collected above.
val keyInfo = pending.getNext(key, objectKey)
if (keyInfo != null) {
// This group was generated last time, use it.
pending.recordUsed(keyInfo)
// Move the slot table to the location where the information about this group is
// stored. The slot information will move once the changes are applied so moving the
// current of the slot table is sufficient.
val location = keyInfo.location
// Determine what index this group is in. This is used for inserting nodes into the
// group.
nodeIndex = pending.nodePositionOf(keyInfo) + pending.startIndex
// Determine how to move the slot group to the correct position.
val relativePosition = pending.slotPositionOf(keyInfo)
val currentRelativePosition = relativePosition - pending.groupIndex
pending.registerMoveSlot(relativePosition, pending.groupIndex)
recordReaderMoving(location)
reader.reposition(location)
if (currentRelativePosition > 0) {
// The slot group must be moved, record the move to be performed during apply.
recordSlotEditingOperation { _, slots, _ ->
slots.moveGroup(currentRelativePosition)
}
}
startReaderGroup(isNode, data)
} else {
// The group is new, go into insert mode. All child groups will written to the
// insertTable until the group is complete which will schedule the groups to be
// inserted into in the table.
reader.beginEmpty()
inserting = true
if (collectKeySources)
recordSourceKeyInfo(key)
ensureWriter()
writer.beginInsert()
val startIndex = writer.currentGroup
when {
isNode -> writer.startNode(EMPTY)
data != null -> writer.startData(key, objectKey ?: EMPTY, data)
else -> writer.startGroup(key, objectKey ?: EMPTY)
}
insertAnchor = writer.anchor(startIndex)
val insertKeyInfo = KeyInfo(
key = key,
objectKey = -1,
location = insertedGroupVirtualIndex(startIndex),
nodes = -1,
index = 0
)
pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex)
pending.recordUsed(insertKeyInfo)
newPending = Pending(
mutableListOf(),
if (isNode) 0 else nodeIndex
)
}
}
enterGroup(isNode, newPending)
}
private fun enterGroup(isNode: Boolean, newPending: Pending?) {
// When entering a group all the information about the parent should be saved, to be
// restored when end() is called, and all the tracking counters set to initial state for the
// group.
pendingStack.push(pending)
this.pending = newPending
this.nodeIndexStack.push(nodeIndex)
if (isNode) nodeIndex = 0
this.groupNodeCountStack.push(groupNodeCount)
groupNodeCount = 0
}
private fun exitGroup(expectedNodeCount: Int, inserting: Boolean) {
// Restore the parent's state updating them if they have changed based on changes in the
// children. For example, if a group generates nodes then the number of generated nodes will
// increment the node index and the group's node count. If the parent is tracking structural
// changes in pending then restore that too.
val previousPending = pendingStack.pop()
if (previousPending != null && !inserting) {
previousPending.groupIndex++
}
this.pending = previousPending
this.nodeIndex = nodeIndexStack.pop() + expectedNodeCount
this.groupNodeCount = this.groupNodeCountStack.pop() + expectedNodeCount
}
private fun end(isNode: Boolean) {
// All the changes to the group (or node) have been recorded. All new nodes have been
// inserted but it has yet to determine which need to be removed or moved. Note that the
// changes are relative to the first change in the list of nodes that are changing.
if (inserting) {
val parent = writer.parent
updateCompoundKeyWhenWeExitGroup(writer.groupKey(parent), writer.groupObjectKey(parent))
} else {
val parent = reader.parent
updateCompoundKeyWhenWeExitGroup(reader.groupKey(parent), reader.groupObjectKey(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.toSet()
val placedKeys = mutableSetOf<KeyInfo>()
var currentIndex = 0
val currentEnd = current.size
var previousIndex = 0
val previousEnd = previous.size
// Traverse the list of changes to determine startNode movement
var nodeOffset = 0
while (previousIndex < previousEnd) {
val previousInfo = previous[previousIndex]
if (!usedKeys.contains(previousInfo)) {
// If the key info was not used the group was deleted, remove the nodes in the
// group
val deleteOffset = pending.nodePositionOf(previousInfo)
recordRemoveNode(deleteOffset + pending.startIndex, previousInfo.nodes)
pending.updateNodeCount(previousInfo.location, 0)
recordReaderMoving(previousInfo.location)
reader.reposition(previousInfo.location)
recordDelete()
reader.skipGroup()
// Remove any invalidations pending for the group being removed. These are no
// longer part of the composition. The group being composed is one after the
// start of the group.
invalidations.removeRange(
previousInfo.location,
previousInfo.location + reader.groupSize(previousInfo.location)
)
previousIndex++
continue
}
if (previousInfo in placedKeys) {
// If the group was already placed in the correct location, skip it.
previousIndex++
continue
}
if (currentIndex < currentEnd) {
// At this point current should match previous unless the group is new or was
// moved.
val currentInfo = current[currentIndex]
if (currentInfo !== previousInfo) {
val nodePosition = pending.nodePositionOf(currentInfo)
placedKeys.add(currentInfo)
if (nodePosition != nodeOffset) {
val updatedCount = pending.updatedNodeCountOf(currentInfo)
recordMoveNode(
nodePosition + pending.startIndex,
nodeOffset + pending.startIndex, updatedCount
)
pending.registerMoveNode(nodePosition, nodeOffset, updatedCount)
} // else the nodes are already in the correct position
} else {
// The correct nodes are in the right location
previousIndex++
}
currentIndex++
nodeOffset += pending.updatedNodeCountOf(currentInfo)
}
}
// If there are any current nodes left they where inserted into the right location
// when the group began so the rest are ignored.
realizeMovement()
// We have now processed the entire list so move the slot table to the end of the list
// by moving to the last key and skipping it.
if (previous.size > 0) {
recordReaderMoving(reader.groupEnd)
reader.skipToGroupEnd()
}
}
// Detect removing nodes at the end. No pending is created in this case we just have more
// nodes in the previous composition than we expect (i.e. we are not yet at an end)
val removeIndex = nodeIndex
while (!reader.isGroupEnd) {
val startSlot = reader.currentGroup
recordDelete()
val nodesToRemove = reader.skipGroup()
recordRemoveNode(removeIndex, nodesToRemove)
invalidations.removeRange(startSlot, reader.currentGroup)
}
val inserting = inserting
if (inserting) {
if (isNode) {
registerInsertUpFixup()
expectedNodeCount = 1
}
reader.endEmpty()
val parentGroup = writer.parent
writer.endGroup()
if (!reader.inEmpty) {
val virtualIndex = insertedGroupVirtualIndex(parentGroup)
writer.endInsert()
writer.close()
recordInsert(insertAnchor)
this.inserting = false
if (!slotTable.isEmpty) {
updateNodeCount(virtualIndex, 0)
updateNodeCountOverrides(virtualIndex, expectedNodeCount)
}
}
} else {
if (isNode) recordUp()
recordEndGroup()
val parentGroup = reader.parent
val parentNodeCount = updatedNodeCount(parentGroup)
if (expectedNodeCount != parentNodeCount) {
updateNodeCountOverrides(parentGroup, expectedNodeCount)
}
if (isNode) {
expectedNodeCount = 1
}
reader.endGroup()
realizeMovement()
}
exitGroup(expectedNodeCount, inserting)
}
/**
* Recompose any invalidate child groups of the current parent group. This should be called
* after the group is started but on or before the first child group. It is intended to be
* called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children
* are invalid it will call [skipReaderToGroupEnd].
*/
private fun recomposeToGroupEnd() {
val wasComposing = isComposing
isComposing = true
var recomposed = false
val parent = reader.parent
val end = parent + reader.groupSize(parent)
val recomposeIndex = nodeIndex
val recomposeCompoundKey = currentCompoundKeyHash
val oldGroupNodeCount = groupNodeCount
var oldGroup = parent
var firstInRange = invalidations.firstInRange(reader.currentGroup, end)
while (firstInRange != null) {
val location = firstInRange.location
invalidations.removeLocation(location)
recomposed = true
reader.reposition(location)
val newGroup = reader.currentGroup
// Record the changes to the applier location
recordUpsAndDowns(oldGroup, newGroup, parent)
oldGroup = newGroup
// Calculate the node index (the distance index in the node this groups nodes are
// located in the parent node).
nodeIndex = nodeIndexOf(
location,
newGroup,
parent,
recomposeIndex
)
// Calculate the compound hash code (a semi-unique code for every group in the
// composition used to restore saved state).
currentCompoundKeyHash = compoundKeyOf(
reader.parent(newGroup),
parent,
recomposeCompoundKey
)
firstInRange.scope.compose(this)
// Restore the parent of the reader to the previous parent
reader.restoreParent(parent)
// Using slots.current here ensures composition always walks forward even if a component
// before the current composition is invalidated when performing this composition. Any
// such components will be considered invalid for the next composition. Skipping them
// prevents potential infinite recomposes at the cost of potentially missing a compose
// as well as simplifies the apply as it always modifies the slot table in a forward
// direction.
firstInRange = invalidations.firstInRange(reader.currentGroup, end)
}
if (recomposed) {
recordUpsAndDowns(oldGroup, parent, parent)
reader.skipToGroupEnd()
val parentGroupNodes = updatedNodeCount(parent)
nodeIndex = recomposeIndex + parentGroupNodes
groupNodeCount = oldGroupNodeCount + parentGroupNodes
} else {
// No recompositions were requested in the range, skip it.
skipReaderToGroupEnd()
}
currentCompoundKeyHash = recomposeCompoundKey
isComposing = wasComposing
}
/**
* The index in the insertTable overlap with indexes the slotTable so the group index used to
* track newly inserted groups is set to be negative offset from -2. This reserves -1 as the
* root index which is the parent value returned by the root groups of the slot table.
*
* This function will also restore a virtual index to its index in the insertTable which is
* not needed here but could be useful for debugging.
*/
private fun insertedGroupVirtualIndex(index: Int) = -2 - index
/**
* As operations to insert and remove nodes are recorded, the number of nodes that will be in
* the group after changes are applied is maintained in a side overrides table. This method
* updates that count and then updates any parent groups that include the nodes this group
* emits.
*/
private fun updateNodeCountOverrides(group: Int, newCount: Int) {
// The value of group can be negative which indicates it is tracking an inserted group
// instead of an existing group. The index is a virtual index calculated by
// insertedGroupVirtualIndex which corresponds to the location of the groups to insert in
// the insertTable.
val currentCount = updatedNodeCount(group)
if (currentCount != newCount) {
// Update the overrides
val delta = newCount - currentCount
var current = group
var minPending = pendingStack.size - 1
while (current != -1) {
val newCurrentNodes = updatedNodeCount(current) + delta
updateNodeCount(current, newCurrentNodes)
for (pendingIndex in minPending downTo 0) {
val pending = pendingStack.peek(pendingIndex)
if (pending != null && pending.updateNodeCount(current, newCurrentNodes)) {
minPending = pendingIndex - 1
break
}
}
@Suppress("LiftReturnOrAssignment")
if (current < 0) {
current = reader.parent
} else {
if (reader.isNode(current)) break
current = reader.parent(current)
}
}
}
}
/**
* Calculates the node index (the index in the child list of a node will appear in the
* resulting tree) for [group]. Passing in [recomposeGroup] and its node index in
* [recomposeIndex] allows the calculation to exit early if there is no node group between
* [group] and [recomposeGroup].
*/
private fun nodeIndexOf(
groupLocation: Int,
group: Int,
recomposeGroup: Int,
recomposeIndex: Int
): Int {
// Find the anchor group which is either the recomposeGroup or the first parent node
var anchorGroup = reader.parent(group)
while (anchorGroup != recomposeGroup) {
if (reader.isNode(anchorGroup)) break
anchorGroup = reader.parent(anchorGroup)
}
var index = if (reader.isNode(anchorGroup)) 0 else recomposeIndex
// An early out if the group and anchor are the same
if (anchorGroup == group) return index
// Walk down from the 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 += updatedNodeCount(current)
current = end
}
break
}
return index
}
private fun updatedNodeCount(group: Int): Int {
if (group < 0) return nodeCountVirtualOverrides?.let { it[group] } ?: 0
val nodeCounts = nodeCountOverrides
if (nodeCounts != null) {
val override = nodeCounts[group]
if (override >= 0) return override
}
return reader.nodeCount(group)
}
private fun updateNodeCount(group: Int, count: Int) {
if (updatedNodeCount(group) != count) {
if (group < 0) {
val virtualCounts = nodeCountVirtualOverrides ?: run {
val newCounts = HashMap<Int, Int>()
nodeCountVirtualOverrides = newCounts
newCounts
}
virtualCounts[group] = count
} else {
val nodeCounts = nodeCountOverrides ?: run {
val newCounts = IntArray(reader.size)
newCounts.fill(-1)
nodeCountOverrides = newCounts
newCounts
}
nodeCounts[group] = count
}
}
}
private fun clearUpdatedNodeCounts() {
nodeCountOverrides = null
nodeCountVirtualOverrides = null
}
/**
* Records the operations necessary to move the applier the node affected by the previous
* group to the new group.
*/
private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) {
val reader = reader
val nearestCommonRoot = reader.nearestCommonRootOf(
oldGroup,
newGroup,
commonRoot
)
// Record ups for the nodes between oldGroup and nearestCommonRoot
var current = oldGroup
while (current > 0 && current != nearestCommonRoot) {
if (reader.isNode(current)) recordUp()
current = reader.parent(current)
}
// Record downs from nearestCommonRoot to newGroup
doRecordDownsFor(newGroup, nearestCommonRoot)
}
private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) {
if (group > 0 && group != nearestCommonRoot) {
doRecordDownsFor(reader.parent(group), nearestCommonRoot)
if (reader.isNode(group)) recordDown(reader.nodeAt(group))
}
}
/**
* Calculate the compound key (a semi-unique key produced for every group in the composition)
* for [group]. Passing in the [recomposeGroup] and [recomposeKey] allows this method to exit
* early.
*/
private fun compoundKeyOf(group: Int, recomposeGroup: Int, recomposeKey: Int): Int {
return if (group == recomposeGroup) recomposeKey else (
compoundKeyOf(
reader.parent(group),
recomposeGroup,
recomposeKey
) rol 3
) xor (
if (reader.hasObjectKey(group))
reader.groupObjectKey(group)?.hashCode() ?: 0
else reader.groupKey(group)
)
}
/**
* Invalidate all known RecomposeScopes. Used by [Recomposer] to bring known composers
* back into a known good state after a period of time when snapshot changes were not
* being observed.
*/
internal fun invalidateAll() {
slotTable.slots.forEach { (it as? RecomposeScopeImpl)?.invalidate() }
}
internal fun invalidate(scope: RecomposeScopeImpl): InvalidationResult {
if (scope.defaultsInScope) {
scope.defaultsInvalid = true
}
val anchor = scope.anchor
if (anchor == null || insertTable.ownsAnchor(anchor) || !anchor.valid)
return InvalidationResult.IGNORED // The scope has not yet entered the composition
val location = anchor.toIndexFor(slotTable)
if (location < 0)
return InvalidationResult.IGNORED // The scope was removed from the composition
invalidations.insertIfMissing(location, scope)
if (isComposing && location >= reader.currentGroup) {
// if we are invalidating a scope that is going to be traversed during this
// composition.
return InvalidationResult.IMMINENT
}
parentReference.invalidate(this)
return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
}
/**
* Skip a group. Skips the group at the current location. This is only valid to call if the
* composition is not inserting.
*/
@ComposeCompilerApi
fun skipCurrentGroup() {
if (invalidations.isEmpty()) {
skipGroup()
} else {
val reader = reader
val key = reader.groupKey
val dataKey = reader.groupObjectKey
updateCompoundKeyWhenWeEnterGroup(key, dataKey)
startReaderGroup(reader.isNode, null)
recomposeToGroupEnd()
reader.endGroup()
updateCompoundKeyWhenWeExitGroup(key, dataKey)
}
}
private fun skipReaderToGroupEnd() {
groupNodeCount = reader.parentNodes
reader.skipToGroupEnd()
}
/**
* Skip to the end of the group opened by [startGroup].
*/
@ComposeCompilerApi
fun skipToGroupEnd() {
check(groupNodeCount == 0) { "No nodes can be emitted before calling skipAndEndGroup" }
currentRecomposeScope?.used = false
if (invalidations.isEmpty()) {
skipReaderToGroupEnd()
} else {
recomposeToGroupEnd()
}
}
/**
* 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 the lambda returned by
* [androidx.compose.runtime.invalidate].
*/
@ComposeCompilerApi
fun startRestartGroup(key: Int) {
start(key, null, false, null)
addRecomposeScope()
}
@ComposeCompilerApi
fun startRestartGroup(key: Int, sourceInformation: String?) {
start(key, null, false, sourceInformation)
addRecomposeScope()
}
private fun addRecomposeScope() {
if (inserting) {
val scope = RecomposeScopeImpl(this)
invalidateStack.push(scope)
updateValue(scope)
} else {
val invalidation = invalidations.removeLocation(reader.parent)
val scope = reader.next() as RecomposeScopeImpl
scope.requiresRecompose = invalidation != null
invalidateStack.push(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
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 wrong order.
val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
else null
scope?.requiresRecompose = false
val result = if (scope != null && (scope.used || collectParameterInformation)) {
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
}
/**
* Synchronously compose the initial composition of [block]. This collects all the changes
* which must be applied by [applyChanges] to build the tree implied by [block].
*/
@InternalComposeApi
fun composeInitial(block: @Composable () -> Unit) {
trace("Compose:recompose") {
var complete = false
val wasComposing = isComposing
isComposing = true
try {
startRoot()
startGroup(invocationKey, invocation)
invokeComposable(this, block)
endGroup()
endRoot()
complete = true
} finally {
isComposing = wasComposing
if (!complete) abortRoot()
}
}
}
/**
* Synchronously recompose all invalidated groups. This collects the changes which must be
* applied by [applyChanges] to have an effect.
*/
@InternalComposeApi
fun recompose(): Boolean {
if (invalidations.isNotEmpty()) {
trace("Compose:recompose") {
nodeIndex = 0
var complete = false
val wasComposing = isComposing
isComposing = true
try {
startRoot()
skipCurrentGroup()
endRoot()
complete = true
} finally {
isComposing = wasComposing
if (!complete) abortRoot()
}
}
return changes.isNotEmpty()
}
return false
}
internal fun hasInvalidations() = invalidations.isNotEmpty()
@Suppress("UNCHECKED_CAST")
private val SlotReader.node get() = node(parent) as N
@Suppress("UNCHECKED_CAST")
private fun SlotReader.nodeAt(index: Int) = node(index) as N
private fun validateNodeExpected() {
check(nodeExpected) {
"A call to createNode(), emitNode() or useNode() expected was not expected"
}
nodeExpected = false
}
private fun validateNodeNotExpected() {
check(!nodeExpected) { "A call to createNode(), emitNode() or useNode() expected" }
}
/**
* Add a raw change to the change list. Once [record] is called, the operation is realized
* into the change list. The helper routines below reduce the number of operations that must
* be realized to change the previous tree to the new tree as well as update the slot table
* to prepare for the next composition.
*/
private fun record(change: Change<N>) {
changes.add(change)
}
/**
* Record a change ensuring, when it is applied, that the applier is focused on the current
* node.
*/
private fun recordApplierOperation(change: Change<N>) {
realizeUps()
realizeDowns()
record(change)
}
/**
* Record a change that will insert, remove or move a slot table group. This ensures the slot
* table is prepared for the change by ensuring the parent group is started and then ended
* as the group is left.
*/
private fun recordSlotEditingOperation(change: Change<N>) {
realizeOperationLocation()
recordSlotEditing()
record(change)
}
/**
* Record a change ensuring, when it is applied, the write matches the current slot in the
* reader.
*/
private fun recordSlotTableOperation(forParent: Boolean = false, change: Change<N>) {
realizeOperationLocation(forParent)
record(change)
}
// Navigation of the node tree is performed by recording all the locations of the nodes as
// they are traversed by the reader and recording them in the downNodes array. When the node
// navigation is realized all the downs in the down nodes is played to the applier.
//
// If an up is recorded before the corresponding down is realized then it is simply removed
// from the downNodes stack.
private var pendingUps = 0
private var downNodes = Stack<N>()
private fun realizeUps() {
val count = pendingUps
if (count > 0) {
pendingUps = 0
record { applier, _, _ -> repeat(count) { applier.up() } }
}
}
private fun realizeDowns(nodes: Array<N>) {
record { applier, _, _ ->
for (index in nodes.indices) {
applier.down(nodes[index])
}
}
}
private fun realizeDowns() {
if (downNodes.isNotEmpty()) {
@Suppress("UNCHECKED_CAST")
realizeDowns(downNodes.toArray())
downNodes.clear()
}
}
private fun recordDown(node: N) {
@Suppress("UNCHECKED_CAST")
downNodes.push(node)
}
private fun recordUp() {
if (downNodes.isNotEmpty()) {
downNodes.pop()
} else {
pendingUps++
}
}
// Navigating the writer slot is performed relatively as the location of a group in the writer
// might be different than it is in the reader as groups can be inserted, deleted, or moved.
//
// writersReaderDelta tracks the difference between reader's current slot the current of
// the writer must be before the recorded change is applied. Moving the writer to a location
// is performed by advancing the writer the same the number of slots traversed by the reader
// since the last write change. This works transparently for inserts. For deletes the number
// of nodes deleted needs to be added to writersReaderDelta. When slots move the delta is
// updated as if the move has already taken place. The delta is updated again once the group
// begin edited is complete.
//
// The SlotTable requires that the group that contains any moves, inserts or removes must have
// the group that contains the moved, inserted or removed groups be started with a startGroup
// and terminated with a endGroup so the effects of the inserts, deletes, and moves can be
// recorded correctly in its internal data structures. The startedGroups stack maintains the
// groups that must be closed before we can move past the started group.
/**
* The skew or delta between where the writer will be and where the reader is now. This can
* be thought of as the unrealized distance the writer must move to match the current slot in
* the reader. When an operation affects the slot table the writer location must be realized
* by moving the writer slot table the unrealized distance.
*/
private var writersReaderDelta = 0
/**
* Record whether any groups were stared. If no groups were started then the root group
* doesn't need to be started or ended either.
*/
private var startedGroup = false
/**
* A stack of the location of the groups that were started.
*/
private val startedGroups = IntStack()
private fun realizeOperationLocation(forParent: Boolean = false) {
val location = if (forParent) reader.parent else reader.currentGroup
val distance = location - writersReaderDelta
require(distance >= 0) { "Tried to seek backward" }
if (distance > 0) {
record { _, slots, _ -> slots.advanceBy(distance) }
writersReaderDelta = location
}
}
private fun recordInsert(anchor: Anchor) {
if (insertFixups.isEmpty()) {
recordSlotEditingOperation { _, slots, _ ->
slots.beginInsert()
slots.moveFrom(insertTable, anchor.toIndexFor(insertTable))
slots.endInsert()
}
} else {
val fixups = insertFixups.toMutableList()
insertFixups.clear()
realizeUps()
realizeDowns()
recordSlotEditingOperation { applier, slots, rememberManager ->
insertTable.write { writer ->
for (fixup in fixups) {
fixup(applier, writer, rememberManager)
}
}
slots.beginInsert()
slots.moveFrom(insertTable, anchor.toIndexFor(insertTable))
slots.endInsert()
}
}
}
private fun recordFixup(change: Change<N>) {
insertFixups.add(change)
}
private val insertUpFixups = Stack<Change<N>>()
private fun recordInsertUpFixup(change: Change<N>) {
insertUpFixups.push(change)
}
private fun registerInsertUpFixup() {
insertFixups.add(insertUpFixups.pop())
}
/**
* When a group is removed the reader will move but the writer will not so to ensure both the
* writer and reader are tracking the same slot we advance the [writersReaderDelta] to
* account for the removal.
*/
private fun recordDelete() {
recordSlotEditingOperation(change = removeCurrentGroupInstance)
writersReaderDelta += reader.groupSize
}
/**
* Called when reader current is moved directly, such as when a group moves, to [location].
*/
private fun recordReaderMoving(location: Int) {
val distance = reader.currentGroup - writersReaderDelta
// Ensure the next skip will account for the distance we have already travelled.
writersReaderDelta = location - distance
}
private fun recordSlotEditing() {
// During initial composition (when the slot table is empty), no group needs
// to be started.
if (!slotTable.isEmpty) {
val reader = reader
val location = reader.parent
if (startedGroups.peekOr(-1) != location) {
if (!startedGroup) {
// We need to ensure the root group is started.
recordSlotTableOperation(change = startRootGroup)
startedGroup = true
}
val anchor = reader.anchor(location)
startedGroups.push(location)
recordSlotTableOperation { _, slots, _ -> slots.ensureStarted(anchor) }
}
}
}
private fun recordEndGroup() {
val location = reader.parent
val currentStartedGroup = startedGroups.peekOr(-1)
check(currentStartedGroup <= location) { "Missed recording an endGroup" }
if (startedGroups.peekOr(-1) == location) {
startedGroups.pop()
recordSlotTableOperation(change = endGroupInstance)
}
}
private fun recordEndRoot() {
if (startedGroup) {
recordSlotTableOperation(change = endGroupInstance)
startedGroup = false
}
}
private fun finalizeCompose() {
realizeUps()
check(pendingStack.isEmpty()) { "Start/end imbalance" }
check(startedGroups.isEmpty()) { "Missed recording an endGroup()" }
cleanUpCompose()
}
private fun cleanUpCompose() {
pending = null
nodeIndex = 0
groupNodeCount = 0
writersReaderDelta = 0
currentCompoundKeyHash = 0
nodeExpected = false
startedGroup = false
startedGroups.clear()
clearUpdatedNodeCounts()
}
private var previousRemove = -1
private var previousMoveFrom = -1
private var previousMoveTo = -1
private var previousCount = 0
private fun recordRemoveNode(nodeIndex: Int, count: Int) {
if (count > 0) {
check(nodeIndex >= 0) { "Invalid remove index $nodeIndex" }
if (previousRemove == nodeIndex) previousCount += count
else {
realizeMovement()
previousRemove = nodeIndex
previousCount = count
}
}
}
private fun recordMoveNode(from: Int, to: Int, count: Int) {
if (count > 0) {
if (previousCount > 0 && previousMoveFrom == from - previousCount &&
previousMoveTo == to - previousCount
) {
previousCount += count
} else {
realizeMovement()
previousMoveFrom = from
previousMoveTo = to
previousCount = count
}
}
}
private fun realizeMovement() {
val count = previousCount
previousCount = 0
if (count > 0) {
if (previousRemove >= 0) {
val removeIndex = previousRemove
previousRemove = -1
recordApplierOperation { applier, _, _ -> applier.remove(removeIndex, count) }
} else {
val from = previousMoveFrom
previousMoveFrom = -1
val to = previousMoveTo
previousMoveTo = -1
recordApplierOperation { applier, _, _ -> applier.move(from, to, count) }
}
}
}
/**
* A holder that will dispose of its [CompositionReference] when it leaves the composition
* that will not have its reference made visible to user code.
*/
// This warning becomes an error if its advice is followed since Composer needs its type param
@Suppress("RemoveRedundantQualifierName", "DEPRECATION")
private class CompositionReferenceHolder<T>(
val ref: Composer<T>.CompositionReferenceImpl
) : CompositionLifecycleObserver {
override fun onLeave() {
ref.dispose()
}
}
private inner class CompositionReferenceImpl(
val scope: RecomposeScopeImpl,
override val compoundHashKey: Int,
override val collectingKeySources: Boolean,
override val collectingParameterInformation: Boolean
) : CompositionReference() {
var inspectionTables: MutableSet<MutableSet<CompositionData>>? = null
val composers = mutableSetOf<Composer<*>>()
fun dispose() {
if (composers.isNotEmpty()) {
inspectionTables?.let {
for (composer in composers) {
for (table in it)
table.remove(composer.slotTable)
}
}
composers.clear()
}
}
override fun registerComposer(composer: Composer<*>) {
super.registerComposer(composer)
composers.add(composer)
}
override fun unregisterComposer(composer: Composer<*>) {
inspectionTables?.forEach { it.remove(composer.slotTable) }
composers.remove(composer)
super.unregisterComposer(composer)
}
override fun registerComposerWithRoot(composer: Composer<*>) {
parentReference.registerComposerWithRoot(composer)
}
override fun unregisterComposerWithRoot(composer: Composer<*>) {
parentReference.unregisterComposerWithRoot(composer)
}
override val effectCoroutineContext: CoroutineContext
get() = parentReference.effectCoroutineContext
override fun composeInitial(composer: Composer<*>, composable: @Composable () -> Unit) {
parentReference.composeInitial(composer, composable)
}
override fun invalidate(composer: Composer<*>) {
// 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.
parentReference.invalidate(this@Composer)
parentReference.invalidate(composer)
}
override fun <T> getAmbient(key: Ambient<T>): T {
val anchor = scope.anchor
return if (anchor != null && anchor.valid) {
parentAmbient(key, anchor.toIndexFor(slotTable))
} else {
// The composition is composing and the ambient has not landed in the slot table
// yet. This is a synchronous read from a sub-composition so the current ambient
parentAmbient(key)
}
}
override fun getAmbientScope(): AmbientMap {
return ambientScopeAt(scope.anchor?.toIndexFor(slotTable) ?: 0)
}
override fun recordInspectionTable(table: MutableSet<CompositionData>) {
(
inspectionTables ?: HashSet<MutableSet<CompositionData>>().also {
inspectionTables = it
}
).add(table)
}
override fun startComposing() {
childrenComposing++
}
override fun doneComposing() {
childrenComposing--
}
}
private fun updateCompoundKeyWhenWeEnterGroup(groupKey: Int, dataKey: Any?) {
if (dataKey == null)
updateCompoundKeyWhenWeEnterGroupKeyHash(groupKey)
else
updateCompoundKeyWhenWeEnterGroupKeyHash(dataKey.hashCode())
}
@OptIn(ExperimentalComposeApi::class)
private fun updateCompoundKeyWhenWeEnterGroupKeyHash(keyHash: Int) {
currentCompoundKeyHash = (currentCompoundKeyHash rol 3) xor keyHash
}
private fun updateCompoundKeyWhenWeExitGroup(groupKey: Int, dataKey: Any?) {
if (dataKey == null)
updateCompoundKeyWhenWeExitGroupKeyHash(groupKey)
else
updateCompoundKeyWhenWeExitGroupKeyHash(dataKey.hashCode())
}
@OptIn(ExperimentalComposeApi::class)
private fun updateCompoundKeyWhenWeExitGroupKeyHash(groupKey: Int) {
currentCompoundKeyHash = (currentCompoundKeyHash xor groupKey.hashCode()) ror 3
}
}
/**
* A helper receiver scope class used by [ComposeNode] to help write code to initialized and update a
* node.
*
* @see ComposeNode
*/
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
inline class Updater<T> constructor(
@PublishedApi internal val composer: Composer<*>
) {
/**
* Set the value property of the emitted node.
*
* Schedules [block] to be run when the node is first created or when [value] is different
* than the previous composition.
*
* @see update
*/
@Suppress("NOTHING_TO_INLINE") // Inlining the compare has noticeable impact
inline fun set(
value: Int,
noinline block: T.(value: Int) -> Unit
) = with(composer) {
if (inserting || nextSlot() != value) {
updateValue(value)
composer.apply(value, block)
}
}
/**
* Set the value property of the emitted node.
*
* Schedules [block] to be run when the node is first created or when [value] is different
* than the previous composition.
*
* @see update
*/
fun <V> set(
value: V,
block: T.(value: V) -> Unit
) = with(composer) {
if (inserting || nextSlot() != value) {
updateValue(value)
composer.apply(value, block)
}
}
/**
* Update the value of a property of the emitted node.
*
* Schedules [block] to be run when [value] is different than the previous composition. It is
* different than [set] in that it does not run when the node is created. This is used when
* initial value set by the [ComposeNode] in the constructor callback already has the correct value.
* For example, use [update} when [value] is passed into of the classes constructor
* parameters.
*
* @see set
*/
@Suppress("NOTHING_TO_INLINE") // Inlining the compare has noticeable impact
inline fun update(
value: Int,
noinline block: T.(value: Int) -> Unit
) = with(composer) {
if (inserting || nextSlot() != value) {
updateValue(value)
composer.apply(value, block)
}
}
/**
* Update the value of a property of the emitted node.
*
* Schedules [block] to be run when [value] is different than the previous composition. It is
* different than [set] in that it does not run when the node is created. This is used when
* initial value set by the [ComposeNode] in the constructor callback already has the correct value.
* For example, use [update} when [value] is passed into of the classes constructor
* parameters.
*
* @see set
*/
fun <V> update(
value: V,
block: T.(value: V) -> Unit
) = with(composer) {
if (inserting || nextSlot() != value) {
updateValue(value)
composer.apply(value, block)
}
}
/**
* Initialize emitted node.
*
* Schedule [block] to be executed after the node is created.
*
* This is only executed once. The can be used to call a method or set a value on a node
* instance that is required to be set after one or more other properties have been set.
*
* @see reconcile
*/
fun init(block: T.() -> Unit) {
if (composer.inserting) composer.apply<Unit, T>(Unit, { block() })
}
/**
* Reconcile the node to the current state.
*
* This is used when [set] and [update] are insufficient to update the state of the node
* based on changes passed to the function calling [ComposeNode].
*
* Schedules [block] to execute. As this unconditionally schedules [block] to executed it
* might be executed unnecessarily as no effort is taken to ensure it only executes when the
* values [block] captures have changed. It is highly recommended that [set] and [update] be
* used instead as they will only schedule their blocks to executed when the value passed to
* them has changed.
*/
fun reconcile(block: T.() -> Unit) {
composer.apply<Unit, T>(Unit, { this.block() })
}
}
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
inline class SkippableUpdater<T> constructor(
@PublishedApi internal val composer: Composer<*>
) {
inline fun update(block: Updater<T>.() -> Unit) {
composer.startReplaceableGroup(0x1e65194f)
Updater<T>(composer).block()
composer.endReplaceableGroup()
}
}
private fun SlotWriter.removeCurrentGroup(rememberManager: RememberManager) {
// Notify the lifecycle manager of any observers leaving the slot table
// The notification order should ensure that listeners are notified of leaving
// in opposite order that they are notified of entering.
// To ensure this order, we call `enters` as a pre-order traversal
// of the group tree, and then call `leaves` in the inverse order.
for (slot in groupSlots()) {
@Suppress("DEPRECATION")
when (slot) {
is CompositionLifecycleObserver -> {
rememberManager.leaving(slot)
}
is RememberObserver -> {
rememberManager.forgetting(slot)
}
is RecomposeScopeImpl -> {
val composer = slot.composer
if (composer != null) {
composer.pendingInvalidScopes = true
slot.composer = null
}
}
}
}
removeGroup()
}
// Mutable list
private fun <K, V> multiMap() = HashMap<K, LinkedHashSet<V>>()
private fun <K, V> HashMap<K, LinkedHashSet<V>>.put(key: K, value: V) = getOrPut(key) {
LinkedHashSet()
}.add(value)
private fun <K, V> HashMap<K, LinkedHashSet<V>>.remove(key: K, value: V) =
get(key)?.let {
it.remove(value)
if (it.isEmpty()) remove(key)
}
private fun <K, V> HashMap<K, LinkedHashSet<V>>.pop(key: K) = get(key)?.firstOrNull()?.also {
remove(key, it)
}
private fun getKey(value: Any?, left: Any?, right: Any?): Any? = (value as? JoinedKey)?.let {
if (it.left == left && it.right == right) value
else getKey(it.left, left, right) ?: getKey(
it.right,
left,
right
)
}
// Observation helpers
// These helpers enable storing observation pairs of value to scope instances in a list sorted by
// the value hash as a primary key and the scope hash as the secondary key. This results in a
// multi-set that allows finding an observation/scope pairs in O(log N) and a worst case of
// insert into and remove from the array of O(log N) + O(N) where N is the number of total pairs.
// This also enables finding all scopes in O(log N) + O(M) for value V where M is the number of
// scopes recorded for value V.
// The layout of the array is a sequence of sorted pairs where the even slots are values and the
// odd slots are recompose scopes. Storing the pairs in this fashion saves an instance per pair.
// Since inserts and removes are common this algorithm tends to be O(N^2) where N is the number of
// inserts and removes. Using a balanced binary tree might be better here, at the cost of
// significant added complexity and more memory, as it would have close to O(N log N) for N
// inserts and removes. The mechanics of pointer chasing on lower-end and/or low-power processors,
// however, might outweigh the benefits for moderate sizes of N.
private fun MutableList<Any>.insertIfMissing(value: Any, scope: RecomposeScopeImpl) {
val index = find(value, scope)
if (index < 0) {
val offset = -(index + 1)
if (size - index > 16) {
// Use an allocation to save the cost one of the two moves implied by add()/add()
// for large moves.
addAll(offset, listOf(value, scope))
} else {
add(offset, value)
add(offset + 1, scope)
}
}
}
private fun MutableList<Any>.removeValueScope(value: Any, scope: RecomposeScopeImpl): Boolean {
val index = find(value, scope)
if (index >= 0) {
subList(index, index + 2).clear()
return true
}
return false
}
private inline fun MutableList<Any>.removeValueIf(
predicate: (value: Any, scope: RecomposeScopeImpl) -> Boolean
) {
var copyLocation = 0
for (index in 0 until size / 2) {
val slot = index * 2
val value = get(slot)
val scope = get(slot + 1) as RecomposeScopeImpl
if (!predicate(value, scope)) {
if (copyLocation != slot) {
// Keep the value by copying over a value that has been moved or removed.
set(copyLocation++, value)
set(copyLocation++, scope)
} else {
// No slots have been removed yet, just update the copy location
copyLocation += 2
}
}
}
if (copyLocation < size) {
// Delete any left-over slots.
subList(copyLocation, size).clear()
}
}
/**
* Iterate through all the scopes associated with [value]. Returns `false` if [value] has no scopes
* associated with it.
*/
private inline fun MutableList<Any>.forEachScopeOf(
value: Any,
block: (scope: RecomposeScopeImpl) -> Unit
): Boolean {
val valueHash = identityHashCode(value)
var index = findFirst(valueHash)
var result = false
while (index < size) {
val storedValue = get(index)
if (identityHashCode(storedValue) != valueHash) break
if (storedValue === value) {
val storedScope = get(index + 1) as RecomposeScopeImpl
block(storedScope)
result = true
}
index += 2
}
return result
}
private fun MutableList<Any>.find(value: Any, scope: RecomposeScopeImpl): Int {
val valueHash = identityHashCode(value)
val scopeHash = identityHashCode(scope)
var index = find(identityHashCode(value), identityHashCode(scope))
if (index < 0) return index
while (true) {
if (get(index) === value && get(index + 1) === scope) return index
index++
if (index >= size ||
identityHashCode(get(index)) != valueHash ||
identityHashCode(get(index + 1)) != scopeHash
) {
index--
break
}
}
return -(index + 1)
}
private fun MutableList<Any>.findFirst(valueHash: Int) =
find(valueHash, 0).let { if (it < 0) -(it + 1) else it }
private fun MutableList<Any>.find(valueHash: Int, scopeHash: Int): Int {
var low = 0
var high = (size / 2) - 1
while (low <= high) {
val mid = (low + high).ushr(1) // safe from overflows
val cmp = identityHashCode(get(mid * 2)).compareTo(valueHash).let {
if (it != 0) it else identityHashCode(get(mid * 2 + 1)).compareTo(scopeHash)
}
when {
cmp < 0 -> low = mid + 1
cmp > 0 -> high = mid - 1
else -> return mid * 2 // found
}
}
return -(low * 2 + 1) // not found
}
// Invalidation helpers
private fun MutableList<Invalidation>.findLocation(location: Int): Int {
var low = 0
var high = size - 1
while (low <= high) {
val mid = (low + high).ushr(1) // safe from overflows
val midVal = get(mid)
val cmp = midVal.location.compareTo(location)
when {
cmp < 0 -> low = mid + 1
cmp > 0 -> high = mid - 1
else -> return mid // key found
}
}
return -(low + 1) // key not found
}
private fun MutableList<Invalidation>.insertIfMissing(location: Int, scope: RecomposeScopeImpl) {
val index = findLocation(location)
if (index < 0) {
add(-(index + 1), Invalidation(scope, location))
}
}
private fun MutableList<Invalidation>.firstInRange(start: Int, end: Int): Invalidation? {
val index = findLocation(start).let { if (it < 0) -(it + 1) else it }
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 = findLocation(start).let { if (it < 0) -(it + 1) else it }
while (index < size) {
val validation = get(index)
if (validation.location < end) removeAt(index)
else break
}
}
private fun Boolean.asInt() = if (this) 1 else 0
private fun Int.asBool() = this != 0
val currentComposer: Composer<*> @Composable get() {
throw NotImplementedError("Implemented as an intrinsic")
}
internal fun invokeComposable(composer: Composer<*>, composable: @Composable () -> Unit) {
@Suppress("UNCHECKED_CAST")
val realFn = composable as Function2<Composer<*>, Int, Unit>
realFn(composer, 1)
}
internal fun <T> invokeComposableForResult(
composer: Composer<*>,
composable: @Composable () -> T
): T {
@Suppress("UNCHECKED_CAST")
val realFn = composable as Function2<Composer<*>, Int, T>
return realFn(composer, 1)
}
private fun SlotReader.distanceFrom(index: Int, root: Int): Int {
var count = 0
var current = index
while (current > 0 && current != root) {
current = parent(current)
count++
}
return count
}
// find the nearest common root
private fun SlotReader.nearestCommonRootOf(a: Int, b: Int, common: Int): Int {
// Early outs, to avoid calling distanceFrom in trivial cases
if (a == b) return a // A group is the nearest common root of itself
if (a == common || b == common) return common // If either is common then common is nearest
if (parent(a) == b) return b // if b is a's parent b is the nearest common root
if (parent(b) == a) return a // if a is b's parent a is the nearest common root
if (parent(a) == parent(b)) return parent(a) // if a an b share a parent it is common
// Find the nearest using distance from common
var currentA = a
var currentB = b
val aDistance = distanceFrom(a, common)
val bDistance = distanceFrom(b, common)
repeat(aDistance - bDistance) { currentA = parent(currentA) }
repeat(bDistance - aDistance) { currentB = parent(currentB) }
// Both ca and cb are now the same distance from a known common root,
// therefore, the first parent that is the same is the lowest common root.
while (currentA != currentB) {
currentA = parent(currentA)
currentB = parent(currentB)
}
// ca == cb so it doesn't matter which is returned
return currentA
}
private val removeCurrentGroupInstance: Change<*> = { _, slots, rememberManager ->
slots.removeCurrentGroup(rememberManager)
}
private val endGroupInstance: Change<*> = { _, slots, _ -> slots.endGroup() }
private val startRootGroup: Change<*> = { _, slots, _ -> slots.ensureStarted(0) }
private val KeyInfo.joinedKey: Any get() = if (objectKey != null) JoinedKey(key, objectKey) else key
/*
* Integer keys are arbitrary values in the biload range. The do not need to be unique as if
* there is a chance they will collide with a compiler generated key they are paired with a
* OpaqueKey to ensure they are unique.
*/
// rootKey doesn't need a corresponding OpaqueKey as it never has sibling nodes and will always
// a unique key.
private const val rootKey = 100
// An arbitrary value paired with a boxed Int or a JoinKey data key.
private const val nodeKey = 125
@PublishedApi
internal const val invocationKey = 200
@PublishedApi
@Suppress("HiddenTypeParameter")
internal val invocation = OpaqueKey("provider")
@PublishedApi
internal const val providerKey = 201
@PublishedApi
@Suppress("HiddenTypeParameter")
internal val provider = OpaqueKey("provider")
@PublishedApi
internal const val ambientMapKey = 202
@PublishedApi
@Suppress("HiddenTypeParameter")
internal val ambientMap = OpaqueKey("ambientMap")
@PublishedApi
internal const val providerValuesKey = 203
@PublishedApi
@Suppress("HiddenTypeParameter")
internal val providerValues = OpaqueKey("providerValues")
@PublishedApi
internal const val providerMapsKey = 204
@PublishedApi
@Suppress("HiddenTypeParameter")
internal val providerMaps = OpaqueKey("providers")
@PublishedApi
internal const val referenceKey = 206
@PublishedApi
@Suppress("HiddenTypeParameter")
internal val reference = OpaqueKey("reference")