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 
17 package androidx.compose.ui.focus
18 
19 import androidx.compose.ui.ComposeUiFlags
20 import androidx.compose.ui.ExperimentalComposeUiApi
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
23 import androidx.compose.ui.focus.CustomDestinationResult.None
24 import androidx.compose.ui.focus.CustomDestinationResult.RedirectCancelled
25 import androidx.compose.ui.focus.CustomDestinationResult.Redirected
26 import androidx.compose.ui.focus.FocusDirection.Companion.Exit
27 import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
28 import androidx.compose.ui.focus.FocusRequester.Companion.Redirect
29 import androidx.compose.ui.focus.FocusStateImpl.Active
30 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
31 import androidx.compose.ui.focus.FocusStateImpl.Captured
32 import androidx.compose.ui.focus.FocusStateImpl.Inactive
33 import androidx.compose.ui.internal.checkPreconditionNotNull
34 import androidx.compose.ui.layout.BeyondBoundsLayout
35 import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
36 import androidx.compose.ui.modifier.ModifierLocalModifierNode
37 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
38 import androidx.compose.ui.node.ModifierNodeElement
39 import androidx.compose.ui.node.Nodes
40 import androidx.compose.ui.node.ObserverModifierNode
41 import androidx.compose.ui.node.observeReads
42 import androidx.compose.ui.node.requireOwner
43 import androidx.compose.ui.node.visitAncestors
44 import androidx.compose.ui.node.visitSelfAndAncestors
45 import androidx.compose.ui.node.visitSubtreeIf
46 import androidx.compose.ui.platform.InspectorInfo
47 import androidx.compose.ui.util.trace
48 
49 internal class FocusTargetNode(
50     focusability: Focusability = Focusability.Always,
51     private val onFocusChange: ((previous: FocusState, current: FocusState) -> Unit)? = null,
52     private val onDispatchEventsCompleted: ((FocusTargetNode) -> Unit)? = null
53 ) :
54     CompositionLocalConsumerModifierNode,
55     FocusTargetModifierNode,
56     ObserverModifierNode,
57     ModifierLocalModifierNode,
58     Modifier.Node() {
59 
60     private var isProcessingCustomExit = false
61     private var isProcessingCustomEnter = false
62 
63     // During a transaction, changes to the state are stored as uncommitted focus state. At the
64     // end of the transaction, this state is stored as committed focus state.
65     private var committedFocusState: FocusStateImpl? = null
66 
67     override val shouldAutoInvalidate = false
68 
69     override var focusState: FocusStateImpl
70         get() {
71             if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
72                 if (!isAttached) return Inactive
73                 val focusOwner = requireOwner().focusOwner
74                 val activeNode = focusOwner.activeFocusTargetNode ?: return Inactive
75                 return if (this === activeNode) {
76                     if (focusOwner.isFocusCaptured) Captured else Active
77                 } else {
78                     if (activeNode.isAttached) {
<lambda>null79                         activeNode.visitAncestors(Nodes.FocusTarget) {
80                             if (this === it) return ActiveParent
81                         }
82                     }
83                     Inactive
84                 }
85             } else {
<lambda>null86                 return focusTransactionManager?.run { uncommittedFocusState }
87                     ?: committedFocusState
88                     ?: Inactive
89             }
90         }
91         set(value) {
92             if (!@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
<lambda>null93                 with(requireTransactionManager()) { uncommittedFocusState = value }
94             }
95         }
96 
97     @Deprecated(
98         message = "Use the version accepting FocusDirection",
99         replaceWith = ReplaceWith("this.requestFocus()"),
100         level = DeprecationLevel.HIDDEN
101     )
requestFocusnull102     override fun requestFocus(): Boolean {
103         return requestFocus(FocusDirection.Enter)
104     }
105 
requestFocusnull106     override fun requestFocus(focusDirection: FocusDirection): Boolean {
107         trace("FocusTransactions:requestFocus") {
108             if (!fetchFocusProperties().canFocus) return false
109             return if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
110                 when (performCustomRequestFocus(focusDirection)) {
111                     None -> performRequestFocus()
112                     Redirected -> true
113                     Cancelled,
114                     RedirectCancelled -> false
115                 }
116             } else {
117                 requireTransactionManager().withNewTransaction(
118                     onCancelled = { if (node.isAttached) dispatchFocusCallbacks() }
119                 ) {
120                     when (performCustomRequestFocus(focusDirection)) {
121                         None -> performRequestFocus()
122                         Redirected -> true
123                         Cancelled,
124                         RedirectCancelled -> false
125                     }
126                 }
127             }
128         }
129     }
130 
131     override var focusability: Focusability = focusability
132         set(value) {
133             if (field != value) {
134                 field = value
135                 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
136                     if (
137                         isAttached &&
138                             this === requireOwner().focusOwner.activeFocusTargetNode &&
139                             !field.canFocus(this)
140                     ) {
141                         clearFocus(forced = true, refreshFocusEvents = true)
142                     }
143                 } else {
144                     // Avoid invalidating if we have not been initialized yet: there is no need to
145                     // invalidate since these property changes cannot affect anything.
146                     if (isAttached && isInitialized()) {
147                         // Invalidate focus if needed
148                         onObservedReadsChanged()
149                     }
150                 }
151             }
152         }
153 
154     var previouslyFocusedChildHash: Int = 0
155 
156     val beyondBoundsLayoutParent: BeyondBoundsLayout?
157         get() = ModifierLocalBeyondBoundsLayout.current
158 
onObservedReadsChangednull159     override fun onObservedReadsChanged() {
160         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
161             invalidateFocus()
162         } else {
163             val previousFocusState = focusState
164             invalidateFocus()
165             if (previousFocusState != focusState) dispatchFocusCallbacks()
166         }
167     }
168 
onAttachnull169     override fun onAttach() {
170         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) return
171         invalidateFocusTarget()
172     }
173 
onResetnull174     override fun onReset() {
175         // The focused item is being removed from a lazy list, so we need to clear focus.
176         // This is called after onEndApplyChanges, so we can safely clear focus from the owner,
177         // which could trigger an initial focus scenario.
178         @OptIn(ExperimentalComposeUiApi::class)
179         if (ComposeUiFlags.isClearFocusOnResetEnabled && focusState.isFocused) {
180             requireOwner()
181                 .focusOwner
182                 .clearFocus(
183                     force = true,
184                     refreshFocusEvents = true,
185                     clearOwnerFocus = true,
186                     focusDirection = Exit
187                 )
188         }
189     }
190 
191     /** Clears focus if this focus target has it. */
onDetachnull192     override fun onDetach() {
193         when (focusState) {
194             // Clear focus from the current FocusTarget.
195             // This currently clears focus from the entire hierarchy, but we can change the
196             // implementation so that focus is sent to the immediate focus parent.
197             Active,
198             Captured -> {
199                 val focusOwner = requireOwner().focusOwner
200                 focusOwner.clearFocus(
201                     force = true,
202                     refreshFocusEvents = true,
203                     clearOwnerFocus = false,
204                     focusDirection = Exit
205                 )
206                 // We don't clear the owner's focus yet, because this could trigger an initial
207                 // focus scenario after the focus is cleared. Instead, we schedule invalidation
208                 // after onApplyChanges. The FocusInvalidationManager contains the invalidation
209                 // logic and calls clearFocus() on the owner after all the nodes in the hierarchy
210                 // are invalidated.
211                 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
212                     focusOwner.scheduleInvalidationForOwner()
213                 } else {
214                     invalidateFocusTarget()
215                 }
216             }
217             // This node might be reused, so reset the state to Inactive.
218             ActiveParent ->
219                 if (!@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
220                     requireTransactionManager().withNewTransaction { focusState = Inactive }
221                 }
222             Inactive -> {}
223         }
224         // This node might be reused, so we reset its state.
225         committedFocusState = null
226     }
227 
228     /**
229      * Visits parent [FocusPropertiesModifierNode]s and runs
230      * [FocusPropertiesModifierNode.applyFocusProperties] on each parent. This effectively collects
231      * an aggregated focus state.
232      */
fetchFocusPropertiesnull233     internal fun fetchFocusProperties(): FocusProperties {
234         val properties = FocusPropertiesImpl()
235         properties.canFocus = focusability.canFocus(this)
236         visitSelfAndAncestors(Nodes.FocusProperties, untilType = Nodes.FocusTarget) {
237             it.applyFocusProperties(properties)
238         }
239         return properties
240     }
241 
fetchCustomEnterOrExitnull242     private inline fun fetchCustomEnterOrExit(
243         focusDirection: FocusDirection,
244         block: (FocusRequester) -> Unit,
245         enterOrExit: FocusProperties.(FocusEnterExitScope) -> Unit
246     ) {
247         val focusProperties = fetchFocusProperties()
248         val scope = CancelIndicatingFocusBoundaryScope(focusDirection)
249         val focusTransactionManager = focusTransactionManager
250         val generationBefore = focusTransactionManager?.generation ?: 0
251         val focusOwner = requireOwner().focusOwner
252         val activeNodeBefore = focusOwner.activeFocusTargetNode
253         focusProperties.enterOrExit(scope)
254         val generationAfter = focusTransactionManager?.generation ?: 0
255         val activeNodeAfter = focusOwner.activeFocusTargetNode
256         if (scope.isCanceled) {
257             block(Cancel)
258         } else if (
259             generationBefore != generationAfter ||
260                 (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled &&
261                     (activeNodeBefore !== activeNodeAfter && activeNodeAfter != null))
262         ) {
263             block(Redirect)
264         }
265     }
266 
267     /**
268      * Fetch custom enter destination associated with this [focusTarget].
269      *
270      * Custom focus enter properties are specified as a lambda. If the user runs code in this lambda
271      * that triggers a focus search, or some other focus change that causes focus to leave the
272      * sub-hierarchy associated with this node, we could end up in a loop as that operation will
273      * trigger another invocation of the lambda associated with the focus exit property. This
274      * function prevents that re-entrant scenario by ensuring there is only one concurrent
275      * invocation of this lambda.
276      */
fetchCustomEnternull277     internal inline fun fetchCustomEnter(
278         focusDirection: FocusDirection,
279         block: (FocusRequester) -> Unit
280     ) {
281         if (!isProcessingCustomEnter) {
282             isProcessingCustomEnter = true
283             try {
284                 fetchCustomEnterOrExit(focusDirection, block) { it.onEnter() }
285             } finally {
286                 isProcessingCustomEnter = false
287             }
288         }
289     }
290 
291     /**
292      * Fetch custom exit destination associated with this [focusTarget].
293      *
294      * Custom focus exit properties are specified as a lambda. If the user runs code in this lambda
295      * that triggers a focus search, or some other focus change that causes focus to leave the
296      * sub-hierarchy associated with this node, we could end up in a loop as that operation will
297      * trigger another invocation of the lambda associated with the focus exit property. This
298      * function prevents that re-entrant scenario by ensuring there is only one concurrent
299      * invocation of this lambda.
300      */
fetchCustomExitnull301     internal inline fun fetchCustomExit(
302         focusDirection: FocusDirection,
303         block: (FocusRequester) -> Unit
304     ) {
305         if (!isProcessingCustomExit) {
306             isProcessingCustomExit = true
307             try {
308                 fetchCustomEnterOrExit(focusDirection, block) { it.onExit() }
309             } finally {
310                 isProcessingCustomExit = false
311             }
312         }
313     }
314 
commitFocusStatenull315     internal fun commitFocusState() {
316         with(requireTransactionManager()) {
317             committedFocusState =
318                 checkPreconditionNotNull(uncommittedFocusState) {
319                     "committing a node that was not updated in the current transaction"
320                 }
321         }
322     }
323 
invalidateFocusnull324     internal fun invalidateFocus() {
325         if (!isInitialized()) initializeFocusState()
326         when (focusState) {
327             // Clear focus from the current FocusTarget.
328             // This currently clears focus from the entire hierarchy, but we can change the
329             // implementation so that focus is sent to the immediate focus parent.
330             Active,
331             Captured -> {
332                 lateinit var focusProperties: FocusProperties
333                 observeReads { focusProperties = fetchFocusProperties() }
334                 if (!focusProperties.canFocus) {
335                     requireOwner().focusOwner.clearFocus(force = true)
336                 }
337             }
338             ActiveParent,
339             Inactive -> {}
340         }
341     }
342 
343     /**
344      * Triggers [onFocusChange] and sends a "Focus Event" up the hierarchy that asks all
345      * [FocusEventModifierNode]s to recompute their observed focus state.
346      */
dispatchFocusCallbacksnull347     internal fun dispatchFocusCallbacks() {
348         val previousOrInactive = committedFocusState ?: Inactive
349         val focusState = focusState
350         // Avoid invoking callback when we initialize the state (from `null` to Inactive) or
351         // if we are detached and go from Inactive to `null` - there isn't a conceptual focus
352         // state change here
353         if (previousOrInactive != focusState) {
354             onFocusChange?.invoke(previousOrInactive, focusState)
355         }
356         visitSelfAndAncestors(Nodes.FocusEvent, untilType = Nodes.FocusTarget) {
357             // TODO(251833873): Consider caching it.getFocusState().
358             it.onFocusEvent(it.getFocusState())
359         }
360         onDispatchEventsCompleted?.invoke(this)
361     }
362 
dispatchFocusCallbacksnull363     internal fun dispatchFocusCallbacks(previousState: FocusState, newState: FocusState) {
364         val focusOwner = requireOwner().focusOwner
365         val activeNode = focusOwner.activeFocusTargetNode
366         if (previousState != newState) onFocusChange?.invoke(previousState, newState)
367         visitSelfAndAncestors(Nodes.FocusEvent, untilType = Nodes.FocusTarget) {
368             if (activeNode !== focusOwner.activeFocusTargetNode) {
369                 // Stop sending events, as focus changed in a callback
370                 return@visitSelfAndAncestors
371             }
372             it.onFocusEvent(newState)
373         }
374         onDispatchEventsCompleted?.invoke(this)
375     }
376 
377     internal object FocusTargetElement : ModifierNodeElement<FocusTargetNode>() {
createnull378         override fun create() = FocusTargetNode()
379 
380         override fun update(node: FocusTargetNode) {}
381 
inspectablePropertiesnull382         override fun InspectorInfo.inspectableProperties() {
383             name = "focusTarget"
384         }
385 
hashCodenull386         override fun hashCode() = "focusTarget".hashCode()
387 
388         override fun equals(other: Any?) = other === this
389     }
390 
391     internal fun isInitialized(): Boolean =
392         if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) true
393         else committedFocusState != null
394 
395     internal fun initializeFocusState(initialFocusState: FocusStateImpl? = null) {
396         fun isInActiveSubTree(): Boolean {
397             visitAncestors(Nodes.FocusTarget) {
398                 if (!it.isInitialized()) return@visitAncestors
399 
400                 return when (it.focusState) {
401                     ActiveParent -> true
402                     Active,
403                     Captured,
404                     Inactive -> false
405                 }
406             }
407             return false
408         }
409 
410         fun hasActiveChild(): Boolean {
411             visitSubtreeIf(Nodes.FocusTarget) {
412                 if (!it.isInitialized()) return@visitSubtreeIf true
413 
414                 when (it.focusState) {
415                     Active,
416                     ActiveParent,
417                     Captured -> return true
418                     Inactive -> return@visitSubtreeIf false
419                 }
420             }
421             return false
422         }
423 
424         check(!isInitialized()) { "Re-initializing focus target node." }
425 
426         if (!@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
427             requireTransactionManager().withNewTransaction {
428                 // Note: hasActiveChild() is expensive since it searches the entire subtree. So we
429                 // only do this if we are part of the active subtree.
430                 this.focusState =
431                     initialFocusState
432                         ?: if (isInActiveSubTree() && hasActiveChild()) ActiveParent else Inactive
433             }
434         }
435     }
436 }
437 
requireTransactionManagernull438 internal fun FocusTargetNode.requireTransactionManager(): FocusTransactionManager {
439     return requireOwner().focusOwner.focusTransactionManager
440 }
441 
442 internal val FocusTargetNode.focusTransactionManager: FocusTransactionManager?
443     get() = node.coordinator?.layoutNode?.owner?.focusOwner?.focusTransactionManager
444 
invalidateFocusTargetnull445 internal fun FocusTargetNode.invalidateFocusTarget() {
446     requireOwner().focusOwner.scheduleInvalidation(this)
447 }
448