1 /*
2  * Copyright 2021 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.ExperimentalComposeUiApi
20 import androidx.compose.ui.Modifier
21 import androidx.compose.ui.node.ModifierNodeElement
22 import androidx.compose.ui.platform.InspectorInfo
23 
24 /**
25  * Properties that are applied to [focusTarget] that is the first child of the
26  * [FocusPropertiesModifierNode] that sets these properties.
27  *
28  * @see [focusProperties]
29  */
30 interface FocusProperties {
31     /**
32      * When set to false, indicates that the [focusTarget] that this is applied to can no longer
33      * take focus. If the [focusTarget] is currently focused, setting this property to false will
34      * end up clearing focus.
35      */
36     var canFocus: Boolean
37 
38     /**
39      * A custom item to be used when the user requests the focus to move to the "next" item.
40      *
41      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
42      */
43     var next: FocusRequester
44         get() = FocusRequester.Default
45         set(_) {}
46 
47     /**
48      * A custom item to be used when the user requests the focus to move to the "previous" item.
49      *
50      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
51      */
52     var previous: FocusRequester
53         get() = FocusRequester.Default
54         set(_) {}
55 
56     /**
57      * A custom item to be used when the user moves focus "up".
58      *
59      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
60      */
61     var up: FocusRequester
62         get() = FocusRequester.Default
63         set(_) {}
64 
65     /**
66      * A custom item to be used when the user moves focus "down".
67      *
68      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
69      */
70     var down: FocusRequester
71         get() = FocusRequester.Default
72         set(_) {}
73 
74     /**
75      * A custom item to be used when the user requests a focus moves to the "left" item.
76      *
77      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
78      */
79     var left: FocusRequester
80         get() = FocusRequester.Default
81         set(_) {}
82 
83     /**
84      * A custom item to be used when the user requests a focus moves to the "right" item.
85      *
86      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
87      */
88     var right: FocusRequester
89         get() = FocusRequester.Default
90         set(_) {}
91 
92     /**
93      * A custom item to be used when the user requests a focus moves to the "left" in LTR mode and
94      * "right" in RTL mode.
95      *
96      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
97      */
98     var start: FocusRequester
99         get() = FocusRequester.Default
100         set(_) {}
101 
102     /**
103      * A custom item to be used when the user requests a focus moves to the "right" in LTR mode and
104      * "left" in RTL mode.
105      *
106      * @sample androidx.compose.ui.samples.CustomFocusOrderSample
107      */
108     var end: FocusRequester
109         get() = FocusRequester.Default
110         set(_) {}
111 
112     /**
113      * A custom item to be used when the user requests focus to move focus in
114      * ([FocusDirection.Enter]). An automatic [Enter][FocusDirection.Enter]" can be triggered when
115      * we move focus to a focus group that is not itself focusable. In this case, users can use the
116      * the focus direction that triggered the move in to determine the next item to be focused on.
117      *
118      * When you set the [enter] property, provide a lambda that takes the FocusDirection that
119      * triggered the enter as an input, and provides a [FocusRequester] as an output. You can return
120      * a custom destination by providing a [FocusRequester] attached to that destination, a
121      * [Cancel][FocusRequester.Cancel] to cancel the focus enter or
122      * [Default][FocusRequester.Default] to use the default focus enter behavior.
123      *
124      * @sample androidx.compose.ui.samples.CustomFocusEnterSample
125      */
126     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
127     @ExperimentalComposeUiApi
128     @get:ExperimentalComposeUiApi
129     @set:ExperimentalComposeUiApi
130     @set:Deprecated("Use onEnter instead", ReplaceWith("onEnter"))
131     var enter: (FocusDirection) -> FocusRequester
<lambda>null132         get() = { FocusRequester.Default }
133         set(value) {
134             onEnter = value.toUsingEnterExitScope()
135         }
136 
137     /**
138      * A custom item to be used when the user requests focus to move focus in
139      * ([FocusDirection.Enter]). An automatic [Enter][FocusDirection.Enter]" can be triggered when
140      * we move focus to a focus group that is not itself focusable. In this case, users can use the
141      * the focus direction that triggered the move in to determine the next item to be focused on.
142      *
143      * When you set the [onEnter] property, provide a lambda with the [FocusEnterExitScope] scope,
144      * having the [FocusEnterExitScope.requestedFocusDirection] that triggered the enter as an
145      * input. If redirection is required, use [FocusRequester.requestFocus] and if the focus change
146      * should be canceled, use [FocusEnterExitScope.cancelFocusChange].
147      *
148      * @sample androidx.compose.ui.samples.CustomFocusEnterSample
149      */
150     var onEnter: FocusEnterExitScope.() -> Unit
<lambda>null151         get() = {}
152         set(_) {}
153 
154     /**
155      * A custom item to be used when the user requests focus to move out ([FocusDirection.Exit]). An
156      * automatic [Exit][FocusDirection.Exit] can be triggered when we move focus outside the edge of
157      * a parent. In this case, users can use the focus direction that triggered the move out to
158      * determine the next focus destination.
159      *
160      * When you set the [exit] property, provide a lambda that takes the FocusDirection that
161      * triggered the exit as an input, and provides a [FocusRequester] as an output. You can return
162      * a custom destination by providing a [FocusRequester] attached to that destination, a
163      * [Cancel][FocusRequester.Cancel] to cancel the focus exit or [Default][FocusRequester.Default]
164      * to use the default focus exit behavior.
165      *
166      * @sample androidx.compose.ui.samples.CustomFocusExitSample
167      */
168     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
169     @ExperimentalComposeUiApi
170     @get:ExperimentalComposeUiApi
171     @set:ExperimentalComposeUiApi
172     @set:Deprecated("Use onExit instead", ReplaceWith("onExit"))
173     var exit: (FocusDirection) -> FocusRequester
<lambda>null174         get() = { FocusRequester.Default }
175         set(value) {
176             onExit = value.toUsingEnterExitScope()
177         }
178 
179     /**
180      * A custom item to be used when the user requests focus to move out ([FocusDirection.Exit]). An
181      * automatic [Exit][FocusDirection.Exit] can be triggered when we move focus outside the edge of
182      * a parent. In this case, users can use the focus direction that triggered the move out to
183      * determine the next focus destination.
184      *
185      * When you set the [onExit] property, provide a lambda with the [FocusEnterExitScope] scope,
186      * having the [FocusEnterExitScope.requestedFocusDirection] that triggered the exit as an input.
187      * If redirection is required, use [FocusRequester.requestFocus] and if the focus change should
188      * be canceled, use [FocusEnterExitScope.cancelFocusChange].
189      *
190      * @sample androidx.compose.ui.samples.CustomFocusExitSample
191      */
192     var onExit: FocusEnterExitScope.() -> Unit
<lambda>null193         get() = {}
194         set(_) {}
195 }
196 
197 /**
198  * A utility to use when upgrading from [FocusProperties.enter] and [FocusProperties.exit] to
199  * [FocusProperties.onEnter] and [FocusProperties.onExit].
200  */
toUsingEnterExitScopenull201 private fun ((FocusDirection) -> FocusRequester).toUsingEnterExitScope():
202     FocusEnterExitScope.() -> Unit = {
203     val focusRequester = invoke(requestedFocusDirection)
204     if (focusRequester === FocusRequester.Cancel) {
205         cancelFocusChange()
206     } else if (focusRequester !== FocusRequester.Default) {
207         focusRequester.requestFocus()
208     }
209 }
210 
211 /**
212  * Receiver scope for [FocusProperties.onEnter] and [FocusProperties.onExit]. Developers can change
213  * focus with [FocusRequester.requestFocus] to change the focus or [cancelFocusChange] to stop the
214  * focus from changing.
215  */
216 sealed interface FocusEnterExitScope {
217     /**
218      * The direction used to get into (with [FocusProperties.onEnter]) or leave (with
219      * [FocusProperties.onExit]) focus.
220      */
221     val requestedFocusDirection: FocusDirection
222 
223     /** Stop focus from changing. */
cancelFocusChangenull224     fun cancelFocusChange()
225 
226     @ExperimentalComposeUiApi
227     @Deprecated("Use cancelFocusChange instead", replaceWith = ReplaceWith("cancelFocusChange"))
228     fun cancelFocus() = cancelFocusChange()
229 }
230 
231 internal class CancelIndicatingFocusBoundaryScope(
232     override val requestedFocusDirection: FocusDirection,
233 ) : FocusEnterExitScope {
234     var isCanceled = false
235         private set
236 
237     override fun cancelFocusChange() {
238         isCanceled = true
239     }
240 }
241 
242 internal class FocusPropertiesImpl : FocusProperties {
243     override var canFocus: Boolean = true
244     override var next: FocusRequester = FocusRequester.Default
245     override var previous: FocusRequester = FocusRequester.Default
246     override var up: FocusRequester = FocusRequester.Default
247     override var down: FocusRequester = FocusRequester.Default
248     override var left: FocusRequester = FocusRequester.Default
249     override var right: FocusRequester = FocusRequester.Default
250     override var start: FocusRequester = FocusRequester.Default
251     override var end: FocusRequester = FocusRequester.Default
<lambda>null252     override var onEnter: FocusEnterExitScope.() -> Unit = {}
<lambda>null253     override var onExit: FocusEnterExitScope.() -> Unit = {}
254 }
255 
256 /**
257  * This modifier allows you to specify properties that are accessible to [focusTarget]s further down
258  * the modifier chain or on child layout nodes.
259  *
260  * @sample androidx.compose.ui.samples.FocusPropertiesSample
261  */
focusPropertiesnull262 fun Modifier.focusProperties(scope: FocusProperties.() -> Unit): Modifier =
263     this then FocusPropertiesElement(scope)
264 
265 private data class FocusPropertiesElement(val scope: FocusPropertiesScope) :
266     ModifierNodeElement<FocusPropertiesNode>() {
267     override fun create() = FocusPropertiesNode(scope)
268 
269     override fun update(node: FocusPropertiesNode) {
270         node.focusPropertiesScope = scope
271     }
272 
273     override fun InspectorInfo.inspectableProperties() {
274         name = "focusProperties"
275         properties["scope"] = scope
276     }
277 }
278 
279 private class FocusPropertiesNode(
280     var focusPropertiesScope: FocusPropertiesScope,
281 ) : FocusPropertiesModifierNode, Modifier.Node() {
282 
applyFocusPropertiesnull283     override fun applyFocusProperties(focusProperties: FocusProperties) {
284         focusPropertiesScope.apply(focusProperties)
285     }
286 }
287