1 /*
<lambda>null2  * Copyright 2024 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.xr.compose.subspace.node
18 
19 import androidx.xr.compose.subspace.layout.CombinedSubspaceModifier
20 import androidx.xr.compose.subspace.layout.Placeable
21 import androidx.xr.compose.subspace.layout.SubspaceModifier
22 import androidx.xr.compose.subspace.layout.findInstance
23 import androidx.xr.compose.subspace.layout.traverseSelfThenAncestors
24 import androidx.xr.compose.subspace.layout.traverseSelfThenDescendants
25 import androidx.xr.compose.unit.VolumeConstraints
26 
27 private val SentinelHead =
28     object : SubspaceModifier.Node() {
29         override fun toString() = "<Head>"
30     }
31 
32 /** See [androidx.compose.ui.node.NodeChain] */
33 internal class SubspaceModifierNodeChain(private val subspaceLayoutNode: SubspaceLayoutNode) {
34     private var current: MutableList<SubspaceModifier>? = null
35     private var buffer: MutableList<SubspaceModifier>? = null
36     internal val tail: SubspaceModifier.Node = subspaceLayoutNode.measurableLayout.tail
37     internal var head: SubspaceModifier.Node = tail
38     private var inMeasurePass: Boolean = false
39 
padChainnull40     private fun padChain(): SubspaceModifier.Node {
41         val currentHead = head
42         currentHead.parent = SentinelHead
43         SentinelHead.child = currentHead
44         return SentinelHead
45     }
46 
trimChainnull47     private fun trimChain(): SubspaceModifier.Node {
48         val result = SentinelHead.child ?: tail
49         result.parent = null
50         SentinelHead.child = null
51         return result
52     }
53 
updateFromnull54     internal fun updateFrom(modifier: SubspaceModifier) {
55         val paddedHead = padChain()
56         val before = current
57         val beforeSize = before?.size ?: 0
58         val after = modifier.fillVector(buffer ?: mutableListOf())
59         var i = 0
60 
61         if (beforeSize == 0) {
62             // Common case where we are initializing the chain or the previous size is zero.
63             var node = paddedHead
64             while (i < after.size) {
65                 val next = after[i]
66                 val parent = node
67                 node = createAndInsertNodeAsChild(next, parent)
68                 i++
69             }
70         } else if (after.size == 0) {
71             // Common case where we are removing all the modifiers.
72             var node = paddedHead.child
73             while (node != null && i < beforeSize) {
74                 node = removeNode(node).child
75                 i++
76             }
77         } else {
78             // Find the diffs between before and after sets. This is not as complex as base Compose
79             // which
80             // does a full diff. Revisit this if we see any performance issues with dynamic
81             // modifiers.
82             var node = paddedHead
83 
84             // First match as many same-type modifiers at the beginning of the lists.
85             checkNotNull(before) { "prior modifier list should be non-empty" }
86             while (i < beforeSize && i < after.size && before[i]::class == after[i]::class) {
87                 node = checkNotNull(node.child) { "child should not be null" }
88                 if (before[i] != after[i]) {
89                     updateNode(node, after[i])
90                 }
91                 i++
92             }
93 
94             // Then remove the remaining existing modifiers when we have a structural change.
95             var nodeToDelete = node.child
96             var beforeIndex = i
97             while (nodeToDelete != null && beforeIndex < beforeSize) {
98                 nodeToDelete = removeNode(nodeToDelete).child
99                 beforeIndex++
100             }
101 
102             // Finally add the remaining new modifiers.
103             while (i < after.size) {
104                 val next = after[i]
105                 val parent = node
106                 node = createAndInsertNodeAsChild(next, parent)
107                 i++
108             }
109         }
110 
111         current = after
112         // Clear the before vector to allow old modifiers to be Garbage Collected.
113         buffer = before?.also { it.clear() }
114         head = trimChain()
115     }
116 
117     /**
118      * Marks all nodes in the chain as attached to a [SubspaceLayout].
119      *
120      * This should be called *before* [runOnAttach] is called. We check that the node is not already
121      * attached as this method may be called more than necessary to ensure proper state.
122      */
<lambda>null123     internal fun markAsAttached() = headToTail { if (!it.isAttached) it.markAsAttached() }
124 
<lambda>null125     internal fun runOnAttach() = headToTail { if (it.isAttached) it.onAttach() }
126 
127     /**
128      * Marks all nodes in the chain as detached from a [SubspaceLayout].
129      *
130      * This should be called *after* [runOnDetach] is called. We check that the node is attached as
131      * this method may be called more than necessary to ensure proper state.
132      */
<lambda>null133     internal fun markAsDetached() = tailToHead { if (it.isAttached) it.markAsDetached() }
134 
<lambda>null135     internal fun runOnDetach() = tailToHead { if (it.isAttached) it.onDetach() }
136 
measureChainnull137     internal fun measureChain(
138         constraints: VolumeConstraints,
139         wrappedMeasureBlock: (VolumeConstraints) -> Placeable,
140     ): Placeable {
141         val layoutNode = getAll<SubspaceLayoutModifierNode>().firstOrNull()
142         if (layoutNode == null || inMeasurePass) {
143             inMeasurePass = false
144             return wrappedMeasureBlock(constraints)
145         }
146         inMeasurePass = true
147         val placeable = layoutNode.requireCoordinator().measure(constraints)
148         inMeasurePass = false
149         return placeable
150     }
151 
152     /** Executes [block] for each modifier in the chain starting at the head. */
headToTailnull153     internal inline fun headToTail(block: (SubspaceModifier.Node) -> Unit) =
154         head.traverseSelfThenDescendants().forEach(block)
155 
156     /** Executes [block] for each modifier in the chain starting at the tail. */
157     internal inline fun tailToHead(block: (SubspaceModifier.Node) -> Unit) =
158         tail.traverseSelfThenAncestors().forEach(block)
159 
160     /** Returns all nodes of the given type in the chain in the declared modifier order. */
161     internal inline fun <reified T> getAll(): Sequence<T> =
162         head.traverseSelfThenDescendants().filterIsInstance<T>()
163 
164     /**
165      * Returns the last node of the given type in the chain if it exists, null otherwise.
166      *
167      * When considering only one instance of a modifier type, prefer the last instance.
168      */
169     internal inline fun <reified T> getLast(): T? =
170         tail.traverseSelfThenAncestors().findInstance<T>()
171 
172     private fun createAndInsertNodeAsChild(
173         element: SubspaceModifier,
174         parent: SubspaceModifier.Node,
175     ): SubspaceModifier.Node {
176         val node = (element as SubspaceModifierNodeElement<*>).create()
177         node.layoutNode = subspaceLayoutNode
178         return insertChild(node, parent)
179     }
180 
insertChildnull181     private fun insertChild(
182         node: SubspaceModifier.Node,
183         parent: SubspaceModifier.Node,
184     ): SubspaceModifier.Node {
185         val theChild = parent.child
186         if (theChild != null) {
187             theChild.parent = node
188             node.child = theChild
189         }
190         parent.child = node
191         node.parent = parent
192         if (!node.isAttached) {
193             node.markAsAttached()
194             node.onAttach()
195         }
196         return node
197     }
198 
updateNodenull199     private fun updateNode(node: SubspaceModifier.Node, modifier: SubspaceModifier) {
200         (modifier as SubspaceModifierNodeElement<*>).updateUnsafe(node)
201     }
202 
removeNodenull203     private fun removeNode(node: SubspaceModifier.Node): SubspaceModifier.Node {
204         val child = node.child
205         val parent = node.parent
206         if (child != null) {
207             child.parent = parent
208             node.child = null
209         }
210         if (parent != null) {
211             parent.child = child
212             node.parent = null
213         }
214         if (node.isAttached) {
215             node.onDetach()
216             node.markAsDetached()
217         }
218         return parent!!
219     }
220 }
221 
updateUnsafenull222 private fun <T : SubspaceModifier.Node> SubspaceModifierNodeElement<T>.updateUnsafe(
223     node: SubspaceModifier.Node
224 ) {
225     @Suppress("UNCHECKED_CAST") update(node as T)
226 }
227 
fillVectornull228 private fun SubspaceModifier.fillVector(
229     result: MutableList<SubspaceModifier>
230 ): MutableList<SubspaceModifier> {
231     val capacity = result.size.coerceAtLeast(16)
232     val stack = ArrayList<SubspaceModifier>(capacity).also { it.add(this) }
233     var predicate: ((SubspaceModifier) -> Boolean)? = null
234     while (stack.isNotEmpty()) {
235         when (val next = stack.removeAt(stack.size - 1)) {
236             is CombinedSubspaceModifier -> {
237                 stack.add(next.inner)
238                 stack.add(next.outer)
239             }
240             is SubspaceModifierNodeElement<*> -> result.add(next as SubspaceModifier)
241 
242             // some other [androidx.compose.ui.node.Modifier] implementation that we don't know
243             // about...
244             // late-allocate the predicate only once for the entire stack
245             else ->
246                 next.all(
247                     predicate
248                         ?: { element: SubspaceModifier ->
249                                 result.add(element)
250                                 true
251                             }
252                             .also { predicate = it }
253                 )
254         }
255     }
256     return result
257 }
258