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.compose.ui.spatial
18 
19 import androidx.collection.IntObjectMap
20 import androidx.collection.intObjectMapOf
21 import androidx.collection.mutableObjectListOf
22 import androidx.compose.ui.ComposeUiFlags
23 import androidx.compose.ui.ExperimentalComposeUiApi
24 import androidx.compose.ui.currentTimeMillis
25 import androidx.compose.ui.geometry.MutableRect
26 import androidx.compose.ui.geometry.Offset
27 import androidx.compose.ui.graphics.Matrix
28 import androidx.compose.ui.graphics.isIdentity
29 import androidx.compose.ui.node.DelegatableNode
30 import androidx.compose.ui.node.DelegatableNode.RegistrationHandle
31 import androidx.compose.ui.node.LayoutNode
32 import androidx.compose.ui.node.NodeCoordinator
33 import androidx.compose.ui.node.Nodes
34 import androidx.compose.ui.postDelayed
35 import androidx.compose.ui.removePost
36 import androidx.compose.ui.unit.IntOffset
37 import androidx.compose.ui.unit.IntSize
38 import androidx.compose.ui.unit.plus
39 import androidx.compose.ui.unit.round
40 import androidx.compose.ui.unit.toOffset
41 import androidx.compose.ui.util.trace
42 import kotlin.math.max
43 
44 internal class RectManager(
45     /** [LayoutNode.semanticsId] to [LayoutNode] mapping, maintained by Owner. */
46     private val layoutNodes: IntObjectMap<LayoutNode> = intObjectMapOf(),
47 ) {
48     val rects: RectList = RectList()
49 
50     private val throttledCallbacks = ThrottledCallbacks()
51     private val callbacks = mutableObjectListOf<() -> Unit>()
52     private var isDirty = false
53     private var isScreenOrWindowDirty = false
54     private var isFragmented = false
55     private var dispatchToken: Any? = null
56     private var scheduledDispatchDeadline: Long = -1
57     private val dispatchLambda = {
58         dispatchToken = null
59         trace("OnPositionedDispatch") { dispatchCallbacks() }
60     }
61 
62     fun invalidate() {
63         isDirty = true
64     }
65 
66     fun updateOffsets(
67         screenOffset: IntOffset,
68         windowOffset: IntOffset,
69         viewToWindowMatrix: Matrix
70     ) {
71         val analysis = viewToWindowMatrix.analyzeComponents()
72         isScreenOrWindowDirty =
73             throttledCallbacks.updateOffsets(
74                 screenOffset,
75                 windowOffset,
76                 if (analysis.hasNonTranslationComponents) viewToWindowMatrix else null,
77             ) || isScreenOrWindowDirty
78     }
79 
80     // TODO: we need to make sure these are dispatched after draw if needed
81     fun dispatchCallbacks() {
82         val currentTime = currentTimeMillis()
83 
84         // For ThrottledCallbacks on global changes we need to make sure they are all called for any
85         // change
86         val isDispatchGlobalCallbacks = isDirty || isScreenOrWindowDirty
87 
88         // TODO: we need to move this to avoid double-firing
89         if (isDirty) {
90             isDirty = false
91             callbacks.forEach { it() }
92             rects.forEachUpdatedRect { id, topLeft, bottomRight ->
93                 throttledCallbacks.fireOnUpdatedRect(id, topLeft, bottomRight, currentTime)
94             }
95             rects.clearUpdated()
96         }
97         if (isScreenOrWindowDirty) {
98             isScreenOrWindowDirty = false
99             throttledCallbacks.fireOnRectChangedEntries(currentTime)
100         }
101         if (isDispatchGlobalCallbacks) {
102             throttledCallbacks.fireGlobalChangeEntries(currentTime)
103         }
104         // The hierarchy is "settled" in terms of nodes being added/removed for this frame
105         // This makes it a reasonable time to "defragment" the RectList data structure. This
106         // will keep operations on this data structure efficient over time. This is a fairly
107         // cheap operation to run, so we just do it every time
108         if (isFragmented) {
109             isFragmented = false
110             // TODO: if we want to take advantage of the "generation", we will be motivated to
111             //  call this less often. Alternatively we could track the number of remove() calls
112             //  we have made and only call defragment once it exceeds a certain percentage of
113             //  the overall list.
114             rects.defragment()
115         }
116         // this gets called frequently, but we might need to schedule it more often to ensure that
117         // debounced callbacks get fired
118         throttledCallbacks.triggerDebounced(currentTime)
119     }
120 
121     fun scheduleDebounceCallback(ensureSomethingScheduled: Boolean) {
122         val canExitEarly = !ensureSomethingScheduled || dispatchToken != null
123         val nextDeadline = throttledCallbacks.minDebounceDeadline
124         if (nextDeadline < 0 && canExitEarly) {
125             return
126         }
127         val currentScheduledDeadline = scheduledDispatchDeadline
128         if (currentScheduledDeadline == nextDeadline && canExitEarly) {
129             return
130         }
131         if (dispatchToken != null) {
132             removePost(dispatchToken)
133         }
134         val currentTime = currentTimeMillis()
135         val nextFrameIsh = currentTime + 16
136         val deadline = max(nextDeadline, nextFrameIsh)
137         scheduledDispatchDeadline = deadline
138         val delay = deadline - currentTime
139         dispatchToken = postDelayed(delay, dispatchLambda)
140     }
141 
142     fun currentRectInfo(id: Int, node: DelegatableNode): RelativeLayoutBounds? {
143         var result: RelativeLayoutBounds? = null
144         rects.withRect(id) { l, t, r, b ->
145             result =
146                 rectInfoFor(
147                     node = node,
148                     topLeft = packXY(l, t),
149                     bottomRight = packXY(r, b),
150                     windowOffset = throttledCallbacks.windowOffset,
151                     screenOffset = throttledCallbacks.screenOffset,
152                     viewToWindowMatrix = throttledCallbacks.viewToWindowMatrix,
153                 )
154         }
155         return result
156     }
157 
158     fun registerOnChangedCallback(callback: () -> Unit): Any? {
159         callbacks.add(callback)
160         return callback
161     }
162 
163     fun registerOnRectChangedCallback(
164         id: Int,
165         throttleMillis: Long,
166         debounceMillis: Long,
167         node: DelegatableNode,
168         callback: (RelativeLayoutBounds) -> Unit
169     ): RegistrationHandle {
170         return throttledCallbacks.registerOnRectChanged(
171             id,
172             throttleMillis,
173             debounceMillis,
174             node,
175             callback,
176         )
177     }
178 
179     fun registerOnGlobalLayoutCallback(
180         id: Int,
181         throttleMillis: Long,
182         debounceMillis: Long,
183         node: DelegatableNode,
184         callback: (RelativeLayoutBounds) -> Unit
185     ): RegistrationHandle {
186         return throttledCallbacks.registerOnGlobalChange(
187             id = id,
188             throttleMillis = throttleMillis,
189             debounceMillis = debounceMillis,
190             node = node,
191             callback = callback,
192         )
193     }
194 
195     fun unregisterOnChangedCallback(token: Any?) {
196         @Suppress("UNCHECKED_CAST")
197         token as? (() -> Unit) ?: return
198         callbacks.remove(token)
199     }
200 
201     fun invalidateCallbacksFor(layoutNode: LayoutNode) {
202         isDirty = true
203         rects.markUpdated(layoutNode.semanticsId)
204         scheduleDebounceCallback(ensureSomethingScheduled = true)
205     }
206 
207     fun updateFlagsFor(layoutNode: LayoutNode, focusable: Boolean, gesturable: Boolean) {
208         if (layoutNode.isAttached) {
209             rects.updateFlagsFor(
210                 value = layoutNode.semanticsId,
211                 focusable = focusable,
212                 gesturable = gesturable
213             )
214         }
215     }
216 
217     fun onLayoutLayerPositionalPropertiesChanged(layoutNode: LayoutNode) {
218         @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
219         val outerToInnerOffset = layoutNode.outerToInnerOffset()
220         if (outerToInnerOffset.isSet) {
221             // translational properties only. AABB still valid.
222             layoutNode.outerToInnerOffset = outerToInnerOffset
223             layoutNode.outerToInnerOffsetDirty = false
224             layoutNode.forEachChild {
225                 // NOTE: this calls rectlist.move(...) so does not need to be recursive
226                 // TODO: we could potentially move to a single call of `updateSubhierarchy(...)`
227                 onLayoutPositionChanged(it, it.outerCoordinator.position, false)
228             }
229             invalidateCallbacksFor(layoutNode)
230         } else {
231             // there are rotations/skews/scales going on, so we need to do a more expensive update
232             insertOrUpdateTransformedNodeSubhierarchy(layoutNode)
233         }
234     }
235 
236     fun onLayoutPositionChanged(
237         layoutNode: LayoutNode,
238         position: IntOffset,
239         firstPlacement: Boolean
240     ) {
241         @OptIn(ExperimentalComposeUiApi::class) if (!ComposeUiFlags.isRectTrackingEnabled) return
242         // Our goal here is to get the right "root" coordinates for every layout. We can use
243         // LayoutCoordinates.localToRoot to calculate this somewhat readily, however this function
244         // is getting called with a very high frequency and so it is important that extracting these
245         // coordinates remains relatively cheap to limit the overhead of this tracking. The
246         // LayoutCoordinates will traverse up the entire "spine" of the hierarchy, so as we do this
247         // calculation for many nodes, we would be making many redundant calculations. In order to
248         // minimize this, we store the "offsetFromRoot" of each layout node as we calculate it, and
249         // attempt to utilize this value when calculating it for a node that is below it.
250         // Additionally, we calculate and cache the parent's "outer to inner offset" which may
251         val delegate = layoutNode.measurePassDelegate
252         val width = delegate.measuredWidth
253         val height = delegate.measuredHeight
254 
255         val parent = layoutNode.parent
256         val offset: IntOffset
257 
258         val lastOffset = layoutNode.offsetFromRoot
259         val lastSize = layoutNode.lastSize
260         val lastWidth = lastSize.width
261         val lastHeight = lastSize.height
262 
263         var hasNonTranslationTransformations = false
264 
265         if (parent != null) {
266             val parentOffsetDirty = parent.outerToInnerOffsetDirty
267             val parentOffset = parent.offsetFromRoot
268             val prevOuterToInnerOffset = parent.outerToInnerOffset
269 
270             offset =
271                 if (parentOffset.isSet) {
272                     val parentOuterInnerOffset =
273                         if (parentOffsetDirty) {
274                             val it = parent.outerToInnerOffset()
275 
276                             parent.outerToInnerOffset = it
277                             parent.outerToInnerOffsetDirty = false
278                             it
279                         } else {
280                             prevOuterToInnerOffset
281                         }
282                     hasNonTranslationTransformations = !parentOuterInnerOffset.isSet
283                     parentOffset + parentOuterInnerOffset + position
284                 } else {
285                     layoutNode.outerCoordinator.positionInRoot()
286                 }
287         } else {
288             // root
289             offset = position
290         }
291 
292         // If unset is returned then that means there is a rotation/skew/scale
293         if (hasNonTranslationTransformations || !offset.isSet) {
294             insertOrUpdateTransformedNode(layoutNode, position, firstPlacement)
295             return
296         }
297 
298         layoutNode.offsetFromRoot = offset
299         layoutNode.lastSize = IntSize(width, height)
300 
301         val l = offset.x
302         val t = offset.y
303         val r = l + width
304         val b = t + height
305 
306         if (!firstPlacement && offset == lastOffset && lastWidth == width && lastHeight == height) {
307             return
308         }
309 
310         insertOrUpdate(layoutNode, firstPlacement, l, t, r, b)
311     }
312 
313     private fun insertOrUpdateTransformedNodeSubhierarchy(layoutNode: LayoutNode) {
314         layoutNode.forEachChild {
315             insertOrUpdateTransformedNode(it, it.outerCoordinator.position, false)
316             insertOrUpdateTransformedNodeSubhierarchy(it)
317         }
318     }
319 
320     private val cachedRect = MutableRect(0f, 0f, 0f, 0f)
321 
322     private fun insertOrUpdateTransformedNode(
323         layoutNode: LayoutNode,
324         position: IntOffset,
325         firstPlacement: Boolean,
326     ) {
327         val coord = layoutNode.outerCoordinator
328         val delegate = layoutNode.measurePassDelegate
329         val width = delegate.measuredWidth
330         val height = delegate.measuredHeight
331         val rect = cachedRect
332 
333         rect.set(
334             left = position.x.toFloat(),
335             top = position.y.toFloat(),
336             right = (position.x + width).toFloat(),
337             bottom = (position.y + height).toFloat(),
338         )
339 
340         coord.boundingRectInRoot(rect)
341 
342         val l = rect.left.toInt()
343         val t = rect.top.toInt()
344         val r = rect.right.toInt()
345         val b = rect.bottom.toInt()
346         val id = layoutNode.semanticsId
347         // NOTE: we call update here instead of move since the subhierarchy will not be moved by a
348         // simple delta since we are dealing with rotation/skew/scale/etc.
349         if (firstPlacement || !rects.update(id, l, t, r, b)) {
350             val parentId = layoutNode.parent?.semanticsId ?: -1
351             rects.insert(
352                 id,
353                 l,
354                 t,
355                 r,
356                 b,
357                 parentId = parentId,
358                 focusable = layoutNode.nodes.has(Nodes.FocusTarget),
359                 gesturable = layoutNode.nodes.has(Nodes.PointerInput)
360             )
361         }
362         invalidate()
363     }
364 
365     private fun insertOrUpdate(
366         layoutNode: LayoutNode,
367         firstPlacement: Boolean,
368         l: Int,
369         t: Int,
370         r: Int,
371         b: Int,
372     ) {
373         val id = layoutNode.semanticsId
374         if (firstPlacement || !rects.move(id, l, t, r, b)) {
375             val parentId = layoutNode.parent?.semanticsId ?: -1
376             rects.insert(
377                 id,
378                 l,
379                 t,
380                 r,
381                 b,
382                 parentId = parentId,
383                 focusable = layoutNode.nodes.has(Nodes.FocusTarget),
384                 gesturable = layoutNode.nodes.has(Nodes.PointerInput)
385             )
386         }
387         invalidate()
388     }
389 
390     private fun NodeCoordinator.positionInRoot(): IntOffset {
391         // TODO: can we use offsetFromRoot here to speed up calculation?
392         var position = Offset.Zero
393         var coordinator: NodeCoordinator? = this
394         while (coordinator != null) {
395             val layer = coordinator.layer
396             position += coordinator.position
397             coordinator = coordinator.wrappedBy
398             if (layer != null) {
399                 val matrix = layer.underlyingMatrix
400                 val analysis = matrix.analyzeComponents()
401                 if (analysis == 0b11) continue
402                 val hasNonTranslationComponents = analysis and 0b10 == 0
403                 if (hasNonTranslationComponents) {
404                     return IntOffset.Max
405                 }
406                 position = matrix.map(position)
407             }
408         }
409         return position.round()
410     }
411 
412     private fun NodeCoordinator.boundingRectInRoot(rect: MutableRect) {
413         // TODO: can we use offsetFromRoot here to speed up calculation?
414         var coordinator: NodeCoordinator? = this
415         while (coordinator != null) {
416             val layer = coordinator.layer
417             rect.translate(coordinator.position.toOffset())
418             coordinator = coordinator.wrappedBy
419             if (layer != null) {
420                 val matrix = layer.underlyingMatrix
421                 if (!matrix.isIdentity()) {
422                     matrix.map(rect)
423                 }
424             }
425         }
426     }
427 
428     private fun LayoutNode.outerToInnerOffset(): IntOffset {
429         val terminator = outerCoordinator
430         var position = Offset.Zero
431         var coordinator: NodeCoordinator? = innerCoordinator
432         while (coordinator != null) {
433             if (coordinator === terminator) break
434             val layer = coordinator.layer
435             position += coordinator.position
436             coordinator = coordinator.wrappedBy
437             if (layer != null) {
438                 val matrix = layer.underlyingMatrix
439                 val analysis = matrix.analyzeComponents()
440                 if (analysis.isIdentity) continue
441                 if (analysis.hasNonTranslationComponents) {
442                     return IntOffset.Max
443                 }
444                 position = matrix.map(position)
445             }
446         }
447         return position.round()
448     }
449 
450     fun remove(layoutNode: LayoutNode) {
451         rects.remove(layoutNode.semanticsId)
452         invalidate()
453         isFragmented = true
454     }
455 
456     /**
457      * For occlusion calculation, returns whether the [LayoutNode] corresponding to [targetId] is
458      * drawn first compared to the [LayoutNode] corresponding to [otherId].
459      *
460      * @return true when the target will be drawn first, false for everything else, including when
461      *   either is an ancestor of the other.
462      */
463     internal fun isTargetDrawnFirst(targetId: Int, otherId: Int): Boolean {
464         var nodeA = layoutNodes[targetId] ?: return false
465         var nodeB = layoutNodes[otherId] ?: return false
466 
467         if (nodeA.depth == 0 || nodeB.depth == 0) {
468             // Fail early when either is Root
469             return false
470         }
471 
472         while (nodeA.depth > nodeB.depth) {
473             nodeA = nodeA.parent ?: return false // Early check avoids these null parent cases
474         }
475 
476         if (nodeA === nodeB) {
477             // Node B was parent of Node A
478             return false
479         }
480 
481         while (nodeB.depth > nodeA.depth) {
482             nodeB = nodeB.parent ?: return false
483         }
484 
485         if (nodeA === nodeB) {
486             // Node A was parent of Node B
487             return false
488         }
489 
490         // Keep track of the branching parent for both nodes, the draw order of these decide how
491         // [targetId] and [otherId] compare.
492         var lastParentA: LayoutNode = nodeA
493         var lastParentB: LayoutNode = nodeB
494 
495         while (nodeA !== nodeB) {
496             lastParentA = nodeA
497             lastParentB = nodeB
498             // Null parent means we went past the root without finding common ancestor, shouldn't
499             // happen but we only return true when we know the target is drawn first
500             nodeA = nodeA.parent ?: return false
501             nodeB = nodeB.parent ?: return false
502         }
503 
504         // Lower zIndex is drawn first, otherwise lower placeOrder decides draw order
505         if (lastParentA.measurePassDelegate.zIndex == lastParentB.measurePassDelegate.zIndex) {
506             return lastParentA.placeOrder < lastParentB.placeOrder
507         } else {
508             return lastParentA.measurePassDelegate.zIndex < lastParentB.measurePassDelegate.zIndex
509         }
510     }
511 }
512 
513 /**
514  * Returns true if the offset is not IntOffset.Max. In this class we are using `IntOffset.Max` to be
515  * a sentinel value for "unspecified" so that we can avoid boxing.
516  */
517 private val IntOffset.isSet: Boolean
518     get() = this != IntOffset.Max
519 
520 /**
521  * We have logic that looks at whether or not a Matrix is an identity matrix, in which case we avoid
522  * doing expensive matrix calculations. Additionally, even if the matrix is non-identity, we can
523  * avoid a lot of extra work if the matrix is only doing translations, and no rotations/skews/scale.
524  *
525  * Since checking for these conditions involves a lot of overlapping work, we have this bespoke
526  * function which will return an Int that encodes the answer to both questions. If the 2nd bit of
527  * the result is set, this means that there are no rotations/skews/scales. If the first bit of the
528  * result is set, it means that there are no translations.
529  *
530  * This also means that the result of `0b11` indicates that it is the identity matrix.
531  */
Matrixnull532 private fun Matrix.analyzeComponents(): Int {
533     // See top-level comment
534     val v = values
535     if (v.size < 16) return 0
536     val isIdentity3x3 =
537         v[0] == 1f &&
538             v[1] == 0f &&
539             v[2] == 0f &&
540             v[4] == 0f &&
541             v[5] == 1f &&
542             v[6] == 0f &&
543             v[8] == 0f &&
544             v[9] == 0f &&
545             v[10] == 1f
546 
547     // translation components
548     val hasNoTranslationComponents = v[12] == 0f && v[13] == 0f && v[14] == 0f && v[15] == 1f
549 
550     return isIdentity3x3.toInt() shl 1 or hasNoTranslationComponents.toInt()
551 }
552 
553 @Suppress("NOTHING_TO_INLINE")
554 private inline val Int.isIdentity: Boolean
555     get() = this == 0b11
556 
557 @Suppress("NOTHING_TO_INLINE")
558 private inline val Int.hasNonTranslationComponents: Boolean
559     get() = this and 0b10 == 0
560 
toIntnull561 @Suppress("NOTHING_TO_INLINE") internal inline fun Boolean.toInt(): Int = if (this) 1 else 0
562