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