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