1 /*
<lambda>null2  * Copyright 2020 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.focus.FocusDirection.Companion.Down
22 import androidx.compose.ui.focus.FocusDirection.Companion.Enter
23 import androidx.compose.ui.focus.FocusDirection.Companion.Exit
24 import androidx.compose.ui.focus.FocusDirection.Companion.Left
25 import androidx.compose.ui.focus.FocusDirection.Companion.Next
26 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
27 import androidx.compose.ui.focus.FocusDirection.Companion.Right
28 import androidx.compose.ui.focus.FocusDirection.Companion.Up
29 import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
30 import androidx.compose.ui.focus.FocusRequester.Companion.Default
31 import androidx.compose.ui.focus.FocusRequester.Companion.Redirect
32 import androidx.compose.ui.focus.FocusStateImpl.Active
33 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
34 import androidx.compose.ui.focus.FocusStateImpl.Captured
35 import androidx.compose.ui.focus.FocusStateImpl.Inactive
36 import androidx.compose.ui.geometry.Rect
37 import androidx.compose.ui.layout.findRootCoordinates
38 import androidx.compose.ui.node.Nodes
39 import androidx.compose.ui.node.requireOwner
40 import androidx.compose.ui.node.visitAncestors
41 import androidx.compose.ui.node.visitChildren
42 import androidx.compose.ui.unit.LayoutDirection
43 import androidx.compose.ui.unit.LayoutDirection.Ltr
44 import androidx.compose.ui.unit.LayoutDirection.Rtl
45 
46 /**
47  * Search up the component tree for any parent/parents that have specified a custom focus order.
48  * Allowing parents higher up the hierarchy to overwrite the focus order specified by their
49  * children.
50  *
51  * @param focusDirection the focus direction passed to [FocusManager.moveFocus] that triggered this
52  *   focus search.
53  * @param layoutDirection the current system [LayoutDirection].
54  */
55 internal fun FocusTargetNode.customFocusSearch(
56     focusDirection: FocusDirection,
57     layoutDirection: LayoutDirection
58 ): FocusRequester {
59     val focusProperties = fetchFocusProperties()
60     return when (focusDirection) {
61         Next -> focusProperties.next
62         Previous -> focusProperties.previous
63         Up -> focusProperties.up
64         Down -> focusProperties.down
65         Left ->
66             when (layoutDirection) {
67                 Ltr -> focusProperties.start
68                 Rtl -> focusProperties.end
69             }.takeUnless { it === Default } ?: focusProperties.left
70         Right ->
71             when (layoutDirection) {
72                 Ltr -> focusProperties.end
73                 Rtl -> focusProperties.start
74             }.takeUnless { it === Default } ?: focusProperties.right
75         // TODO(b/183746982): add focus order API for "In" and "Out".
76         //  Developers can to specify a custom "In" to specify which child should be visited when
77         //  the user presses dPad center. (They can also redirect the "In" to some other item).
78         //  Developers can specify a custom "Out" to specify which composable should take focus
79         //  when the user presses the back button.
80         Enter,
81         Exit -> {
82             val scope = CancelIndicatingFocusBoundaryScope(focusDirection)
83             with(focusProperties) {
84                 val focusTransactionManager = focusTransactionManager
85                 val generationBefore = focusTransactionManager?.generation ?: 0
86                 val focusOwner = requireOwner().focusOwner
87                 val activeNodeBefore = focusOwner.activeFocusTargetNode
88                 if (focusDirection == Enter) {
89                     scope.onEnter()
90                 } else {
91                     scope.onExit()
92                 }
93                 val generationAfter = focusTransactionManager?.generation ?: 0
94                 if (scope.isCanceled) {
95                     Cancel
96                 } else if (
97                     generationBefore != generationAfter ||
98                         (@OptIn(ExperimentalComposeUiApi::class)
99                         ComposeUiFlags.isTrackFocusEnabled &&
100                             activeNodeBefore !== focusOwner.activeFocusTargetNode)
101                 ) {
102                     Redirect
103                 } else {
104                     Default
105                 }
106             }
107         }
108         else -> error("invalid FocusDirection")
109     }
110 }
111 
112 /**
113  * Moves focus based on the requested focus direction.
114  *
115  * @param focusDirection The requested direction to move focus.
116  * @param layoutDirection Whether the layout is RTL or LTR.
117  * @param previouslyFocusedRect The bounds of the previously focused item.
118  * @param onFound This lambda is invoked if focus search finds the next focus node.
119  * @return if no focus node is found, we return false. If we receive a cancel, we return null
120  *   otherwise we return the result of [onFound].
121  */
focusSearchnull122 internal fun FocusTargetNode.focusSearch(
123     focusDirection: FocusDirection,
124     layoutDirection: LayoutDirection,
125     previouslyFocusedRect: Rect?,
126     onFound: (FocusTargetNode) -> Boolean
127 ): Boolean? {
128     return when (focusDirection) {
129         Next,
130         Previous -> oneDimensionalFocusSearch(focusDirection, onFound)
131         Left,
132         Right,
133         Up,
134         Down -> twoDimensionalFocusSearch(focusDirection, previouslyFocusedRect, onFound)
135         Enter -> {
136             // we search among the children of the active item.
137             val direction =
138                 when (layoutDirection) {
139                     Rtl -> Left
140                     Ltr -> Right
141                 }
142             findActiveFocusNode()
143                 ?.twoDimensionalFocusSearch(direction, previouslyFocusedRect, onFound)
144         }
145         Exit ->
146             findActiveFocusNode()?.findNonDeactivatedParent().let {
147                 if (it == null || it == this) false else onFound.invoke(it)
148             }
149         else -> error("Focus search invoked with invalid FocusDirection $focusDirection")
150     }
151 }
152 
153 /**
154  * Returns the bounding box of the focus layout area in the root or [Rect.Zero] if the FocusModifier
155  * has not had a layout.
156  */
focusRectnull157 internal fun FocusTargetNode.focusRect(): Rect =
158     coordinator?.let { it.findRootCoordinates().localBoundingBoxOf(it, clipBounds = false) }
159         ?: Rect.Zero
160 
161 /** Whether this node should be considered when searching for the next item during a traversal. */
162 internal val FocusTargetNode.isEligibleForFocusSearch: Boolean
163     get() = coordinator?.layoutNode?.isPlaced == true && coordinator?.layoutNode?.isAttached == true
164 
165 internal val FocusTargetNode.activeChild: FocusTargetNode?
166     get() {
167         if (!node.isAttached) return null
<lambda>null168         visitChildren(Nodes.FocusTarget) {
169             if (!it.node.isAttached) return@visitChildren
170             when (it.focusState) {
171                 Active,
172                 ActiveParent,
173                 Captured -> return it
174                 Inactive -> return@visitChildren
175             }
176         }
177         return null
178     }
179 
findActiveFocusNodenull180 internal fun FocusTargetNode.findActiveFocusNode(): FocusTargetNode? {
181     if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
182         val activeNode = requireOwner().focusOwner.activeFocusTargetNode
183         return if (activeNode != null && activeNode.isAttached) activeNode else null
184     } else {
185         when (focusState) {
186             Active,
187             Captured -> return this
188             ActiveParent -> {
189                 visitChildren(Nodes.FocusTarget) { node ->
190                     node.findActiveFocusNode()?.let {
191                         return it
192                     }
193                 }
194                 return null
195             }
196             Inactive -> return null
197         }
198     }
199 }
200 
201 @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
findNonDeactivatedParentnull202 private fun FocusTargetNode.findNonDeactivatedParent(): FocusTargetNode? {
203     visitAncestors(Nodes.FocusTarget) { if (it.fetchFocusProperties().canFocus) return it }
204     return null
205 }
206