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