1 /*
<lambda>null2  * Copyright 2020 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.compose.ui.Modifier
20 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
21 import androidx.compose.ui.node.ModifierNodeElement
22 import androidx.compose.ui.node.SemanticsModifierNode
23 import androidx.compose.ui.platform.AtomicInt
24 import androidx.compose.ui.platform.InspectorInfo
25 
26 private var lastIdentifier = AtomicInt(0)
27 
28 internal fun generateSemanticsId() = lastIdentifier.addAndGet(1)
29 
30 /**
31  * A [Modifier.Element] that adds semantics key/value for use in testing, accessibility, and similar
32  * use cases.
33  */
34 @JvmDefaultWithCompatibility
35 interface SemanticsModifier : Modifier.Element {
36     @Deprecated(
37         message =
38             "SemanticsModifier.id is now unused and has been set to a fixed value. " +
39                 "Retrieve the id from LayoutInfo instead.",
40         replaceWith = ReplaceWith("")
41     )
42     val id: Int
43         get() = -1
44 
45     /**
46      * The SemanticsConfiguration holds substantive data, especially a list of key/value pairs such
47      * as (label -> "buttonName").
48      */
49     val semanticsConfiguration: SemanticsConfiguration
50 }
51 
52 internal class CoreSemanticsModifierNode(
53     var mergeDescendants: Boolean,
54     var isClearingSemantics: Boolean,
55     var properties: SemanticsPropertyReceiver.() -> Unit
56 ) : Modifier.Node(), SemanticsModifierNode {
57     override val shouldClearDescendantSemantics: Boolean
58         get() = isClearingSemantics
59 
60     override val shouldMergeDescendantSemantics: Boolean
61         get() = mergeDescendants
62 
applySemanticsnull63     override fun SemanticsPropertyReceiver.applySemantics() {
64         properties()
65     }
66 }
67 
68 internal class EmptySemanticsModifier : Modifier.Node(), SemanticsModifierNode {
applySemanticsnull69     override fun SemanticsPropertyReceiver.applySemantics() {}
70 }
71 
72 /**
73  * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
74  *
75  * The provided lambda receiver scope provides "key = value"-style setters for any
76  * [SemanticsPropertyKey]. Additionally, chaining multiple semantics modifiers is also a supported
77  * style.
78  *
79  * The resulting semantics produce two [SemanticsNode] trees:
80  *
81  * The "unmerged tree" rooted at [SemanticsOwner.unmergedRootSemanticsNode] has one [SemanticsNode]
82  * per layout node which has any [SemanticsModifier] on it. This [SemanticsNode] contains all the
83  * properties set in all the [SemanticsModifier]s on that node.
84  *
85  * The "merged tree" rooted at [SemanticsOwner.rootSemanticsNode] has equal-or-fewer nodes: it
86  * simplifies the structure based on [mergeDescendants] and [clearAndSetSemantics]. For most
87  * purposes (especially accessibility, or the testing of accessibility), the merged semantics tree
88  * should be used.
89  *
90  * @param mergeDescendants Whether the semantic information provided by the owning component and its
91  *   descendants should be treated as one logical entity. Most commonly set on
92  *   screen-reader-focusable items such as buttons or form fields. In the merged semantics tree, all
93  *   descendant nodes (except those themselves marked [mergeDescendants]) will disappear from the
94  *   tree, and their properties will get merged into the parent's configuration (using a merging
95  *   algorithm that varies based on the type of property -- for example, text properties will get
96  *   concatenated, separated by commas). In the unmerged semantics tree, the node is simply marked
97  *   with [SemanticsConfiguration.isMergingSemanticsOfDescendants].
98  * @param properties properties to add to the semantics. [SemanticsPropertyReceiver] will be
99  *   provided in the scope to allow access for common properties and its values.
100  *
101  *   Note: The [properties] block should be used to set semantic properties or semantic actions.
102  *   Don't call [SemanticsModifierNode.applySemantics] from within the [properties] block. It will
103  *   result in an infinite loop.
104  */
semanticsnull105 fun Modifier.semantics(
106     mergeDescendants: Boolean = false,
107     properties: (SemanticsPropertyReceiver.() -> Unit)
108 ): Modifier =
109     this then AppendedSemanticsElement(mergeDescendants = mergeDescendants, properties = properties)
110 
111 // Implement SemanticsModifier to allow tooling to inspect the semantics configuration
112 internal class AppendedSemanticsElement(
113     val mergeDescendants: Boolean,
114     val properties: (SemanticsPropertyReceiver.() -> Unit)
115 ) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
116 
117     // This should only ever be called by layout inspector
118     override val semanticsConfiguration: SemanticsConfiguration
119         get() =
120             SemanticsConfiguration().apply {
121                 isMergingSemanticsOfDescendants = mergeDescendants
122                 properties()
123             }
124 
125     override fun create(): CoreSemanticsModifierNode {
126         return CoreSemanticsModifierNode(
127             mergeDescendants = mergeDescendants,
128             isClearingSemantics = false,
129             properties = properties
130         )
131     }
132 
133     override fun update(node: CoreSemanticsModifierNode) {
134         node.mergeDescendants = mergeDescendants
135         node.properties = properties
136     }
137 
138     override fun InspectorInfo.inspectableProperties() {
139         name = "semantics"
140         properties["mergeDescendants"] = mergeDescendants
141         addSemanticsPropertiesFrom(semanticsConfiguration)
142     }
143 
144     override fun equals(other: Any?): Boolean {
145         if (this === other) return true
146         if (other !is AppendedSemanticsElement) return false
147 
148         if (mergeDescendants != other.mergeDescendants) return false
149         if (properties !== other.properties) return false
150 
151         return true
152     }
153 
154     override fun hashCode(): Int {
155         var result = mergeDescendants.hashCode()
156         result = 31 * result + properties.hashCode()
157         return result
158     }
159 }
160 
161 /**
162  * Clears the semantics of all the descendant nodes and sets new semantics.
163  *
164  * In the merged semantics tree, this clears the semantic information provided by the node's
165  * descendants (but not those of the layout node itself, if any) and sets the provided semantics.
166  * (In the unmerged tree, the semantics node is marked with
167  * "[SemanticsConfiguration.isClearingSemantics]", but nothing is actually cleared.)
168  *
169  * Compose's default semantics provide baseline usability for screen-readers, but this can be used
170  * to provide a more polished screen-reader experience: for example, clearing the semantics of a
171  * group of tiny buttons, and setting equivalent actions on the card containing them.
172  *
173  * @param properties properties to add to the semantics. [SemanticsPropertyReceiver] will be
174  *   provided in the scope to allow access for common properties and its values.
175  *
176  *   Note: The [properties] lambda should be used to set semantic properties or semantic actions.
177  *   Don't call [SemanticsModifierNode.applySemantics] from within the [properties] block. It will
178  *   result in an infinite loop.
179  */
Modifiernull180 fun Modifier.clearAndSetSemantics(properties: (SemanticsPropertyReceiver.() -> Unit)): Modifier =
181     this then ClearAndSetSemanticsElement(properties)
182 
183 // Implement SemanticsModifier to allow tooling to inspect the semantics configuration
184 internal class ClearAndSetSemanticsElement(val properties: SemanticsPropertyReceiver.() -> Unit) :
185     ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
186 
187     // This should only ever be called by layout inspector
188     override val semanticsConfiguration: SemanticsConfiguration
189         get() =
190             SemanticsConfiguration().apply {
191                 isMergingSemanticsOfDescendants = false
192                 isClearingSemantics = true
193                 properties()
194             }
195 
196     override fun create(): CoreSemanticsModifierNode {
197         return CoreSemanticsModifierNode(
198             mergeDescendants = false,
199             isClearingSemantics = true,
200             properties = properties
201         )
202     }
203 
204     override fun update(node: CoreSemanticsModifierNode) {
205         node.properties = properties
206     }
207 
208     override fun InspectorInfo.inspectableProperties() {
209         name = "clearAndSetSemantics"
210         addSemanticsPropertiesFrom(semanticsConfiguration)
211     }
212 
213     override fun equals(other: Any?): Boolean {
214         if (this === other) return true
215         if (other !is ClearAndSetSemanticsElement) return false
216 
217         if (properties !== other.properties) return false
218 
219         return true
220     }
221 
222     override fun hashCode(): Int {
223         return properties.hashCode()
224     }
225 }
226 
addSemanticsPropertiesFromnull227 private fun InspectorInfo.addSemanticsPropertiesFrom(
228     semanticsConfiguration: SemanticsConfiguration
229 ) {
230     properties["properties"] =
231         semanticsConfiguration.associate { (key, value) -> key.name to value }
232 }
233