[go: nahoru, domu]

blob: 522ddce684a0a7e76f6c23df4d95a2121c546707 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(InternalComposeApi::class)
package androidx.compose.runtime
@InternalComposeApi
class SlotReader(val table: SlotTable) {
var current = 0
private set
var currentEnd = table.size
private set
var nodeIndex: Int = 0
private set
internal val startStack = IntStack()
private val slots = table.slots
private var currentGroup: Group? = null
private var emptyCount = 0
private val nodeIndexStack = IntStack()
init {
require(table.gapStart == currentEnd) { "Gap is not at the end of the slot table" }
}
/**
* Determine the slot at [current] is the start of a group and not at the end of the current
* group.
*/
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 Group
/**
* Determine if the slot is start of a node.
*/
val isNode get() = calculateCurrentGroup()?.isNode ?: false
/**
* 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].asGroup.isNode
/**
* Determine if the reader is at the end of a group and an [endGroup] or [endNode] is expected.
*/
val isGroupEnd get() = inEmpty || current == currentEnd
/**
* Determine if a [beginEmpty] has been called.
*/
val inEmpty get() = emptyCount > 0
/**
* Get the size of the group at [current]. Will throw an exception if the [isGroup] is `false`.
*/
val groupSize get() = assumeGroup().slots
/**
* 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].asGroup.slots
/**
* Get location the end of the currently started group.
*/
val groupEnd get() = currentEnd
/**
* Get location of the end of the group at [index].
*/
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 0 if the [current] is not a group.
*/
val groupKey get() = if (current < currentEnd) {
val group = calculateCurrentGroup()
if (group != null) group.key else 0
} else 0
/**
* Get the data key for the current group. Returns null if the [current] is not a group.
*/
val groupDataKey get() =
if (current < currentEnd) calculateCurrentGroup()?.dataKey else null
/**
* 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].asGroup.key
/**
* Return the location of the parent group of the [current]
*/
val parentLocation: Int get() = startStack.peekOr(0)
/**
* Return the number of nodes where emitted into the current group.
*/
val parentNodes: Int get() =
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()].asGroup.slots
/**
* Get the value stored at [anchor].
*/
@Suppress("KotlinOperator")
fun get(anchor: Anchor) = if (anchor.loc >= 0) slots[anchor.loc] else EMPTY
/**
* Get the value stored at [index].
*/
@Suppress("KotlinOperator")
fun get(index: Int) = if (emptyCount > 0) EMPTY else slots[index]
/**
* Get the value of the slot at [current] or [EMPTY] if at then end of a group. During empty
* mode this value is always [EMPTY] which is the value a newly inserted slot.
*/
fun next(): Any? {
if (emptyCount > 0) return EMPTY
currentGroup = null
return if (current < currentEnd) slots[current++] else EMPTY
}
/**
* Begin reporting empty for all calls to next() or get(). beginEmpty() can be nested and must
* be called with a balanced number of endEmpty()
*/
fun beginEmpty() { emptyCount++ }
/**
* End reporting [EMPTY] for calls to [next] and [get],
*/
fun endEmpty() {
require(emptyCount > 0) { "Unbalanced begin/end empty" }
emptyCount--
}
/**
* Close the slot reader. After all [SlotReader]s have been closed the [SlotTable] a
* [SlotWriter] can be created.
*/
fun close() = table.close(this)
/**
* Start a group. Passing an EMPTY as the key will enter the group without validating the key.
*/
fun startGroup() = startGroup(GROUP)
/**
* Start a node.
*/
fun startNode() = startGroup(NODE)
/**
* Start a data group.
*/
fun startDataGroup() = startGroup(DATA)
/**
* Skip a group. Must be called at the start of a group.
*/
fun skipGroup(): Int {
require(emptyCount == 0) { "Cannot skip while in an empty region" }
val group = assumeGroup()
current += group.slots + 1
currentGroup = null
val count = if (group.isNode) 1 else group.nodes
nodeIndex += count
return count
}
/**
* Start a node.
*/
fun skipNode() = skipGroup()
/**
* Skip to the end of the current group.
*/
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()].asGroup.nodes + nodeIndexStack.peek()
currentGroup = null
current = currentEnd
}
fun reposition(value: Int) {
current = value
currentGroup = null
}
/**
* End the current group. Must be called after the corresponding [startGroup].
*/
fun endGroup() {
if (emptyCount == 0) {
require(current == currentEnd)
val startLocation = startStack.pop()
if (startStack.isEmpty()) return
val parentLocation = startStack.peekOr(0)
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
}
}
/**
* End a node
*/
fun endNode() = endGroup()
/**
* Extract the keys from this point to the end of the group. The current is left unaffected.
* Must be called inside a group.
*/
fun extractKeys(): MutableList<KeyInfo> {
val result = mutableListOf<KeyInfo>()
if (emptyCount > 0) return result
val oldCurrent = current
val oldNodeIndex = nodeIndex
var index = 0
while (current < currentEnd) {
val location = current
val group = slots[location].asGroup
result.add(KeyInfo(
group.key,
group.dataKey,
location,
skipGroup(),
index++,
group
))
}
current = oldCurrent
this.nodeIndex = oldNodeIndex
return result
}
override fun toString(): String = "SlotReader(current=$current, emptyCount=$emptyCount)"
private fun startGroup(kind: GroupKind) {
if (emptyCount <= 0) {
startStack.push(current)
nodeIndexStack.push(nodeIndex)
nodeIndex = 0
val group = assumeGroup()
currentEnd = current + group.slots + 1
require(group.kind == kind) { "Group kind changed" }
current++
currentGroup = null
}
}
private fun calculateCurrentGroup(): Group? =
(currentGroup ?: slots[current] as? Group)?.also { currentGroup = it }
private fun assumeGroup(): Group = calculateCurrentGroup()
?: error("Expected a group start")
}
@PublishedApi
internal val EMPTY = SlotTable.EMPTY
@InternalComposeApi
class SlotWriter internal constructor(val table: SlotTable) {
var current = 0
internal val slots get() = table.slots
internal fun effectiveIndex(index: Int) = table.effectiveIndex(index)
internal var currentEnd = table.slots.size
private var startStack = IntStack()
private val nodeCountStack = IntStack()
private val endStack = IntStack()
private var nodeCount = 0
private var insertCount = 0
private var pendingClear = false
/**
* Return true if the current slot starts a group
*/
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 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? Group)?.isNode ?: false
/**
* Return the number of nodes in the group. isGroup must be true or this will throw.
*/
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).asGroup.slots
/**
* Get the number of nodes emitted to the group prior to the current slot.
*/
val nodeIndex get() = nodeCount
/**
* Get the total number of nodes emitted in the group containing the current.
*/
val parentNodes: Int
get() {
return if (startStack.isEmpty()) 0
else slots[effectiveIndex(startStack.peek())].asGroup.nodes
}
/**
* Return the start location of the nearest group that contains [current].
*/
val parentLocation: Int get() = startStack.peekOr(-1)
/**
* True if the writer has been closed
*/
var closed = false
private set
/**
* Return the start location of the nearest group that contains the slot at [anchor].
*/
fun parentIndex(anchor: Anchor): Int {
val group = get(anchor).asGroup
val location = table.anchorLocation(anchor)
val parent = group.parent
if (parent != null) {
// Scan the slot table for the parent slot.
// The parent is, at most parent.slots - group.slots - 1 before location.
val start = (location - (parent.slots - group.slots) - 1).let { if (it < 0) 0 else it }
for (probe in start until location) {
if (get(probe) === parent) return probe
}
}
error("Could not find parent of group at $location")
}
/**
* Get the value at an Anchor
*/
@Suppress("KotlinOperator")
fun get(anchor: Anchor) = if (anchor.loc >= 0) slots[anchor.loc] else SlotTable.EMPTY
/**
* Get the value at the index'th slot.
*/
@Suppress("KotlinOperator")
fun get(index: Int) = effectiveIndex(index).let {
if (it < slots.size) slots[it] else SlotTable.EMPTY
}
fun close() {
closed = true
table.close(this)
// Ensure, for readers, there is no gap
moveGapTo(table.size)
}
/**
* Set the value of the next slot.
*/
fun update(value: Any?): Any? {
val result = skip()
set(value)
return result
}
/**
* 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
}
/**
* Skip the current slot without updating. If the slot table is inserting then and [EMPTY] slot
* is added and [skip] return [EMPTY].
*/
fun skip(): Any? {
if (insertCount > 0) {
insert(1)
}
val index = current++
return slots[table.effectiveIndex(index)]
}
/**
* Skip [amount] slots in the slot table. If the slot table is inserting then this
* adds [amount] [EMPTY] slots.
*
* Skip cannot skip outside the current group.
*/
fun skip(amount: Int) {
if (insertCount > 0) {
insert(amount)
} else {
val location = current + amount
require(location <= currentEnd) {
"Cannot skip outside the current group ($currentEnd)"
}
current = location
}
}
/**
* Skip to the end of the current group.
*/
fun skipToGroupEnd() { current = currentEnd }
/**
* If the start of a group was skipped using [skip], calling [ensureStarted] puts the writer
* into the same state as if [startGroup] or [startNode] was called on the group starting at
* [location]. If, after starting, the group, [current] is not a the end of the group or
* [current] is not at the start of a group for which [location] is not location the parent
* group, an exception is thrown.
*
* Calling [ensureStarted] implies that an [endGroup] should be called once the end of the
* group is reached.
*/
fun ensureStarted(location: Int) {
require(insertCount <= 0) { "Cannot call ensureStarted() while inserting" }
require(location in 0 until current) { "$location is out of range 0..${current - 1}" }
val parentLoc = parentLocation
if (parentLoc != location) {
if (startStack.isEmpty() && location > 0) ensureStarted(0)
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)) {
"Started group must be a subgroup of the group at $parentLocation"
}
val oldCurrent = current
current = location
startGroup(newParent.key, newParent.dataKey, newParent.kind, newParent.data)
current = oldCurrent
}
}
fun ensureStarted(anchor: Anchor) = ensureStarted(anchor.location(table))
/**
* Begin inserting at the current location. beginInsert() can be nested and must be called with
* a balanced number of endInsert()
*/
fun beginInsert() {
insertCount++
}
/**
* Ends inserting.
*/
fun endInsert() {
require(insertCount > 0) { "Unbalenced begin/end insert" }
insertCount--
}
/**
* Enter the group at current without changing it. Requires not currently inserting.
*/
fun startGroup() {
require(insertCount == 0) { "Key must be supplied when inserting" }
startGroup(0, null, GROUP, null)
}
/**
* Start a group with a integer key
*/
fun startGroup(key: Int) = startGroup(key, null, GROUP, null)
/**
* Start a group with a data key
*/
fun startGroup(key: Int, dataKey: Any?) = startGroup(key, dataKey, GROUP, null)
/**
* Start a group with a data key and auxiliary data.
*/
fun startGroup(key: Int, dataKey: Any?, data: Any?) =
startGroup(key, dataKey, if (data != null) DATA else GROUP, data)
private fun startGroup(key: Int, dataKey: Any?, kind: GroupKind, data: Any?) {
val inserting = insertCount > 0
val parent = if (startStack.isEmpty()) null else get(startStack.peek()).asGroup
startStack.push(current)
nodeCountStack.push(nodeCount)
// Record the end location as relative to the end of the slot table so when we pop it back
// off again all inserts and removes that happened while a child group was open are already
// reflected into its value.
endStack.push(slots.size - table.gapLen - currentEnd)
currentEnd = if (inserting) {
update(Group(kind, key, dataKey, parent, data))
nodeCount = 0
current
} else {
val group = advance().asGroup
require(group.kind == kind) { "Group kind changed" }
require(key == 0 || group.key == key) { "Group key changed" }
require(key == 0 || group.dataKey == dataKey) { "Group dataKey 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
}
}
/**
* Skip a group. Must be called at the start of a group.
*/
fun skipGroup(): Int {
require(insertCount == 0) { "Cannot skip while inserting" }
return advanceToNextGroup()
}
/**
* End the current group. Must be called after the corresponding startGroup().
*/
fun endGroup(): Int {
require(startStack.isNotEmpty()) {
"Invalid state. Unbalanced calls to startGroup() and endGroup()"
}
val inserting = insertCount > 0
require(inserting || current == currentEnd) { "Expected to be at the end of a group" }
// Update group length
val startLocation = startStack.pop()
val group = get(startLocation).asGroup
val cur = current
val oldSlots = group.slots
val oldNodes = group.nodes
val newSlots = cur - startLocation - 1
val newNodes = nodeCount
group.slots = newSlots
group.nodes = newNodes
currentEnd = (slots.size - table.gapLen) - endStack.pop()
if (nodeCountStack.isEmpty()) {
table.clearGap()
} else if (startStack.isNotEmpty()) {
nodeCount = nodeCountStack.pop()
val parent = get(startStack.peek()).asGroup
if (group.parent == parent) {
nodeCount += if (inserting) {
if (group.isNode) 1 else newNodes
} else {
if (group.isNode) 0 else newNodes - oldNodes
}
} else {
// If we are closing a group whose parent is not the now current group then the
// slot writer was seek'ed to the group and the parents of this group need to be
// updated to reflect any changes to the groups nodes or slots.
val slotsDelta = newSlots - oldSlots
var nodesDelta = if (group.isNode) 0 else newNodes - oldNodes
if (slotsDelta != 0 || nodesDelta != 0) {
var currentGroup = group.parent
while (
currentGroup != null &&
currentGroup != parent &&
(nodesDelta != 0 || slotsDelta != 0)
) {
currentGroup.slots += slotsDelta
currentGroup.nodes += nodesDelta
if (currentGroup.isNode) nodesDelta = 0
currentGroup = currentGroup.parent
}
}
nodeCount += nodesDelta
}
}
return newNodes
}
/**
* Move the offset'th group after the current group to the current location.
*/
fun moveGroup(offset: Int) {
require(insertCount == 0) { "Cannot move a group while inserting" }
val oldCurrent = current
val oldNodeCount = nodeCount
// Find the group to move
var count = offset
while (count > 0) {
advanceToNextGroup()
count--
}
// Move the current one here by first inserting room for it then copying it over the spot
// then removing the old slot.
val moveLocation = current
advanceToNextGroup()
val moveLen = current - moveLocation
current = oldCurrent
insert(moveLen)
// insert inserted moveLen slots which moved moveLocation
val newMoveLocation = moveLocation + moveLen
current = oldCurrent
nodeCount = oldNodeCount
slots.copyInto(slots, effectiveIndex(current),
effectiveIndex(newMoveLocation), effectiveIndex(newMoveLocation) + moveLen)
// Before we remove the old location, move any anchors
table.moveAnchors(newMoveLocation, current, moveLen)
// Remove the now duplicate entries
val anchorsRemoved = remove(moveLocation + moveLen, moveLen)
require(!anchorsRemoved) { "Unexpectedly removed anchors" }
}
/**
* Remove a group. Must be called at group.
*/
fun removeGroup(): Boolean {
require(insertCount == 0) { "Cannot remove group while inserting" }
val oldCurrent = current
val count = advanceToNextGroup()
val anchorsRemoved = remove(oldCurrent, current - oldCurrent)
current = oldCurrent
nodeCount -= count
return anchorsRemoved
}
/**
* Returns an iterator for the slots of group.
*/
fun groupSlots(): Iterator<Any?> {
val start = current
val oldCount = nodeCount
advanceToNextGroup()
val end = current
current = start
nodeCount = oldCount
return object : Iterator<Any?> {
var current = start + 1
override fun hasNext(): Boolean = current < end
override fun next(): Any? = slots[effectiveIndex(current++)]
}
}
/**
* Enter an existing node
*/
fun startNode() {
require(insertCount == 0) { "Key must be supplied when inserting" }
startGroup(0, null, NODE, null)
}
/**
* Start a node.
*/
fun startNode(key: Any?) = startGroup(125, key, NODE, null)
/**
* Start a node
*/
fun startNode(key: Any?, node: Any?) = startGroup(125, key, NODE, node)
/**
* End a node
*/
fun endNode() = endGroup()
/**
* Start a data node.
*/
fun startData(key: Int, dataKey: Any?, data: Any?) = startGroup(key, dataKey, DATA, data)
/**
* End a data node
*/
fun endData() = endGroup()
/**
* Skip a node
*/
fun skipNode() = skipGroup()
/**
* Move (insert and then delete) the group at [location] from [slots]. All anchors in the range
* (including [location]) are moved to the slot table for which this is a reader.
*
* It is required that the writer be inserting.
*
* @return a list of the anchors that were moved
*/
fun moveFrom(table: SlotTable, location: Int): List<Anchor> {
require(insertCount > 0)
if (location == 0 && current == 0 && this.table.size == 0) {
// Special moving the entire slot table into an empty table, just swap the slots
// and the anchors.
table.write {
val sourceSlots = table.slots
val sourceAnchors = table.anchors
val sourceGapStart = table.gapStart
val sourceGapLen = table.gapLen
val destTable = this.table
val destSlots = destTable.slots
val destAnchors = destTable.anchors
destTable.slots = sourceSlots
destTable.anchors = sourceAnchors
destTable.gapStart = sourceGapStart
destTable.gapLen = sourceGapLen
table.slots = destSlots
table.anchors = destAnchors
table.gapStart = 0
table.gapLen = 0
}
return this.table.anchors
}
return table.write { tableWriter ->
val sourceStart = location
val slotsToMove = tableWriter.groupSize(sourceStart) + 1
// Make room in the table
insert(slotsToMove)
// Copy the slots to the
val sourceSlots = table.slots
val destSlots = slots
val destStart = current
val sourceEnd = sourceStart + slotsToMove
// Move the gap to make the location contiguous. The remove at the end will do this
// as well so doing this early makes this code easier as the gap can be ignored.
tableWriter.moveGapTo(sourceEnd)
sourceSlots.copyInto(destSlots, current, sourceStart, sourceEnd)
val group = get(destStart).asGroup
// Update the sizes of the parents of the group that was moved.
var currentGroup = group.parent
val slotsDelta = group.slots + 1
var nodesDelta = if (group.isNode) 1 else group.nodes
while (currentGroup != null) {
currentGroup.slots -= slotsDelta
currentGroup.nodes -= nodesDelta
if (currentGroup.isNode) nodesDelta = 0
currentGroup = currentGroup.parent
}
// Update the parent of the group moved.
group.parent = get(startStack.peek()).asGroup
// Extract the anchors in range
val startAnchors = table.anchors.locationOf(sourceStart)
val endAnchors = table.anchors.locationOf(sourceEnd)
val anchors = if (startAnchors < endAnchors) {
val sourceAnchors = table.anchors
val anchors = ArrayList<Anchor>(endAnchors - startAnchors)
// update the anchor locations to their new location
for (index in startAnchors until endAnchors) {
val sourceAnchor = sourceAnchors[index]
sourceAnchor.loc = sourceAnchor.loc - sourceStart + destStart
anchors.add(sourceAnchor)
}
// Insert them into the new table
val insertLocation = this.table.anchors.locationOf(current)
this.table.anchors.addAll(insertLocation, anchors)
// Remove them from the old table
sourceAnchors.subList(startAnchors, endAnchors).clear()
anchors
} else emptyList<Anchor>()
// Now remove the range from the table.
val anchorsRemoved = tableWriter.remove(sourceStart, slotsToMove)
require(!anchorsRemoved) { "Removing anchors that should have been moved" }
// Update the node count.
nodeCount += group.nodes
// Move current passed the insert
current += slotsToMove
anchors
}
}
/**
* Allocate an anchor for a location. As content is inserted and removed from the slot table the
* anchor is updated to reflect those changes. For example, if an anchor is requested for an
* group, the anchor will report the location of that group even if the group is moved in the slot
* table. If the position referenced by the anchor is removed, the anchor location is set to -1.
*/
fun anchor(index: Int = current): Anchor = table.anchor(index)
private fun advance(): Any? {
if (current >= currentEnd) {
return SlotTable.EMPTY
}
val index = current++
return slots[effectiveIndex(index)]
}
private fun advanceToNextGroup(): Int {
val groupStart = advance().asGroup
current += groupStart.slots
return if (groupStart.isNode) 1 else groupStart.nodes
}
private fun moveGapTo(index: Int) {
if (table.gapLen > 0 && table.gapStart != index) {
trace("SlotTable:moveGap") {
pendingClear = false
if (table.anchors.isNotEmpty()) table.updateAnchors(index)
if (index < table.gapStart) {
slots.copyInto(slots, index + table.gapLen,
index, table.gapStart)
} else {
slots.copyInto(slots, table.gapStart,
table.gapStart + table.gapLen,
index + table.gapLen)
}
table.gapStart = index
pendingClear = true
}
} else {
table.gapStart = index
}
}
private fun insert(size: Int) {
if (size > 0) {
moveGapTo(current)
if (table.gapLen < size) {
trace("SlotTable:grow") {
// Create a bigger gap
val oldCapacity = slots.size
val oldSize = slots.size - table.gapLen
// Double the size of the array, but at least MIN_GROWTH_SIZE and >= size
val newCapacity = kotlin.math.max(
kotlin.math.max(oldCapacity * 2, oldSize + size),
MIN_GROWTH_SIZE
)
val newSlots = arrayOfNulls<Any?>(newCapacity)
val newGapLen = newCapacity - oldSize
val oldGapEnd = table.gapStart + table.gapLen
val newGapEnd = table.gapStart + newGapLen
// Copy the old array into the new array
slots.copyInto(newSlots, 0, 0, table.gapStart)
slots.copyInto(newSlots, newGapEnd, oldGapEnd, oldCapacity)
// Update the anchors
if (table.anchors.isNotEmpty()) table.anchorGapResize(newGapLen - table.gapLen)
// Update the gap and slots
table.slots = newSlots
table.gapLen = newGapLen
}
}
if (currentEnd >= table.gapStart) currentEnd += size
table.gapStart += size
table.gapLen -= size
repeat(size) {
slots[current + it] = SlotTable.EMPTY
}
pendingClear = true
}
}
internal fun remove(start: Int, len: Int): Boolean {
return if (len > 0) {
pendingClear = false
var anchorsRemoved = false
if (table.gapLen == 0) {
// If there is no current gap, just make the removed slots the gap
table.gapStart = start
if (table.anchors.isNotEmpty()) anchorsRemoved = table.removeAnchors(start, len)
table.gapLen = len
} else {
// Move the gap to the startGroup + len location and set the gap startGroup to
// startGroup and gap len to len + gapLen
val removeEnd = start + len
moveGapTo(removeEnd)
if (table.anchors.isNotEmpty()) anchorsRemoved = table.removeAnchors(start, len)
table.gapStart = start
table.gapLen += len
}
if (currentEnd >= table.gapStart) currentEnd -= len
pendingClear = true
anchorsRemoved
} else false
}
override fun toString(): String {
if (pendingClear) {
pendingClear = false
table.clearGap()
}
val gap = if (table.gapLen > 0)
"${table.gapStart}-${table.gapStart + table.gapLen - 1}"
else
"none"
return "SlotWriter" +
"(current=$current, " +
"size=${slots.size - table.gapLen}, " +
"gap=${gap}${if (insertCount > 0) ", inserting" else ""})"
}
}
private fun Group.isDecendentOf(parent: Group?): Boolean {
if (parent == null) return true
var current = this.parent
while (current != null) {
if (current == parent) return true
current = current.parent
}
return false
}
private val Any?.asGroup: Group
get() = this as? Group ?: error("Expected a group")
internal fun Group(kind: GroupKind, key: Int, dataKey: Any?, parent: Group?, data: Any?) =
when (kind) {
NODE -> {
NodeGroup(key, dataKey, parent).also { it.node = data }
}
DATA -> {
DataGroup(key, dataKey, parent, data)
}
else ->
if (dataKey != null)
DataKeyGroup(key, dataKey, parent)
else
Group(key, parent)
}
internal open class Group(
val key: Int,
var parent: Group?
) {
var slots: Int = 0
var nodes: Int = 0
open val kind: GroupKind get() = GROUP
open val dataKey: Any? get() = null
open val isNode get() = false
open val node: Any? get() = null
open val data: Any? get() = null
}
internal class DataKeyGroup(
key: Int,
override val dataKey: Any?,
parent: Group?
) : Group(key, parent)
internal class NodeGroup(
key: Int,
override val dataKey: 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: Int,
override val dataKey: Any?,
parent: Group?,
override var data: Any?
) : Group(key, parent) {
override val kind: GroupKind get() = DATA
}
/**
* A gap buffer implementation of the composition slot space. A slot space can be thought of as
* a custom List<Any?> that optimizes around inserts and removes.
*
* Slots stores slots, groups, and nodes.
*
* Slot - A slot is the primitive base type of the slot space. It is of type Any? and can hold
* any value.
* Group - A group is a keyed group of slots. The group counts the number of slots and nodes it
* contains.
* Node - A node is a special group that is counted by the containing groups.
*
* All groups and nodes are just grouping of slots and use slots to describe the groups. At
* the root of a slot space is a group. Groups count the number nodes that are in the group. A node
* only counts as one node in its group regardless of the number of nodes it contains.
*
* ASIDE:
* The intent is that groups represent memoized function calls and nodes represent views. For
* example:
*
* @sample androidx.compose.runtime.samples.initialGroup
*
* the `LinearLayout` here would be a node (the linear layout view). The node contains the
* groups for the child views of the linear layout.
*
* If contact's composition looks like:
*
* @sample androidx.compose.runtime.samples.contactSample
*
* then composing contact into the linear layout would add two views to the linear layout's
* children. The composition of contact creates groups, one for each text view. The groups for each
* contact would be able to report that it produces two views (that is the group created for
* Contact has two nodes). Summing the nodes in the group produces the number of views (as
* each node corresponds to a view).
*
* If the order of jim and bob change:
*
* @sample androidx.compose.runtime.samples.reorderedGroup
*
* the previous result can be reused by moving the views generated bob's group before jim's (or vis
* versa). A composition algorithm could use the key information for each group to determine if they
* can be switched. For example, since the first contact's group has two nodes the composition
* algorithm can infer that the beginning of jim's views starts at 2 and contains 2 view. To move
* jim in front of bob, move the 2 views from offset 2 to offset 0. If contact is immutable, for
* example, Contact would only need to be recomposed if the value of jim or bob change.
*/
class SlotTable(internal var slots: Array<Any?> = arrayOf()) {
private var readers = 0
private var writer = false
internal var gapStart: Int = 0
internal var gapLen: Int = 0
internal var anchors: ArrayList<Anchor> = arrayListOf()
/**
* Read the slot table in [block]. Any number of readers can be created but a slot table cannot
* be read while it is being written to.
*
* @see SlotReader
*/
@InternalComposeApi
inline fun <T> read(block: (reader: SlotReader) -> T): T = openReader().let { reader ->
try {
block(reader)
} finally {
reader.close()
}
}
/**
* Write to the slot table in [block]. Only one writer can be created for a slot table at a
* time and all readers must be closed an do readers can be created while the slot table is
* being written to.
*
* @see SlotWriter
*/
@InternalComposeApi
inline fun <T> write(block: (writer: SlotWriter) -> T): T = openWriter().let { writer ->
try {
block(writer)
} finally {
writer.close()
}
}
/**
* Return a list of locations of slot table that contain the groups that contain [location].
*
* [groupPathTo] creates a reader so it cannot be called when the slot table is being written
* to.
*/
@InternalComposeApi
fun groupPathTo(location: Int): List<Int> {
require(location < size)
val path = mutableListOf<Int>()
read { reader ->
var current = 0
loop@ while (true) {
path.add(current)
if (current == location) break
current++
while (current < location && !reader.isGroup(current)) current++
if (current == location && !reader.isGroup(current)) break
while (current <= location) {
val end = reader.groupSize(current) + current + 1
if (location < end) continue@loop
current = end
}
break
}
}
return path
}
/**
* Open a reader. Any number of readers can be created but a slot table cannot be read while
* it is being written to.
*
* @see SlotReader
*/
@InternalComposeApi
fun openReader(): SlotReader {
if (writer) error("Cannot read while a writer is pending")
readers++
return SlotReader(this)
}
/**
* Open a writer. Only one writer can be created for a slot table at a time and all readers
* must be closed an do readers can be created while the slot table is being written to.
*
* @see SlotWriter
*/
@InternalComposeApi
fun openWriter(): SlotWriter {
if (writer) error("Cannot start a writer when another writer is pending")
if (readers > 0) error("Cannot start a writer when a reader is pending")
writer = true
return SlotWriter(this)
}
/**
* Ensure a slot table is well-formed by verifying the internal structure of the slot table
* is consistent. This method will throw an exception when it detects inconsistency in the
* internal structure of the slot table. A slot table can be invalid (contain incorrect
* information about a composition) but still be well-formed but all valid slot tables are
* well-formed.
*/
@TestOnly
@InternalComposeApi
fun verifyWellFormed() {
var current = 0
fun validateGroup(parentLocation: Int, parent: Group?): Int {
val location = current++
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 Group) current++
// Validate the child groups
var nodeCount = 0
while (current < end) {
nodeCount += validateGroup(location, group)
}
require(group.nodes == nodeCount) {
"Incorrect node count for group at $location, expected ${
group.nodes
}, received $nodeCount"
}
return if (group.isNode) 1 else nodeCount
}
// Verify the groups are well-formed
require(gapStart == size) { "Gap is not at the end of the table" }
if (size > 0)
validateGroup(0, null)
// Verify the anchors are well-formed
var lastLocation = -1
for (anchor in anchors) {
val location = anchor.location(this)
require(location in 0..size) { "Location out of bound" }
require(lastLocation < location) { "Anchor is out of order" }
lastLocation = location
}
}
/**
* The number of active slots in the slot table. The current capacity of the slot table is at
* lease [size].
*/
@InternalComposeApi
val size: Int get() = slots.size - gapLen
internal fun close(reader: SlotReader) {
require(reader.table === this && readers > 0) { "Unexpected reader close()" }
readers--
}
internal fun close(writer: SlotWriter) {
require(writer.table === this && this.writer) { "Unexpected writer close()" }
this.writer = false
clearGap()
}
internal fun effectiveIndex(index: Int) = if (index < gapStart) index else gapLen + index
internal fun clearGap() = repeat(gapLen) { i -> slots[gapStart + i] = null }
internal fun anchor(index: Int): Anchor {
// TODO: Consider a buffer gap list of anchors if middle inserts and deletes are common
val anchorIndex = effectiveIndex(index)
val location = anchors.search(anchorIndex)
return if (location < 0) {
val anchor = Anchor(anchorIndex)
anchors.add(-(location + 1), anchor)
anchor
} else anchors[location]
}
internal fun updateAnchors(gapMovedTo: Int) {
if (gapStart < gapMovedTo) {
// Gap is moving up
// All anchors between the new gap and the old gap switch to be anchored to the
// front of the table instead of the end.
val rangeStart = gapStart + gapLen
val rangeEnd = gapMovedTo + gapLen
var index = anchors.locationOf(rangeStart)
while (index < anchors.size) {
val anchor = anchors[index]
if (anchor.loc < rangeEnd) {
anchor.loc -= gapLen
index++
} else break
}
} else {
// Gap is moving down. All anchors between gapMoveTo and gapStart need now to be
// anchored to the end of the table instead of the front of the table.
val rangeStart = gapMovedTo
val rangeEnd = gapStart
var index = anchors.locationOf(rangeStart)
while (index < anchors.size) {
val anchor = anchors[index]
if (anchor.loc < rangeEnd) {
anchor.loc += gapLen
index++
} else break
}
}
}
internal fun anchorGapResize(delta: Int) {
val start = anchors.locationOf(gapStart + gapLen)
for (index in start until anchors.size)
anchors[index].loc += delta
}
internal fun removeAnchors(gapStart: Int, size: Int): Boolean {
val removeStart = gapStart
val removeEnd = gapStart + size
var index = anchors.locationOf(gapStart + size).let {
if (it >= anchors.size) it - 1 else it
}
var anchorsRemoved = false
while (index >= 0) {
val anchor = anchors[index]
if (anchor.loc >= removeStart) {
if (anchor.loc < removeEnd) {
anchor.loc = -1
anchors.removeAt(index)
anchorsRemoved = true
}
index--
} else break
}
return anchorsRemoved
}
internal fun moveAnchors(originalLocation: Int, newLocation: Int, size: Int) {
val effectiveStart = effectiveIndex(originalLocation)
val effectiveEnd = effectiveIndex(originalLocation + size)
// Remove all the anchors in range from the original location
val index = anchors.locationOf(effectiveStart)
val removedAnchors = mutableListOf<Anchor>()
if (index >= 0) {
while (index < anchors.size) {
val anchor = anchors[index]
if (anchor.loc >= effectiveStart && anchor.loc < effectiveEnd) {
removedAnchors.add(anchor)
anchors.removeAt(index)
} else break
}
}
// Insert the anchors into there new location
for (anchor in removedAnchors) {
val location = anchorLocation(anchor)
val newAnchorLocation = location - originalLocation + newLocation
val effectiveLocation = effectiveIndex(newAnchorLocation)
anchor.loc = effectiveLocation
val insertIndex = anchors.locationOf(effectiveLocation)
anchors.add(insertIndex, anchor)
}
}
internal fun anchorLocation(anchor: Anchor) = anchor.loc.let {
if (it > gapStart) it - gapLen else it
}
internal fun ownsAnchor(anchor: Anchor?): Boolean {
if (anchor == null) return false
val loc = anchor.loc
if (loc < 0) return false
val location = anchors.search(loc)
if (location < 0) return false
return anchors[location] === anchor
}
@InternalComposeApi
companion object {
@InternalComposeApi
val EMPTY = object : Any() {
override fun toString(): String {
return "EMPTY"
}
}
}
}
private fun ArrayList<Anchor>.locationOf(index: Int) =
search(index).let { if (it >= 0) it else -(it + 1) }
private fun ArrayList<Anchor>.search(index: Int) = binarySearch { it.loc.compareTo(index) }
/**
* Information about groups and their keys.
*/
@InternalComposeApi
class KeyInfo internal constructor(
/**
* The group key.
*/
val key: Int,
/**
* The data key for the group
*/
val dataKey: Any?,
/**
* The location of the group.
*/
val location: Int,
/**
* The number of nodes in the group. If the group is a node this is always 1.
*/
val nodes: Int,
/**
* The index of the key info in the list returned by extractKeys
*/
val index: Int,
/**
* The group
*/
internal val group: Group
)
@InternalComposeApi
class Anchor(internal var loc: Int) {
val valid get() = loc >= 0
fun location(slots: SlotTable) = slots.anchorLocation(this)
}
private typealias GroupKind = Int
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