1 /* 2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.runtime.changelist 18 19 import androidx.compose.runtime.Anchor 20 import androidx.compose.runtime.ComposerImpl 21 import androidx.compose.runtime.Composition 22 import androidx.compose.runtime.CompositionContext 23 import androidx.compose.runtime.ControlledComposition 24 import androidx.compose.runtime.IntStack 25 import androidx.compose.runtime.InternalComposeApi 26 import androidx.compose.runtime.MovableContentState 27 import androidx.compose.runtime.MovableContentStateReference 28 import androidx.compose.runtime.RecomposeScopeImpl 29 import androidx.compose.runtime.RememberObserverHolder 30 import androidx.compose.runtime.SlotReader 31 import androidx.compose.runtime.SlotTable 32 import androidx.compose.runtime.Stack 33 import androidx.compose.runtime.internal.IntRef 34 import androidx.compose.runtime.runtimeCheck 35 36 internal class ComposerChangeListWriter( 37 /** 38 * The [Composer][ComposerImpl] that is building this ChangeList. The Composer's state may be 39 * used to determine how the ChangeList should be written to. 40 */ 41 private val composer: ComposerImpl, 42 /** The ChangeList that will be written to */ 43 var changeList: ChangeList 44 ) { 45 private val reader: SlotReader 46 get() = composer.reader 47 48 /** 49 * Record whether any groups were stared. If no groups were started then the root group doesn't 50 * need to be started or ended either. 51 */ 52 private var startedGroup: Boolean = false 53 54 /** A stack of the location of the groups that were started. */ 55 private val startedGroups = IntStack() 56 57 /** 58 * When inserting movable content, the group start and end is handled elsewhere. This flag lets 59 * us disable automatic insertion of the root group for movable content. Set to `false` when 60 * inserting movable content. 61 */ 62 var implicitRootStart: Boolean = true 63 64 // Navigating the writer slot is performed relatively as the location of a group in the writer 65 // might be different than it is in the reader as groups can be inserted, deleted, or moved. 66 // 67 // writersReaderDelta tracks the difference between reader's current slot the current of 68 // the writer must be before the recorded change is applied. Moving the writer to a location 69 // is performed by advancing the writer the same the number of slots traversed by the reader 70 // since the last write change. This works transparently for inserts. For deletes the number 71 // of nodes deleted needs to be added to writersReaderDelta. When slots move the delta is 72 // updated as if the move has already taken place. The delta is updated again once the group 73 // begin edited is complete. 74 // 75 // The SlotTable requires that the group that contains any moves, inserts or removes must have 76 // the group that contains the moved, inserted or removed groups be started with a startGroup 77 // and terminated with a endGroup so the effects of the inserts, deletes, and moves can be 78 // recorded correctly in its internal data structures. The startedGroups stack maintains the 79 // groups that must be closed before we can move past the started group. 80 81 /** 82 * The skew or delta between where the writer will be and where the reader is now. This can be 83 * thought of as the unrealized distance the writer must move to match the current slot in the 84 * reader. When an operation affects the slot table the writer location must be realized by 85 * moving the writer slot table the unrealized distance. 86 */ 87 private var writersReaderDelta: Int = 0 88 89 // Navigation of the node tree is performed by recording all the locations of the nodes as 90 // they are traversed by the reader and recording them in the downNodes array. When the node 91 // navigation is realized all the downs in the down nodes is played to the applier. 92 // 93 // If an up is recorded before the corresponding down is realized then it is simply removed 94 // from the downNodes stack. 95 private var pendingUps = 0 96 private var pendingDownNodes = Stack<Any?>() 97 98 private var removeFrom = -1 99 private var moveFrom = -1 100 private var moveTo = -1 101 private var moveCount = 0 102 pushApplierOperationPreamblenull103 private fun pushApplierOperationPreamble() { 104 pushPendingUpsAndDowns() 105 } 106 pushSlotEditingOperationPreamblenull107 private fun pushSlotEditingOperationPreamble() { 108 realizeOperationLocation() 109 recordSlotEditing() 110 } 111 pushSlotTableOperationPreamblenull112 private fun pushSlotTableOperationPreamble(useParentSlot: Boolean = false) { 113 realizeOperationLocation(useParentSlot) 114 } 115 116 /** Called when reader current is moved directly, such as when a group moves, to [location]. */ moveReaderRelativeTonull117 fun moveReaderRelativeTo(location: Int) { 118 // Ensure the next skip will account for the distance we have already travelled. 119 writersReaderDelta += location - reader.currentGroup 120 } 121 moveReaderToAbsolutenull122 fun moveReaderToAbsolute(location: Int) { 123 writersReaderDelta = location 124 } 125 recordSlotEditingnull126 fun recordSlotEditing() { 127 // During initial composition (when the slot table is empty), no group needs 128 // to be started. 129 if (reader.size > 0) { 130 val reader = reader 131 val location = reader.parent 132 133 if (startedGroups.peekOr(invalidGroupLocation) != location) { 134 ensureRootStarted() 135 136 if (location > 0) { 137 val anchor = reader.anchor(location) 138 startedGroups.push(location) 139 ensureGroupStarted(anchor) 140 } 141 } 142 } 143 } 144 ensureRootStartednull145 private fun ensureRootStarted() { 146 if (!startedGroup && implicitRootStart) { 147 pushSlotTableOperationPreamble() 148 changeList.pushEnsureRootStarted() 149 startedGroup = true 150 } 151 } 152 ensureGroupStartednull153 private fun ensureGroupStarted(anchor: Anchor) { 154 pushSlotTableOperationPreamble() 155 changeList.pushEnsureGroupStarted(anchor) 156 startedGroup = true 157 } 158 realizeOperationLocationnull159 private fun realizeOperationLocation(forParent: Boolean = false) { 160 val location = if (forParent) reader.parent else reader.currentGroup 161 val distance = location - writersReaderDelta 162 runtimeCheck(distance >= 0) { "Tried to seek backward" } 163 if (distance > 0) { 164 changeList.pushAdvanceSlotsBy(distance) 165 writersReaderDelta = location 166 } 167 } 168 169 val pastParent: Boolean 170 get() = reader.parent - writersReaderDelta < 0 171 withChangeListnull172 inline fun withChangeList(newChangeList: ChangeList, block: () -> Unit) { 173 val previousChangeList = changeList 174 try { 175 changeList = newChangeList 176 block() 177 } finally { 178 changeList = previousChangeList 179 } 180 } 181 withoutImplicitRootStartnull182 inline fun withoutImplicitRootStart(block: () -> Unit) { 183 val previousImplicitRootStart = implicitRootStart 184 try { 185 implicitRootStart = false 186 block() 187 } finally { 188 implicitRootStart = previousImplicitRootStart 189 } 190 } 191 remembernull192 fun remember(value: RememberObserverHolder) { 193 changeList.pushRemember(value) 194 } 195 rememberPausingScopenull196 fun rememberPausingScope(scope: RecomposeScopeImpl) { 197 changeList.pushRememberPausingScope(scope) 198 } 199 startResumingScopenull200 fun startResumingScope(scope: RecomposeScopeImpl) { 201 changeList.pushStartResumingScope(scope) 202 } 203 endResumingScopenull204 fun endResumingScope(scope: RecomposeScopeImpl) { 205 changeList.pushEndResumingScope(scope) 206 } 207 updateValuenull208 fun updateValue(value: Any?, groupSlotIndex: Int) { 209 pushSlotTableOperationPreamble(useParentSlot = true) 210 changeList.pushUpdateValue(value, groupSlotIndex) 211 } 212 updateAnchoredValuenull213 fun updateAnchoredValue(value: Any?, anchor: Anchor, groupSlotIndex: Int) { 214 // Because this uses an anchor, it can be performed without positioning the writer. 215 changeList.pushUpdateAnchoredValue(value, anchor, groupSlotIndex) 216 } 217 appendValuenull218 fun appendValue(anchor: Anchor, value: Any?) { 219 // Because this uses an anchor, it can be performed without positioning the writer. 220 changeList.pushAppendValue(anchor, value) 221 } 222 trimValuesnull223 fun trimValues(count: Int) { 224 if (count > 0) { 225 pushSlotEditingOperationPreamble() 226 changeList.pushTrimValues(count) 227 } 228 } 229 resetSlotsnull230 fun resetSlots() { 231 changeList.pushResetSlots() 232 } 233 updateAuxDatanull234 fun updateAuxData(data: Any?) { 235 pushSlotTableOperationPreamble() 236 changeList.pushUpdateAuxData(data) 237 } 238 endRootnull239 fun endRoot() { 240 if (startedGroup) { 241 pushSlotTableOperationPreamble() 242 pushSlotTableOperationPreamble() 243 changeList.pushEndCurrentGroup() 244 startedGroup = false 245 } 246 } 247 endCurrentGroupnull248 fun endCurrentGroup() { 249 val location = reader.parent 250 val currentStartedGroup = startedGroups.peekOr(-1) 251 runtimeCheck(currentStartedGroup <= location) { "Missed recording an endGroup" } 252 if (startedGroups.peekOr(-1) == location) { 253 pushSlotTableOperationPreamble() 254 startedGroups.pop() 255 changeList.pushEndCurrentGroup() 256 } 257 } 258 skipToEndOfCurrentGroupnull259 fun skipToEndOfCurrentGroup() { 260 changeList.pushSkipToEndOfCurrentGroup() 261 } 262 removeCurrentGroupnull263 fun removeCurrentGroup() { 264 /* 265 When a group is removed the reader will move but the writer will not so to ensure both 266 the writer and reader are tracking the same slot we advance `writersReaderDelta` to 267 account for the removal. 268 */ 269 pushSlotEditingOperationPreamble() 270 changeList.pushRemoveCurrentGroup() 271 writersReaderDelta += reader.groupSize 272 } 273 insertSlotsnull274 fun insertSlots(anchor: Anchor, from: SlotTable) { 275 pushPendingUpsAndDowns() 276 pushSlotEditingOperationPreamble() 277 realizeNodeMovementOperations() 278 changeList.pushInsertSlots(anchor, from) 279 } 280 insertSlotsnull281 fun insertSlots(anchor: Anchor, from: SlotTable, fixups: FixupList) { 282 pushPendingUpsAndDowns() 283 pushSlotEditingOperationPreamble() 284 realizeNodeMovementOperations() 285 changeList.pushInsertSlots(anchor, from, fixups) 286 } 287 moveCurrentGroupnull288 fun moveCurrentGroup(offset: Int) { 289 pushSlotEditingOperationPreamble() 290 changeList.pushMoveCurrentGroup(offset) 291 } 292 endCompositionScopenull293 fun endCompositionScope(action: (Composition) -> Unit, composition: Composition) { 294 changeList.pushEndCompositionScope(action, composition) 295 } 296 useNodenull297 fun useNode(node: Any?) { 298 pushApplierOperationPreamble() 299 changeList.pushUseNode(node) 300 } 301 updateNodenull302 fun <T, V> updateNode(value: V, block: T.(V) -> Unit) { 303 pushApplierOperationPreamble() 304 changeList.pushUpdateNode(value, block) 305 } 306 removeNodenull307 fun removeNode(nodeIndex: Int, count: Int) { 308 if (count > 0) { 309 runtimeCheck(nodeIndex >= 0) { "Invalid remove index $nodeIndex" } 310 if (removeFrom == nodeIndex) { 311 moveCount += count 312 } else { 313 realizeNodeMovementOperations() 314 removeFrom = nodeIndex 315 moveCount = count 316 } 317 } 318 } 319 moveNodenull320 fun moveNode(from: Int, to: Int, count: Int) { 321 if (count > 0) { 322 if (moveCount > 0 && moveFrom == from - moveCount && moveTo == to - moveCount) { 323 moveCount += count 324 } else { 325 realizeNodeMovementOperations() 326 moveFrom = from 327 moveTo = to 328 moveCount = count 329 } 330 } 331 } 332 releaseMovableContentnull333 fun releaseMovableContent() { 334 pushPendingUpsAndDowns() 335 if (startedGroup) { 336 skipToEndOfCurrentGroup() 337 endRoot() 338 } 339 } 340 endNodeMovementnull341 fun endNodeMovement() { 342 realizeNodeMovementOperations() 343 } 344 endNodeMovementAndDeleteNodenull345 fun endNodeMovementAndDeleteNode(nodeIndex: Int, group: Int) { 346 endNodeMovement() 347 pushPendingUpsAndDowns() 348 val nodeCount = if (reader.isNode(group)) 1 else reader.nodeCount(group) 349 if (nodeCount > 0) { 350 removeNode(nodeIndex, nodeCount) 351 } 352 } 353 realizeNodeMovementOperationsnull354 private fun realizeNodeMovementOperations() { 355 if (moveCount > 0) { 356 if (removeFrom >= 0) { 357 realizeRemoveNode(removeFrom, moveCount) 358 removeFrom = -1 359 } else { 360 realizeMoveNode(moveTo, moveFrom, moveCount) 361 362 moveFrom = -1 363 moveTo = -1 364 } 365 moveCount = 0 366 } 367 } 368 realizeRemoveNodenull369 private fun realizeRemoveNode(removeFrom: Int, moveCount: Int) { 370 pushApplierOperationPreamble() 371 changeList.pushRemoveNode(removeFrom, moveCount) 372 } 373 realizeMoveNodenull374 private fun realizeMoveNode(to: Int, from: Int, count: Int) { 375 pushApplierOperationPreamble() 376 changeList.pushMoveNode(to, from, count) 377 } 378 moveUpnull379 fun moveUp() { 380 realizeNodeMovementOperations() 381 if (pendingDownNodes.isNotEmpty()) { 382 pendingDownNodes.pop() 383 } else { 384 pendingUps++ 385 } 386 } 387 moveDownnull388 fun moveDown(node: Any?) { 389 realizeNodeMovementOperations() 390 pendingDownNodes.push(node) 391 } 392 pushPendingUpsAndDownsnull393 private fun pushPendingUpsAndDowns() { 394 if (pendingUps > 0) { 395 changeList.pushUps(pendingUps) 396 pendingUps = 0 397 } 398 399 if (pendingDownNodes.isNotEmpty()) { 400 changeList.pushDowns(pendingDownNodes.toArray()) 401 pendingDownNodes.clear() 402 } 403 } 404 sideEffectnull405 fun sideEffect(effect: () -> Unit) { 406 changeList.pushSideEffect(effect) 407 } 408 determineMovableContentNodeIndexnull409 fun determineMovableContentNodeIndex(effectiveNodeIndexOut: IntRef, anchor: Anchor) { 410 pushPendingUpsAndDowns() 411 changeList.pushDetermineMovableContentNodeIndex(effectiveNodeIndexOut, anchor) 412 } 413 copyNodesToNewAnchorLocationnull414 fun copyNodesToNewAnchorLocation(nodes: List<Any?>, effectiveNodeIndex: IntRef) { 415 changeList.pushCopyNodesToNewAnchorLocation(nodes, effectiveNodeIndex) 416 } 417 418 @OptIn(InternalComposeApi::class) copySlotTableToAnchorLocationnull419 fun copySlotTableToAnchorLocation( 420 resolvedState: MovableContentState?, 421 parentContext: CompositionContext, 422 from: MovableContentStateReference, 423 to: MovableContentStateReference, 424 ) { 425 changeList.pushCopySlotTableToAnchorLocation(resolvedState, parentContext, from, to) 426 } 427 428 @OptIn(InternalComposeApi::class) releaseMovableGroupAtCurrentnull429 fun releaseMovableGroupAtCurrent( 430 composition: ControlledComposition, 431 parentContext: CompositionContext, 432 reference: MovableContentStateReference 433 ) { 434 changeList.pushReleaseMovableGroupAtCurrent(composition, parentContext, reference) 435 } 436 endMovableContentPlacementnull437 fun endMovableContentPlacement() { 438 changeList.pushEndMovableContentPlacement() 439 writersReaderDelta = 0 440 } 441 includeOperationsInnull442 fun includeOperationsIn(other: ChangeList, effectiveNodeIndex: IntRef? = null) { 443 changeList.pushExecuteOperationsIn(other, effectiveNodeIndex) 444 } 445 finalizeCompositionnull446 fun finalizeComposition() { 447 pushPendingUpsAndDowns() 448 runtimeCheck(startedGroups.isEmpty()) { "Missed recording an endGroup()" } 449 } 450 resetTransientStatenull451 fun resetTransientState() { 452 startedGroup = false 453 startedGroups.clear() 454 writersReaderDelta = 0 455 } 456 deactivateCurrentGroupnull457 fun deactivateCurrentGroup() { 458 pushSlotTableOperationPreamble() 459 changeList.pushDeactivateCurrentGroup() 460 } 461 462 companion object { 463 private const val invalidGroupLocation = -2 464 } 465 } 466