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