1 /*
<lambda>null2  * Copyright 2022 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.focus
18 
19 import androidx.collection.MutableScatterSet
20 import androidx.collection.mutableScatterSetOf
21 import androidx.compose.ui.ComposeUiFlags
22 import androidx.compose.ui.ExperimentalComposeUiApi
23 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
24 import androidx.compose.ui.focus.FocusStateImpl.Inactive
25 import androidx.compose.ui.internal.checkPrecondition
26 import androidx.compose.ui.node.Nodes
27 import androidx.compose.ui.node.visitAncestors
28 import androidx.compose.ui.node.visitSelfAndChildren
29 import androidx.compose.ui.util.fastForEach
30 
31 /**
32  * The [FocusInvalidationManager] allows us to schedule focus related nodes for invalidation. These
33  * nodes are invalidated after onApplyChanges. It does this by registering an onApplyChangesListener
34  * when nodes are scheduled for invalidation.
35  */
36 internal class FocusInvalidationManager(
37     private val onRequestApplyChangesListener: (() -> Unit) -> Unit,
38     private val invalidateOwnerFocusState: () -> Unit,
39     private val rootFocusStateFetcher: () -> FocusState,
40     private val activeFocusTargetNodeFetcher: () -> FocusTargetNode?
41 ) {
42     private val focusTargetNodes = mutableScatterSetOf<FocusTargetNode>()
43     private val focusEventNodes = mutableScatterSetOf<FocusEventModifierNode>()
44     private val focusTargetNodesLegacy = mutableListOf<FocusTargetNode>()
45     private val focusEventNodesLegacy = mutableListOf<FocusEventModifierNode>()
46     private val focusPropertiesNodesLegacy = mutableListOf<FocusPropertiesModifierNode>()
47     private val focusTargetsWithInvalidatedFocusEventsLegacy = mutableListOf<FocusTargetNode>()
48 
49     private var isInvalidationScheduled = false
50 
51     fun scheduleInvalidation(node: FocusTargetNode) {
52         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
53             focusTargetNodes.scheduleInvalidation(node)
54         } else {
55             focusTargetNodesLegacy.scheduleInvalidationLegacy(node)
56         }
57     }
58 
59     fun scheduleInvalidation(node: FocusEventModifierNode) {
60         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
61             focusEventNodes.scheduleInvalidation(node)
62         } else {
63             focusEventNodesLegacy.scheduleInvalidationLegacy(node)
64         }
65     }
66 
67     fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
68         focusPropertiesNodesLegacy.scheduleInvalidationLegacy(node)
69     }
70 
71     fun scheduleInvalidationForOwner() {
72         setUpOnRequestApplyChangesListener()
73     }
74 
75     fun hasPendingInvalidation(): Boolean {
76         return if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
77             isInvalidationScheduled
78         } else {
79             focusTargetNodesLegacy.isNotEmpty() ||
80                 focusPropertiesNodesLegacy.isNotEmpty() ||
81                 focusEventNodesLegacy.isNotEmpty()
82         }
83     }
84 
85     private fun <T> MutableScatterSet<T>.scheduleInvalidation(node: T) {
86         if (add(node)) {
87             setUpOnRequestApplyChangesListener()
88         }
89     }
90 
91     private fun setUpOnRequestApplyChangesListener() {
92         if (!isInvalidationScheduled) {
93             onRequestApplyChangesListener.invoke(::invalidateNodes)
94             isInvalidationScheduled = true
95         }
96     }
97 
98     private fun <T> MutableList<T>.scheduleInvalidationLegacy(node: T) {
99         if (add(node)) {
100             // If this is the first node scheduled for invalidation,
101             // we set up a listener that runs after onApplyChanges.
102             if (
103                 focusTargetNodesLegacy.size +
104                     focusEventNodesLegacy.size +
105                     focusPropertiesNodesLegacy.size == 1
106             ) {
107                 onRequestApplyChangesListener.invoke(::invalidateNodes)
108             }
109         }
110     }
111 
112     private fun invalidateNodes() {
113         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
114             invalidateNodesOptimized()
115         } else {
116             invalidateNodesLegacy()
117         }
118     }
119 
120     private fun invalidateNodesOptimized() {
121         val activeFocusTargetNode = activeFocusTargetNodeFetcher()
122         if (activeFocusTargetNode == null) {
123             // If there is no active focus node, dispatch the Inactive state to event nodes.
124             focusEventNodes.forEach { it.onFocusEvent(Inactive) }
125         } else if (activeFocusTargetNode.isAttached) {
126             if (focusTargetNodes.contains(activeFocusTargetNode)) {
127                 activeFocusTargetNode.invalidateFocus()
128             }
129 
130             val activeFocusTargetNodeState = activeFocusTargetNode.focusState
131             var traversedFocusTargetCount = 0
132             activeFocusTargetNode.visitAncestors(
133                 Nodes.FocusTarget or Nodes.FocusEvent,
134                 includeSelf = true
135             ) {
136                 // Keep track of whether we traversed past the first target node ancestor of the
137                 // active focus target node, so that all the subsequent event nodes are sent the
138                 // ActiveParent state rather than Active/Captured.
139                 if (it.isKind(Nodes.FocusTarget)) traversedFocusTargetCount++
140 
141                 // Don't send events to event nodes that were not invalidated.
142                 if (it !is FocusEventModifierNode || !focusEventNodes.contains(it)) {
143                     return@visitAncestors
144                 }
145 
146                 // Event nodes that are between the active focus target and the first ancestor
147                 // target receive the Active/Captured state, while the event nodes further up
148                 // receive the ActiveParent state.
149                 if (traversedFocusTargetCount <= 1) {
150                     it.onFocusEvent(activeFocusTargetNodeState)
151                 } else {
152                     it.onFocusEvent(ActiveParent)
153                 }
154 
155                 // Remove the event node from the list of invalidated nodes, so that we only send a
156                 // single event per node.
157                 focusEventNodes.remove(it)
158             }
159 
160             // Send the Inactive state to the event nodes that are not in the active node ancestors.
161             focusEventNodes.forEach { it.onFocusEvent(Inactive) }
162         }
163 
164         invalidateOwnerFocusState()
165         focusTargetNodes.clear()
166         focusEventNodes.clear()
167         isInvalidationScheduled = false
168     }
169 
170     private fun invalidateNodesLegacy() {
171         if (!rootFocusStateFetcher().hasFocus) {
172             // If root doesn't have focus, skip full invalidation and default to the Inactive state.
173             focusEventNodesLegacy.fastForEach { it.onFocusEvent(Inactive) }
174             focusTargetNodesLegacy.fastForEach { node ->
175                 if (node.isAttached && !node.isInitialized()) {
176                     node.initializeFocusState(Inactive)
177                 }
178             }
179             focusTargetNodesLegacy.clear()
180             focusEventNodesLegacy.clear()
181             focusPropertiesNodesLegacy.clear()
182             focusTargetsWithInvalidatedFocusEventsLegacy.clear()
183             invalidateOwnerFocusState()
184             return
185         }
186 
187         // Process all the invalidated FocusProperties nodes.
188         focusPropertiesNodesLegacy.fastForEach {
189             // We don't need to invalidate a focus properties node if it was scheduled for
190             // invalidation earlier in the composition but was then removed.
191             if (!it.node.isAttached) return@fastForEach
192 
193             it.visitSelfAndChildren(Nodes.FocusTarget) { focusTarget ->
194                 focusTargetNodesLegacy.add(focusTarget)
195             }
196         }
197         focusPropertiesNodesLegacy.clear()
198 
199         // Process all the focus events nodes.
200         focusEventNodesLegacy.fastForEach { focusEventNode ->
201             // When focus nodes are removed, the corresponding focus events are scheduled for
202             // invalidation. If the focus event was also removed, we don't need to invalidate it.
203             // We call onFocusEvent with the default value, just to make it easier for the user,
204             // so that they don't have to keep track of whether they caused a focused item to be
205             // removed (Which would cause it to lose focus).
206             if (!focusEventNode.node.isAttached) {
207                 focusEventNode.onFocusEvent(Inactive)
208                 return@fastForEach
209             }
210 
211             var requiresUpdate = true
212             var aggregatedNode = false
213             var focusTarget: FocusTargetNode? = null
214             focusEventNode.visitSelfAndChildren(Nodes.FocusTarget) {
215 
216                 // If there are multiple focus targets associated with this focus event node,
217                 // we need to calculate the aggregated state.
218                 if (focusTarget != null) {
219                     aggregatedNode = true
220                 }
221 
222                 focusTarget = it
223 
224                 // If the associated focus node is already scheduled for invalidation, it will
225                 // send an onFocusEvent if the invalidation causes a focus state change.
226                 // However this onFocusEvent was invalidated, so we have to ensure that we call
227                 // onFocusEvent even if the focus state didn't change.
228                 if (it in focusTargetNodesLegacy) {
229                     requiresUpdate = false
230                     focusTargetsWithInvalidatedFocusEventsLegacy.add(it)
231                     return@visitSelfAndChildren
232                 }
233             }
234 
235             if (requiresUpdate) {
236                 focusEventNode.onFocusEvent(
237                     if (aggregatedNode) {
238                         focusEventNode.getFocusState()
239                     } else {
240                         focusTarget?.focusState ?: Inactive
241                     }
242                 )
243             }
244         }
245         focusEventNodesLegacy.clear()
246 
247         // Process all the focus target nodes.
248         focusTargetNodesLegacy.fastForEach {
249             // We don't need to invalidate the focus target if it was scheduled for invalidation
250             // earlier in the composition but was then removed.
251             if (!it.isAttached) return@fastForEach
252 
253             val preInvalidationState = it.focusState
254             it.invalidateFocus()
255             if (
256                 preInvalidationState != it.focusState ||
257                     it in focusTargetsWithInvalidatedFocusEventsLegacy
258             ) {
259                 it.dispatchFocusCallbacks()
260             }
261         }
262         focusTargetNodesLegacy.clear()
263         // Clear the set so we can reuse it
264         focusTargetsWithInvalidatedFocusEventsLegacy.clear()
265 
266         invalidateOwnerFocusState()
267 
268         checkPrecondition(focusPropertiesNodesLegacy.isEmpty()) {
269             "Unprocessed FocusProperties nodes"
270         }
271         checkPrecondition(focusEventNodesLegacy.isEmpty()) { "Unprocessed FocusEvent nodes" }
272         checkPrecondition(focusTargetNodesLegacy.isEmpty()) { "Unprocessed FocusTarget nodes" }
273     }
274 }
275