1 /*
2 * 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 package androidx.compose.ui.node
17
18 import androidx.compose.ui.Modifier
19 import androidx.compose.ui.areObjectsOfSameType
20 import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction
21
22 /**
23 * Allows [Modifier.Node] classes to traverse up/down the Node tree for classes of the same type or
24 * for a particular key (traverseKey).
25 *
26 * Note: The actual traversals are done in extension functions (see bottom of file).
27 */
28 interface TraversableNode : DelegatableNode {
29 val traverseKey: Any
30
31 companion object {
32 /**
33 * Tree traversal actions for the traverseDescendantsIf related functions:
34 * - Continue - continue the traversal
35 * - SkipSubtreeAndContinue - continue the traversal BUT skip the matching node's subtree
36 * (this is a rarer case)
37 * - CancelTraversal - cancels the traversal (returns from function call)
38 *
39 * To see examples of all the actions, see TraversableModifierNodeTest. For a return/cancel
40 * example specifically, see
41 * traverseSubtreeWithSameKeyIf_cancelTraversalOfDifferentClassSameKey().
42 */
43 enum class TraverseDescendantsAction {
44 ContinueTraversal,
45 SkipSubtreeAndContinueTraversal,
46 CancelTraversal
47 }
48 }
49 }
50
51 // *********** Nearest Traversable Ancestor methods ***********
52 /** Finds the nearest traversable ancestor with a matching [key]. */
DelegatableNodenull53 fun DelegatableNode.findNearestAncestor(key: Any?): TraversableNode? {
54 visitAncestors(Nodes.Traversable) {
55 if (key == it.traverseKey) {
56 return it
57 }
58 }
59 return null
60 }
61
62 /** Finds the nearest ancestor of the same class and key. */
findNearestAncestornull63 fun <T> T.findNearestAncestor(): T? where T : TraversableNode {
64 visitAncestors(Nodes.Traversable) {
65 if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
66 @Suppress("UNCHECKED_CAST") return it as T
67 }
68 }
69 return null
70 }
71
72 // *********** Traverse Ancestors methods ***********
73 /**
74 * Executes [block] for all ancestors with a matching [key].
75 *
76 * Note: The parameter [block]'s return boolean value will determine if the traversal will continue
77 * (true = continue, false = cancel).
78 *
79 * @sample androidx.compose.ui.samples.traverseAncestorsWithKeyDemo
80 */
traverseAncestorsnull81 fun DelegatableNode.traverseAncestors(key: Any?, block: (TraversableNode) -> Boolean) {
82 visitAncestors(Nodes.Traversable) {
83 val continueTraversal =
84 if (key == it.traverseKey) {
85 block(it)
86 } else {
87 true
88 }
89 if (!continueTraversal) return
90 }
91 }
92
93 /**
94 * Executes [block] for all ancestors of the same class and key.
95 *
96 * Note: The parameter [block]'s return boolean value will determine if the traversal will continue
97 * (true = continue, false = cancel).
98 *
99 * @sample androidx.compose.ui.samples.traverseAncestorsDemo
100 */
traverseAncestorsnull101 fun <T> T.traverseAncestors(block: (T) -> Boolean) where T : TraversableNode {
102 visitAncestors(Nodes.Traversable) {
103 val continueTraversal =
104 if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
105 @Suppress("UNCHECKED_CAST") block(it as T)
106 } else {
107 true
108 }
109 if (!continueTraversal) return
110 }
111 }
112
113 // *********** Traverse Children methods ***********
114 /**
115 * Executes [block] for all direct children of the node with a matching [key].
116 *
117 * Note 1: This stops at the children and does not include grandchildren and so on down the tree.
118 *
119 * Note 2: The parameter [block]'s return boolean value will determine if the traversal will
120 * continue (true = continue, false = cancel).
121 *
122 * @sample androidx.compose.ui.samples.traverseChildrenWithKeyDemo
123 */
DelegatableNodenull124 fun DelegatableNode.traverseChildren(key: Any?, block: (TraversableNode) -> Boolean) {
125 visitChildren(Nodes.Traversable) {
126 val continueTraversal =
127 if (key == it.traverseKey) {
128 block(it)
129 } else {
130 true
131 }
132 if (!continueTraversal) return
133 }
134 }
135
136 /**
137 * Executes [block] for all direct children of the node that are of the same class.
138 *
139 * Note 1: This stops at the children and does not include grandchildren and so on down the tree.
140 *
141 * Note 2: The parameter [block]'s return boolean value will determine if the traversal will
142 * continue (true = continue, false = cancel).
143 *
144 * @sample androidx.compose.ui.samples.traverseChildrenDemo
145 */
traverseChildrennull146 fun <T> T.traverseChildren(block: (T) -> Boolean) where T : TraversableNode {
147 visitChildren(Nodes.Traversable) {
148 val continueTraversal =
149 if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
150 @Suppress("UNCHECKED_CAST") block(it as T)
151 } else {
152 true
153 }
154 if (!continueTraversal) return
155 }
156 }
157
158 // *********** Traverse Descendants methods ***********
159 /**
160 * Conditionally executes [block] for each descendant with a matching [key].
161 *
162 * Note 1: For nodes that do not have the same key, it will continue to execute the [block] for
163 * descendants below that non-matching node (where there may be a node that matches).
164 *
165 * Note 2: The parameter [block]'s return value [TraverseDescendantsAction] will determine the next
166 * step in the traversal.
167 *
168 * @sample androidx.compose.ui.samples.traverseDescendantsWithKeyDemo
169 */
DelegatableNodenull170 fun DelegatableNode.traverseDescendants(
171 key: Any?,
172 block: (TraversableNode) -> TraverseDescendantsAction
173 ) {
174 visitSubtreeIf(Nodes.Traversable) {
175 val action =
176 if (key == it.traverseKey) {
177 block(it)
178 } else {
179 TraverseDescendantsAction.ContinueTraversal
180 }
181 if (action == TraverseDescendantsAction.CancelTraversal) return
182
183 // visitSubtreeIf() requires a true to continue down the subtree and a false if you
184 // want to skip the subtree, so we check if the action is NOT EQUAL to the subtree
185 // to trigger false if the action is Skip subtree and true otherwise.
186 action != TraverseDescendantsAction.SkipSubtreeAndContinueTraversal
187 }
188 }
189
190 /**
191 * Conditionally executes [block] for each descendant of the same class.
192 *
193 * Note 1: For nodes that do not have the same key, it will continue to execute the [block] for the
194 * descendants below that non-matching node (where there may be a node that matches).
195 *
196 * Note 2: The parameter [block]'s return value [TraverseDescendantsAction] will determine the next
197 * step in the traversal.
198 *
199 * @sample androidx.compose.ui.samples.traverseDescendantsDemo
200 */
traverseDescendantsnull201 fun <T> T.traverseDescendants(block: (T) -> TraverseDescendantsAction) where T : TraversableNode {
202 visitSubtreeIf(Nodes.Traversable) {
203 val action =
204 if (this.traverseKey == it.traverseKey && areObjectsOfSameType(this, it)) {
205 @Suppress("UNCHECKED_CAST") block(it as T)
206 } else {
207 TraverseDescendantsAction.ContinueTraversal
208 }
209 if (action == TraverseDescendantsAction.CancelTraversal) return
210
211 // visitSubtreeIf() requires a true to continue down the subtree and a false if you
212 // want to skip the subtree, so we check if the action is NOT EQUAL to the subtree
213 // to trigger false if the action is Skip subtree and true otherwise.
214 action != TraverseDescendantsAction.SkipSubtreeAndContinueTraversal
215 }
216 }
217