Merge "Refactor composition restart to navigate directly to the invalid group" into androidx-master-dev
diff --git a/compose/compose-runtime/api/0.1.0-dev11.txt b/compose/compose-runtime/api/0.1.0-dev11.txt
index ed6342d..0b35e6a 100644
--- a/compose/compose-runtime/api/0.1.0-dev11.txt
+++ b/compose/compose-runtime/api/0.1.0-dev11.txt
@@ -45,6 +45,9 @@
method public void start(N?, N? instance);
}
+ public final class BitwiseOperatorsKt {
+ }
+
public final class BuildableMap<K, V> implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<K,V> {
ctor public BuildableMap(kotlinx.collections.immutable.PersistentMap<K,? extends V> map);
method public kotlinx.collections.immutable.PersistentMap<K,V> component1();
@@ -207,7 +210,6 @@
}
public final class KeyInfo {
- ctor public KeyInfo(Object key, int location, int nodes, int index);
method public int getIndex();
method public Object getKey();
method public int getLocation();
@@ -324,8 +326,10 @@
method public Object? get(int index);
method public int getCurrent();
method public int getCurrentEnd();
+ method public Object! getGroupData();
method public int getGroupEnd();
method public Object! getGroupKey();
+ method public Object! getGroupNode();
method public int getGroupSize();
method public boolean getInEmpty();
method public int getNodeIndex();
@@ -350,8 +354,10 @@
method public void startNode(Object key);
property public final int current;
property public final int currentEnd;
+ property public final Object! groupData;
property public final int groupEnd;
property public final Object! groupKey;
+ property public final Object! groupNode;
property public final int groupSize;
property public final boolean inEmpty;
property public final boolean isGroup;
@@ -389,6 +395,7 @@
method public androidx.compose.Anchor anchor(int index = current);
method public void beginInsert();
method public void close();
+ method public int endData();
method public int endGroup();
method public void endInsert();
method public int endNode();
@@ -419,9 +426,12 @@
method public int skipGroup();
method public int skipNode();
method public void skipToGroupEnd();
+ method public void startData(Object key, Object? data);
method public void startGroup(Object key);
method public void startNode(Object key);
+ method public void startNode(Object key, Object? node);
method public Object? update(Object? value);
+ method public void updateData(Object? value);
property public final boolean closed;
property public final int current;
property public final int groupSize;
diff --git a/compose/compose-runtime/api/api_lint.ignore b/compose/compose-runtime/api/api_lint.ignore
index b197564..8f0c08c 100644
--- a/compose/compose-runtime/api/api_lint.ignore
+++ b/compose/compose-runtime/api/api_lint.ignore
@@ -59,5 +59,9 @@
Missing nullability on parameter `value` in method `containsValue`
MissingNullability: androidx.compose.BuildableMap#get(Object) parameter #0:
Missing nullability on parameter `key` in method `get`
+MissingNullability: androidx.compose.SlotReader#getGroupData():
+ Missing nullability on method `getGroupData` return
MissingNullability: androidx.compose.SlotReader#getGroupKey():
Missing nullability on method `getGroupKey` return
+MissingNullability: androidx.compose.SlotReader#getGroupNode():
+ Missing nullability on method `getGroupNode` return
diff --git a/compose/compose-runtime/api/current.txt b/compose/compose-runtime/api/current.txt
index ed6342d..0b35e6a 100644
--- a/compose/compose-runtime/api/current.txt
+++ b/compose/compose-runtime/api/current.txt
@@ -45,6 +45,9 @@
method public void start(N?, N? instance);
}
+ public final class BitwiseOperatorsKt {
+ }
+
public final class BuildableMap<K, V> implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<K,V> {
ctor public BuildableMap(kotlinx.collections.immutable.PersistentMap<K,? extends V> map);
method public kotlinx.collections.immutable.PersistentMap<K,V> component1();
@@ -207,7 +210,6 @@
}
public final class KeyInfo {
- ctor public KeyInfo(Object key, int location, int nodes, int index);
method public int getIndex();
method public Object getKey();
method public int getLocation();
@@ -324,8 +326,10 @@
method public Object? get(int index);
method public int getCurrent();
method public int getCurrentEnd();
+ method public Object! getGroupData();
method public int getGroupEnd();
method public Object! getGroupKey();
+ method public Object! getGroupNode();
method public int getGroupSize();
method public boolean getInEmpty();
method public int getNodeIndex();
@@ -350,8 +354,10 @@
method public void startNode(Object key);
property public final int current;
property public final int currentEnd;
+ property public final Object! groupData;
property public final int groupEnd;
property public final Object! groupKey;
+ property public final Object! groupNode;
property public final int groupSize;
property public final boolean inEmpty;
property public final boolean isGroup;
@@ -389,6 +395,7 @@
method public androidx.compose.Anchor anchor(int index = current);
method public void beginInsert();
method public void close();
+ method public int endData();
method public int endGroup();
method public void endInsert();
method public int endNode();
@@ -419,9 +426,12 @@
method public int skipGroup();
method public int skipNode();
method public void skipToGroupEnd();
+ method public void startData(Object key, Object? data);
method public void startGroup(Object key);
method public void startNode(Object key);
+ method public void startNode(Object key, Object? node);
method public Object? update(Object? value);
+ method public void updateData(Object? value);
property public final boolean closed;
property public final int current;
property public final int groupSize;
diff --git a/compose/compose-runtime/api/public_plus_experimental_0.1.0-dev11.txt b/compose/compose-runtime/api/public_plus_experimental_0.1.0-dev11.txt
index ed6342d..0b35e6a 100644
--- a/compose/compose-runtime/api/public_plus_experimental_0.1.0-dev11.txt
+++ b/compose/compose-runtime/api/public_plus_experimental_0.1.0-dev11.txt
@@ -45,6 +45,9 @@
method public void start(N?, N? instance);
}
+ public final class BitwiseOperatorsKt {
+ }
+
public final class BuildableMap<K, V> implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<K,V> {
ctor public BuildableMap(kotlinx.collections.immutable.PersistentMap<K,? extends V> map);
method public kotlinx.collections.immutable.PersistentMap<K,V> component1();
@@ -207,7 +210,6 @@
}
public final class KeyInfo {
- ctor public KeyInfo(Object key, int location, int nodes, int index);
method public int getIndex();
method public Object getKey();
method public int getLocation();
@@ -324,8 +326,10 @@
method public Object? get(int index);
method public int getCurrent();
method public int getCurrentEnd();
+ method public Object! getGroupData();
method public int getGroupEnd();
method public Object! getGroupKey();
+ method public Object! getGroupNode();
method public int getGroupSize();
method public boolean getInEmpty();
method public int getNodeIndex();
@@ -350,8 +354,10 @@
method public void startNode(Object key);
property public final int current;
property public final int currentEnd;
+ property public final Object! groupData;
property public final int groupEnd;
property public final Object! groupKey;
+ property public final Object! groupNode;
property public final int groupSize;
property public final boolean inEmpty;
property public final boolean isGroup;
@@ -389,6 +395,7 @@
method public androidx.compose.Anchor anchor(int index = current);
method public void beginInsert();
method public void close();
+ method public int endData();
method public int endGroup();
method public void endInsert();
method public int endNode();
@@ -419,9 +426,12 @@
method public int skipGroup();
method public int skipNode();
method public void skipToGroupEnd();
+ method public void startData(Object key, Object? data);
method public void startGroup(Object key);
method public void startNode(Object key);
+ method public void startNode(Object key, Object? node);
method public Object? update(Object? value);
+ method public void updateData(Object? value);
property public final boolean closed;
property public final int current;
property public final int groupSize;
diff --git a/compose/compose-runtime/api/public_plus_experimental_current.txt b/compose/compose-runtime/api/public_plus_experimental_current.txt
index ed6342d..0b35e6a 100644
--- a/compose/compose-runtime/api/public_plus_experimental_current.txt
+++ b/compose/compose-runtime/api/public_plus_experimental_current.txt
@@ -45,6 +45,9 @@
method public void start(N?, N? instance);
}
+ public final class BitwiseOperatorsKt {
+ }
+
public final class BuildableMap<K, V> implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<K,V> {
ctor public BuildableMap(kotlinx.collections.immutable.PersistentMap<K,? extends V> map);
method public kotlinx.collections.immutable.PersistentMap<K,V> component1();
@@ -207,7 +210,6 @@
}
public final class KeyInfo {
- ctor public KeyInfo(Object key, int location, int nodes, int index);
method public int getIndex();
method public Object getKey();
method public int getLocation();
@@ -324,8 +326,10 @@
method public Object? get(int index);
method public int getCurrent();
method public int getCurrentEnd();
+ method public Object! getGroupData();
method public int getGroupEnd();
method public Object! getGroupKey();
+ method public Object! getGroupNode();
method public int getGroupSize();
method public boolean getInEmpty();
method public int getNodeIndex();
@@ -350,8 +354,10 @@
method public void startNode(Object key);
property public final int current;
property public final int currentEnd;
+ property public final Object! groupData;
property public final int groupEnd;
property public final Object! groupKey;
+ property public final Object! groupNode;
property public final int groupSize;
property public final boolean inEmpty;
property public final boolean isGroup;
@@ -389,6 +395,7 @@
method public androidx.compose.Anchor anchor(int index = current);
method public void beginInsert();
method public void close();
+ method public int endData();
method public int endGroup();
method public void endInsert();
method public int endNode();
@@ -419,9 +426,12 @@
method public int skipGroup();
method public int skipNode();
method public void skipToGroupEnd();
+ method public void startData(Object key, Object? data);
method public void startGroup(Object key);
method public void startNode(Object key);
+ method public void startNode(Object key, Object? node);
method public Object? update(Object? value);
+ method public void updateData(Object? value);
property public final boolean closed;
property public final int current;
property public final int groupSize;
diff --git a/compose/compose-runtime/api/restricted_0.1.0-dev11.txt b/compose/compose-runtime/api/restricted_0.1.0-dev11.txt
index ed6342d..0b35e6a 100644
--- a/compose/compose-runtime/api/restricted_0.1.0-dev11.txt
+++ b/compose/compose-runtime/api/restricted_0.1.0-dev11.txt
@@ -45,6 +45,9 @@
method public void start(N?, N? instance);
}
+ public final class BitwiseOperatorsKt {
+ }
+
public final class BuildableMap<K, V> implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<K,V> {
ctor public BuildableMap(kotlinx.collections.immutable.PersistentMap<K,? extends V> map);
method public kotlinx.collections.immutable.PersistentMap<K,V> component1();
@@ -207,7 +210,6 @@
}
public final class KeyInfo {
- ctor public KeyInfo(Object key, int location, int nodes, int index);
method public int getIndex();
method public Object getKey();
method public int getLocation();
@@ -324,8 +326,10 @@
method public Object? get(int index);
method public int getCurrent();
method public int getCurrentEnd();
+ method public Object! getGroupData();
method public int getGroupEnd();
method public Object! getGroupKey();
+ method public Object! getGroupNode();
method public int getGroupSize();
method public boolean getInEmpty();
method public int getNodeIndex();
@@ -350,8 +354,10 @@
method public void startNode(Object key);
property public final int current;
property public final int currentEnd;
+ property public final Object! groupData;
property public final int groupEnd;
property public final Object! groupKey;
+ property public final Object! groupNode;
property public final int groupSize;
property public final boolean inEmpty;
property public final boolean isGroup;
@@ -389,6 +395,7 @@
method public androidx.compose.Anchor anchor(int index = current);
method public void beginInsert();
method public void close();
+ method public int endData();
method public int endGroup();
method public void endInsert();
method public int endNode();
@@ -419,9 +426,12 @@
method public int skipGroup();
method public int skipNode();
method public void skipToGroupEnd();
+ method public void startData(Object key, Object? data);
method public void startGroup(Object key);
method public void startNode(Object key);
+ method public void startNode(Object key, Object? node);
method public Object? update(Object? value);
+ method public void updateData(Object? value);
property public final boolean closed;
property public final int current;
property public final int groupSize;
diff --git a/compose/compose-runtime/api/restricted_current.txt b/compose/compose-runtime/api/restricted_current.txt
index ed6342d..0b35e6a 100644
--- a/compose/compose-runtime/api/restricted_current.txt
+++ b/compose/compose-runtime/api/restricted_current.txt
@@ -45,6 +45,9 @@
method public void start(N?, N? instance);
}
+ public final class BitwiseOperatorsKt {
+ }
+
public final class BuildableMap<K, V> implements kotlin.jvm.internal.markers.KMappedMarker java.util.Map<K,V> {
ctor public BuildableMap(kotlinx.collections.immutable.PersistentMap<K,? extends V> map);
method public kotlinx.collections.immutable.PersistentMap<K,V> component1();
@@ -207,7 +210,6 @@
}
public final class KeyInfo {
- ctor public KeyInfo(Object key, int location, int nodes, int index);
method public int getIndex();
method public Object getKey();
method public int getLocation();
@@ -324,8 +326,10 @@
method public Object? get(int index);
method public int getCurrent();
method public int getCurrentEnd();
+ method public Object! getGroupData();
method public int getGroupEnd();
method public Object! getGroupKey();
+ method public Object! getGroupNode();
method public int getGroupSize();
method public boolean getInEmpty();
method public int getNodeIndex();
@@ -350,8 +354,10 @@
method public void startNode(Object key);
property public final int current;
property public final int currentEnd;
+ property public final Object! groupData;
property public final int groupEnd;
property public final Object! groupKey;
+ property public final Object! groupNode;
property public final int groupSize;
property public final boolean inEmpty;
property public final boolean isGroup;
@@ -389,6 +395,7 @@
method public androidx.compose.Anchor anchor(int index = current);
method public void beginInsert();
method public void close();
+ method public int endData();
method public int endGroup();
method public void endInsert();
method public int endNode();
@@ -419,9 +426,12 @@
method public int skipGroup();
method public int skipNode();
method public void skipToGroupEnd();
+ method public void startData(Object key, Object? data);
method public void startGroup(Object key);
method public void startNode(Object key);
+ method public void startNode(Object key, Object? node);
method public Object? update(Object? value);
+ method public void updateData(Object? value);
property public final boolean closed;
property public final int current;
property public final int groupSize;
diff --git a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/BitwiseOperators.kt b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/BitwiseOperators.kt
new file mode 100644
index 0000000..ec31452
--- /dev/null
+++ b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/BitwiseOperators.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 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:Suppress("NOTHING_TO_INLINE")
+
+package androidx.compose
+
+@OptIn(ExperimentalStdlibApi::class)
+internal inline infix fun Int.ror(other: Int) = this.rotateRight(other)
+
+@OptIn(ExperimentalStdlibApi::class)
+internal inline infix fun Int.rol(other: Int) = this.rotateLeft(other)
\ No newline at end of file
diff --git a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Composer.kt b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Composer.kt
index a20d3c3..50d9c20 100644
--- a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Composer.kt
+++ b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Composer.kt
@@ -44,16 +44,20 @@
) -> Unit
private class GroupInfo(
- /** The current location of the slot relative to the start location of the pending slot changes
+ /**
+ * 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
+ /**
+ * 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 */
+ /**
+ * The current number of nodes the group contains after changes have been applied
+ */
var nodeCount: Int
)
@@ -68,7 +72,6 @@
* structure of the tree is detected.
*/
private class Pending(
- val parentKeyInfo: KeyInfo,
val keyInfos: MutableList<KeyInfo>,
val startIndex: Int
) {
@@ -78,16 +81,14 @@
require(startIndex >= 0) { "Invalid start index" }
}
- var nodeCount = parentKeyInfo.nodes
-
private val usedKeys = mutableListOf<KeyInfo>()
private val groupInfos = run {
var runningNodeIndex = 0
- val result = hashMapOf<Any?, GroupInfo>()
+ val result = hashMapOf<Group, GroupInfo>()
for (index in 0 until keyInfos.size) {
- val key = keyInfos[index]
- result[key] = GroupInfo(index, runningNodeIndex, key.nodes)
- runningNodeIndex += key.nodes
+ val keyInfo = keyInfos[index]
+ result[keyInfo.group] = GroupInfo(index, runningNodeIndex, keyInfo.nodes)
+ runningNodeIndex += keyInfo.nodes
}
result
}
@@ -153,27 +154,29 @@
}
fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) {
- groupInfos[keyInfo] = GroupInfo(-1, insertIndex, 0)
+ groupInfos[keyInfo.group] = 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
+ fun updateNodeCount(group: Group, 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) {
- nodeCount += difference
- groupInfos.values.forEach { group ->
- if (group.nodeIndex >= index && group != it)
- group.nodeIndex += difference
+ groupInfos.values.forEach { childGroupInfo ->
+ if (childGroupInfo.nodeIndex >= index && childGroupInfo != groupInfo)
+ childGroupInfo.nodeIndex += difference
}
}
+ return true
}
+ return false
}
- 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
+ fun slotPositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.group]?.slotIndex ?: -1
+ fun nodePositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.group]?.nodeIndex ?: -1
+ fun updatedNodeCountOf(keyInfo: KeyInfo) = groupInfos[keyInfo.group]?.nodeCount ?: keyInfo.nodes
}
private val RootKey = OpaqueKey("root")
@@ -343,15 +346,14 @@
private var nodeIndexStack = IntStack()
private var groupNodeCount: Int = 0
private var groupNodeCountStack = IntStack()
+ private val nodeCountOverrides = HashMap<Group, Int>()
private var collectKeySources = false
- private val keyHashesStack = IntStack()
- private var childrenAllowed = true
+ private var nodeExpected = false
private var invalidations: MutableList<Invalidation> = mutableListOf()
private val entersStack = IntStack()
- private val providersStack = Stack<Pair<Int, AmbientMap>>().apply {
- push(-1 to buildableMapOf())
- }
+ private var parentProvider: AmbientMap = buildableMapOf()
+ private val providerUpdates = HashMap<Group, AmbientMap>()
private var providersInvalid = false
private val providersInvalidStack = IntStack()
@@ -382,12 +384,15 @@
private val insertTable = SlotTable()
private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
+ private var hasProvider = false
private var insertAnchor: Anchor = insertTable.anchor(0)
private val insertFixups = mutableListOf<Change<N>>()
protected fun composeRoot(block: () -> Unit) {
startRoot()
+ startGroup(invocation)
block()
+ endGroup()
endRoot()
}
@@ -411,7 +416,7 @@
* @see [startMovableGroup]
* @see [startRestartGroup]
*/
- fun startReplaceableGroup(key: Int) = start(key, false)
+ fun startReplaceableGroup(key: Int) = start(key, false, null)
/**
* Indicates the end of a "Replaceable Group" at the current execution position. A
@@ -452,7 +457,7 @@
* @see [startReplaceableGroup]
* @see [startRestartGroup]
*/
- fun startMovableGroup(key: Any) = start(key, false)
+ fun startMovableGroup(key: Any) = start(key, false, null)
/**
* Indicates the end of a "Movable Group" at the current execution position. A Movable Group is
@@ -493,10 +498,9 @@
reader = slotTable.openReader()
startGroup(RootKey)
parentReference?.let { parentRef ->
- val parentScope = parentRef.getAmbientScope()
+ parentProvider = parentRef.getAmbientScope()
providersInvalidStack.push(providersInvalid.asInt())
- providersInvalid = changed(parentScope)
- providersStack.push(0 to parentScope)
+ providersInvalid = changed(parentProvider)
}
}
@@ -507,9 +511,6 @@
fun endRoot() {
endGroup()
recordEndRoot()
- if (parentReference != null) {
- providersStack.pop()
- }
finalizeCompose()
reader.close()
}
@@ -520,7 +521,6 @@
fun abortRoot() {
cleanUpCompose()
pendingStack.clear()
- keyHashesStack.clear()
nodeIndexStack.clear()
groupNodeCountStack.clear()
entersStack.clear()
@@ -528,6 +528,7 @@
invalidateStack.clear()
reader.close()
currentCompoundKeyHash = 0
+ nodeExpected = false
}
/**
@@ -595,6 +596,8 @@
changes.clear()
}
+ providerUpdates.clear()
+
@Suppress("ReplaceManualRangeWithIndicesCalls") // Avoids allocation of an iterator
for (index in 0 until invalidationAnchors.size) {
val (anchor, invalidation) = invalidationAnchors[index]
@@ -637,7 +640,7 @@
*
* @param key The key for the group
*/
- fun startGroup(key: Any) = start(key, false)
+ fun startGroup(key: Any) = start(key, false, null)
/**
* End the current group.
@@ -662,29 +665,27 @@
* @param key the key for the node.
*/
fun startNode(key: Any) {
- start(key, true)
- childrenAllowed = false
+ start(key, true, null)
+ nodeExpected = true
}
// Deprecated
fun <T : N> emitNode(factory: () -> T) {
+ validateNodeExpected()
if (inserting) {
val insertIndex = nodeIndexStack.peek()
// The pending is the pending information for where the node is being inserted.
// pending will be null here when the parent was inserted too.
- pending?.let { it.nodeCount++ }
groupNodeCount++
recordFixup { applier, slots, _ ->
val node = factory()
- slots.update(node)
+ slots.node = node
applier.insert(insertIndex, node)
applier.down(node)
}
} else {
- recordDown()
- reader.next() // Skip node slot
+ recordDown(reader.node)
}
- childrenAllowed = true
}
/**
@@ -693,20 +694,19 @@
*/
@Suppress("UNUSED")
fun <T : N> createNode(factory: () -> T) {
+ validateNodeExpected()
require(inserting) { "createNode() can only be called when inserting" }
val insertIndex = nodeIndexStack.peek()
// see emitNode
- pending?.let { it.nodeCount++ }
groupNodeCount++
recordFixup { applier, slots, _ ->
val node = factory()
- slots.update(node)
+ slots.node = node
applier.insert(insertIndex, node)
applier.down(node)
}
// Allocate a slot that will be fixed-up by the above operation.
writer.skip()
- childrenAllowed = true
}
/**
@@ -714,17 +714,16 @@
* inserting.
*/
fun emitNode(node: N) {
+ validateNodeExpected()
require(inserting) { "emitNode() called when not inserting" }
val insertIndex = nodeIndexStack.peek()
// see emitNode
- pending?.let { it.nodeCount++ }
groupNodeCount++
- writer.update(node)
+ writer.node = node
recordApplierOperation { applier, _, _ ->
applier.insert(insertIndex, node)
applier.down(node)
}
- childrenAllowed = true
}
/**
@@ -733,12 +732,11 @@
* location as [emitNode] or [createNode] as called even if the value is unused.
*/
fun useNode(): N {
+ validateNodeExpected()
require(!inserting) { "useNode() called while inserting" }
- recordDown()
- val result = reader.next()
- childrenAllowed = true
- @Suppress("UNCHECKED_CAST")
- return result as N
+ val result = reader.node
+ recordDown(result)
+ return result
}
/**
@@ -768,7 +766,10 @@
/**
* Return the next value in the slot table and advance the current location.
*/
- fun nextSlot(): Any? = if (inserting) EMPTY else reader.next()
+ 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
@@ -812,8 +813,7 @@
recordSlotTableOperation(-1) { _, slots, lifecycleManager ->
if (value is CompositionLifecycleObserver)
lifecycleManager.entering(value)
- val previous = slots.update(value)
- when (previous) {
+ when (val previous = slots.update(value)) {
is CompositionLifecycleObserver ->
lifecycleManager.leaving(previous)
is RecomposeScope ->
@@ -826,20 +826,31 @@
}
/**
- * Return the provider scope provided at [location]
- */
- private fun providedScopeAt(location: Int, reader: SlotReader): AmbientMap {
- require(reader.groupKey(location) == provider)
- val valuesGroupSize = reader.groupSize(location + 1)
- val mapLocation = location + valuesGroupSize + 3 // one for each group slot
- @Suppress("UNCHECKED_CAST")
- return reader.get(mapLocation) as AmbientMap
- }
-
- /**
* Return the current ambient scope which was provided by a parent group.
*/
- private fun currentAmbientScope(): AmbientMap = providersStack.peek().second
+ private fun currentAmbientScope(): AmbientMap {
+ if (inserting && hasProvider) {
+ var group: Group? = writer.group(writer.parentLocation)
+ while (group != null) {
+ if (group.key === ambientMap) {
+ @Suppress("UNCHECKED_CAST")
+ return group.data as AmbientMap
+ }
+ group = group.parent
+ }
+ }
+ if (slotTable.size > 0) {
+ var group: Group? = reader.group(reader.parentLocation)
+ while (group != null) {
+ if (group.key === ambientMap) {
+ @Suppress("UNCHECKED_CAST")
+ return providerUpdates[group] ?: group.data as AmbientMap
+ }
+ group = group.parent
+ }
+ }
+ return parentProvider
+ }
/**
* Return the ambient scope for the location provided. If this is while the composer is
@@ -854,20 +865,17 @@
return currentAmbientScope()
}
- // Find the nearest provider group to location and return the scope recorded there.
- val groupPath = slotTable.groupPathTo(location)
- return slotTable.read { reader ->
- groupPath.lastOrNull { reader.groupKey(it) == provider }?.let {
- providedScopeAt(it, reader)
- } ?: buildableMapOf()
+ if (location >= 0) {
+ var group: Group? = slotTable.read { it.group(location) }
+ while (group != null) {
+ if (group.key == ambientMap) {
+ @Suppress("UNCHECKED_CAST")
+ return providerUpdates[group] ?: group.data as AmbientMap
+ }
+ group = group.parent
+ }
}
- }
-
- /**
- * Record [scope] as the current scope provided by the current group.
- */
- private fun pushProviderScope(scope: AmbientMap) {
- providersStack.push(reader.startStack.peekOr(0) to scope)
+ return parentProvider
}
/**
@@ -896,11 +904,12 @@
startGroup(providerValues)
val currentProviders = invokeComposableForResult(this) { ambientMapOf(values) }
endGroup()
- val providersStackSize = providersStack.size
- val invalid = if (inserting) {
- val providers = updateProviderMapGroup(parentScope, currentProviders)
- pushProviderScope(providers)
- false
+ val providers: AmbientMap
+ val invalid: Boolean
+ if (inserting) {
+ providers = updateProviderMapGroup(parentScope, currentProviders)
+ invalid = false
+ hasProvider = true
} else {
val current = reader.current
@@ -912,8 +921,7 @@
// skipping is true iff parentScope has not changed.
if (!skipping || oldValues != currentProviders) {
- val providers = updateProviderMapGroup(parentScope, currentProviders)
- pushProviderScope(providers)
+ 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
@@ -921,33 +929,26 @@
// 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.
- providers != oldScope
+ invalid = providers != oldScope
} else {
// Nothing has changed
skipGroup()
- pushProviderScope(oldScope)
- false
+ providers = oldScope
+ invalid = false
}
}
- // If the provider scope has changed then we need prevent skipping until endProviders()
- // is called.
+ if (invalid && !inserting) {
+ providerUpdates[reader.group] = providers
+ }
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = invalid
-
- require(providersStackSize + 1 == providersStack.size) {
- "The provider stack was not updated correctly"
- }
-
- startGroup(invocation)
+ start(ambientMap, false, providers)
}
internal fun endProviders() {
endGroup()
- val group = reader.startStack.peekOr(0)
endGroup()
- val (topGroup, _) = providersStack.pop()
- require(group == topGroup)
providersInvalid = providersInvalidStack.pop().asBool()
}
@@ -1004,6 +1005,23 @@
private fun ensureWriter() {
if (writer.closed) {
writer = insertTable.openWriter()
+ 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(EMPTY)
+ } else {
+ if (data != null && reader.groupData !== data) {
+ recordSlotEditingOperation { _, slots, _ ->
+ slots.updateData(data)
+ }
+ }
+ reader.startGroup(EMPTY)
}
}
@@ -1011,30 +1029,25 @@
* Important: This is a short-cut for the full version of [start] and should be kept in sync
* with its implementation. This version avoids boxing for [Int] keys.
*/
- private fun start(key: Int, isNode: Boolean) {
+ private fun start(key: Int, isNode: Boolean, data: Any?) {
if (!inserting && pending == null && key == reader.groupKey) {
- require(childrenAllowed) { "A call to createNode(), emitNode() or useNode() expected" }
+ validateNodeNotExpected()
- if (!isNode) {
- updateCompoundKeyWhenWeEnterGroupKeyHash(key)
- }
- if (isNode) reader.startNode(EMPTY) else reader.startGroup(EMPTY)
-
+ updateCompoundKeyWhenWeEnterGroupKeyHash(key)
+ startReaderGroup(isNode, data)
enterGroup(isNode, null)
} else {
- start(key as Any, isNode)
+ start(key as Any, isNode, data)
}
}
- private fun start(key: Any, isNode: Boolean) {
+ private fun start(key: Any, isNode: Boolean, data: Any?) {
// !! IMPORTANT !! If there are changes to this method there might need to be
// corresponding changes to the Int short cut method above.
- require(childrenAllowed) { "A call to createNode(), emitNode() or useNode() expected" }
+ validateNodeNotExpected()
- if (!isNode) {
- updateCompoundKeyWhenWeEnterGroup(key)
- }
+ updateCompoundKeyWhenWeEnterGroup(key)
// 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.
@@ -1042,9 +1055,13 @@
reader.beginEmpty()
if (collectKeySources)
recordSourceKeyInfo(key)
- if (isNode) writer.startNode(key) else writer.startGroup(key)
+ when {
+ isNode -> writer.startNode(key)
+ data != null -> writer.startData(key, data)
+ else -> writer.startGroup(key)
+ }
pending?.let { pending ->
- val insertKeyInfo = KeyInfo(key, -1, 0, -1)
+ val insertKeyInfo = KeyInfo(key, -1, 0, -1, writer.parentGroup)
pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex)
pending.recordUsed(insertKeyInfo)
}
@@ -1056,11 +1073,9 @@
val slotKey = reader.groupKey
if (slotKey == key) {
// The group is the same as what was generated last time.
- if (isNode) reader.startNode(key) else reader.startGroup(key)
+ startReaderGroup(isNode, data)
} else {
- val nodes = reader.parentNodes - reader.nodeIndex
pending = Pending(
- KeyInfo(0, -1, nodes, -1),
reader.extractKeys(),
nodeIndex
)
@@ -1097,7 +1112,7 @@
slots.moveGroup(currentRelativePosition)
}
}
- if (isNode) reader.startNode(key) else reader.startGroup(key)
+ 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
@@ -1110,13 +1125,13 @@
ensureWriter()
writer.beginInsert()
val insertLocation = writer.current
- writer.startGroup(key)
+ if (isNode) writer.startNode(key) else writer.startGroup(key)
insertAnchor = writer.anchor(insertLocation)
- val insertKeyInfo = KeyInfo(key, -1, 0, -1)
+ val insertKeyInfo = KeyInfo(key, -1, 0, -1, writer.parentGroup)
pending.registerInsert(insertKeyInfo, nodeIndex - pending.startIndex)
pending.recordUsed(insertKeyInfo)
newPending = Pending(
- insertKeyInfo, mutableListOf(),
+ mutableListOf(),
if (isNode) 0 else nodeIndex
)
}
@@ -1143,12 +1158,8 @@
// 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) {
- // Update the parent count of nodes
- previousPending.updateNodeCount(pending?.parentKeyInfo, expectedNodeCount)
- if (!inserting) {
- previousPending.groupIndex++
- }
+ if (previousPending != null && !inserting) {
+ previousPending.groupIndex++
}
this.pending = previousPending
this.nodeIndex = nodeIndexStack.pop() + expectedNodeCount
@@ -1159,9 +1170,13 @@
// 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 (!isNode) {
- updateCompoundKeyWhenWeExitGroup()
- }
+
+ updateCompoundKeyWhenWeExitGroup(
+ if (inserting)
+ writer.group(writer.parentLocation).key
+ else
+ reader.group(reader.parentLocation).key
+ )
var expectedNodeCount = groupNodeCount
val pending = pending
if (pending != null && pending.keyInfos.size > 0) {
@@ -1190,7 +1205,7 @@
// group
val deleteOffset = pending.nodePositionOf(previousInfo)
recordRemoveNode(deleteOffset + pending.startIndex, previousInfo.nodes)
- pending.updateNodeCount(previousInfo, 0)
+ pending.updateNodeCount(previousInfo.group, 0)
recordReaderMoving(previousInfo.location)
reader.reposition(previousInfo.location)
recordDelete()
@@ -1260,7 +1275,6 @@
invalidations.removeRange(startSlot, reader.current)
}
- // Cache the current [inserting] state for the pending update below.
val inserting = inserting
if (inserting) {
if (isNode) {
@@ -1268,16 +1282,23 @@
expectedNodeCount = 1
}
reader.endEmpty()
+ val group = writer.parentGroup
writer.endGroup()
if (!reader.inEmpty) {
writer.endInsert()
writer.close()
recordInsert(insertAnchor)
this.inserting = false
+ nodeCountOverrides[group] = 0
+ updateNodeCountOverrides(group, expectedNodeCount)
}
} else {
if (isNode) recordUp()
recordEndGroup()
+ val group = reader.parentGroup
+ if (expectedNodeCount != group.nodes) {
+ updateNodeCountOverrides(group, expectedNodeCount)
+ }
if (isNode) {
expectedNodeCount = 1
reader.endNode()
@@ -1290,107 +1311,59 @@
}
/**
- * Skip to a sibling group that contains location given. This also ensures the nodeIndex is
- * correctly updated to reflect any groups skipped.
+ * 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 skipToGroupContaining(location: Int) {
- val reader = reader
- while (reader.current < location) {
- if (reader.isGroupEnd) return
- if (reader.isGroup) {
- if (location < reader.groupSize + reader.current) return
- nodeIndex += reader.skipGroup()
- } else {
- reader.next()
- }
- }
- }
-
- /**
- * Enter a group that contains the location. This updates the composer state as if the group was
- * generated with no changes.
- */
- private fun enterGroups(location: Int, level: Int): Int {
- val reader = reader
- var currentLevel = level
- while (true) {
- skipToGroupContaining(location)
- if (reader.current == location) {
- break
- } else {
- enterGroup(reader.isNode, null)
- currentLevel++
- if (reader.isNode) {
- reader.startNode(EMPTY)
- recordDown()
- reader.next() // skip navigation slot
- nodeIndex = 0
- } else {
- updateCompoundKeyWhenWeEnterGroup(reader.groupKey)
- // If the current group is an ambient provider, add the map to the ambient
- // scope stack
- if (reader.groupKey === provider) {
- val current = reader.current
- providersStack.push(current to providedScopeAt(current, reader))
- }
- reader.startGroup(EMPTY)
- }
- }
- }
- return currentLevel
- }
-
- /**
- * Exit any groups that were entered until a sibling of maxLocation is reached.
- */
- private fun exitGroups(location: Int, level: Int): Int {
- val reader = reader
- var currentProviderScope = providersStack.peek().first
- var currentLevel = level
- while (currentLevel > 0) {
- skipToGroupContaining(location)
- if (reader.isGroupEnd) {
- val startLocation = reader.parentLocation
-
- // If the current group is a provider scope, pop it off the stack as this
- // exits the scope.
- if (reader.startStack.peekOr(0) == currentProviderScope) {
- providersStack.pop()
- currentProviderScope = providersStack.peek().first
- }
-
- val isNode = reader.isNode(startLocation)
- if (isNode) recordUp()
- else updateCompoundKeyWhenWeExitGroup()
- recordEndGroup()
- reader.endGroup()
- currentLevel--
- exitGroup(if (isNode) 1 else groupNodeCount, false)
- } else break
- }
- return currentLevel
- }
-
- private fun recomposeComponentRange(start: Int, end: Int) {
+ private fun recomposeToGroupEnd() {
val wasComposing = isComposing
isComposing = true
var recomposed = false
+ val start = reader.parentLocation
+ val end = start + reader.groupSize(start) + 1
+ val recomposeGroup = reader.group(start)
+ val recomposeIndex = nodeIndex
+ val recomposeCompoundKey = currentCompoundKeyHash
+ val oldGroupNodeCount = groupNodeCount
+ var oldGroup = recomposeGroup
+
var firstInRange = invalidations.firstInRange(start, end)
- var level = 0
- val previousParent = reader.parentLocation
while (firstInRange != null) {
val location = firstInRange.location
invalidations.removeLocation(location)
- level = exitGroups(location, level)
- level = enterGroups(location, level)
+ recomposed = true
+
+ reader.reposition(location)
+ val newGroup = reader.group
+
+ // Record the changes to the applier location
+ recordUpsAndDowns(oldGroup, newGroup, recomposeGroup)
+ 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,
+ start,
+ recomposeGroup,
+ recomposeIndex
+ )
+
+ // Calculate the compound hash code (a semi-unique code for every group in the
+ // composition used to restore saved state).
+ currentCompoundKeyHash = compoundKeyOf(
+ newGroup.parent,
+ recomposeGroup,
+ recomposeCompoundKey
+ )
firstInRange.scope.compose(this)
- 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
@@ -1401,15 +1374,155 @@
}
if (recomposed) {
- exitGroups(end, level)
- require(reader.parentLocation == previousParent) { "Group enter mismatch" }
+ recordUpsAndDowns(oldGroup, recomposeGroup, recomposeGroup)
+ val parentGroup = reader.parentGroup
+ reader.skipToGroupEnd()
+ val parentGroupNodes = (nodeCountOverrides[parentGroup] ?: parentGroup.nodes)
+ nodeIndex = recomposeIndex + parentGroupNodes
+ groupNodeCount = oldGroupNodeCount + parentGroupNodes
} else {
// No recompositions were requested in the range, skip it.
skipReaderToGroupEnd()
}
+ currentCompoundKeyHash = recomposeCompoundKey
+
isComposing = wasComposing
}
+ /**
+ * 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: Group, newCount: Int) {
+ val currentCount = nodeCountOverrides[group] ?: group.nodes
+ if (currentCount != newCount) {
+ // Update the overrides
+ val delta = newCount - currentCount
+ var current: Group? = group
+
+ var minPending = pendingStack.size - 1
+ while (current != null) {
+ val newCurrentNodes = (nodeCountOverrides[current] ?: current.nodes) + delta
+ nodeCountOverrides[current] = newCurrentNodes
+ for (pendingIndex in minPending downTo 0) {
+ val pending = pendingStack.peek(pendingIndex)
+ if (pending != null && pending.updateNodeCount(current, newCurrentNodes)) {
+ minPending = pendingIndex - 1
+ break
+ }
+ }
+ if (current.isNode) break
+ current = current.parent
+ }
+ }
+ }
+
+ /**
+ * 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: Group,
+ recomposeLocation: Int,
+ recomposeGroup: Group,
+ recomposeIndex: Int
+ ): Int {
+ // Find the anchor group which is either the recomposeGroup or the first parent node
+ var anchorGroup = group.parent ?: error("Invalid group")
+ while (anchorGroup != recomposeGroup) {
+ if (anchorGroup.isNode) break
+ anchorGroup = anchorGroup.parent ?: error("group not contained in recompose group")
+ }
+
+ var index = if (anchorGroup.isNode) 0 else recomposeIndex
+
+ // An early out if the group and anchor sizes are the same as the index must then be index
+ if (anchorGroup.slots == group.slots) return index
+
+ // Find the location of the anchor group
+ val anchorLocation =
+ if (anchorGroup == recomposeGroup) {
+ recomposeLocation
+ } else {
+ // anchor node must be between recomposeLocation and groupLocation but no farther
+ // back than anchorGroup.size - group.size + 1 because anchorGroup contains group
+ var location = recomposeLocation
+ val anchorLimit = groupLocation - (anchorGroup.slots - group.slots + 1)
+ if (location < anchorLimit) location = anchorLimit
+ while (reader.get(location) !== anchorGroup)
+ location++
+ location
+ }
+
+ // Walk down from the anchor group counting nodes of siblings in front of this group
+ var current = anchorLocation
+ val nodeIndexLimit = index + ((nodeCountOverrides[anchorGroup] ?: anchorGroup.nodes) -
+ group.nodes)
+ loop@ while (index < nodeIndexLimit) {
+ if (current == groupLocation) break
+ current++
+ while (!reader.isGroup(current)) current++
+ while (current < groupLocation) {
+ val currentGroup = reader.group(current)
+ val end = currentGroup.slots + current + 1
+ if (groupLocation < end) continue@loop
+ index += nodeCountOverrides[currentGroup] ?: currentGroup.nodes
+
+ current = end
+ }
+ break
+ }
+ return index
+ }
+
+ /**
+ * Records the operations necessary to move the applier the node affected by the previous
+ * group to the new group.
+ */
+ private fun recordUpsAndDowns(oldGroup: Group, newGroup: Group, commonRoot: Group) {
+ val nearestCommonRoot = nearestCommonRootOf(
+ oldGroup,
+ newGroup,
+ commonRoot
+ ) ?: commonRoot
+
+ // Record ups for the nodes between oldGroup and nearestCommonRoot
+ var current: Group? = oldGroup
+ while (current != null && current != nearestCommonRoot) {
+ if (current.isNode) recordUp()
+ current = current.parent
+ }
+
+ // Record downs from nearestCommonRoot to newGroup
+ doRecordDownsFor(newGroup, nearestCommonRoot)
+ }
+
+ private fun doRecordDownsFor(group: Group?, nearestCommonRoot: Group?) {
+ if (group != null && group != nearestCommonRoot) {
+ doRecordDownsFor(group.parent, nearestCommonRoot)
+ @Suppress("UNCHECKED_CAST")
+ if (group.isNode) recordDown(group.node as N)
+ }
+ }
+
+ /**
+ * 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: Group?, recomposeGroup: Group, recomposeKey: Int): Int {
+ return if (group == recomposeGroup) recomposeKey else (compoundKeyOf(
+ (group ?: error("Detached group")).parent,
+ recomposeGroup,
+ recomposeKey
+ ) rol 3) xor group.key.hashCode()
+ }
+
internal fun invalidate(scope: RecomposeScope): InvalidationResult {
val location = scope.anchor?.location(slotTable)
?: return InvalidationResult.IGNORED // The scope never entered the composition
@@ -1439,8 +1552,12 @@
skipGroup()
} else {
val reader = reader
- val current = reader.current
- recomposeComponentRange(current, current + reader.groupSize)
+ val key = reader.groupKey
+ updateCompoundKeyWhenWeEnterGroup(key)
+ startReaderGroup(reader.isNode, reader.groupData)
+ recomposeToGroupEnd()
+ reader.endGroup()
+ updateCompoundKeyWhenWeExitGroup(key)
}
}
@@ -1457,8 +1574,7 @@
if (invalidations.isEmpty()) {
skipReaderToGroupEnd()
} else {
- val parentLocation = reader.parentLocation
- recomposeComponentRange(parentLocation, parentLocation + reader.parentSlots + 1)
+ recomposeToGroupEnd()
}
}
@@ -1470,7 +1586,7 @@
*/
fun startRestartGroup(key: Int) {
val location = reader.current
- start(key, false)
+ start(key, false, null)
if (inserting) {
val scope = RecomposeScope(this)
invalidateStack.push(scope)
@@ -1534,8 +1650,35 @@
internal fun hasInvalidations() = invalidations.isNotEmpty()
+ @Suppress("UNCHECKED_CAST")
+ private var SlotWriter.node
+ get() = nodeGroup.node as N
+ set(value) { nodeGroup.node = value }
+ private val SlotWriter.nodeGroup get() = get(current - 1) as NodeGroup
+ private fun SlotWriter.nodeGroupAt(location: Int) = get(location) as NodeGroup
+ private fun SlotWriter.nodeAt(location: Int) = nodeGroupAt(location).node
+ @Suppress("UNCHECKED_CAST")
+ private val SlotReader.node get() = nodeGroupAt(current - 1).node as N
+ private fun SlotReader.nodeGroupAt(location: Int) = get(location) as NodeGroup
+ @Suppress("UNCHECKED_CAST")
+ private fun SlotReader.nodeAt(location: Int) = nodeGroupAt(location).node as N
+
+ private fun validateNodeExpected() {
+ require(nodeExpected) {
+ "A call to createNode(), emitNode() or useNode() expected was not expected"
+ }
+ nodeExpected = false
+ }
+
+ private fun validateNodeNotExpected() {
+ require(!nodeExpected) { "A call to createNode(), emitNode() or useNode() expected" }
+ }
+
/**
- * Add a raw change to the change list.
+ * 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)
@@ -1547,6 +1690,7 @@
* and the slot table writer slot is the same as the current reader's slot.
*/
private fun recordOperation(change: Change<N>) {
+ realizeInsertUps()
realizeUps()
realizeDowns()
realizeOperationLocation(0)
@@ -1558,17 +1702,23 @@
* node.
*/
private fun recordApplierOperation(change: Change<N>) {
- realizeInsertApplier()
+ realizeInsertUps()
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 be ensuring the parent group is started and then ended
+ * as the group is left.
+ */
private fun recordSlotEditingOperation(offset: Int = 0, change: Change<N>) {
realizeOperationLocation(offset)
recordSlotEditing()
record(change)
}
+
/**
* Record a change ensuring, when it is applied, the write matches the current slot in the
* reader.
@@ -1579,19 +1729,14 @@
}
// 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 with the
- // corresponding location of the start node for the location. When the node navigation is
- // realized all the downs in the down nodes is played to the applier and recording their
- // locations in the realizedDowns stack. Downing the applier, the current realized downs
- // are checked if they still apply and a corresponding up is called if they are not.
+ // 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 realizedDowns = IntStack()
private var pendingUps = 0
private var downNodes = Stack<N>()
- private var downLocations = IntStack()
private fun realizeUps() {
val count = pendingUps
@@ -1601,39 +1746,32 @@
}
}
- private fun realizeDowns() {
- if (downNodes.isNotEmpty()) {
- for (index in 0 until downLocations.size) {
- realizedDowns.push(downLocations.peek(index))
+ private fun realizeDowns(nodes: Array<N>) {
+ record { applier, _, _ ->
+ for (index in nodes.indices) {
+ applier.down(nodes[index])
}
- @Suppress("UNCHECKED_CAST")
- val nodes = Array(downNodes.size) { index -> downNodes.peek(index) as Any } as Array<N>
- record { applier, _, _ ->
- for (index in nodes.indices) {
- applier.down(nodes[index])
- }
- }
- downNodes.clear()
- downLocations.clear()
}
}
- private fun recordDown() {
+ private fun realizeDowns() {
+ if (downNodes.isNotEmpty()) {
+ @Suppress("UNCHECKED_CAST")
+ realizeDowns(downNodes.toArray())
+ downNodes.clear()
+ }
+ }
+
+ private fun recordDown(node: N) {
@Suppress("UNCHECKED_CAST")
- downNodes.push(reader.get(reader.current) as N)
- downLocations.push(reader.parentLocation)
+ downNodes.push(node)
}
private fun recordUp() {
if (downNodes.isNotEmpty()) {
downNodes.pop()
- downLocations.pop()
} else {
- val parentLocation = reader.parentLocation
- if (realizedDowns.peekOr(-1) == parentLocation) {
- pendingUps++
- realizedDowns.pop()
- }
+ pendingUps++
}
}
@@ -1643,7 +1781,7 @@
pendingInsertUps++
}
- private fun realizeInsertApplier() {
+ private fun realizeInsertUps() {
if (pendingInsertUps > 0) {
val count = pendingInsertUps
record { applier, _, _ -> repeat(count) { applier.up() } }
@@ -1657,7 +1795,7 @@
// 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
+ // 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.
@@ -1668,8 +1806,23 @@
// 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(offset: Int) {
@@ -1707,7 +1860,7 @@
}
private fun recordFixup(change: Change<N>) {
- realizeInsertApplier()
+ realizeInsertUps()
val anchor = insertAnchor
val start = insertTable.anchorLocation(anchor)
val location = writer.current - start
@@ -1717,10 +1870,6 @@
insertFixups.add(change)
}
- private val removeCurrentGroupInstance: Change<N> = { _, slots, lifecycleManager ->
- removeCurrentGroup(slots, lifecycleManager)
- }
-
/**
* 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
@@ -1756,13 +1905,11 @@
}
}
- private val skipToEndGroupInstance: Change<N> = { _, slots, _ -> slots.skipToGroupEnd() }
private fun recordSkipToGroupEnd() {
recordSlotTableOperation(change = skipToEndGroupInstance)
writersReaderDelta = reader.current
}
- private val endGroupInstance: Change<N> = { _, slots, _ -> slots.endGroup() }
private fun recordEndGroup() {
val location = reader.parentLocation
val currentStartedGroup = startedGroups.peekOr(-1)
@@ -1781,13 +1928,10 @@
}
private fun finalizeCompose() {
- realizeInsertApplier()
+ realizeInsertUps()
realizeUps()
require(pendingStack.isEmpty()) { "Start/end imbalance" }
require(startedGroups.isEmpty()) { "Missed recording an endGroup()" }
- require(providersStack.size == 1) {
- "Provider stack imbalance, stack size ${providersStack.size}"
- }
cleanUpCompose()
}
@@ -1796,10 +1940,11 @@
nodeIndex = 0
groupNodeCount = 0
writersReaderDelta = 0
- startedGroups.clear()
+ currentCompoundKeyHash = 0
+ nodeExpected = false
startedGroup = false
- providersStack.clear()
- providersStack.push(-1 to buildableMapOf())
+ startedGroups.clear()
+ nodeCountOverrides.clear()
}
private var previousRemove = -1
@@ -1896,16 +2041,12 @@
updateCompoundKeyWhenWeEnterGroupKeyHash(groupKey.hashCode())
}
- @OptIn(ExperimentalStdlibApi::class)
private fun updateCompoundKeyWhenWeEnterGroupKeyHash(keyHash: Int) {
- keyHashesStack.push(keyHash)
- currentCompoundKeyHash = currentCompoundKeyHash.rotateLeft(3) xor keyHash
+ currentCompoundKeyHash = (currentCompoundKeyHash rol 3) xor keyHash
}
- @OptIn(ExperimentalStdlibApi::class)
- private fun updateCompoundKeyWhenWeExitGroup() {
- val keyHash = keyHashesStack.pop()
- currentCompoundKeyHash = (currentCompoundKeyHash xor keyHash).rotateRight(3)
+ private fun updateCompoundKeyWhenWeExitGroup(groupKey: Any) {
+ currentCompoundKeyHash = (currentCompoundKeyHash xor groupKey.hashCode()) ror 3
}
}
@@ -1999,7 +2140,7 @@
is CompositionLifecycleObserver -> {
lifecycleManager.leaving(slot)
}
- is GroupStart -> {
+ is Group -> {
groupEndStack.push(groupEnd)
groupEnd = index + slot.slots
}
@@ -2115,6 +2256,50 @@
return realFn(composer)
}
+private fun Group.distanceFrom(root: Group): Int {
+ var count = 0
+ var current: Group? = this
+ while (current != null && current != root) {
+ current = current.parent
+ count++
+ }
+ return count
+}
+
+// find the nearest common root
+private fun nearestCommonRootOf(a: Group, b: Group, common: Group): Group? {
+ // 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 (a.parent == b) return b // if b is a's parent b is the nearest common root
+ if (b.parent == a) return a // if a is b's parent a is the nearest common root
+ if (a.parent == b.parent) return a.parent // if a an b share a parent it is the nearest common
+
+ // Find the nearest using distance from common
+ var currentA: Group? = a
+ var currentB: Group? = b
+ val aDistance = a.distanceFrom(common)
+ val bDistance = b.distanceFrom(common)
+ repeat(aDistance - bDistance) { currentA = currentA?.parent }
+ repeat(bDistance - aDistance) { currentB = currentB?.parent }
+
+ // 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 = currentA?.parent
+ currentB = currentB?.parent
+ }
+
+ // ca == cb so it doesn't matter which is returned
+ return currentA
+}
+
+private val removeCurrentGroupInstance: Change<*> = { _, slots, lifecycleManager ->
+ removeCurrentGroup(slots, lifecycleManager)
+}
+private val skipToEndGroupInstance: Change<*> = { _, slots, _ -> slots.skipToGroupEnd() }
+private val endGroupInstance: Change<*> = { _, slots, _ -> slots.endGroup() }
+
@PublishedApi
internal val invocation = OpaqueKey("invocation")
@@ -2122,6 +2307,9 @@
internal val provider = OpaqueKey("provider")
@PublishedApi
+internal val ambientMap = OpaqueKey("ambientMap")
+
+@PublishedApi
internal val providerValues = OpaqueKey("providerValues")
@PublishedApi
diff --git a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/SlotTable.kt b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/SlotTable.kt
index 9aef41a..cbb94ad 100644
--- a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/SlotTable.kt
+++ b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/SlotTable.kt
@@ -26,7 +26,7 @@
internal val startStack = IntStack()
private val slots = table.slots
- private var currentGroup: GroupStart? = null
+ private var currentGroup: Group? = null
private var emptyCount = 0
private val nodeIndexStack = IntStack()
@@ -40,10 +40,14 @@
*/
val isGroup get() = current < currentEnd && calculateCurrentGroup() != null
+ internal val group get() = assumeGroup()
+ internal fun group(location: Int) = slots[location].asGroup
+ internal val parentGroup get() = slots[parentLocation].asGroup
+
/**
* Determine if the slot at [index] is the start of a group
*/
- fun isGroup(index: Int) = slots[index] is GroupStart
+ fun isGroup(index: Int) = slots[index] is Group
/**
* Determine if the slot is start of a node.
@@ -54,7 +58,7 @@
* Determine if the slot at [location] is a node group. This will throw if the slot at
* [location] is not a node.
*/
- fun isNode(location: Int) = slots[location].asGroupStart.isNode
+ fun isNode(location: Int) = slots[location].asGroup.isNode
/**
* Determine if the reader is at the end of a group and an [endGroup] or [endNode] is expected.
@@ -75,7 +79,7 @@
* Get the size of the group at [index]. Will throw an exception if [index] is not a group
* start.
*/
- fun groupSize(index: Int) = slots[index].asGroupStart.slots
+ fun groupSize(index: Int) = slots[index].asGroup.slots
/**
* Get location the end of the currently started group.
@@ -85,7 +89,12 @@
/**
* Get location of the end of the group at [index].
*/
- fun groupEnd(index: Int) = index + slots[index].asGroupStart.slots + 1
+ fun groupEnd(index: Int) = index + slots[index].asGroup.slots + 1
+
+ /**
+ * Get the data for the current group. Returns null if [current] is not a group
+ */
+ val groupData get() = if (current < currentEnd) calculateCurrentGroup()?.data else null
/**
* Get the key of the current group. Returns [EMPTY] if the [current] is not a group.
@@ -93,10 +102,15 @@
val groupKey get() = if (current < currentEnd) calculateCurrentGroup()?.key ?: EMPTY else EMPTY
/**
+ * Get the node associated with the group if there is one.
+ */
+ val groupNode get() = (assumeGroup() as? NodeGroup)?.node
+
+ /**
* Get the key of the group at [index]. Will throw an exception if [index] is not a group
* start.
*/
- fun groupKey(index: Int) = slots[index].asGroupStart.key
+ fun groupKey(index: Int) = slots[index].asGroup.key
/**
* Return the location of the parent group of the [current]
@@ -107,13 +121,13 @@
* Return the number of nodes where emitted into the current group.
*/
val parentNodes: Int get() =
- if (startStack.isEmpty()) 0 else slots[startStack.peek()].asGroupStart.nodes
+ if (startStack.isEmpty()) 0 else slots[startStack.peek()].asGroup.nodes
/**
* Return the number of slots are in the current group.
*/
val parentSlots: Int get() =
- if (startStack.isEmpty()) 0 else slots[startStack.peek()].asGroupStart.slots
+ if (startStack.isEmpty()) 0 else slots[startStack.peek()].asGroup.slots
/**
* Get the value stored at [anchor].
@@ -191,7 +205,7 @@
fun skipToGroupEnd() {
require(emptyCount == 0) { "Cannot skip the enclosing group while in an empty region" }
require(startStack.isNotEmpty()) { "No enclosing group to skip" }
- nodeIndex = slots[startStack.peek()].asGroupStart.nodes + nodeIndexStack.peek()
+ nodeIndex = slots[startStack.peek()].asGroup.nodes + nodeIndexStack.peek()
currentGroup = null
current = currentEnd
}
@@ -209,8 +223,8 @@
val startLocation = startStack.pop()
if (startStack.isEmpty()) return
val parentLocation = startStack.peekOr(0)
- val group = slots[startLocation].asGroupStart
- val parentGroup = slots[parentLocation].asGroupStart
+ val group = slots[startLocation].asGroup
+ val parentGroup = slots[parentLocation].asGroup
nodeIndex = nodeIndexStack.pop() + if (group.isNode) 1 else nodeIndex
currentEnd = parentGroup.slots + parentLocation + 1
currentGroup = null
@@ -234,8 +248,8 @@
var index = 0
while (current < currentEnd) {
val location = current
- val key = slots[location].asGroupStart.key
- result.add(KeyInfo(key, location, skipGroup(), index++))
+ val group = slots[location].asGroup
+ result.add(KeyInfo(group.key, location, skipGroup(), index++, group))
}
current = oldCurrent
this.nodeIndex = oldNodeIndex
@@ -251,16 +265,16 @@
nodeIndex = 0
val group = assumeGroup()
currentEnd = current + group.slots + 1
- require(group.kind == kind) { "Group kind changed" }
+ require(group.kind == kind || key == EMPTY) { "Group kind changed" }
require(key == EMPTY || key == group.key) { "Group key changed" }
current++
currentGroup = null
}
}
- private fun calculateCurrentGroup(): GroupStart? =
- (currentGroup ?: slots[current] as? GroupStart)?.also { currentGroup = it }
- private fun assumeGroup(): GroupStart = calculateCurrentGroup()
+ private fun calculateCurrentGroup(): Group? =
+ (currentGroup ?: slots[current] as? Group)?.also { currentGroup = it }
+ private fun assumeGroup(): Group = calculateCurrentGroup()
?: error("Expected a group start")
}
@@ -284,28 +298,32 @@
/**
* Return true if the current slot starts a group
*/
- val isGroup get() = current < currentEnd && get(current) is GroupStart
+ val isGroup get() = current < currentEnd && get(current) is Group
/**
* Return true if the slot at index starts a gorup
*/
- fun isGroup(index: Int) = get(index) is GroupStart
+ fun isGroup(index: Int) = get(index) is Group
+
+ internal fun group(location: Int) = slots[effectiveIndex(location)].asGroup
+
+ internal val parentGroup: Group get() = group(parentLocation)
/**
* Return true if the current slot starts a node. A node is a kind of group so this will
* return true for isGroup as well.
*/
- val isNode get() = current < currentEnd && (get(current) as? GroupStart)?.isNode ?: false
+ val isNode get() = current < currentEnd && (get(current) as? Group)?.isNode ?: false
/**
* Return the number of nodes in the group. isGroup must be true or this will throw.
*/
- val groupSize get() = get(current).asGroupStart.slots
+ val groupSize get() = get(current).asGroup.slots
/**
* Return the size of the group at index. isGroup(index) must be true of this will throw.
*/
- fun groupSize(index: Int): Int = get(index).asGroupStart.slots
+ fun groupSize(index: Int): Int = get(index).asGroup.slots
/**
* Get the number of nodes emitted to the group prior to the current slot.
@@ -318,7 +336,7 @@
val parentNodes: Int
get() {
return if (startStack.isEmpty()) 0
- else slots[effectiveIndex(startStack.peek())].asGroupStart.nodes
+ else slots[effectiveIndex(startStack.peek())].asGroup.nodes
}
/**
@@ -336,7 +354,7 @@
* Return the start location of the nearest group that contains the slot at [anchor].
*/
fun parentIndex(anchor: Anchor): Int {
- val group = get(anchor).asGroupStart
+ val group = get(anchor).asGroup
val location = table.anchorLocation(anchor)
val parent = group.parent
if (parent != null) {
@@ -381,7 +399,14 @@
}
/**
- * Set the value at the current slot.
+ * Updates the data for a data group
+ */
+ fun updateData(value: Any?) {
+ (get(current) as? DataGroup ?: error("Expected a data group")).data = value
+ }
+
+ /**
+ * Set the value at the slot previous to current.
*/
fun set(value: Any?) {
slots[effectiveIndex(current - 1)] = value
@@ -438,8 +463,8 @@
val parentLoc = parentLocation
if (parentLoc != location) {
if (startStack.isEmpty() && location > 0) ensureStarted(0)
- val currentParent = if (parentLoc >= 0) get(parentLocation).asGroupStart else null
- val newParent = get(location).asGroupStart
+ val currentParent = if (parentLoc >= 0) get(parentLocation).asGroup else null
+ val newParent = get(location).asGroup
// The new parent must be a (possibly indirect) child of the current parent
require(newParent.isDecendentOf(currentParent)) {
@@ -448,7 +473,7 @@
val oldCurrent = current
current = location
- startGroup(newParent.key, newParent.kind)
+ startGroup(newParent.key, newParent.kind, newParent.data)
current = oldCurrent
}
}
@@ -477,11 +502,11 @@
* @param key The group key. Passing EMPTY will retain as was written last time.
* An EMPTY key is not valid when inserting groups.
*/
- fun startGroup(key: Any) = startGroup(key, GROUP)
+ fun startGroup(key: Any) = startGroup(key, GROUP, null)
- private fun startGroup(key: Any, kind: GroupKind) {
+ private fun startGroup(key: Any, kind: GroupKind, data: Any?) {
val inserting = insertCount > 0
- val parent = if (startStack.isEmpty()) null else get(startStack.peek()).asGroupStart
+ val parent = if (startStack.isEmpty()) null else get(startStack.peek()).asGroup
startStack.push(current)
nodeCountStack.push(nodeCount)
@@ -491,13 +516,18 @@
endStack.push(slots.size - table.gapLen - currentEnd)
currentEnd = if (inserting) {
require(key != SlotTable.EMPTY) { "Inserting an EMPTY key" }
- update(GroupStart(kind, key, parent))
+ update(Group(kind, key, parent, data))
nodeCount = 0
current
} else {
- val group = advance().asGroupStart
+ val group = advance().asGroup
require(group.kind == kind) { "Group kind changed" }
require(key == SlotTable.EMPTY || group.key == key) { "Group key changed" }
+ if (kind == DATA) {
+ (group as? DataGroup ?: error("Expected a data group")).data = data
+ } else if (kind == NODE && data != null) {
+ (group as? NodeGroup ?: error("Expected a node group")).node = data
+ }
nodeCount = group.nodes
current + group.slots
}
@@ -523,7 +553,7 @@
// Update group length
val startLocation = startStack.pop()
- val group = get(startLocation).asGroupStart
+ val group = get(startLocation).asGroup
val cur = current
val oldSlots = group.slots
val oldNodes = group.nodes
@@ -536,7 +566,7 @@
table.clearGap()
} else if (startStack.isNotEmpty()) {
nodeCount = nodeCountStack.pop()
- val parent = get(startStack.peek()).asGroupStart
+ val parent = get(startStack.peek()).asGroup
if (group.parent == parent) {
nodeCount += if (inserting) {
if (group.isNode) 1 else newNodes
@@ -639,7 +669,12 @@
/**
* Start a node.
*/
- fun startNode(key: Any) = startGroup(key, NODE)
+ fun startNode(key: Any) = startGroup(key, NODE, null)
+
+ /**
+ * Start a node
+ */
+ fun startNode(key: Any, node: Any?) = startGroup(key, NODE, node)
/**
* End a node
@@ -647,6 +682,16 @@
fun endNode() = endGroup()
/**
+ * Start a data node.
+ */
+ fun startData(key: Any, data: Any?) = startGroup(key, DATA, data)
+
+ /**
+ * End a data node
+ */
+ fun endData() = endGroup()
+
+ /**
* Skip a node
*/
fun skipNode() = skipGroup()
@@ -702,7 +747,7 @@
tableWriter.moveGapTo(sourceEnd)
sourceSlots.copyInto(destSlots, current, sourceStart, sourceEnd)
- val group = get(destStart).asGroupStart
+ val group = get(destStart).asGroup
// Update the sizes of the parents of the group that was moved.
var currentGroup = group.parent
@@ -716,7 +761,7 @@
}
// Update the parent of the group moved.
- group.parent = get(startStack.peek()).asGroupStart
+ group.parent = get(startStack.peek()).asGroup
// Extract the anchors in range
val startAnchors = table.anchors.locationOf(sourceStart)
@@ -773,7 +818,7 @@
}
private fun advanceToNextGroup(): Int {
- val groupStart = advance().asGroupStart
+ val groupStart = advance().asGroup
current += groupStart.slots
return if (groupStart.isNode) 1 else groupStart.nodes
@@ -878,7 +923,7 @@
}
}
-private fun GroupStart.isDecendentOf(parent: GroupStart?): Boolean {
+private fun Group.isDecendentOf(parent: Group?): Boolean {
if (parent == null) return true
var current = this.parent
while (current != null) {
@@ -888,17 +933,43 @@
return false
}
-private val Any?.asGroupStart: GroupStart
- get() = this as? GroupStart ?: error("Expected a group start")
+private val Any?.asGroup: Group
+ get() = this as? Group ?: error("Expected a group")
-internal class GroupStart(
- val kind: GroupKind,
+internal fun Group(kind: GroupKind, key: Any, parent: Group?, data: Any?) =
+ when (kind) {
+ NODE -> NodeGroup(key, parent).also { it.node = data }
+ DATA -> DataGroup(key, parent, data)
+ else -> Group(key, parent)
+ }
+
+internal open class Group(
val key: Any,
- var parent: GroupStart?
+ var parent: Group?
) {
var slots: Int = 0
var nodes: Int = 0
- val isNode get() = kind == NODE
+ open val kind: GroupKind get() = GROUP
+ open val isNode get() = false
+ open val node: Any? get() = null
+ open val data: Any? get() = null
+}
+
+internal class NodeGroup(
+ key: Any,
+ parent: Group?
+) : Group(key, parent) {
+ override val kind: GroupKind get() = NODE
+ override val isNode get() = true
+ override var node: Any? = null
+}
+
+internal class DataGroup(
+ key: Any,
+ parent: Group?,
+ override var data: Any?
+) : Group(key, parent) {
+ override val kind: GroupKind get() = DATA
}
/**
@@ -1047,17 +1118,20 @@
fun verifyWellFormed() {
var current = 0
- fun validateGroup(parentLocation: Int, parent: GroupStart?): Int {
+ fun validateGroup(parentLocation: Int, parent: Group?): Int {
val location = current++
- val group = slots[location].asGroupStart
+ val group = slots[location].asGroup
require(group.parent == parent) { "Incorrect parent for group at $location" }
val end = location + group.slots + 1
val parentEnd = parentLocation + (parent?.slots?.let { it + 1 } ?: size)
require(end <= size) { "Group extends past then end of its table at $location" }
require(end <= parentEnd) { "Group extends past its parent at $location" }
+ require(!group.isNode || group.node != null) {
+ "Node groups must have a node at $location"
+ }
// Find the first child
- while (current < end && slots[current] !is GroupStart) current++
+ while (current < end && slots[current] !is Group) current++
// Validate the child groups
var nodeCount = 0
@@ -1225,11 +1299,12 @@
/**
* Information about groups and their keys.
*/
-class KeyInfo(
+class KeyInfo internal constructor(
/**
* The group key.
*/
val key: Any,
+
/**
* The location of the group.
*/
@@ -1243,7 +1318,12 @@
/**
* The index of the key info in the list returned by extractKeys
*/
- val index: Int
+ val index: Int,
+
+ /**
+ * The group
+ */
+ internal val group: Group
)
class Anchor(internal var loc: Int) {
@@ -1255,5 +1335,6 @@
private const val GROUP: GroupKind = 0
private const val NODE: GroupKind = 1
+private const val DATA: GroupKind = 2
private const val MIN_GROWTH_SIZE = 128
diff --git a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Stack.kt b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Stack.kt
index d6d2fb4..d825775 100644
--- a/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Stack.kt
+++ b/compose/compose-runtime/src/commonMain/kotlin/androidx/compose/Stack.kt
@@ -29,6 +29,8 @@
fun isEmpty() = backing.isEmpty()
fun isNotEmpty() = !isEmpty()
fun clear() = backing.clear()
+ @Suppress("UNCHECKED_CAST")
+ fun toArray(): Array<T> = Array<Any?>(backing.size) { backing[it] } as Array<T>
}
internal class IntStack {
diff --git a/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/CompositionTests.kt b/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/CompositionTests.kt
index 51a2515..2ce4b94 100644
--- a/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/CompositionTests.kt
+++ b/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/CompositionTests.kt
@@ -2060,6 +2060,269 @@
test()
}
}
+
+ @Test
+ fun evenOddRecomposeGroup() {
+ var includeEven = true
+ var includeOdd = true
+ val invalidates = mutableListOf<() -> Unit>()
+
+ fun invalidateComposition() {
+ for (invalidate in invalidates) {
+ invalidate()
+ }
+ invalidates.clear()
+ }
+
+ @Composable
+ fun MockComposeScope.wrapper(children: @Composable() () -> Unit) {
+ children()
+ }
+
+ @Composable
+ fun MockComposeScope.emitText() {
+ invalidates.add(invalidate)
+ if (includeOdd) {
+ key(1) {
+ text("odd 1")
+ }
+ }
+ if (includeEven) {
+ key(2) {
+ text("even 2")
+ }
+ }
+ if (includeOdd) {
+ key(3) {
+ text("odd 3")
+ }
+ }
+ if (includeEven) {
+ key(4) {
+ text("even 4")
+ }
+ }
+ }
+
+ @Composable
+ fun MockComposeScope.test() {
+ linear {
+ wrapper {
+ emitText()
+ }
+ emitText()
+ wrapper {
+ emitText()
+ }
+ emitText()
+ }
+ }
+
+ fun MockViewValidator.wrapper(children: () -> Unit) {
+ children()
+ }
+
+ fun MockViewValidator.emitText() {
+ if (includeOdd) {
+ text("odd 1")
+ }
+ if (includeEven) {
+ text("even 2")
+ }
+ if (includeOdd) {
+ text("odd 3")
+ }
+ if (includeEven) {
+ text("even 4")
+ }
+ }
+
+ fun MockViewValidator.test() {
+ linear {
+ wrapper {
+ emitText()
+ }
+ emitText()
+ wrapper {
+ emitText()
+ }
+ emitText()
+ }
+ }
+
+ val myComposition = compose {
+ test()
+ }
+
+ fun validate() {
+ validate(myComposition.root) {
+ test()
+ }
+ }
+ validate()
+
+ includeEven = false
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ includeEven = true
+ includeOdd = false
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ includeEven = false
+ includeOdd = false
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ includeEven = true
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ includeOdd = true
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+ }
+
+ @Test
+ fun evenOddWithMovement() {
+ var includeEven = true
+ var includeOdd = true
+ var order = listOf(1, 2, 3, 4)
+ val invalidates = mutableListOf<() -> Unit>()
+
+ fun invalidateComposition() {
+ for (invalidate in invalidates) {
+ invalidate()
+ }
+ invalidates.clear()
+ }
+
+ @Composable
+ fun MockComposeScope.wrapper(children: @Composable() () -> Unit) {
+ children()
+ }
+
+ @Composable
+ fun MockComposeScope.emitText(all: Boolean) {
+ invalidates.add(invalidate)
+ for (i in order) {
+ if (i % 2 == 1 && (all || includeOdd)) {
+ key(i) {
+ text("odd $i")
+ }
+ }
+ if (i % 2 == 0 && (all || includeEven)) {
+ key(i) {
+ text("even $i")
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun MockComposeScope.test() {
+ linear {
+ invalidates.add(invalidate)
+ for (i in order) {
+ key(i) {
+ text("group $i")
+ if (i == 2 || (includeEven && includeOdd)) {
+ text("including everything")
+ } else {
+ if (includeEven) {
+ text("including evens")
+ }
+ if (includeOdd) {
+ text("including odds")
+ }
+ }
+ emitText(i == 2)
+ }
+ }
+ emitText(false)
+ }
+ }
+
+ fun MockViewValidator.emitText(all: Boolean) {
+ for (i in order) {
+ if (i % 2 == 1 && (includeOdd || all)) {
+ text("odd $i")
+ }
+ if (i % 2 == 0 && (includeEven || all)) {
+ text("even $i")
+ }
+ }
+ }
+
+ fun MockViewValidator.test() {
+ linear {
+ for (i in order) {
+ text("group $i")
+ if (i == 2 || (includeEven && includeOdd)) {
+ text("including everything")
+ } else {
+ if (includeEven) {
+ text("including evens")
+ }
+ if (includeOdd) {
+ text("including odds")
+ }
+ }
+ emitText(i == 2)
+ }
+ emitText(false)
+ }
+ }
+
+ val myComposition = compose {
+ test()
+ }
+
+ fun validate() {
+ validate(myComposition.root) {
+ test()
+ }
+ }
+ validate()
+
+ order = listOf(1, 2, 4, 3)
+ includeEven = false
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ order = listOf(1, 4, 2, 3)
+ includeEven = true
+ includeOdd = false
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ order = listOf(3, 4, 2, 1)
+ includeEven = false
+ includeOdd = false
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ order = listOf(4, 3, 2, 1)
+ includeEven = true
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+
+ order = listOf(1, 2, 3, 4)
+ includeOdd = true
+ invalidateComposition()
+ myComposition.expectChanges()
+ validate()
+ }
}
private fun <T> assertArrayEquals(message: String, expected: Array<T>, received: Array<T>) {
diff --git a/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/SlotTableTests.kt b/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/SlotTableTests.kt
index 494dc0a..c5ad857 100644
--- a/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/SlotTableTests.kt
+++ b/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/SlotTableTests.kt
@@ -512,7 +512,7 @@
repeat(10) {
writer.startGroup(0)
repeat(3) {
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
}
assertEquals(3, writer.endGroup())
@@ -541,7 +541,7 @@
writer.startGroup(0)
repeat(3) {
writer.startGroup(0)
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
assertEquals(1, writer.endGroup())
}
@@ -565,7 +565,7 @@
writer.beginInsert()
repeat(2) {
writer.startGroup(0)
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
assertEquals(1, writer.endGroup())
}
@@ -594,7 +594,7 @@
writer.startGroup(0)
repeat(3) {
writer.startGroup(0)
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
assertEquals(1, writer.endGroup())
}
@@ -638,12 +638,12 @@
writer.beginInsert()
writer.startGroup(rootKey)
writer.startGroup(0)
- writer.startNode(1)
+ writer.startNode(1, 1)
repeat(10) {
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.startGroup(0)
repeat(3) {
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
}
assertEquals(3, writer.endGroup())
@@ -664,12 +664,12 @@
writer.beginInsert()
writer.startGroup(rootKey)
writer.startGroup(0)
- writer.startNode(1)
+ writer.startNode(1, 1)
repeat(10) {
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.startGroup(0)
repeat(3) {
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
}
assertEquals(3, writer.endGroup())
@@ -712,7 +712,7 @@
writer.startGroup(rootKey)
writer.startGroup(0)
repeat(10) {
- writer.startNode(1)
+ writer.startNode(1, 1)
writer.endNode()
}
writer.endGroup()
@@ -749,7 +749,7 @@
writer.startGroup(0)
writer.startGroup(1)
writer.endGroup()
- writer.startNode(2)
+ writer.startNode(2, 2)
writer.endNode()
writer.endGroup()
writer.endInsert()
@@ -779,7 +779,7 @@
assertEquals(false, writer.isGroup)
writer.endGroup()
assertEquals(true, writer.isNode)
- writer.startNode(2)
+ writer.startNode(2, 2)
assertEquals(false, writer.isNode)
writer.endNode()
writer.endGroup()
@@ -802,7 +802,7 @@
}
fun element(key: Any, block: () -> Unit) {
- writer.startNode(key)
+ writer.startNode(key, key)
block()
writer.endNode()
}
@@ -1000,7 +1000,7 @@
}
fun element(key: Any, block: () -> Unit) {
- writer.startNode(key)
+ writer.startNode(key, key)
block()
writer.endNode()
}
@@ -1150,7 +1150,7 @@
}
val movedAnchors = mutableSetOf<Anchor>()
- slotsToMove.forEachIndexed() { index, anchor ->
+ slotsToMove.forEachIndexed { index, anchor ->
try {
if (anchor !in movedAnchors) {
destTable.write { writer ->
@@ -1223,7 +1223,7 @@
}
fun element(key: Any, block: () -> Unit) {
- writer.startNode(key)
+ writer.startNode(key, key)
block()
writer.endNode()
}
@@ -1260,7 +1260,7 @@
}
fun element(key: Int, block: () -> Unit) {
- writer.startNode(key)
+ writer.startNode(key, key)
block()
writer.endNode()
}
diff --git a/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/mock/View.kt b/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/mock/View.kt
index 1951853..8c6eb96 100644
--- a/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/mock/View.kt
+++ b/compose/compose-runtime/src/unitTest/kotlin/androidx/compose/mock/View.kt
@@ -16,8 +16,8 @@
package androidx.compose.mock
-fun indent(indent: Int) {
- repeat(indent) { print(' ') }
+fun indent(indent: Int, builder: StringBuilder) {
+ repeat(indent) { builder.append(' ') }
}
open class View {
@@ -25,20 +25,23 @@
val children = mutableListOf<View>()
val attributes = mutableMapOf<String, Any>()
- fun render(indent: Int = 0) {
- indent(indent)
- print("<$name$attributesAsString")
+ private fun render(indent: Int = 0, builder: StringBuilder) {
+ indent(indent, builder)
+ builder.append("<$name$attributesAsString")
if (children.size > 0) {
- println(">")
- children.forEach { it.render(indent + 2) }
- indent(indent)
- println("</$name>")
+ builder.appendln(">")
+ children.forEach { it.render(indent + 2, builder) }
+ indent(indent, builder)
+ builder.appendln("</$name>")
} else {
- println(" />")
+ builder.appendln(" />")
}
}
- fun addAt(index: Int, view: View) { children.add(index, view) }
+ fun addAt(index: Int, view: View) {
+ children.add(index, view)
+ }
+
fun removeAt(index: Int, count: Int) {
if (index < children.count()) {
if (count == 1) {
@@ -48,6 +51,7 @@
}
}
}
+
fun moveAt(from: Int, to: Int, count: Int) {
if (count == 1) {
val insertLocation = if (from > to) to else (to - 1)
@@ -90,4 +94,9 @@
children.map { it.toString() }.joinToString(" ")
override fun toString() = "<$name$attributesAsString>$childrenAsString</$name>"
+
+ fun toFmtString() = StringBuilder().let {
+ render(0, it)
+ it.toString()
+ }
}
diff --git a/ui/ui-tooling/src/main/java/androidx/ui/tooling/SlotTree.kt b/ui/ui-tooling/src/main/java/androidx/ui/tooling/SlotTree.kt
index 1a903aa..13a3569 100644
--- a/ui/ui-tooling/src/main/java/androidx/ui/tooling/SlotTree.kt
+++ b/ui/ui-tooling/src/main/java/androidx/ui/tooling/SlotTree.kt
@@ -111,10 +111,10 @@
val key = convertKey(groupKey)
val nodeGroup = isNode
val end = current + groupSize
+ val node = if (nodeGroup) groupNode else null
next()
val data = mutableListOf<Any?>()
val children = mutableListOf<Group>()
- val node = if (nodeGroup) next() else null
while (current < end && isGroup) {
children.add(getGroup())
}