| /* |
| * 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. |
| */ |
| |
| package androidx.compose |
| |
| import java.util.Stack |
| |
| internal typealias Change<N> = ( |
| applier: Applier<N>, |
| slots: SlotWriter, |
| lifecycleManager: LifeCycleManager |
| ) -> 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 LifeCycleManager { |
| fun entering(holder: CompositionLifecycleObserverHolder): CompositionLifecycleObserverHolder |
| fun leaving(holder: CompositionLifecycleObserverHolder) |
| } |
| |
| interface Recomposer { |
| fun scheduleRecompose() |
| fun recomposeSync() |
| } |
| |
| /** |
| * 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 parentKeyInfo: KeyInfo, |
| val keyInfos: MutableList<KeyInfo>, |
| val startIndex: Int |
| ) { |
| var groupIndex: Int = 0 |
| |
| init { |
| assert(startIndex >= 0) { "Invalid start index" } |
| } |
| |
| var nodeCount = parentKeyInfo.nodes |
| |
| private val usedKeys = mutableListOf<KeyInfo>() |
| private val groupInfos = run { |
| var runningNodeIndex = 0 |
| keyInfos.withIndex().associateTo(mutableMapOf()) { (index, key) -> |
| Pair(key, GroupInfo(index, runningNodeIndex, key.nodes)).also { |
| runningNodeIndex += key.nodes |
| } |
| } |
| } |
| |
| /** |
| * 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 (keyInfo in keyInfos) { |
| @Suppress("ReplacePutWithAssignment") |
| it.put(keyInfo.key, keyInfo) |
| } |
| } |
| } |
| |
| /** |
| * Get the next key information for the given key. |
| */ |
| fun getNext(key: Any): KeyInfo? = keyMap.pop(key) |
| |
| /** |
| * 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 |
| } |
| } |
| } |
| |
| fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) { |
| groupInfos[keyInfo] = GroupInfo(-1, insertIndex, 0) |
| } |
| |
| fun updateNodeCount(keyInfo: KeyInfo?, newCount: Int) { |
| groupInfos[keyInfo]?.let { |
| val index = it.nodeIndex |
| val difference = newCount - it.nodeCount |
| it.nodeCount = newCount |
| if (difference != 0) { |
| nodeCount += difference |
| groupInfos.values.forEach { |
| group -> if (group.nodeIndex >= index && group != it) |
| group.nodeIndex += difference |
| } |
| } |
| } |
| } |
| |
| fun slotPositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo]?.slotIndex ?: -1 |
| fun nodePositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo]?.nodeIndex ?: -1 |
| fun updatedNodeCountOf(keyInfo: KeyInfo) = groupInfos[keyInfo]?.nodeCount ?: keyInfo.nodes |
| } |
| |
| private object RootKey |
| |
| private class Invalidation( |
| val scope: RecomposeScope, |
| var location: Int |
| ) |
| |
| internal class RecomposeScope(val compose: (invalidate: (sync: Boolean) -> Unit) -> Unit) { |
| var anchor: Anchor? = null |
| var invalidate: ((sync: Boolean) -> Unit)? = null |
| val valid: Boolean get() = anchor?.valid ?: false |
| } |
| |
| private val IGNORE_RECOMPOSE: (sync: Boolean) -> Unit = {} |
| |
| // TODO(lmr): this could be named MutableTreeComposer |
| open class Composer<N>( |
| private val slotTable: SlotTable, |
| private val applier: Applier<N>, |
| private val recomposer: Recomposer? |
| ) : Composition<N>() { |
| private val changes = mutableListOf<Change<N>>() |
| private val lifecycleObservers = mutableMapOf< |
| CompositionLifecycleObserverHolder, |
| CompositionLifecycleObserverHolder |
| >() |
| private val pendingStack = Stack<Pending?>() |
| private var pending: Pending? = null |
| private val keyStack = Stack<KeyInfo?>() |
| private var parentKeyInfo: KeyInfo? = null |
| private var nodeIndex: Int = 0 |
| private var nodeIndexStack = IntStack() |
| private var groupNodeCount: Int = 0 |
| private var groupNodeCountStack = IntStack() |
| |
| private var childrenAllowed = true |
| private var invalidations: MutableList<Invalidation> = mutableListOf() |
| private val entersStack = IntStack() |
| private val insertedProviders = Stack<Ambient.Holder<*>>() |
| private val invalidateStack = Stack<RecomposeScope>() |
| internal var ambientReference: CompositionReference? = null |
| |
| private val changesAppliedObservers = mutableListOf<() -> Unit>() |
| |
| private fun dispatchChangesAppliedObservers() { |
| val listeners = changesAppliedObservers.toTypedArray() |
| changesAppliedObservers.clear() |
| listeners.forEach { it() } |
| } |
| |
| internal fun addChangesAppliedObserver(l: () -> Unit) { |
| changesAppliedObservers.add(l) |
| } |
| |
| internal fun removeChangesAppliedObserver(l: () -> Unit) { |
| changesAppliedObservers.remove(l) |
| } |
| |
| // Temporary to allow staged changes. This will move into a sub-object that represents an active |
| // composition created by startRoot() and recomposeComponentRange() |
| private lateinit var slots: SlotReader |
| |
| protected fun composeRoot(block: () -> Unit) { |
| slotTable.read { |
| slots = it |
| startGroup(RootKey) |
| block() |
| endGroup() |
| finalizeCompose() |
| } |
| } |
| |
| fun startRoot() { |
| slots = slotTable.openReader() |
| startGroup(RootKey) |
| } |
| |
| fun endRoot() { |
| endGroup() |
| finalizeCompose() |
| slots.close() |
| } |
| |
| override val inserting: Boolean get() = slots.inEmpty |
| |
| fun applyChanges() { |
| invalidateStack.clear() |
| val enters = mutableSetOf<CompositionLifecycleObserverHolder>() |
| val leaves = mutableSetOf<CompositionLifecycleObserverHolder>() |
| val manager = object : LifeCycleManager { |
| override fun entering( |
| holder: CompositionLifecycleObserverHolder |
| ): CompositionLifecycleObserverHolder { |
| return lifecycleObservers.getOrPut(holder) { |
| enters.add(holder) |
| holder |
| }.apply { count++ } |
| } |
| |
| override fun leaving(holder: CompositionLifecycleObserverHolder) { |
| if (--holder.count == 0) { |
| leaves.add(holder) |
| } |
| } |
| } |
| |
| val invalidationAnchors = invalidations.map { slotTable.anchor(it.location) to it } |
| |
| // Apply all changes |
| slotTable.write { slots -> |
| changes.forEach { change -> change(applier, slots, manager) } |
| changes.clear() |
| } |
| |
| for ((anchor, invalidation) in invalidationAnchors) { |
| invalidation.location = slotTable.anchorLocation(anchor) |
| } |
| |
| // Send lifecycle leaves |
| 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 lifecycle enters |
| for (holder in enters) { |
| holder.instance.onEnter() |
| } |
| dispatchChangesAppliedObservers() |
| } |
| |
| override fun startGroup(key: Any) = start(key, START_GROUP) |
| override fun endGroup() = end(END_GROUP) |
| |
| override fun skipGroup() { |
| recordSkip(START_GROUP) |
| groupNodeCount += slots.skipGroup() |
| } |
| |
| fun skipGroup(@Suppress("UNUSED_PARAMETER") key: Any) { |
| nextSlot() |
| skipValue() |
| skipGroupAndRecomposeRange() |
| } |
| |
| override fun startNode(key: Any) { |
| start(key, START_NODE) |
| childrenAllowed = false |
| } |
| |
| // Deprecated |
| override fun <T : N> emitNode(factory: () -> T) { |
| if (inserting) { |
| // The previous pending is the pending information for where the node is being inserted. |
| // They must exist as we are in insert mode and entering inserting mode created them. |
| val insertIndex = nodeIndexStack.peek() |
| pending!!.nodeCount++ |
| groupNodeCount++ |
| recordOperation { applier, slots, _ -> |
| val node = factory() |
| slots.update(node) |
| applier.insert(insertIndex, node) |
| applier.down(node) |
| } |
| } else { |
| recordDown() |
| slots.next() // Skip node slot |
| } |
| childrenAllowed = true |
| } |
| |
| override fun <T : N> createNode(factory: () -> T) { |
| // The previous pending is the pending information for where the node is being inserted. |
| // They must exist as we are in insert mode and entering inserting mode created them. |
| val insertIndex = nodeIndexStack.peek() |
| pending!!.nodeCount++ |
| groupNodeCount++ |
| recordOperation { applier, slots, _ -> |
| val node = factory() |
| slots.update(node) |
| applier.insert(insertIndex, node) |
| applier.down(node) |
| } |
| childrenAllowed = true |
| } |
| |
| override fun emitNode(node: N) { |
| assert(inserting) { "emitNode() called when not inserting" } |
| val insertIndex = nodeIndexStack.peek() |
| pending!!.nodeCount++ |
| groupNodeCount++ |
| recordOperation { applier, slots, _ -> |
| slots.update(node) |
| applier.insert(insertIndex, node) |
| applier.down(node) |
| } |
| childrenAllowed = true |
| } |
| |
| override fun useNode(): N { |
| assert(!inserting) { "useNode() called while inserting" } |
| recordDown() |
| val result = slots.next() |
| childrenAllowed = true |
| @Suppress("UNCHECKED_CAST") |
| return result as N |
| } |
| |
| override fun endNode() { |
| end(END_NODE) |
| } |
| |
| override fun <V, T> apply(value: V, block: T.(V) -> Unit) { |
| recordOperation { applier, _, _ -> |
| @Suppress("UNCHECKED_CAST") |
| (applier.current as T).block(value) |
| } |
| } |
| |
| override fun joinKey(left: Any?, right: Any?): Any = |
| getKey(slots.get(slots.current), left, right) |
| ?: JoinedKey(left, right) |
| |
| override fun nextSlot(): Any? = slots.next() |
| |
| override fun skipValue() = recordSlotNext() |
| |
| fun <T> changed(value: T): Boolean { |
| return if (nextSlot() != value || inserting) { |
| updateValue(value) |
| true |
| } else { |
| skipValue() |
| false |
| } |
| } |
| |
| override fun updateValue(value: Any?) { |
| recordOperation { _, slots, lifecycleManager -> |
| val previous = if (value is CompositionLifecycleObserver) { |
| val slotValue = lifecycleManager.entering( |
| CompositionLifecycleObserverHolder( |
| value |
| ) |
| ) |
| slots.update(slotValue) |
| } else slots.update(value) |
| if (previous is CompositionLifecycleObserverHolder) { |
| lifecycleManager.leaving(previous) |
| } |
| } |
| } |
| |
| internal fun <T> startProvider(p: Ambient.Holder<T>, value: T) { |
| startGroup(provider) |
| changed(p) |
| if (inserting) { |
| insertedProviders.push(p) |
| } |
| if (changed(value)) { |
| invalidateConsumers(p.ambient) |
| } |
| startGroup(invocation) |
| } |
| |
| internal fun endProvider() { |
| endGroup() |
| if (inserting) { |
| insertedProviders.pop() |
| } |
| endGroup() |
| } |
| |
| internal fun <T> consume(key: Ambient<T>): T { |
| startGroup(consumer) |
| changed(key) |
| changed(invalidateStack.peek()) |
| val result = parentAmbient(key) |
| endGroup() |
| return result |
| } |
| |
| /** |
| * Create or use a memoized `CompositionReference` instance at this position in the slot table. |
| */ |
| fun buildReference(): CompositionReference { |
| startGroup(reference) |
| |
| // NOTE(lmr): VERY important to call nextValue() here instead of nextSlot() |
| var ref = nextValue() as? CompositionReference |
| if (ref != null && !inserting) { |
| skipValue() |
| } else { |
| ref = AmbientReferenceImpl(invalidateStack.peek()) |
| updateValue(ref) |
| } |
| endGroup() |
| |
| return ref |
| } |
| |
| internal fun <T> parentAmbient(key: Ambient<T>): T { |
| // Enumerate the parents that have been inserted |
| if (insertedProviders.isNotEmpty()) { |
| var current = insertedProviders.size - 1 |
| while (current >= 0) { |
| val element = insertedProviders[current] |
| if (element is Ambient.Holder<*> && element.ambient === key) { |
| @Suppress("UNCHECKED_CAST") |
| return element.value as? T ?: key.defaultValue |
| } |
| current-- |
| } |
| } |
| |
| // Enumerate the parents that were also in the previous composition |
| var current = slots.startStack.size - 1 |
| while (current > 0) { |
| val index = slots.startStack.peek(current) |
| val sentinel = slots.get(index - 1) |
| if (sentinel === provider) { |
| val element = slots.get(index + 1) |
| if (element is Ambient.Holder<*> && element.ambient === key) { |
| @Suppress("UNCHECKED_CAST") |
| return element.value as? T ?: key.defaultValue |
| } |
| } |
| current-- |
| } |
| |
| val ref = ambientReference |
| |
| if (ref != null) { |
| return ref.getAmbient(key) |
| } |
| |
| return key.defaultValue |
| } |
| |
| private fun <T> parentAmbient(key: Ambient<T>, location: Int): T { |
| |
| var index = location |
| |
| while (index >= 0) { |
| // A recomposable (i.e. a component) will always be in the first slot after the group |
| // marker. This is true because that is where the recompose routine will look for it. |
| // If the slot does not hold a recomposable it is not a recomposable group so skip it. |
| if (slots.isGroup(index)) { |
| val sentinel = slots.get(index - 1) |
| when { |
| sentinel === provider -> { |
| val element = slots.get(index + 1) |
| if (element is Ambient.Holder<*> && element.ambient == key) { |
| @Suppress("UNCHECKED_CAST") |
| return element.value as T |
| } |
| } |
| } |
| } |
| index -= 1 |
| } |
| |
| val ref = ambientReference |
| |
| if (ref != null) { |
| return ref.getAmbient(key) |
| } |
| |
| return key.defaultValue |
| } |
| |
| private fun <T> invalidateConsumers(key: Ambient<T>) { |
| // Inserting components don't have children yet. |
| if (!inserting) { |
| // Get the parent size from the slot table |
| val startStack = slots.startStack |
| val containingGroupIndex = if (startStack.isEmpty()) 1 else startStack.peek() |
| val start = containingGroupIndex + 1 |
| val end = start + slots.groupSize(containingGroupIndex) |
| var index = start |
| |
| // Check the slots in range for recomposabile instances |
| loop@ while (index < end) { |
| // A recomposable (i.e. a component) will always be in the first slot after the |
| // group marker. This is true because that is where the recompose routine will look |
| // for it. If the slot does not hold a recomposable it is not a recomposable group |
| // so skip it. |
| if (slots.isGroup(index)) { |
| val sentinel = slots.get(index - 1) |
| when { |
| sentinel === consumer -> { |
| val ambient = slots.get(index + 1) |
| if (ambient == key) { |
| val scope = slots.get(index + 2) |
| if (scope is RecomposeScope) { |
| scope.invalidate?.let { it(false) } |
| } |
| } |
| } |
| sentinel === provider -> { |
| val element = slots.get(index + 1) |
| if (element is Ambient.Holder<*> && element.ambient == key) { |
| index += slots.groupSize(index) |
| continue@loop |
| } |
| } |
| sentinel === reference -> { |
| val element = slots.get(index + 1) |
| if (element is CompositionLifecycleObserverHolder) { |
| val subElement = element.instance as CompositionReference |
| subElement.invalidateConsumers(key) |
| } |
| } |
| } |
| } |
| index += 1 |
| } |
| } |
| } |
| |
| val changeCount get() = changes.size |
| |
| internal val currentInvalidate: RecomposeScope? get() = |
| invalidateStack.let { if (it.isNotEmpty()) it.peek() else null } |
| |
| private fun start(key: Any, action: SlotAction) { |
| assert(childrenAllowed) { "A call to creadNode(), emitNode() or useNode() expected" } |
| if (pending == null) { |
| val slotKey = slots.next() |
| if (slotKey == key) { |
| // The group is the same as what was generated last time. |
| slots.start(action) |
| recordSlotNext() |
| recordStart(action) |
| } else { |
| // The group is different than was generated last time. We need to collect all the |
| // previously generated groups to be able to determine if the we are inserting a new |
| // group or an old group just moved. |
| if (slotKey !== SlotTable.EMPTY) |
| slots.previous() |
| val nodes = slots.parentNodes - slots.nodeIndex |
| pending = Pending( |
| KeyInfo(0, -1, nodes, -1), |
| slots.extractItemKeys(), |
| nodeIndex |
| ) |
| } |
| } |
| |
| val pending = pending |
| var newKeyInfo: KeyInfo? = null |
| 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) |
| 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. |
| slots.current = keyInfo.location |
| slots.next() // Skip key |
| slots.start(action) |
| |
| // 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) |
| if (currentRelativePosition > 0) { |
| // The slot group must be moved, record the move to be performed during apply. |
| recordOperation { _, slots, _ -> |
| slots.moveItem(currentRelativePosition) |
| slots.skip() // Skip the key |
| slots.start(action) |
| } |
| } else { |
| // The slot group is already in the correct location. This can happen, for |
| // example, during an insert. If only one group is inserted, for example, the |
| // slot groups of the sibling after the insert will be in the right locations |
| // and need not be moved. |
| recordSlotNext() // Skip the key |
| recordStart(action) |
| } |
| newKeyInfo = keyInfo |
| } else { |
| // The group is new, go into insert mode. All child groups will be inserted until |
| // this group is complete. |
| if (!slots.inEmpty) { |
| recordOperation { _, slots, _ -> slots.beginInsert() } |
| } |
| slots.beginEmpty() |
| recordOperation { _, slots, _ -> |
| slots.update(key) |
| slots.start(action) |
| } |
| val insertKeyInfo = KeyInfo(key, -1, 0, -1) |
| pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex) |
| pending.recordUsed(insertKeyInfo) |
| newPending = Pending( |
| insertKeyInfo, mutableListOf(), |
| if (action == START_NODE) 0 else nodeIndex |
| ) |
| } |
| } |
| |
| enterGroup(action, newPending, newKeyInfo) |
| } |
| |
| private fun enterGroup(action: SlotAction, newPending: Pending?, newKeyInfo: KeyInfo?) { |
| // 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.keyStack.push(parentKeyInfo) |
| parentKeyInfo = newKeyInfo |
| this.nodeIndexStack.push(nodeIndex) |
| if (action == START_NODE) nodeIndex = 0 |
| this.groupNodeCountStack.push(groupNodeCount) |
| groupNodeCount = 0 |
| } |
| |
| private fun end(action: SlotAction) { |
| // 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. |
| |
| 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 movedKeys = 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, 0) |
| recordOperation { _, slots, lifecycleManager -> |
| removeCurrentItem(slots, lifecycleManager) |
| } |
| |
| // 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 item (the first slot being the key). |
| invalidations.removeRange(previousInfo.location + 1, |
| previousInfo.location + slots.groupSize(previousInfo.location + 1)) |
| previousIndex++ |
| continue |
| } |
| |
| if (previousInfo in movedKeys) { |
| // If the group was already moved to 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) |
| if (nodePosition != nodeOffset) { |
| val updatedCount = pending.updatedNodeCountOf(currentInfo) |
| recordMoveNode(nodePosition + pending.startIndex, |
| nodeOffset + pending.startIndex, updatedCount) |
| pending.registerMoveNode(nodePosition, nodeOffset, updatedCount) |
| movedKeys.add(currentInfo) |
| } // 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 |
| // during 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) { |
| slots.reportUncertainNodeCount() |
| slots.current = previous[previous.size - 1].location |
| slots.skipItem() |
| } |
| } |
| |
| // 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 (!slots.isGroupEnd) { |
| val startSlot = slots.current |
| slots.next() // Skip key |
| val nodesToRemove = slots.skipGroup() |
| recordRemoveNode(removeIndex, nodesToRemove) |
| recordOperation { _, slots, lifeCycleManager -> |
| removeCurrentItem(slots, lifeCycleManager) |
| } |
| invalidations.removeRange(startSlot, slots.current) |
| slots.reportUncertainNodeCount() |
| } |
| |
| if (action == END_GROUP) slots.endGroup() else { |
| expectedNodeCount = 1 |
| slots.endNode() |
| } |
| realizeMovement() |
| if (action == END_NODE) recordUp() |
| recordEnd(action) |
| |
| if (slots.inEmpty) { |
| slots.endEmpty() |
| if (!slots.inEmpty) recordOperation { _, slots, _ -> slots.endInsert() } |
| } |
| |
| // 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() |
| previousPending?.let<Pending, Unit> { previous -> |
| // Update the parent count of nodes |
| previous.updateNodeCount(pending?.parentKeyInfo, expectedNodeCount) |
| previous.groupIndex++ |
| } |
| this.pending = previousPending |
| this.parentKeyInfo = keyStack.pop() |
| this.nodeIndex = nodeIndexStack.pop() + expectedNodeCount |
| this.groupNodeCount = this.groupNodeCountStack.pop() + expectedNodeCount |
| } |
| |
| /** |
| * Skip to a sibling group that contains location given. This also ensures the nodeIndex is |
| * correctly updated to reflect any groups skipped. |
| */ |
| private fun skipToGroupContaining(location: Int) { |
| while (slots.current < location) { |
| if (slots.isGroupEnd) return |
| if (slots.isGroup) { |
| if (location < slots.groupSize + slots.current) return |
| recordSkip(if (slots.isNode) START_NODE else END_NODE) |
| nodeIndex += slots.skipGroup() |
| } else { |
| recordSlotNext() |
| slots.next() |
| } |
| } |
| } |
| |
| /** |
| * Enter a group that contains the location. This updates the composer state as if the group was |
| * generated with no changes. |
| */ |
| private fun recordEnters(location: Int) { |
| while (true) { |
| skipToGroupContaining(location) |
| assert(slots.isGroup && location >= slots.current && |
| location < slots.current + slots.groupSize) { |
| "Could not find group at $location" |
| } |
| if (slots.current == location) { |
| return |
| } else { |
| enterGroup(if (slots.isNode) START_NODE |
| else START_GROUP, null, null) |
| if (slots.isNode) { |
| recordStart(START_NODE) |
| recordDown() |
| entersStack.push(END_NODE) |
| slots.startNode() |
| slots.next() // skip navigation slot |
| nodeIndex = 0 |
| } else { |
| recordStart(START_GROUP) |
| entersStack.push(END_GROUP) |
| slots.startGroup() |
| } |
| } |
| } |
| } |
| |
| /** |
| * Exit any groups that were entered until a sibling of maxLocation is reached. |
| */ |
| private fun recordExits(maxLocation: Int, minStack: Int) { |
| while (entersStack.size > minStack) { |
| skipToGroupContaining(maxLocation) |
| if (slots.isGroupEnd) |
| end(entersStack.pop()) |
| else return |
| } |
| } |
| |
| private fun recomposeComponentRange(start: Int, end: Int) { |
| var recomposed = false |
| |
| var firstInRange = invalidations.firstInRange(start, end) |
| val enterStackSize = entersStack.size |
| while (firstInRange != null) { |
| val location = firstInRange.location |
| |
| invalidations.removeLocation(location) |
| |
| recordExits(location, enterStackSize) |
| recordEnters(location) |
| |
| composeScope(firstInRange.scope) |
| |
| recomposed = true |
| |
| // 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(slots.current, end) |
| } |
| |
| if (recomposed) { |
| recordExits(end, enterStackSize) |
| } else { |
| // No recompositions were requested in the range, skip it. |
| recordSkip(START_GROUP) |
| slots.skipGroup() |
| } |
| } |
| |
| private fun invalidate(scope: RecomposeScope, sync: Boolean) { |
| val location = scope.anchor?.location(slotTable) ?: return |
| assert(location >= 0) { "Invalid anchor" } |
| invalidations.insertIfMissing(location, scope) |
| if (sync) { |
| recomposer?.recomposeSync() |
| } else { |
| recomposer?.scheduleRecompose() |
| } |
| } |
| |
| internal fun skipGroupAndRecomposeRange() { |
| if (invalidations.isEmpty()) { |
| skipGroup() |
| } else { |
| recomposeComponentRange(slots.current, slots.current + slots.groupSize) |
| } |
| } |
| |
| private fun composeScope(scope: RecomposeScope) { |
| val invalidate = startJoin(false, scope.compose) |
| scope.compose(invalidate) |
| doneJoin(false) |
| } |
| |
| fun startJoin( |
| valid: Boolean, |
| compose: (invalidate: (sync: Boolean) -> Unit) -> Unit |
| ): (sync: Boolean) -> Unit { |
| return if (!valid) { |
| val invalidate = if (inserting) { |
| val scope = RecomposeScope(compose) |
| invalidateStack.push(scope) |
| scope.invalidate = { invalidate(scope, it) } |
| recordStart(START_GROUP) |
| recordOperation { _, slots, _ -> |
| scope.anchor = slots.anchor(slots.current - 1) |
| } |
| updateValue(scope) |
| slots.beginEmpty() |
| scope.invalidate |
| } else { |
| invalidations.removeLocation(slots.current) |
| slots.startGroup() |
| @Suppress("UNCHECKED_CAST") |
| val scope = slots.next() as RecomposeScope |
| invalidateStack.push(scope) |
| recordStart(START_GROUP) |
| skipValue() |
| scope.invalidate |
| } |
| enterGroup(START_GROUP, null, null) |
| invalidate ?: IGNORE_RECOMPOSE |
| } else IGNORE_RECOMPOSE |
| } |
| |
| fun doneJoin(valid: Boolean) { |
| if (!valid) { |
| end(END_GROUP) |
| invalidateStack.pop() |
| } else { |
| skipGroupAndRecomposeRange() |
| } |
| } |
| |
| fun recompose() { |
| if (invalidations.isNotEmpty()) { |
| slotTable.read { |
| slots = it |
| nodeIndex = 0 |
| |
| recomposeComponentRange(0, Int.MAX_VALUE) |
| |
| finalizeCompose() |
| } |
| } |
| } |
| |
| private fun record(change: Change<N>) { |
| changes.add(change) |
| } |
| |
| private fun recordOperation(change: Change<N>) { |
| realizeSlots() |
| record(change) |
| } |
| |
| private var slotsStartStack = IntStack() |
| private var slotActions = SlotActions() |
| |
| // Slot movement |
| private fun realizeSlots() { |
| val actionsSize = slotActions.size |
| if (actionsSize > 0) { |
| if (actionsSize == 1) { |
| val action = slotActions.first() |
| when (action) { |
| START_GROUP -> record { _, slots, _ -> slots.startGroup() } |
| END_GROUP -> record { _, slots, _ -> slots.endGroup() } |
| SKIP_GROUP -> record { _, slots, _ -> slots.skipGroup() } |
| START_NODE -> record { _, slots, _ -> slots.startNode() } |
| END_NODE -> record { _, slots, _ -> slots.endNode() } |
| SKIP_NODE -> record { _, slots, _ -> slots.skipNode() } |
| DOWN -> record { applier, slots, _ -> |
| @Suppress("UNCHECKED_CAST") |
| applier.down(slots.skip() as N) |
| } |
| UP -> record { applier, _, _ -> applier.up() } |
| else -> record { _, slots, _ -> slots.current += action - SKIP_SLOTS } |
| } |
| slotActions.clear() |
| slotsStartStack.clear() |
| } else { |
| val actions = slotActions.clone() |
| slotActions.clear() |
| slotsStartStack.clear() |
| record { applier, slots, _ -> |
| actions.forEach { action -> |
| when (action) { |
| START_GROUP -> slots.startGroup() |
| END_GROUP -> slots.endGroup() |
| SKIP_GROUP -> slots.skipGroup() |
| START_NODE -> slots.startNode() |
| END_NODE -> slots.endNode() |
| SKIP_NODE -> slots.skipNode() |
| DOWN -> { |
| @Suppress("UNCHECKED_CAST") |
| applier.down(slots.skip() as N) |
| } |
| UP -> applier.up() |
| else -> slots.current += action - SKIP_SLOTS |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private fun finalRealizeSlots() { |
| when (slotActions.size) { |
| 0 -> Unit |
| 1 -> if (slotActions.first() == SKIP_GROUP) slotActions.clear() else realizeSlots() |
| 2 -> if (slotActions.first() >= SKIP_NODE && |
| slotActions.last() == SKIP_GROUP |
| ) slotActions.clear() |
| else realizeSlots() |
| else -> realizeSlots() |
| } |
| } |
| |
| internal fun finalizeCompose() { |
| finalRealizeSlots() |
| assert(pendingStack.isEmpty()) { "Start end imbalance" } |
| pending = null |
| nodeIndex = 0 |
| groupNodeCount = 0 |
| } |
| |
| private fun recordSlotNext(count: Int = 1) { |
| assert(count >= 1) { "Invalid call to recordSlotNext()" } |
| val actionsSize = slotActions.size |
| if (actionsSize > 0) { |
| // If the last action was also a skip just add this one to the last one |
| val last = slotActions.last() |
| if (last >= SKIP_SLOTS) { |
| slotActions.remove(1) |
| slotActions.add(last + count) |
| return |
| } |
| } |
| slotActions.add(SKIP_SLOTS + count) |
| } |
| |
| private fun recordStart(action: SlotAction) { |
| slotsStartStack.push(slotActions.size) |
| slotActions.add(action) |
| } |
| |
| private fun recordEnd(action: SlotAction) { |
| if (slotsStartStack.isEmpty()) { |
| slotActions.add(action) |
| } else { |
| // skip the entire group |
| val startLocation = slotsStartStack.pop() |
| slotActions.remove(slotActions.size - startLocation) |
| recordSkip(action) |
| } |
| } |
| |
| private fun recordSkip(action: SlotAction) { |
| when (action) { |
| START_GROUP, END_GROUP -> slotActions.add(SKIP_GROUP) |
| START_NODE, END_NODE -> slotActions.add(SKIP_NODE) |
| else -> error("Invalid skip action") |
| } |
| } |
| |
| private fun recordDown() { |
| slotActions.add(DOWN) |
| } |
| |
| private fun recordUp() { |
| slotActions.add(UP) |
| } |
| |
| 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) { |
| assert(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 |
| recordOperation { applier, _, _ -> applier.remove(removeIndex, count) } |
| } else { |
| val from = previousMoveFrom |
| previousMoveFrom = -1 |
| val to = previousMoveTo |
| previousMoveTo = -1 |
| recordOperation { applier, _, _ -> applier.move(from, to, count) } |
| } |
| } |
| } |
| |
| private inner class AmbientReferenceImpl(val scope: RecomposeScope) : CompositionReference, |
| CompositionLifecycleObserver { |
| |
| val composers = mutableSetOf<Composer<*>>() |
| |
| override fun onEnter() { |
| // do nothing |
| } |
| |
| override fun onLeave() { |
| composers.clear() |
| } |
| |
| override fun <T> invalidateConsumers(key: Ambient<T>) { |
| // need to mark the recompose scope that created the reference as invalid |
| invalidate() |
| |
| // loop through every child composer |
| for (composer in composers) { |
| composer.slotTable.read { |
| composer.slots = it |
| composer.nodeIndex = 0 |
| composer.invalidateConsumers(key) |
| } |
| } |
| } |
| |
| override fun <N> registerComposer(composer: Composer<N>) { |
| composers.add(composer) |
| } |
| |
| override fun invalidate() { |
| // continue invalidating up the spine of AmbientReferences |
| ambientReference?.invalidate() |
| |
| scope.invalidate?.invoke(false) |
| } |
| |
| override fun <T> getAmbient(key: Ambient<T>): T { |
| val anchor = scope.anchor |
| return if (anchor != null && anchor.valid) { |
| parentAmbient(key, anchor.location(slotTable)) |
| } else { |
| parentAmbient(key) |
| } |
| } |
| } |
| } |
| |
| private fun removeCurrentItem(slots: SlotWriter, lifecycleManager: LifeCycleManager) { |
| // 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. |
| |
| var groupEnd = Int.MAX_VALUE |
| var index = 0 |
| val groupEndStack = IntStack() |
| |
| for (slot in slots.itemSlots()) { |
| when (slot) { |
| is CompositionLifecycleObserverHolder -> { |
| lifecycleManager.leaving(slot) |
| } |
| is GroupStart -> { |
| groupEndStack.push(groupEnd) |
| groupEnd = index + slot.slots |
| } |
| } |
| |
| index++ |
| |
| while (index >= groupEnd) { |
| groupEnd = groupEndStack.pop() |
| } |
| } |
| |
| if (groupEndStack.isNotEmpty()) error("Invalid slot structure") |
| |
| // Remove the item from the slot table and notify the FrameManager if any |
| // anchors are orphaned by removing the slots. |
| if (slots.removeItem()) { |
| FrameManager.scheduleCleanup() |
| } |
| } |
| |
| internal typealias SlotAction = Int |
| |
| internal const val START_GROUP: SlotAction = 1 |
| internal const val END_GROUP: SlotAction = START_GROUP + 1 |
| internal const val SKIP_GROUP: SlotAction = END_GROUP + 1 |
| internal const val START_NODE: SlotAction = SKIP_GROUP + 1 |
| internal const val END_NODE: SlotAction = START_NODE + 1 |
| internal const val SKIP_NODE: SlotAction = END_NODE + 1 |
| internal const val DOWN: SlotAction = SKIP_NODE + 1 |
| internal const val UP: SlotAction = DOWN + 1 |
| internal const val SKIP_SLOTS: SlotAction = UP + 1 |
| |
| const val DEFAULT_SLOT_ACTIONS_SIZE = 16 |
| |
| private class SlotActions(var actions: IntArray = IntArray(DEFAULT_SLOT_ACTIONS_SIZE)) { |
| var size: Int = 0 |
| |
| fun add(action: SlotAction) { |
| if (size >= actions.size) { |
| actions = actions.copyOf(Math.max(size, actions.size * 2)) |
| } |
| actions[size++] = action |
| } |
| |
| fun remove(count: Int) { |
| assert(count <= size) { "Removing too many actions" } |
| size -= count |
| } |
| |
| fun clear() { |
| size = 0 |
| } |
| |
| fun clone(): SlotActions = SlotActions(actions.copyOf(this.size)).also { it.size = size } |
| |
| override fun toString(): String = actions.take(size).joinToString { |
| when (it) { |
| START_GROUP -> "START_GROUP" |
| END_GROUP -> "END_GROUP" |
| SKIP_GROUP -> "SKIP_GROUP" |
| START_NODE -> "START_NODE" |
| END_NODE -> "END_NODE" |
| SKIP_NODE -> "SKIP_NODE" |
| DOWN -> "DOWN" |
| UP -> "UP" |
| else -> "SKIP_SLOTS(${it - SKIP_SLOTS})" |
| } |
| } |
| |
| fun first(): SlotAction = actions[0] |
| fun last(): SlotAction = actions[size - 1] |
| } |
| |
| // SlotActions helper |
| private inline fun SlotActions.forEach(block: (SlotAction) -> Unit) { |
| for (index in 0 until size) block(actions[index]) |
| } |
| |
| // 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 SlotReader.start(action: SlotAction) = |
| if (action == START_NODE) startNode() |
| else startGroup() |
| |
| private fun SlotWriter.start(action: SlotAction) = |
| if (action == START_NODE) startNode() |
| else startGroup() |
| |
| private fun getKey(value: Any?, left: Any?, right: Any?): Any? = (value as? JoinedKey)?.let { |
| if (it.left == left && it.right == right) value |
| else getKey(it.left, left, right) ?: getKey( |
| it.right, |
| left, |
| right |
| ) |
| } |
| |
| // Invalidation helpers |
| private fun MutableList<Invalidation>.findLocation(location: Int): Int = |
| binarySearch { it.location.compareTo(location) } |
| |
| private fun MutableList<Invalidation>.insertIfMissing(location: Int, scope: RecomposeScope) { |
| 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) { |
| val index = findLocation(location) |
| if (index >= 0) removeAt(index) |
| } |
| |
| 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 |
| } |
| } |