1 /*
2  * 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.semantics
18 
19 import androidx.collection.MutableObjectList
20 import androidx.compose.ui.layout.LayoutInfo
21 
22 /**
23  * This is an internal interface that can be used by [SemanticsListener]s to read semantic
24  * information from layout nodes. The root [SemanticsInfo] can be accessed using
25  * [SemanticsOwner.rootInfo], and particular [SemanticsInfo] can be looked up by their [semanticsId]
26  * by using [SemanticsOwner.get].
27  */
28 internal interface SemanticsInfo : LayoutInfo {
29     /** The semantics configuration (Semantic properties and actions) associated with this node. */
30     val semanticsConfiguration: SemanticsConfiguration?
31 
32     /** Whether the node is transparent. */
isTransparentnull33     fun isTransparent(): Boolean
34 
35     /**
36      * The [SemanticsInfo] of the parent.
37      *
38      * This includes parents that do not have any semantics modifiers.
39      */
40     override val parentInfo: SemanticsInfo?
41 
42     /**
43      * Returns the list of children.
44      *
45      * Please note that this list contains not placed items as well, so you have to manually filter
46      * them. Note that the object is reused so you shouldn't save it for later.
47      */
48     val childrenInfo: List<SemanticsInfo>
49 }
50 
51 /** The semantics parent (nearest ancestor which has semantic properties). */
52 internal fun SemanticsInfo.nearestParentThatHasSemantics(): SemanticsInfo? {
53     var parent = parentInfo
54     while (parent != null) {
55         if (parent.semanticsConfiguration != null) return parent
56         parent = parent.parentInfo
57     }
58     return null
59 }
60 
61 /** The nearest semantics ancestor that is merging descendants. */
findMergingSemanticsParentnull62 internal fun SemanticsInfo.findMergingSemanticsParent(): SemanticsInfo? {
63     var parent = parentInfo
64     while (parent != null) {
65         if (parent.semanticsConfiguration?.isMergingSemanticsOfDescendants == true) return parent
66         parent = parent.parentInfo
67     }
68     return null
69 }
70 
71 /** Merges the semantics of all the children of this node into a single SemanticsConfiguration. */
mergedSemanticsConfigurationnull72 internal fun SemanticsInfo.mergedSemanticsConfiguration(): SemanticsConfiguration? {
73     val unMergedConfig = semanticsConfiguration
74     if (
75         unMergedConfig == null ||
76             !unMergedConfig.isMergingSemanticsOfDescendants ||
77             unMergedConfig.isClearingSemantics
78     ) {
79         return unMergedConfig
80     }
81 
82     var mergedConfig: SemanticsConfiguration = unMergedConfig.copy()
83     val needsMerging: MutableObjectList<SemanticsInfo> =
84         MutableObjectList<SemanticsInfo>(childrenInfo.size).apply { addAll(childrenInfo) }
85 
86     @Suppress("Range") // isNotEmpty ensures removeAt is not called with -1.
87     while (needsMerging.isNotEmpty()) {
88         val childInfo = needsMerging.removeAt(needsMerging.lastIndex)
89         val childConfig = childInfo.semanticsConfiguration
90 
91         // Don't merge children that themselves merge all their descendants (because that
92         // indicates they are independently screen-reader-focusable).
93         if (childConfig == null || childConfig.isMergingSemanticsOfDescendants) continue
94 
95         // Merge child values.
96         mergedConfig.mergeChild(childConfig)
97 
98         // Merge children (unless this child is clearing semantics).
99         if (!childConfig.isClearingSemantics) needsMerging.addAll(childInfo.childrenInfo)
100     }
101 
102     return mergedConfig
103 }
104