1 /*
<lambda>null2  * Copyright 2019 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.geometry.Offset
21 import androidx.compose.ui.geometry.Rect
22 import androidx.compose.ui.layout.AlignmentLine
23 import androidx.compose.ui.layout.LayoutInfo
24 import androidx.compose.ui.layout.boundsInRoot
25 import androidx.compose.ui.layout.boundsInWindow
26 import androidx.compose.ui.layout.positionInRoot
27 import androidx.compose.ui.layout.positionInWindow
28 import androidx.compose.ui.layout.positionOnScreen
29 import androidx.compose.ui.node.LayoutNode
30 import androidx.compose.ui.node.NodeCoordinator
31 import androidx.compose.ui.node.Nodes
32 import androidx.compose.ui.node.RootForTest
33 import androidx.compose.ui.node.SemanticsModifierNode
34 import androidx.compose.ui.node.requireCoordinator
35 import androidx.compose.ui.node.requireLayoutNode
36 import androidx.compose.ui.node.touchBoundsInRoot
37 import androidx.compose.ui.node.useMinimumTouchTarget
38 import androidx.compose.ui.platform.ViewConfiguration
39 import androidx.compose.ui.unit.IntSize
40 import kotlin.contracts.ExperimentalContracts
41 import kotlin.contracts.contract
42 
43 internal fun SemanticsNode(layoutNode: LayoutNode, mergingEnabled: Boolean) =
44     SemanticsNode(
45         layoutNode.nodes.head(Nodes.Semantics)!!.node,
46         mergingEnabled,
47         layoutNode,
48         layoutNode.semanticsConfiguration ?: SemanticsConfiguration()
49     )
50 
51 internal fun SemanticsNode(
52     /*
53      * This is expected to be the outermost semantics modifier on a layout node.
54      */
55     outerSemanticsNode: SemanticsModifierNode,
56     /**
57      * mergingEnabled specifies whether mergeDescendants config has any effect.
58      *
59      * If true, then mergeDescendants nodes will merge up all properties from child semantics nodes
60      * and remove those children from "children", with the exception of nodes that themselves have
61      * mergeDescendants. If false, then mergeDescendants has no effect.
62      *
63      * mergingEnabled is typically true or false consistently on every node of a SemanticsNode tree.
64      */
65     mergingEnabled: Boolean,
66     /** The [LayoutNode] that this is associated with. */
67     layoutNode: LayoutNode = outerSemanticsNode.requireLayoutNode()
68 ) =
69     SemanticsNode(
70         outerSemanticsNode.node,
71         mergingEnabled,
72         layoutNode,
73         layoutNode.semanticsConfiguration ?: SemanticsConfiguration()
74     )
75 
76 /**
77  * A list of key/value pairs associated with a layout node or its subtree.
78  *
79  * Each SemanticsNode takes its id and initial key/value list from the outermost modifier on one
80  * layout node. It also contains the "collapsed" configuration of any other semantics modifiers on
81  * the same layout node, and if "mergeDescendants" is specified and enabled, also the "merged"
82  * configuration of its subtree.
83  */
84 class SemanticsNode
85 internal constructor(
86     internal val outerSemanticsNode: Modifier.Node,
87     val mergingEnabled: Boolean,
88     internal val layoutNode: LayoutNode,
89     internal val unmergedConfig: SemanticsConfiguration,
90 ) {
91     // We emit fake nodes for several cases. One is to prevent the content description clobbering
92     // issue. Another case is  temporary workaround to retrieve default role ordering for Button
93     // and other selection controls.
94     internal var isFake = false
95     private var fakeNodeParent: SemanticsNode? = null
96 
97     internal val isUnmergedLeafNode
98         get() =
99             !isFake &&
100                 replacedChildren.isEmpty() &&
101                 layoutNode.findClosestParentNode {
102                     it.semanticsConfiguration?.isMergingSemanticsOfDescendants == true
103                 } == null
104 
105     /** The [LayoutInfo] that this is associated with. */
106     val layoutInfo: LayoutInfo
107         get() = layoutNode
108 
109     /** The [root][RootForTest] this node is attached to. */
110     val root: RootForTest?
111         get() = layoutNode.owner?.rootForTest
112 
113     /**
114      * For newer AccessibilityNodeInfo-based integration test frameworks, it can be matched in the
115      * extras with key "androidx.compose.ui.semantics.id"
116      */
117     val id: Int = layoutNode.semanticsId
118 
119     // GEOMETRY
120 
121     /**
122      * The rectangle of the touchable area.
123      *
124      * If this is a clickable region, this is the rectangle that accepts touch input. This can be
125      * larger than [size] when the layout is less than [ViewConfiguration.minimumTouchTargetSize]
126      */
127     val touchBoundsInRoot: Rect
128         get() {
129             val entity =
130                 if (unmergedConfig.isMergingSemanticsOfDescendants) {
131                     (layoutNode.outerMergingSemantics ?: outerSemanticsNode)
132                 } else {
133                     outerSemanticsNode
134                 }
135             return entity.node.touchBoundsInRoot(unmergedConfig.useMinimumTouchTarget)
136         }
137 
138     /** The size of the bounding box for this node, with no clipping applied */
139     val size: IntSize
140         get() = findCoordinatorToGetBounds()?.size ?: IntSize.Zero
141 
142     /**
143      * The bounding box for this node relative to the root of this Compose hierarchy, with clipping
144      * applied. To get the bounds with no clipping applied, use Rect([positionInRoot],
145      * [size].toSize())
146      */
147     val boundsInRoot: Rect
148         get() = findCoordinatorToGetBounds()?.takeIf { it.isAttached }?.boundsInRoot() ?: Rect.Zero
149 
150     /**
151      * The position of this node relative to the root of this Compose hierarchy, with no clipping
152      * applied
153      */
154     val positionInRoot: Offset
155         get() =
156             findCoordinatorToGetBounds()?.takeIf { it.isAttached }?.positionInRoot() ?: Offset.Zero
157 
158     /**
159      * The bounding box for this node relative to the window, with clipping applied. To get the
160      * bounds with no clipping applied, use PxBounds([positionInWindow], [size].toSize())
161      */
162     val boundsInWindow: Rect
163         get() =
164             findCoordinatorToGetBounds()?.takeIf { it.isAttached }?.boundsInWindow() ?: Rect.Zero
165 
166     /** The position of this node relative to the window, with no clipping applied */
167     val positionInWindow: Offset
168         get() =
169             findCoordinatorToGetBounds()?.takeIf { it.isAttached }?.positionInWindow()
170                 ?: Offset.Zero
171 
172     /** The position of this node relative to the screen, with no clipping applied */
173     val positionOnScreen: Offset
174         get() =
175             findCoordinatorToGetBounds()?.takeIf { it.isAttached }?.positionOnScreen()
176                 ?: Offset.Zero
177 
178     /**
179      * The bounding box for this node relative to the parent semantics node, with clipping applied.
180      */
181     internal val boundsInParent: Rect
182         get() {
183             val parent = this.parent ?: return Rect.Zero
184             val currentCoordinates =
185                 findCoordinatorToGetBounds()?.takeIf { it.isAttached }?.coordinates
186                     ?: return Rect.Zero
187             return parent.outerSemanticsNode
188                 .requireCoordinator(Nodes.Semantics)
189                 .localBoundingBoxOf(currentCoordinates)
190         }
191 
192     /** Whether this node is transparent. */
193     internal val isTransparent: Boolean
194         get() = findCoordinatorToGetBounds()?.isTransparent() ?: false
195 
196     /**
197      * Returns the position of an [alignment line][AlignmentLine], or [AlignmentLine.Unspecified] if
198      * the line is not provided.
199      */
200     fun getAlignmentLinePosition(alignmentLine: AlignmentLine): Int {
201         return findCoordinatorToGetBounds()?.get(alignmentLine) ?: AlignmentLine.Unspecified
202     }
203 
204     // CHILDREN
205 
206     /**
207      * The list of semantics properties of this node.
208      *
209      * This includes all properties attached as modifiers to the current layout node. In addition,
210      * if mergeDescendants and mergingEnabled are both true, then it also includes the semantics
211      * properties of descendant nodes.
212      */
213     // TODO(b/184376083): This is too expensive for a val (full subtree recreation every call);
214     //               optimize this when the merging algorithm is improved.
215     val config: SemanticsConfiguration
216         get() {
217             if (isMergingSemanticsOfDescendants) {
218                 val mergedConfig = unmergedConfig.copy()
219                 mergeConfig(
220                     // TODO(b/384549982): Pass in the unmerged children instead of an empty list.
221                     mutableListOf(),
222                     mergedConfig
223                 )
224                 return mergedConfig
225             } else {
226                 return unmergedConfig
227             }
228         }
229 
230     private fun mergeConfig(
231         unmergedChildren: MutableList<SemanticsNode>,
232         mergedConfig: SemanticsConfiguration
233     ) {
234         if (!unmergedConfig.isClearingSemantics) {
235             unmergedChildren.forEachUnmergedChild { child ->
236                 // Don't merge children that themselves merge all their descendants (because that
237                 // indicates they're independently screen-reader-focusable).
238                 if (!child.isMergingSemanticsOfDescendants) {
239                     mergedConfig.mergeChild(child.unmergedConfig)
240                     child.mergeConfig(unmergedChildren, mergedConfig)
241                 }
242             }
243         }
244     }
245 
246     private val isMergingSemanticsOfDescendants: Boolean
247         get() = mergingEnabled && unmergedConfig.isMergingSemanticsOfDescendants
248 
249     internal fun unmergedChildren(
250         unmergedChildren: MutableList<SemanticsNode> = mutableListOf(),
251         includeFakeNodes: Boolean = false,
252         includeDeactivatedNodes: Boolean = false
253     ): List<SemanticsNode> {
254         // TODO(lmr): we should be able to do this more efficiently using visitSubtree
255         if (isFake) return emptyList()
256 
257         layoutNode.fillOneLayerOfSemanticsWrappers(unmergedChildren, includeDeactivatedNodes)
258 
259         if (includeFakeNodes) {
260             emitFakeNodes(unmergedChildren)
261         }
262 
263         return unmergedChildren
264     }
265 
266     private fun LayoutNode.fillOneLayerOfSemanticsWrappers(
267         list: MutableList<SemanticsNode>,
268         includeDeactivatedNodes: Boolean
269     ) {
270         // TODO(lmr): visitChildren would be great for this but we would lose the zSorted bit...
271         //  i wonder if we can optimize this for the common case of no z-sortedness going on.
272         zSortedChildren.forEach { child ->
273             // TODO(b/290936195): In some conditions it appears that children here can be
274             //  unattached. We just guard against that here as a "quick fix" but we need to
275             //  understand why this is happening and followup with a proper fix.
276             if (child.isAttached && (includeDeactivatedNodes || !child.isDeactivated)) {
277                 if (child.nodes.has(Nodes.Semantics)) {
278                     list.add(SemanticsNode(child, mergingEnabled))
279                 } else {
280                     child.fillOneLayerOfSemanticsWrappers(list, includeDeactivatedNodes)
281                 }
282             }
283         }
284     }
285 
286     /**
287      * Contains the children in inverse hit test order (i.e. paint order).
288      *
289      * Note that if mergingEnabled and mergeDescendants are both true, then there are no children
290      * (except those that are themselves mergeDescendants).
291      */
292     // TODO(b/184376083): This is too expensive for a val (full subtree recreation every call);
293     //               optimize this when the merging algorithm is improved.
294     val children: List<SemanticsNode>
295         get() = getChildren()
296 
297     /**
298      * Contains the children in inverse hit test order (i.e. paint order).
299      *
300      * Unlike [children] property that includes replaced semantics nodes in unmerged tree, here node
301      * marked as [clearAndSetSemantics] will not have children. This property is primarily used in
302      * Accessibility delegate.
303      */
304     internal val replacedChildren: List<SemanticsNode>
305         get() = getChildren(includeReplacedSemantics = false, includeFakeNodes = true)
306 
307     /**
308      * @param includeReplacedSemantics if true, the result will contain children of nodes marked as
309      *   [clearAndSetSemantics]. For accessibility we always use false, but in testing and debugging
310      *   we should be able to investigate both
311      * @param includeFakeNodes if true, the tree will include fake nodes. For accessibility we set
312      *   to true, but for testing purposes we don't want to expose the fake nodes and therefore set
313      *   to false. When Talkback can properly handle unmerged tree, fake nodes will be removed and
314      *   so will be this parameter.
315      * @param includeDeactivatedNodes set to true if you want to collect the nodes which are
316      *   deactivated. For example, the children of [androidx.compose.ui.layout.SubcomposeLayout]
317      *   which are retained to be reused in future are considered deactivated.
318      */
319     internal fun getChildren(
320         includeReplacedSemantics: Boolean = !mergingEnabled,
321         includeFakeNodes: Boolean = false,
322         includeDeactivatedNodes: Boolean = false
323     ): List<SemanticsNode> {
324         if (!includeReplacedSemantics && unmergedConfig.isClearingSemantics) {
325             return emptyList()
326         }
327 
328         val unmergedChildren = mutableListOf<SemanticsNode>()
329 
330         if (isMergingSemanticsOfDescendants) {
331             // In most common merging scenarios like Buttons, this will return nothing.
332             // In cases like a clickable Row itself containing a Button, this will
333             // return the Button as a child.
334             return findOneLayerOfMergingSemanticsNodes(unmergedChildren)
335         }
336 
337         return unmergedChildren(unmergedChildren, includeFakeNodes, includeDeactivatedNodes)
338     }
339 
340     /** Whether this SemanticNode is the root of a tree or not */
341     val isRoot: Boolean
342         get() = parent == null
343 
344     /** The parent of this node in the tree. */
345     val parent: SemanticsNode?
346         get() {
347             if (fakeNodeParent != null) return fakeNodeParent
348             var node: LayoutNode? = null
349             if (mergingEnabled) {
350                 node =
351                     this.layoutNode.findClosestParentNode {
352                         it.semanticsConfiguration?.isMergingSemanticsOfDescendants == true
353                     }
354             }
355 
356             if (node == null) {
357                 node = this.layoutNode.findClosestParentNode { it.nodes.has(Nodes.Semantics) }
358             }
359 
360             if (node == null) return null
361 
362             return SemanticsNode(node, mergingEnabled)
363         }
364 
365     private fun findOneLayerOfMergingSemanticsNodes(
366         unmergedChildren: MutableList<SemanticsNode>,
367         list: MutableList<SemanticsNode> = mutableListOf()
368     ): List<SemanticsNode> {
369         unmergedChildren.forEachUnmergedChild { child ->
370             if (child.isMergingSemanticsOfDescendants) {
371                 list.add(child)
372             } else {
373                 if (!child.unmergedConfig.isClearingSemantics) {
374                     child.findOneLayerOfMergingSemanticsNodes(unmergedChildren, list)
375                 }
376             }
377         }
378         return list
379     }
380 
381     @Suppress("BanInlineOptIn")
382     @OptIn(ExperimentalContracts::class)
383     private inline fun MutableList<SemanticsNode>.forEachUnmergedChild(
384         block: (SemanticsNode) -> Unit
385     ) {
386         contract { callsInPlace(block) }
387 
388         // This allows block() to invoke unmergedChildren() recursively
389         val start = size
390         unmergedChildren(this)
391         val end = size
392         for (i in start until end) {
393             block(this[i])
394         }
395     }
396 
397     /**
398      * If the node is merging the descendants, we'll use the outermost semantics modifier that has
399      * mergeDescendants == true to report the bounds, size and position of the node. For majority of
400      * use cases it means that accessibility bounds will be equal to the clickable area. Otherwise
401      * the outermost semantics will be used to report bounds, size and position.
402      */
403     internal fun findCoordinatorToGetBounds(): NodeCoordinator? {
404         if (isFake) return parent?.findCoordinatorToGetBounds()
405         val semanticsModifierNode = layoutNode.outerMergingSemantics ?: outerSemanticsNode
406         return semanticsModifierNode.requireCoordinator(Nodes.Semantics)
407     }
408 
409     // Fake nodes
410     private fun emitFakeNodes(unmergedChildren: MutableList<SemanticsNode>) {
411         val nodeRole = this.role
412         if (
413             nodeRole != null &&
414                 unmergedConfig.isMergingSemanticsOfDescendants &&
415                 unmergedChildren.isNotEmpty()
416         ) {
417             val fakeNode = fakeSemanticsNode(nodeRole) { this.role = nodeRole }
418             unmergedChildren.add(fakeNode)
419         }
420 
421         // Fake node for contentDescription clobbering issue
422         if (
423             unmergedConfig.contains(SemanticsProperties.ContentDescription) &&
424                 unmergedChildren.isNotEmpty() &&
425                 unmergedConfig.isMergingSemanticsOfDescendants
426         ) {
427             val contentDescription =
428                 this.unmergedConfig.getOrNull(SemanticsProperties.ContentDescription)?.firstOrNull()
429             if (contentDescription != null) {
430                 val fakeNode =
431                     fakeSemanticsNode(null) { this.contentDescription = contentDescription }
432                 unmergedChildren.add(0, fakeNode)
433             }
434         }
435     }
436 
437     private fun fakeSemanticsNode(
438         role: Role?,
439         properties: SemanticsPropertyReceiver.() -> Unit
440     ): SemanticsNode {
441         val configuration =
442             SemanticsConfiguration().also {
443                 it.isMergingSemanticsOfDescendants = false
444                 it.isClearingSemantics = false
445                 it.properties()
446             }
447         val fakeNode =
448             SemanticsNode(
449                 outerSemanticsNode =
450                     object : SemanticsModifierNode, Modifier.Node() {
451                         override fun SemanticsPropertyReceiver.applySemantics() {
452                             properties()
453                         }
454                     },
455                 mergingEnabled = false,
456                 layoutNode =
457                     LayoutNode(
458                         isVirtual = true,
459                         semanticsId =
460                             if (role != null) roleFakeNodeId() else contentDescriptionFakeNodeId()
461                     ),
462                 unmergedConfig = configuration
463             )
464         fakeNode.isFake = true
465         fakeNode.fakeNodeParent = this
466         return fakeNode
467     }
468 
469     internal fun copyWithMergingEnabled(): SemanticsNode {
470         return SemanticsNode(outerSemanticsNode, true, layoutNode, unmergedConfig)
471     }
472 }
473 
474 internal val LayoutNode.outerMergingSemantics: SemanticsModifierNode?
<lambda>null475     get() = nodes.firstFromHead(Nodes.Semantics) { it.shouldMergeDescendantSemantics }
476 
477 /**
478  * Executes [selector] on every parent of this [LayoutNode] and returns the closest [LayoutNode] to
479  * return `true` from [selector] or null if [selector] returns false for all ancestors.
480  */
findClosestParentNodenull481 internal inline fun LayoutNode.findClosestParentNode(
482     selector: (LayoutNode) -> Boolean
483 ): LayoutNode? {
484     var currentParent = this.parent
485     while (currentParent != null) {
486         if (selector(currentParent)) {
487             return currentParent
488         } else {
489             currentParent = currentParent.parent
490         }
491     }
492 
493     return null
494 }
495 
496 private val SemanticsNode.role
497     get() = this.unmergedConfig.getOrNull(SemanticsProperties.Role)
498 
SemanticsNodenull499 private fun SemanticsNode.contentDescriptionFakeNodeId() = this.id + 2_000_000_000
500 
501 private fun SemanticsNode.roleFakeNodeId() = this.id + 1_000_000_000
502