1 /*
<lambda>null2  * 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.ui.semantics
18 
19 import androidx.collection.IntObjectMap
20 import androidx.collection.MutableIntObjectMap
21 import androidx.collection.intObjectMapOf
22 import androidx.collection.mutableIntObjectMapOf
23 import androidx.compose.ui.geometry.Rect
24 import androidx.compose.ui.node.LayoutNode
25 import androidx.compose.ui.unit.LayoutDirection
26 import androidx.compose.ui.util.fastForEach
27 import kotlin.math.max
28 import kotlin.math.min
29 
30 /**
31  * This function prepares a subtree for `sortByGeometryGroupings` by retrieving all non-container
32  * nodes and adding them to the list to be geometrically sorted. We recurse on containers (if they
33  * exist) and add their sorted children to an optional mapping. The list to be sorted and child
34  * mapping is passed into `sortByGeometryGroupings`.
35  */
36 internal fun SemanticsNode.subtreeSortedByGeometryGrouping(
37     isVisible: (SemanticsNode) -> Boolean,
38     isFocusableContainer: (SemanticsNode) -> Boolean,
39     listToSort: List<SemanticsNode>
40 ): List<SemanticsNode> {
41     // This should be mapping of [containerID: listOfSortedChildren], only populated if there
42     // are container nodes in this level. If there are container nodes, `containerMapToChildren`
43     // would look like {containerId: [sortedChild, sortedChild], containerId: [sortedChild]}
44     val containerMapToChildren = mutableIntObjectMapOf<List<SemanticsNode>>()
45     val geometryList = ArrayList<SemanticsNode>()
46 
47     listToSort.fastForEach { node ->
48         node.geometryDepthFirstSearch(
49             geometryList,
50             isVisible,
51             isFocusableContainer,
52             containerMapToChildren
53         )
54     }
55 
56     return sortByGeometryGroupings(geometryList, isFocusableContainer, containerMapToChildren)
57 }
58 
SemanticsNodenull59 private fun SemanticsNode.geometryDepthFirstSearch(
60     geometryList: ArrayList<SemanticsNode>,
61     isVisible: (SemanticsNode) -> Boolean,
62     isFocusableContainer: (SemanticsNode) -> Boolean,
63     containerMapToChildren: MutableIntObjectMap<List<SemanticsNode>>
64 ) {
65     // We only want to add children that are either traversalGroups or are
66     // screen reader focusable. The child must also be in the current pruned semantics tree.
67     val isTraversalGroup = unmergedConfig.getOrElse(SemanticsProperties.IsTraversalGroup) { false }
68 
69     if ((isTraversalGroup || isFocusableContainer(this)) && isVisible(this)) {
70         geometryList.add(this)
71     }
72     if (isTraversalGroup) {
73         // Recurse and record the container's children, sorted
74         containerMapToChildren[id] =
75             subtreeSortedByGeometryGrouping(isVisible, isFocusableContainer, children)
76     } else {
77         // Otherwise, continue adding children to the list that'll be sorted regardless of hierarchy
78         children.fastForEach { child ->
79             child.geometryDepthFirstSearch(
80                 geometryList,
81                 isVisible,
82                 isFocusableContainer,
83                 containerMapToChildren
84             )
85         }
86     }
87 }
88 
89 /**
90  * Returns the results of geometry groupings, which is determined from 1) grouping nodes into
91  * distinct, non-overlapping rows based on their top/bottom coordinates, then 2) sorting nodes
92  * within each row with the semantics comparator.
93  *
94  * This method approaches traversal order with more nuance than an approach considering only just
95  * hierarchy or only just an individual node's bounds.
96  *
97  * If [containerChildrenMapping] exists, there are additional children to add, as well as the sorted
98  * parent itself
99  */
sortByGeometryGroupingsnull100 internal fun SemanticsNode.sortByGeometryGroupings(
101     parentListToSort: List<SemanticsNode>,
102     isFocusableContainer: (SemanticsNode) -> Boolean = { false },
103     containerChildrenMapping: IntObjectMap<List<SemanticsNode>> = intObjectMapOf()
104 ): List<SemanticsNode> {
105     val layoutIsRtl = layoutInfo.layoutDirection == LayoutDirection.Rtl
106 
107     // RowGroupings list consists of pairs, first = a rectangle of the bounds of the row
108     // and second = the list of nodes in that row
109     val rowGroupings = ArrayList<Pair<Rect, MutableList<SemanticsNode>>>(parentListToSort.size / 2)
110 
111     for (entryIndex in 0..parentListToSort.lastIndex) {
112         val currEntry = parentListToSort[entryIndex]
113         // If this is the first entry, or vertical groups don't overlap
114         if (entryIndex == 0 || !placedEntryRowOverlaps(rowGroupings, currEntry)) {
115             val newRect = currEntry.boundsInWindow
116             rowGroupings.add(Pair(newRect, mutableListOf(currEntry)))
117         } // otherwise, we've already iterated through, found and placed it in a matching group
118     }
119 
120     // Sort the rows from top to bottom
121     rowGroupings.sortWith(TopBottomBoundsComparator)
122 
123     val returnList = ArrayList<SemanticsNode>()
124     val comparator = semanticComparators[if (layoutIsRtl) 0 else 1]
rownull125     rowGroupings.fastForEach { row ->
126         // Sort each individual row's parent nodes
127         row.second.sortWith(comparator)
128         returnList.addAll(row.second)
129     }
130 
131     returnList.sortWith(UnmergedConfigComparator)
132 
133     var i = 0
134     // Afterwards, go in and add the containers' children.
135     while (i <= returnList.lastIndex) {
136         val currNodeId = returnList[i].id
137         // If a parent node is a container, then add its children.
138         // Add all container's children after the container itself.
139         // Because we've already recursed on the containers children, the children should
140         // also be sorted by their traversal index
141         val containersChildrenList = containerChildrenMapping[currNodeId]
142         if (containersChildrenList != null) {
143             val containerIsScreenReaderFocusable = isFocusableContainer(returnList[i])
144             if (!containerIsScreenReaderFocusable) {
145                 // Container is removed if it is not screenreader-focusable
146                 returnList.removeAt(i)
147             } else {
148                 // Increase counter if the container was not removed
149                 i += 1
150             }
151             // Add all the container's children and increase counter by the number of children
152             returnList.addAll(i, containersChildrenList)
153             i += containersChildrenList.size
154         } else {
155             // Advance to the next item
156             i += 1
157         }
158     }
159     return returnList
160 }
161 
162 // check to see if this entry overlaps with any groupings in rowGroupings
placedEntryRowOverlapsnull163 private fun placedEntryRowOverlaps(
164     rowGroupings: ArrayList<Pair<Rect, MutableList<SemanticsNode>>>,
165     node: SemanticsNode
166 ): Boolean {
167     // Conversion to long is needed in order to utilize `until`, which has no float ver
168     val entryTopCoord = node.boundsInWindow.top
169     val entryBottomCoord = node.boundsInWindow.bottom
170     val entryIsEmpty = entryTopCoord >= entryBottomCoord
171 
172     for (currIndex in 0..rowGroupings.lastIndex) {
173         val currRect = rowGroupings[currIndex].first
174         val groupIsEmpty = currRect.top >= currRect.bottom
175         val groupOverlapsEntry =
176             !entryIsEmpty &&
177                 !groupIsEmpty &&
178                 max(entryTopCoord, currRect.top) < min(entryBottomCoord, currRect.bottom)
179 
180         // If it overlaps with this row group, update cover and add node
181         if (groupOverlapsEntry) {
182             val newRect =
183                 currRect.intersect(0f, entryTopCoord, Float.POSITIVE_INFINITY, entryBottomCoord)
184             // Replace the cover rectangle, copying over the old list of nodes
185             rowGroupings[currIndex] = Pair(newRect, rowGroupings[currIndex].second)
186             // Add current node
187             rowGroupings[currIndex].second.add(node)
188             // We've found an overlapping group, return true
189             return true
190         }
191     }
192 
193     // If we've made it here, then there are no groups our entry overlaps with
194     return false
195 }
196 
197 private val semanticComparators: Array<Comparator<SemanticsNode>> =
indexnull198     Array(2) { index ->
199         val comparator =
200             when (index) {
201                 0 -> RtlBoundsComparator
202                 else -> LtrBoundsComparator
203             }
204         comparator
205             // then compare by layoutNode's zIndex and placement order
206             .thenBy(LayoutNode.ZComparator) { it.layoutNode }
207             // then compare by semanticsId to break the tie somehow
208             .thenBy { it.id }
209     }
210 
211 private object LtrBoundsComparator : Comparator<SemanticsNode> {
comparenull212     override fun compare(a: SemanticsNode, b: SemanticsNode): Int {
213         // TODO: boundsInWindow is quite expensive and allocates several objects,
214         // we need to fix this since this is called during sorting
215         val ab = a.boundsInWindow
216         val bb = b.boundsInWindow
217         var r = ab.left.compareTo(bb.left)
218         if (r != 0) return r
219         r = ab.top.compareTo(bb.top)
220         if (r != 0) return r
221         r = ab.bottom.compareTo(bb.bottom)
222         if (r != 0) return r
223         return ab.right.compareTo(bb.right)
224     }
225 }
226 
227 private object RtlBoundsComparator : Comparator<SemanticsNode> {
comparenull228     override fun compare(a: SemanticsNode, b: SemanticsNode): Int {
229         // TODO: boundsInWindow is quite expensive and allocates several objects,
230         // we need to fix this since this is called during sorting
231         val ab = a.boundsInWindow
232         val bb = b.boundsInWindow
233         // We want to compare the right-most bounds, with the largest values first — that way
234         // the nodes will be sorted from right to left. Since `compareTo` returns a positive
235         // number if the first object is greater than the second, we want to call
236         // `b.compareTo(a)`, since we want our values in descending order, rather than
237         // ascending order.
238         var r = bb.right.compareTo(ab.right)
239         if (r != 0) return r
240         // Since in RTL layouts we still read from top to bottom, we compare the top and
241         // bottom bounds as usual.
242         r = ab.top.compareTo(bb.top)
243         if (r != 0) return r
244         r = ab.bottom.compareTo(bb.bottom)
245         if (r != 0) return r
246         // We also want to sort the left bounds in descending order, so call `b.compareTo(a)`
247         // here too.
248         return bb.left.compareTo(ab.left)
249     }
250 }
251 
252 private object TopBottomBoundsComparator : Comparator<Pair<Rect, MutableList<SemanticsNode>>> {
comparenull253     override fun compare(
254         a: Pair<Rect, MutableList<SemanticsNode>>,
255         b: Pair<Rect, MutableList<SemanticsNode>>
256     ): Int {
257         val r = a.first.top.compareTo(b.first.top)
258         if (r != 0) return r
259         return a.first.bottom.compareTo(b.first.bottom)
260     }
261 }
262 
263 // Kotlin `sortWith` should just pull out the highest traversal indices, but keep everything
264 // else in place. If the element does not have a `traversalIndex` then `0f` will be used.
anull265 private val UnmergedConfigComparator: (SemanticsNode, SemanticsNode) -> Int = { a, b ->
266     a.unmergedConfig
267         .getOrElse(SemanticsProperties.TraversalIndex) { 0f }
268         .compareTo(b.unmergedConfig.getOrElse(SemanticsProperties.TraversalIndex) { 0f })
269 }
270