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