1 /*
<lambda>null2  * Copyright 2019 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.foundation
18 
19 import androidx.collection.mutableLongObjectMapOf
20 import androidx.compose.foundation.gestures.PressGestureScope
21 import androidx.compose.foundation.gestures.ScrollableContainerNode
22 import androidx.compose.foundation.gestures.detectTapAndPress
23 import androidx.compose.foundation.gestures.detectTapGestures
24 import androidx.compose.foundation.interaction.HoverInteraction
25 import androidx.compose.foundation.interaction.MutableInteractionSource
26 import androidx.compose.foundation.interaction.PressInteraction
27 import androidx.compose.foundation.internal.requirePrecondition
28 import androidx.compose.runtime.remember
29 import androidx.compose.ui.Modifier
30 import androidx.compose.ui.composed
31 import androidx.compose.ui.focus.Focusability
32 import androidx.compose.ui.geometry.Offset
33 import androidx.compose.ui.hapticfeedback.HapticFeedback
34 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
35 import androidx.compose.ui.input.key.Key
36 import androidx.compose.ui.input.key.KeyEvent
37 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
38 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
39 import androidx.compose.ui.input.key.KeyInputModifierNode
40 import androidx.compose.ui.input.key.key
41 import androidx.compose.ui.input.key.type
42 import androidx.compose.ui.input.pointer.PointerEvent
43 import androidx.compose.ui.input.pointer.PointerEventPass
44 import androidx.compose.ui.input.pointer.PointerEventType
45 import androidx.compose.ui.input.pointer.PointerInputScope
46 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
47 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
48 import androidx.compose.ui.node.DelegatableNode
49 import androidx.compose.ui.node.DelegatingNode
50 import androidx.compose.ui.node.ModifierNodeElement
51 import androidx.compose.ui.node.ObserverModifierNode
52 import androidx.compose.ui.node.PointerInputModifierNode
53 import androidx.compose.ui.node.SemanticsModifierNode
54 import androidx.compose.ui.node.TraversableNode
55 import androidx.compose.ui.node.currentValueOf
56 import androidx.compose.ui.node.invalidateSemantics
57 import androidx.compose.ui.node.observeReads
58 import androidx.compose.ui.node.traverseAncestors
59 import androidx.compose.ui.platform.InspectorInfo
60 import androidx.compose.ui.platform.LocalHapticFeedback
61 import androidx.compose.ui.platform.LocalViewConfiguration
62 import androidx.compose.ui.platform.debugInspectorInfo
63 import androidx.compose.ui.semantics.Role
64 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
65 import androidx.compose.ui.semantics.disabled
66 import androidx.compose.ui.semantics.onClick
67 import androidx.compose.ui.semantics.onLongClick
68 import androidx.compose.ui.semantics.role
69 import androidx.compose.ui.unit.IntSize
70 import androidx.compose.ui.unit.center
71 import androidx.compose.ui.unit.toOffset
72 import kotlinx.coroutines.Job
73 import kotlinx.coroutines.cancelAndJoin
74 import kotlinx.coroutines.coroutineScope
75 import kotlinx.coroutines.delay
76 import kotlinx.coroutines.launch
77 
78 /**
79  * Configure component to receive clicks via input or accessibility "click" event.
80  *
81  * Add this modifier to the element to make it clickable within its bounds and show a default
82  * indication when it's pressed.
83  *
84  * This version has no [MutableInteractionSource] or [Indication] parameters, the default indication
85  * from [LocalIndication] will be used. To specify [MutableInteractionSource] or [Indication], use
86  * the other overload.
87  *
88  * If you are only creating this clickable modifier inside composition, consider using the other
89  * overload and explicitly passing `LocalIndication.current` for improved performance. For more
90  * information see the documentation on the other overload.
91  *
92  * If you need to support double click or long click alongside the single click, consider using
93  * [combinedClickable].
94  *
95  * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a
96  * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do
97  * not need to do this when removing a composable because Compose guarantees it completes via the
98  * snapshot state system.)
99  *
100  * @sample androidx.compose.foundation.samples.ClickableSample
101  * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will appear
102  *   disabled for accessibility services
103  * @param onClickLabel semantic / accessibility label for the [onClick] action
104  * @param role the type of user interface element. Accessibility services might use this to describe
105  *   the element or do customizations
106  * @param onClick will be called when user clicks on the element
107  */
108 @Deprecated(
109     message =
110         "Replaced with new overload that only supports IndicationNodeFactory instances inside LocalIndication, and does not use composed",
111     level = DeprecationLevel.HIDDEN
112 )
113 fun Modifier.clickable(
114     enabled: Boolean = true,
115     onClickLabel: String? = null,
116     role: Role? = null,
117     onClick: () -> Unit
118 ) =
119     composed(
120         inspectorInfo =
121             debugInspectorInfo {
122                 name = "clickable"
123                 properties["enabled"] = enabled
124                 properties["onClickLabel"] = onClickLabel
125                 properties["role"] = role
126                 properties["onClick"] = onClick
127             }
<lambda>null128     ) {
129         val localIndication = LocalIndication.current
130         val interactionSource =
131             if (localIndication is IndicationNodeFactory) {
132                 // We can fast path here as it will be created inside clickable lazily
133                 null
134             } else {
135                 // We need an interaction source to pass between the indication modifier and
136                 // clickable, so
137                 // by creating here we avoid another composed down the line
138                 remember { MutableInteractionSource() }
139             }
140         Modifier.clickable(
141             enabled = enabled,
142             onClickLabel = onClickLabel,
143             onClick = onClick,
144             role = role,
145             indication = localIndication,
146             interactionSource = interactionSource
147         )
148     }
149 
150 /**
151  * Configure component to receive clicks via input or accessibility "click" event.
152  *
153  * Add this modifier to the element to make it clickable within its bounds and show a default
154  * indication when it's pressed.
155  *
156  * This overload will use the [Indication] from [LocalIndication]. Use the other overload to
157  * explicitly provide an [Indication] instance. Note that this overload only supports
158  * [IndicationNodeFactory] instances provided through [LocalIndication] - it is strongly recommended
159  * to migrate to [IndicationNodeFactory], but you can use the other overload if you still need to
160  * support [Indication] instances that are not [IndicationNodeFactory].
161  *
162  * If [interactionSource] is `null`, an internal [MutableInteractionSource] will be lazily created
163  * only when needed. This reduces the performance cost of clickable during composition, as creating
164  * the [indication] can be delayed until there is an incoming
165  * [androidx.compose.foundation.interaction.Interaction]. If you are only passing a remembered
166  * [MutableInteractionSource] and you are never using it outside of clickable, it is recommended to
167  * instead provide `null` to enable lazy creation. If you need the [Indication] to be created
168  * eagerly, provide a remembered [MutableInteractionSource].
169  *
170  * If you need to support double click or long click alongside the single click, consider using
171  * [combinedClickable].
172  *
173  * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a
174  * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do
175  * not need to do this when removing a composable because Compose guarantees it completes via the
176  * snapshot state system.)
177  *
178  * @sample androidx.compose.foundation.samples.ClickableSample
179  * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will appear
180  *   disabled for accessibility services
181  * @param onClickLabel semantic / accessibility label for the [onClick] action
182  * @param role the type of user interface element. Accessibility services might use this to describe
183  *   the element or do customizations
184  * @param interactionSource [MutableInteractionSource] that will be used to dispatch
185  *   [PressInteraction.Press] when this clickable is pressed. If `null`, an internal
186  *   [MutableInteractionSource] will be created if needed.
187  * @param onClick will be called when user clicks on the element
188  */
clickablenull189 fun Modifier.clickable(
190     enabled: Boolean = true,
191     onClickLabel: String? = null,
192     role: Role? = null,
193     interactionSource: MutableInteractionSource? = null,
194     onClick: () -> Unit
195 ): Modifier {
196     @OptIn(ExperimentalFoundationApi::class)
197     return if (ComposeFoundationFlags.isNonComposedClickableEnabled) {
198         this.then(
199             ClickableElement(
200                 interactionSource = interactionSource,
201                 indicationNodeFactory = null,
202                 useLocalIndication = true,
203                 enabled = enabled,
204                 onClickLabel = onClickLabel,
205                 role = role,
206                 onClick = onClick
207             )
208         )
209     } else {
210         composed(
211             inspectorInfo =
212                 debugInspectorInfo {
213                     name = "clickable"
214                     properties["enabled"] = enabled
215                     properties["onClickLabel"] = onClickLabel
216                     properties["role"] = role
217                     properties["interactionSource"] = interactionSource
218                     properties["onClick"] = onClick
219                 }
220         ) {
221             val localIndication = LocalIndication.current
222             val intSource =
223                 interactionSource
224                     ?: if (localIndication is IndicationNodeFactory) {
225                         // We can fast path here as it will be created inside clickable lazily
226                         null
227                     } else {
228                         // We need an interaction source to pass between the indication modifier and
229                         // clickable, so
230                         // by creating here we avoid another composed down the line
231                         remember { MutableInteractionSource() }
232                     }
233             Modifier.clickable(
234                 enabled = enabled,
235                 onClickLabel = onClickLabel,
236                 onClick = onClick,
237                 role = role,
238                 indication = localIndication,
239                 interactionSource = intSource
240             )
241         }
242     }
243 }
244 
245 /**
246  * Configure component to receive clicks via input or accessibility "click" event.
247  *
248  * Add this modifier to the element to make it clickable within its bounds and show an indication as
249  * specified in [indication] parameter.
250  *
251  * If [interactionSource] is `null`, and [indication] is an [IndicationNodeFactory], an internal
252  * [MutableInteractionSource] will be lazily created along with the [indication] only when needed.
253  * This reduces the performance cost of clickable during composition, as creating the [indication]
254  * can be delayed until there is an incoming [androidx.compose.foundation.interaction.Interaction].
255  * If you are only passing a remembered [MutableInteractionSource] and you are never using it
256  * outside of clickable, it is recommended to instead provide `null` to enable lazy creation. If you
257  * need [indication] to be created eagerly, provide a remembered [MutableInteractionSource].
258  *
259  * If [indication] is _not_ an [IndicationNodeFactory], and instead implements the deprecated
260  * [Indication.rememberUpdatedInstance] method, you should explicitly pass a remembered
261  * [MutableInteractionSource] as a parameter for [interactionSource] instead of `null`, as this
262  * cannot be lazily created inside clickable.
263  *
264  * If you need to support double click or long click alongside the single click, consider using
265  * [combinedClickable].
266  *
267  * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a
268  * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do
269  * not need to do this when removing a composable because Compose guarantees it completes via the
270  * snapshot state system.)
271  *
272  * @sample androidx.compose.foundation.samples.ClickableSample
273  * @param interactionSource [MutableInteractionSource] that will be used to dispatch
274  *   [PressInteraction.Press] when this clickable is pressed. If `null`, an internal
275  *   [MutableInteractionSource] will be created if needed.
276  * @param indication indication to be shown when modified element is pressed. By default, indication
277  *   from [LocalIndication] will be used. Pass `null` to show no indication, or current value from
278  *   [LocalIndication] to show theme default
279  * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will appear
280  *   disabled for accessibility services
281  * @param onClickLabel semantic / accessibility label for the [onClick] action
282  * @param role the type of user interface element. Accessibility services might use this to describe
283  *   the element or do customizations
284  * @param onClick will be called when user clicks on the element
285  */
clickablenull286 fun Modifier.clickable(
287     interactionSource: MutableInteractionSource?,
288     indication: Indication?,
289     enabled: Boolean = true,
290     onClickLabel: String? = null,
291     role: Role? = null,
292     onClick: () -> Unit
293 ) =
294     clickableWithIndicationIfNeeded(
295         interactionSource = interactionSource,
296         indication = indication
297     ) { intSource, indicationNodeFactory ->
298         ClickableElement(
299             interactionSource = intSource,
300             indicationNodeFactory = indicationNodeFactory,
301             useLocalIndication = false,
302             enabled = enabled,
303             onClickLabel = onClickLabel,
304             role = role,
305             onClick = onClick
306         )
307     }
308 
309 /**
310  * Configure component to receive clicks, double clicks and long clicks via input or accessibility
311  * "click" event.
312  *
313  * Add this modifier to the element to make it clickable within its bounds.
314  *
315  * If you need only click handling, and no double or long clicks, consider using [clickable]
316  *
317  * This version has no [MutableInteractionSource] or [Indication] parameters, the default indication
318  * from [LocalIndication] will be used. To specify [MutableInteractionSource] or [Indication], use
319  * the other overload.
320  *
321  * If you are only creating this combinedClickable modifier inside composition, consider using the
322  * other overload and explicitly passing `LocalIndication.current` for improved performance. For
323  * more information see the documentation on the other overload.
324  *
325  * Note, if the modifier instance gets re-used between a key down and key up events, the ongoing
326  * input will be aborted.
327  *
328  * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a
329  * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do
330  * not need to do this when removing a composable because Compose guarantees it completes via the
331  * snapshot state system.)
332  *
333  * @sample androidx.compose.foundation.samples.ClickableSample
334  * @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or
335  *   [onDoubleClick] won't be invoked
336  * @param onClickLabel semantic / accessibility label for the [onClick] action
337  * @param role the type of user interface element. Accessibility services might use this to describe
338  *   the element or do customizations
339  * @param onLongClickLabel semantic / accessibility label for the [onLongClick] action
340  * @param onLongClick will be called when user long presses on the element
341  * @param onDoubleClick will be called when user double clicks on the element
342  * @param hapticFeedbackEnabled whether to use the default [HapticFeedback] behavior
343  * @param onClick will be called when user clicks on the element
344  */
345 @Deprecated(
346     message =
347         "Replaced with new overload that only supports IndicationNodeFactory instances inside LocalIndication, and does not use composed",
348     level = DeprecationLevel.HIDDEN
349 )
combinedClickablenull350 fun Modifier.combinedClickable(
351     enabled: Boolean = true,
352     onClickLabel: String? = null,
353     role: Role? = null,
354     onLongClickLabel: String? = null,
355     onLongClick: (() -> Unit)? = null,
356     onDoubleClick: (() -> Unit)? = null,
357     hapticFeedbackEnabled: Boolean = true,
358     onClick: () -> Unit
359 ) =
360     composed(
361         inspectorInfo =
362             debugInspectorInfo {
363                 name = "combinedClickable"
364                 properties["enabled"] = enabled
365                 properties["onClickLabel"] = onClickLabel
366                 properties["role"] = role
367                 properties["onClick"] = onClick
368                 properties["onDoubleClick"] = onDoubleClick
369                 properties["onLongClick"] = onLongClick
370                 properties["onLongClickLabel"] = onLongClickLabel
371                 properties["hapticFeedbackEnabled"] = hapticFeedbackEnabled
372             }
<lambda>null373     ) {
374         val localIndication = LocalIndication.current
375         val interactionSource =
376             if (localIndication is IndicationNodeFactory) {
377                 // We can fast path here as it will be created inside clickable lazily
378                 null
379             } else {
380                 // We need an interaction source to pass between the indication modifier and
381                 // clickable, so
382                 // by creating here we avoid another composed down the line
383                 remember { MutableInteractionSource() }
384             }
385         Modifier.combinedClickable(
386             enabled = enabled,
387             onClickLabel = onClickLabel,
388             onLongClickLabel = onLongClickLabel,
389             onLongClick = onLongClick,
390             onDoubleClick = onDoubleClick,
391             onClick = onClick,
392             role = role,
393             indication = localIndication,
394             interactionSource = interactionSource,
395             hapticFeedbackEnabled = hapticFeedbackEnabled
396         )
397     }
398 
399 /**
400  * Configure component to receive clicks, double clicks and long clicks via input or accessibility
401  * "click" event.
402  *
403  * Add this modifier to the element to make it clickable within its bounds.
404  *
405  * If you need only click handling, and no double or long clicks, consider using [clickable]
406  *
407  * This overload will use the [Indication] from [LocalIndication]. Use the other overload to
408  * explicitly provide an [Indication] instance. Note that this overload only supports
409  * [IndicationNodeFactory] instances provided through [LocalIndication] - it is strongly recommended
410  * to migrate to [IndicationNodeFactory], but you can use the other overload if you still need to
411  * support [Indication] instances that are not [IndicationNodeFactory].
412  *
413  * If [interactionSource] is `null`, an internal [MutableInteractionSource] will be lazily created
414  * only when needed. This reduces the performance cost of combinedClickable during composition, as
415  * creating the [indication] can be delayed until there is an incoming
416  * [androidx.compose.foundation.interaction.Interaction]. If you are only passing a remembered
417  * [MutableInteractionSource] and you are never using it outside of combinedClickable, it is
418  * recommended to instead provide `null` to enable lazy creation. If you need the [Indication] to be
419  * created eagerly, provide a remembered [MutableInteractionSource].
420  *
421  * Note, if the modifier instance gets re-used between a key down and key up events, the ongoing
422  * input will be aborted.
423  *
424  * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a
425  * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do
426  * not need to do this when removing a composable because Compose guarantees it completes via the
427  * snapshot state system.)
428  *
429  * @sample androidx.compose.foundation.samples.ClickableSample
430  * @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or
431  *   [onDoubleClick] won't be invoked
432  * @param onClickLabel semantic / accessibility label for the [onClick] action
433  * @param role the type of user interface element. Accessibility services might use this to describe
434  *   the element or do customizations
435  * @param onLongClickLabel semantic / accessibility label for the [onLongClick] action
436  * @param onLongClick will be called when user long presses on the element
437  * @param onDoubleClick will be called when user double clicks on the element
438  * @param hapticFeedbackEnabled whether to use the default [HapticFeedback] behavior
439  * @param interactionSource [MutableInteractionSource] that will be used to dispatch
440  *   [PressInteraction.Press] when this clickable is pressed. If `null`, an internal
441  *   [MutableInteractionSource] will be created if needed.
442  * @param onClick will be called when user clicks on the element
443  */
combinedClickablenull444 fun Modifier.combinedClickable(
445     enabled: Boolean = true,
446     onClickLabel: String? = null,
447     role: Role? = null,
448     onLongClickLabel: String? = null,
449     onLongClick: (() -> Unit)? = null,
450     onDoubleClick: (() -> Unit)? = null,
451     hapticFeedbackEnabled: Boolean = true,
452     interactionSource: MutableInteractionSource? = null,
453     onClick: () -> Unit
454 ): Modifier {
455     @OptIn(ExperimentalFoundationApi::class)
456     return if (ComposeFoundationFlags.isNonComposedClickableEnabled) {
457         this.then(
458             CombinedClickableElement(
459                 enabled = enabled,
460                 onClickLabel = onClickLabel,
461                 onLongClickLabel = onLongClickLabel,
462                 onLongClick = onLongClick,
463                 onDoubleClick = onDoubleClick,
464                 onClick = onClick,
465                 role = role,
466                 interactionSource = interactionSource,
467                 indicationNodeFactory = null,
468                 useLocalIndication = true,
469                 hapticFeedbackEnabled = hapticFeedbackEnabled
470             )
471         )
472     } else
473         composed(
474             inspectorInfo =
475                 debugInspectorInfo {
476                     name = "combinedClickable"
477                     properties["enabled"] = enabled
478                     properties["onClickLabel"] = onClickLabel
479                     properties["role"] = role
480                     properties["onClick"] = onClick
481                     properties["onDoubleClick"] = onDoubleClick
482                     properties["onLongClick"] = onLongClick
483                     properties["onLongClickLabel"] = onLongClickLabel
484                     properties["hapticFeedbackEnabled"] = hapticFeedbackEnabled
485                 }
486         ) {
487             val localIndication = LocalIndication.current
488             val intSource =
489                 interactionSource
490                     ?: if (localIndication is IndicationNodeFactory) {
491                         // We can fast path here as it will be created inside clickable lazily
492                         null
493                     } else {
494                         // We need an interaction source to pass between the indication modifier and
495                         // clickable, so
496                         // by creating here we avoid another composed down the line
497                         remember { MutableInteractionSource() }
498                     }
499             Modifier.combinedClickable(
500                 enabled = enabled,
501                 onClickLabel = onClickLabel,
502                 onLongClickLabel = onLongClickLabel,
503                 onLongClick = onLongClick,
504                 onDoubleClick = onDoubleClick,
505                 onClick = onClick,
506                 role = role,
507                 indication = localIndication,
508                 interactionSource = intSource,
509                 hapticFeedbackEnabled = hapticFeedbackEnabled
510             )
511         }
512 }
513 
514 @Deprecated(message = "Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
combinedClickablenull515 fun Modifier.combinedClickable(
516     enabled: Boolean = true,
517     onClickLabel: String? = null,
518     role: Role? = null,
519     onLongClickLabel: String? = null,
520     onLongClick: (() -> Unit)? = null,
521     onDoubleClick: (() -> Unit)? = null,
522     onClick: () -> Unit
523 ) =
524     composed(
525         inspectorInfo =
526             debugInspectorInfo {
527                 name = "combinedClickable"
528                 properties["enabled"] = enabled
529                 properties["onClickLabel"] = onClickLabel
530                 properties["role"] = role
531                 properties["onClick"] = onClick
532                 properties["onDoubleClick"] = onDoubleClick
533                 properties["onLongClick"] = onLongClick
534                 properties["onLongClickLabel"] = onLongClickLabel
535             }
<lambda>null536     ) {
537         val localIndication = LocalIndication.current
538         val interactionSource =
539             if (localIndication is IndicationNodeFactory) {
540                 // We can fast path here as it will be created inside clickable lazily
541                 null
542             } else {
543                 // We need an interaction source to pass between the indication modifier and
544                 // clickable, so
545                 // by creating here we avoid another composed down the line
546                 remember { MutableInteractionSource() }
547             }
548         Modifier.combinedClickable(
549             enabled = enabled,
550             onClickLabel = onClickLabel,
551             onLongClickLabel = onLongClickLabel,
552             onLongClick = onLongClick,
553             onDoubleClick = onDoubleClick,
554             onClick = onClick,
555             role = role,
556             indication = localIndication,
557             interactionSource = interactionSource,
558             hapticFeedbackEnabled = true
559         )
560     }
561 
562 /**
563  * Configure component to receive clicks, double clicks and long clicks via input or accessibility
564  * "click" event.
565  *
566  * Add this modifier to the element to make it clickable within its bounds.
567  *
568  * If you need only click handling, and no double or long clicks, consider using [clickable].
569  *
570  * Add this modifier to the element to make it clickable within its bounds.
571  *
572  * If [interactionSource] is `null`, and [indication] is an [IndicationNodeFactory], an internal
573  * [MutableInteractionSource] will be lazily created along with the [indication] only when needed.
574  * This reduces the performance cost of clickable during composition, as creating the [indication]
575  * can be delayed until there is an incoming [androidx.compose.foundation.interaction.Interaction].
576  * If you are only passing a remembered [MutableInteractionSource] and you are never using it
577  * outside of clickable, it is recommended to instead provide `null` to enable lazy creation. If you
578  * need [indication] to be created eagerly, provide a remembered [MutableInteractionSource].
579  *
580  * If [indication] is _not_ an [IndicationNodeFactory], and instead implements the deprecated
581  * [Indication.rememberUpdatedInstance] method, you should explicitly pass a remembered
582  * [MutableInteractionSource] as a parameter for [interactionSource] instead of `null`, as this
583  * cannot be lazily created inside clickable.
584  *
585  * Note, if the modifier instance gets re-used between a key down and key up events, the ongoing
586  * input will be aborted.
587  *
588  * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a
589  * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do
590  * not need to do this when removing a composable because Compose guarantees it completes via the
591  * snapshot state system.)
592  *
593  * @sample androidx.compose.foundation.samples.ClickableSample
594  * @param interactionSource [MutableInteractionSource] that will be used to emit
595  *   [PressInteraction.Press] when this clickable is pressed. If `null`, an internal
596  *   [MutableInteractionSource] will be created if needed.
597  * @param indication indication to be shown when modified element is pressed. By default, indication
598  *   from [LocalIndication] will be used. Pass `null` to show no indication, or current value from
599  *   [LocalIndication] to show theme default
600  * @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or
601  *   [onDoubleClick] won't be invoked
602  * @param onClickLabel semantic / accessibility label for the [onClick] action
603  * @param role the type of user interface element. Accessibility services might use this to describe
604  *   the element or do customizations
605  * @param onLongClickLabel semantic / accessibility label for the [onLongClick] action
606  * @param onLongClick will be called when user long presses on the element
607  * @param onDoubleClick will be called when user double clicks on the element
608  * @param hapticFeedbackEnabled whether to use the default [HapticFeedback] behavior
609  * @param onClick will be called when user clicks on the element
610  */
combinedClickablenull611 fun Modifier.combinedClickable(
612     interactionSource: MutableInteractionSource?,
613     indication: Indication?,
614     enabled: Boolean = true,
615     onClickLabel: String? = null,
616     role: Role? = null,
617     onLongClickLabel: String? = null,
618     onLongClick: (() -> Unit)? = null,
619     onDoubleClick: (() -> Unit)? = null,
620     hapticFeedbackEnabled: Boolean = true,
621     onClick: () -> Unit
622 ) =
623     clickableWithIndicationIfNeeded(
624         interactionSource = interactionSource,
625         indication = indication
626     ) { intSource, indicationNodeFactory ->
627         CombinedClickableElement(
628             interactionSource = intSource,
629             indicationNodeFactory = indicationNodeFactory,
630             useLocalIndication = false,
631             enabled = enabled,
632             onClickLabel = onClickLabel,
633             role = role,
634             onClick = onClick,
635             onLongClickLabel = onLongClickLabel,
636             onLongClick = onLongClick,
637             onDoubleClick = onDoubleClick,
638             hapticFeedbackEnabled = hapticFeedbackEnabled
639         )
640     }
641 
642 @Deprecated(message = "Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
combinedClickablenull643 fun Modifier.combinedClickable(
644     interactionSource: MutableInteractionSource?,
645     indication: Indication?,
646     enabled: Boolean = true,
647     onClickLabel: String? = null,
648     role: Role? = null,
649     onLongClickLabel: String? = null,
650     onLongClick: (() -> Unit)? = null,
651     onDoubleClick: (() -> Unit)? = null,
652     onClick: () -> Unit
653 ) =
654     clickableWithIndicationIfNeeded(
655         interactionSource = interactionSource,
656         indication = indication
657     ) { intSource, indicationNodeFactory ->
658         CombinedClickableElement(
659             interactionSource = intSource,
660             indicationNodeFactory = indicationNodeFactory,
661             useLocalIndication = false,
662             enabled = enabled,
663             onClickLabel = onClickLabel,
664             role = role,
665             onClick = onClick,
666             onLongClickLabel = onLongClickLabel,
667             onLongClick = onLongClick,
668             onDoubleClick = onDoubleClick,
669             hapticFeedbackEnabled = true
670         )
671     }
672 
673 /**
674  * Utility Modifier factory that handles edge cases for [interactionSource], and [indication].
675  * [createClickable] is the lambda that creates the actual clickable element, which will be chained
676  * with [Modifier.indication] if needed.
677  */
clickableWithIndicationIfNeedednull678 internal inline fun Modifier.clickableWithIndicationIfNeeded(
679     interactionSource: MutableInteractionSource?,
680     indication: Indication?,
681     crossinline createClickable: (MutableInteractionSource?, IndicationNodeFactory?) -> Modifier
682 ): Modifier {
683     return this.then(
684         when {
685             // Fast path - indication is managed internally
686             indication is IndicationNodeFactory -> createClickable(interactionSource, indication)
687             // Fast path - no need for indication
688             indication == null -> createClickable(interactionSource, null)
689             // Non-null Indication (not IndicationNodeFactory) with a non-null InteractionSource
690             interactionSource != null ->
691                 Modifier.indication(interactionSource, indication)
692                     .then(createClickable(interactionSource, null))
693             // Non-null Indication (not IndicationNodeFactory) with a null InteractionSource, so we
694             // need
695             // to use composed to create an InteractionSource that can be shared. This should be a
696             // rare
697             // code path and can only be hit from new callers.
698             else ->
699                 Modifier.composed {
700                     val newInteractionSource = remember { MutableInteractionSource() }
701                     Modifier.indication(newInteractionSource, indication)
702                         .then(createClickable(newInteractionSource, null))
703                 }
704         }
705     )
706 }
707 
708 /**
709  * How long to wait before appearing 'pressed' (emitting [PressInteraction.Press]) - if a touch down
710  * will quickly become a drag / scroll, this timeout means that we don't show a press effect.
711  */
712 internal expect val TapIndicationDelay: Long
713 
714 /**
715  * Returns whether the root Compose layout node is hosted in a scrollable container outside of
716  * Compose. On Android this will be whether the root View is in a scrollable ViewGroup, as even if
717  * nothing in the Compose part of the hierarchy is scrollable, if the View itself is in a scrollable
718  * container, we still want to delay presses in case presses in Compose convert to a scroll outside
719  * of Compose.
720  *
721  * Combine this with [hasScrollableContainer], which returns whether a [Modifier] is within a
722  * scrollable Compose layout, to calculate whether this modifier is within some form of scrollable
723  * container, and hence should delay presses.
724  */
isComposeRootInScrollableContainernull725 internal expect fun DelegatableNode.isComposeRootInScrollableContainer(): Boolean
726 
727 /**
728  * Whether the specified [KeyEvent] should trigger a press for a clickable component, i.e. whether
729  * it is associated with a press of an enter key or dpad centre.
730  */
731 private val KeyEvent.isPress: Boolean
732     get() = type == KeyDown && isEnter
733 
734 /**
735  * Whether the specified [KeyEvent] should trigger a click for a clickable component, i.e. whether
736  * it is associated with a release of an enter key or dpad centre.
737  */
738 private val KeyEvent.isClick: Boolean
739     get() = type == KeyUp && isEnter
740 
741 private val KeyEvent.isEnter: Boolean
742     get() =
743         when (key) {
744             Key.DirectionCenter,
745             Key.Enter,
746             Key.NumPadEnter,
747             Key.Spacebar -> true
748             else -> false
749         }
750 
751 private class ClickableElement(
752     private val interactionSource: MutableInteractionSource?,
753     private val indicationNodeFactory: IndicationNodeFactory?,
754     private val useLocalIndication: Boolean,
755     private val enabled: Boolean,
756     private val onClickLabel: String?,
757     private val role: Role?,
758     private val onClick: () -> Unit
759 ) : ModifierNodeElement<ClickableNode>() {
createnull760     override fun create() =
761         ClickableNode(
762             interactionSource = interactionSource,
763             indicationNodeFactory = indicationNodeFactory,
764             useLocalIndication = useLocalIndication,
765             enabled = enabled,
766             onClickLabel = onClickLabel,
767             role = role,
768             onClick = onClick
769         )
770 
771     override fun update(node: ClickableNode) {
772         node.update(
773             interactionSource = interactionSource,
774             indicationNodeFactory = indicationNodeFactory,
775             useLocalIndication = useLocalIndication,
776             enabled = enabled,
777             onClickLabel = onClickLabel,
778             role = role,
779             onClick = onClick
780         )
781     }
782 
inspectablePropertiesnull783     override fun InspectorInfo.inspectableProperties() {
784         name = "clickable"
785         properties["enabled"] = enabled
786         properties["onClick"] = onClick
787         properties["onClickLabel"] = onClickLabel
788         properties["role"] = role
789         properties["interactionSource"] = interactionSource
790         properties["indicationNodeFactory"] = indicationNodeFactory
791     }
792 
equalsnull793     override fun equals(other: Any?): Boolean {
794         if (this === other) return true
795         if (other === null) return false
796         if (this::class != other::class) return false
797 
798         other as ClickableElement
799 
800         if (interactionSource != other.interactionSource) return false
801         if (indicationNodeFactory != other.indicationNodeFactory) return false
802         if (useLocalIndication != other.useLocalIndication) return false
803         if (enabled != other.enabled) return false
804         if (onClickLabel != other.onClickLabel) return false
805         if (role != other.role) return false
806         if (onClick !== other.onClick) return false
807 
808         return true
809     }
810 
hashCodenull811     override fun hashCode(): Int {
812         var result = (interactionSource?.hashCode() ?: 0)
813         result = 31 * result + (indicationNodeFactory?.hashCode() ?: 0)
814         result = 31 * result + useLocalIndication.hashCode()
815         result = 31 * result + enabled.hashCode()
816         result = 31 * result + (onClickLabel?.hashCode() ?: 0)
817         result = 31 * result + (role?.hashCode() ?: 0)
818         result = 31 * result + onClick.hashCode()
819         return result
820     }
821 }
822 
823 private class CombinedClickableElement(
824     private val interactionSource: MutableInteractionSource?,
825     private val indicationNodeFactory: IndicationNodeFactory?,
826     private val useLocalIndication: Boolean,
827     private val enabled: Boolean,
828     private val onClickLabel: String?,
829     private val role: Role?,
830     private val onClick: () -> Unit,
831     private val onLongClickLabel: String?,
832     private val onLongClick: (() -> Unit)?,
833     private val onDoubleClick: (() -> Unit)?,
834     private val hapticFeedbackEnabled: Boolean,
835 ) : ModifierNodeElement<CombinedClickableNode>() {
createnull836     override fun create() =
837         CombinedClickableNode(
838             onClick = onClick,
839             onLongClickLabel = onLongClickLabel,
840             onLongClick = onLongClick,
841             onDoubleClick = onDoubleClick,
842             hapticFeedbackEnabled = hapticFeedbackEnabled,
843             interactionSource = interactionSource,
844             indicationNodeFactory = indicationNodeFactory,
845             useLocalIndication = useLocalIndication,
846             enabled = enabled,
847             onClickLabel = onClickLabel,
848             role = role,
849         )
850 
851     override fun update(node: CombinedClickableNode) {
852         node.hapticFeedbackEnabled = hapticFeedbackEnabled
853         node.update(
854             onClick,
855             onLongClickLabel,
856             onLongClick,
857             onDoubleClick,
858             interactionSource,
859             indicationNodeFactory,
860             useLocalIndication,
861             enabled,
862             onClickLabel,
863             role
864         )
865     }
866 
inspectablePropertiesnull867     override fun InspectorInfo.inspectableProperties() {
868         name = "combinedClickable"
869         properties["indicationNodeFactory"] = indicationNodeFactory
870         properties["interactionSource"] = interactionSource
871         properties["enabled"] = enabled
872         properties["onClickLabel"] = onClickLabel
873         properties["role"] = role
874         properties["onClick"] = onClick
875         properties["onDoubleClick"] = onDoubleClick
876         properties["onLongClick"] = onLongClick
877         properties["onLongClickLabel"] = onLongClickLabel
878         properties["hapticFeedbackEnabled"] = hapticFeedbackEnabled
879     }
880 
equalsnull881     override fun equals(other: Any?): Boolean {
882         if (this === other) return true
883         if (other === null) return false
884         if (this::class != other::class) return false
885 
886         other as CombinedClickableElement
887 
888         if (interactionSource != other.interactionSource) return false
889         if (indicationNodeFactory != other.indicationNodeFactory) return false
890         if (useLocalIndication != other.useLocalIndication) return false
891         if (enabled != other.enabled) return false
892         if (onClickLabel != other.onClickLabel) return false
893         if (role != other.role) return false
894         if (onClick !== other.onClick) return false
895         if (onLongClickLabel != other.onLongClickLabel) return false
896         if (onLongClick !== other.onLongClick) return false
897         if (onDoubleClick !== other.onDoubleClick) return false
898         if (hapticFeedbackEnabled != other.hapticFeedbackEnabled) return false
899 
900         return true
901     }
902 
hashCodenull903     override fun hashCode(): Int {
904         var result = (interactionSource?.hashCode() ?: 0)
905         result = 31 * result + (indicationNodeFactory?.hashCode() ?: 0)
906         result = 31 * result + useLocalIndication.hashCode()
907         result = 31 * result + enabled.hashCode()
908         result = 31 * result + (onClickLabel?.hashCode() ?: 0)
909         result = 31 * result + (role?.hashCode() ?: 0)
910         result = 31 * result + onClick.hashCode()
911         result = 31 * result + (onLongClickLabel?.hashCode() ?: 0)
912         result = 31 * result + (onLongClick?.hashCode() ?: 0)
913         result = 31 * result + (onDoubleClick?.hashCode() ?: 0)
914         result = 31 * result + hapticFeedbackEnabled.hashCode()
915         return result
916     }
917 }
918 
919 internal open class ClickableNode(
920     interactionSource: MutableInteractionSource?,
921     indicationNodeFactory: IndicationNodeFactory?,
922     useLocalIndication: Boolean,
923     enabled: Boolean,
924     onClickLabel: String?,
925     role: Role?,
926     onClick: () -> Unit
927 ) :
928     AbstractClickableNode(
929         interactionSource = interactionSource,
930         indicationNodeFactory = indicationNodeFactory,
931         useLocalIndication = useLocalIndication,
932         enabled = enabled,
933         onClickLabel = onClickLabel,
934         role = role,
935         onClick = onClick
936     ) {
clickPointerInputnull937     override suspend fun PointerInputScope.clickPointerInput() {
938         detectTapAndPress(
939             onPress = { offset ->
940                 if (enabled) {
941                     handlePressInteraction(offset)
942                 }
943             },
944             onTap = { if (enabled) onClick() }
945         )
946     }
947 
updatenull948     fun update(
949         interactionSource: MutableInteractionSource?,
950         indicationNodeFactory: IndicationNodeFactory?,
951         useLocalIndication: Boolean,
952         enabled: Boolean,
953         onClickLabel: String?,
954         role: Role?,
955         onClick: () -> Unit
956     ) {
957         // enabled and onClick are captured inside callbacks, not as an input to detectTapGestures,
958         // so no need need to reset pointer input handling when they change
959         updateCommon(
960             interactionSource = interactionSource,
961             indicationNodeFactory = indicationNodeFactory,
962             useLocalIndication = useLocalIndication,
963             enabled = enabled,
964             onClickLabel = onClickLabel,
965             role = role,
966             onClick = onClick
967         )
968     }
969 
onClickKeyDownEventnull970     final override fun onClickKeyDownEvent(event: KeyEvent) = false
971 
972     final override fun onClickKeyUpEvent(event: KeyEvent): Boolean {
973         onClick()
974         return true
975     }
976 }
977 
978 private class CombinedClickableNode(
979     onClick: () -> Unit,
980     private var onLongClickLabel: String?,
981     private var onLongClick: (() -> Unit)?,
982     private var onDoubleClick: (() -> Unit)?,
983     var hapticFeedbackEnabled: Boolean,
984     interactionSource: MutableInteractionSource?,
985     indicationNodeFactory: IndicationNodeFactory?,
986     useLocalIndication: Boolean,
987     enabled: Boolean,
988     onClickLabel: String?,
989     role: Role?,
990 ) :
991     CompositionLocalConsumerModifierNode,
992     AbstractClickableNode(
993         interactionSource = interactionSource,
994         indicationNodeFactory = indicationNodeFactory,
995         useLocalIndication = useLocalIndication,
996         enabled = enabled,
997         onClickLabel = onClickLabel,
998         role = role,
999         onClick = onClick
1000     ) {
1001     class DoubleKeyClickState(val job: Job) {
1002         var doubleTapMinTimeMillisElapsed: Boolean = false
1003     }
1004 
1005     private val longKeyPressJobs = mutableLongObjectMapOf<Job>()
1006     private val doubleKeyClickStates = mutableLongObjectMapOf<DoubleKeyClickState>()
1007 
clickPointerInputnull1008     override suspend fun PointerInputScope.clickPointerInput() {
1009         detectTapGestures(
1010             onDoubleTap =
1011                 if (enabled && onDoubleClick != null) {
1012                     { onDoubleClick?.invoke() }
1013                 } else null,
1014             onLongPress =
1015                 if (enabled && onLongClick != null) {
1016                     {
1017                         onLongClick?.invoke()
1018                         if (hapticFeedbackEnabled) {
1019                             currentValueOf(LocalHapticFeedback)
1020                                 .performHapticFeedback(HapticFeedbackType.LongPress)
1021                         }
1022                     }
1023                 } else null,
1024             onPress = { offset ->
1025                 if (enabled) {
1026                     handlePressInteraction(offset)
1027                 }
1028             },
1029             onTap = {
1030                 if (enabled) {
1031                     onClick()
1032                 }
1033             }
1034         )
1035     }
1036 
updatenull1037     fun update(
1038         onClick: () -> Unit,
1039         onLongClickLabel: String?,
1040         onLongClick: (() -> Unit)?,
1041         onDoubleClick: (() -> Unit)?,
1042         interactionSource: MutableInteractionSource?,
1043         indicationNodeFactory: IndicationNodeFactory?,
1044         useLocalIndication: Boolean,
1045         enabled: Boolean,
1046         onClickLabel: String?,
1047         role: Role?
1048     ) {
1049         var resetPointerInputHandling = false
1050 
1051         // onClick is captured inside a callback, not as an input to detectTapGestures,
1052         // so no need need to reset pointer input handling
1053 
1054         if (this.onLongClickLabel != onLongClickLabel) {
1055             this.onLongClickLabel = onLongClickLabel
1056             invalidateSemantics()
1057         }
1058 
1059         // We capture onLongClick and onDoubleClick inside the callback, so if the lambda changes
1060         // value we don't want to reset input handling - only reset if they go from not-defined to
1061         // defined, and vice-versa, as that is what is captured in the parameter to
1062         // detectTapGestures.
1063         if ((this.onLongClick == null) != (onLongClick == null)) {
1064             // Adding or removing longClick should cancel any existing press interactions
1065             disposeInteractions()
1066             // Adding or removing longClick should add / remove the corresponding property
1067             invalidateSemantics()
1068             resetPointerInputHandling = true
1069         }
1070 
1071         this.onLongClick = onLongClick
1072 
1073         if ((this.onDoubleClick == null) != (onDoubleClick == null)) {
1074             resetPointerInputHandling = true
1075         }
1076         this.onDoubleClick = onDoubleClick
1077 
1078         // enabled is captured as a parameter to detectTapGestures, so we need to restart detecting
1079         // gestures if it changes.
1080         if (this.enabled != enabled) {
1081             resetPointerInputHandling = true
1082             // Updating is handled inside updateCommon
1083         }
1084 
1085         updateCommon(
1086             interactionSource = interactionSource,
1087             indicationNodeFactory = indicationNodeFactory,
1088             useLocalIndication = useLocalIndication,
1089             enabled = enabled,
1090             onClickLabel = onClickLabel,
1091             role = role,
1092             onClick = onClick
1093         )
1094 
1095         if (resetPointerInputHandling) resetPointerInputHandler()
1096     }
1097 
applyAdditionalSemanticsnull1098     override fun SemanticsPropertyReceiver.applyAdditionalSemantics() {
1099         if (onLongClick != null) {
1100             onLongClick(
1101                 action = {
1102                     onLongClick?.invoke()
1103                     true
1104                 },
1105                 label = onLongClickLabel
1106             )
1107         }
1108     }
1109 
onClickKeyDownEventnull1110     override fun onClickKeyDownEvent(event: KeyEvent): Boolean {
1111         val keyCode = event.key.keyCode
1112         var handledByLongClick = false
1113         if (onLongClick != null) {
1114             if (longKeyPressJobs[keyCode] == null) {
1115                 longKeyPressJobs[keyCode] =
1116                     coroutineScope.launch {
1117                         delay(currentValueOf(LocalViewConfiguration).longPressTimeoutMillis)
1118                         onLongClick?.invoke()
1119                     }
1120                 handledByLongClick = true
1121             }
1122         }
1123         val doubleClickState = doubleKeyClickStates[keyCode]
1124         // This is the second down event, so it might be a double click
1125         if (doubleClickState != null) {
1126             // Within the allowed timeout, so check if this is above the minimum time needed for
1127             // a double click
1128             if (doubleClickState.job.isActive) {
1129                 doubleClickState.job.cancel()
1130                 // If the second down was before the minimum double tap time, don't track this as
1131                 // a double click. Instead, we need to invoke onClick for the previous click, since
1132                 // that is now counted as a standalone click instead of the first of a double click.
1133                 if (!doubleClickState.doubleTapMinTimeMillisElapsed) {
1134                     onClick()
1135                     doubleKeyClickStates.remove(keyCode)
1136                 }
1137             } else {
1138                 // We already invoked onClick because we passed the timeout, so stop tracking this
1139                 // as a double click
1140                 doubleKeyClickStates.remove(keyCode)
1141             }
1142         }
1143         return handledByLongClick
1144     }
1145 
onClickKeyUpEventnull1146     override fun onClickKeyUpEvent(event: KeyEvent): Boolean {
1147         val keyCode = event.key.keyCode
1148         var longClickInvoked = false
1149         if (longKeyPressJobs[keyCode] != null) {
1150             longKeyPressJobs[keyCode]?.let {
1151                 if (it.isActive) {
1152                     it.cancel()
1153                 } else {
1154                     // If we already passed the timeout, we invoked long click already, and so
1155                     // we shouldn't invoke onClick in this case
1156                     longClickInvoked = true
1157                 }
1158             }
1159             longKeyPressJobs.remove(keyCode)
1160         }
1161         if (onDoubleClick != null) {
1162             when {
1163                 // First click
1164                 doubleKeyClickStates[keyCode] == null -> {
1165                     // We only track the second click if the first click was not a long click
1166                     if (!longClickInvoked) {
1167                         doubleKeyClickStates[keyCode] =
1168                             DoubleKeyClickState(
1169                                 coroutineScope.launch {
1170                                     val configuration = currentValueOf(LocalViewConfiguration)
1171                                     val minTime = configuration.doubleTapMinTimeMillis
1172                                     val timeout = configuration.doubleTapTimeoutMillis
1173                                     delay(minTime)
1174                                     doubleKeyClickStates[keyCode]?.doubleTapMinTimeMillisElapsed =
1175                                         true
1176                                     // Delay the remainder until we are at timeout
1177                                     delay(timeout - minTime)
1178                                     // If there was no second key press after the timeout, invoke
1179                                     // onClick as normal
1180                                     onClick()
1181                                 }
1182                             )
1183                     }
1184                 }
1185                 // Second click
1186                 else -> {
1187                     // Invoke onDoubleClick if the second click was not a long click
1188                     if (!longClickInvoked) {
1189                         onDoubleClick?.invoke()
1190                     }
1191                     doubleKeyClickStates.remove(keyCode)
1192                 }
1193             }
1194         } else {
1195             if (!longClickInvoked) {
1196                 onClick()
1197             }
1198         }
1199         return true
1200     }
1201 
onCancelKeyInputnull1202     override fun onCancelKeyInput() {
1203         resetKeyPressState()
1204     }
1205 
onResetnull1206     override fun onReset() {
1207         super.onReset()
1208         resetKeyPressState()
1209     }
1210 
resetKeyPressStatenull1211     private fun resetKeyPressState() {
1212         longKeyPressJobs.apply {
1213             forEachValue { it.cancel() }
1214             clear()
1215         }
1216         doubleKeyClickStates.apply {
1217             forEachValue { it.job.cancel() }
1218             clear()
1219         }
1220     }
1221 }
1222 
1223 internal abstract class AbstractClickableNode(
1224     private var interactionSource: MutableInteractionSource?,
1225     private var indicationNodeFactory: IndicationNodeFactory?,
1226     private var useLocalIndication: Boolean,
1227     enabled: Boolean,
1228     private var onClickLabel: String?,
1229     private var role: Role?,
1230     onClick: () -> Unit
1231 ) :
1232     DelegatingNode(),
1233     PointerInputModifierNode,
1234     KeyInputModifierNode,
1235     SemanticsModifierNode,
1236     TraversableNode,
1237     CompositionLocalConsumerModifierNode,
1238     ObserverModifierNode {
1239     protected var enabled = enabled
1240         private set
1241 
1242     protected var onClick = onClick
1243         private set
1244 
1245     final override val shouldAutoInvalidate: Boolean = false
1246 
1247     private val focusableNode: FocusableNode =
1248         FocusableNode(
1249             interactionSource,
1250             focusability = Focusability.SystemDefined,
1251             onFocusChange = ::onFocusChange
1252         )
1253 
1254     private var localIndicationNodeFactory: IndicationNodeFactory? = null
1255 
1256     private var pointerInputNode: SuspendingPointerInputModifierNode? = null
1257     private var indicationNode: DelegatableNode? = null
1258 
1259     private var pressInteraction: PressInteraction.Press? = null
1260     private var hoverInteraction: HoverInteraction.Enter? = null
1261     private val currentKeyPressInteractions = mutableLongObjectMapOf<PressInteraction.Press>()
1262     private var centerOffset: Offset = Offset.Zero
1263 
1264     // Track separately from interactionSource, as we will create our own internal
1265     // InteractionSource if needed
1266     private var userProvidedInteractionSource: MutableInteractionSource? = interactionSource
1267 
1268     private var lazilyCreateIndication = shouldLazilyCreateIndication()
1269 
shouldLazilyCreateIndicationnull1270     private fun shouldLazilyCreateIndication() = userProvidedInteractionSource == null
1271 
1272     /**
1273      * Handles subclass-specific click related pointer input logic. Hover is already handled
1274      * elsewhere, so this should only handle clicks.
1275      */
1276     abstract suspend fun PointerInputScope.clickPointerInput()
1277 
1278     open fun SemanticsPropertyReceiver.applyAdditionalSemantics() {}
1279 
updateCommonnull1280     protected fun updateCommon(
1281         interactionSource: MutableInteractionSource?,
1282         indicationNodeFactory: IndicationNodeFactory?,
1283         useLocalIndication: Boolean,
1284         enabled: Boolean,
1285         onClickLabel: String?,
1286         role: Role?,
1287         onClick: () -> Unit
1288     ) {
1289         var isIndicationNodeDirty = false
1290         // Compare against userProvidedInteractionSource, as we will create a new InteractionSource
1291         // lazily if the userProvidedInteractionSource is null, and assign it to interactionSource
1292         if (userProvidedInteractionSource != interactionSource) {
1293             disposeInteractions()
1294             userProvidedInteractionSource = interactionSource
1295             this.interactionSource = interactionSource
1296             isIndicationNodeDirty = true
1297         }
1298         if (this.indicationNodeFactory != indicationNodeFactory) {
1299             this.indicationNodeFactory = indicationNodeFactory
1300             isIndicationNodeDirty = true
1301         }
1302         if (this.useLocalIndication != useLocalIndication) {
1303             this.useLocalIndication = useLocalIndication
1304             if (useLocalIndication) {
1305                 // Need to update localIndicationNodeFactory, and start observing changes
1306                 onObservedReadsChanged()
1307             }
1308             isIndicationNodeDirty = true
1309         }
1310         if (this.enabled != enabled) {
1311             if (enabled) {
1312                 delegate(focusableNode)
1313             } else {
1314                 // TODO: Should we remove indicationNode? Previously we always emitted indication
1315                 undelegate(focusableNode)
1316                 disposeInteractions()
1317             }
1318             invalidateSemantics()
1319             this.enabled = enabled
1320         }
1321         if (this.onClickLabel != onClickLabel) {
1322             this.onClickLabel = onClickLabel
1323             invalidateSemantics()
1324         }
1325         if (this.role != role) {
1326             this.role = role
1327             invalidateSemantics()
1328         }
1329         this.onClick = onClick
1330         if (lazilyCreateIndication != shouldLazilyCreateIndication()) {
1331             lazilyCreateIndication = shouldLazilyCreateIndication()
1332             // If we are no longer lazily creating the node, and we haven't created the node yet,
1333             // create it
1334             if (!lazilyCreateIndication && indicationNode == null) isIndicationNodeDirty = true
1335         }
1336         // Create / recreate indication node
1337         if (isIndicationNodeDirty) {
1338             recreateIndicationIfNeeded()
1339         }
1340         focusableNode.update(this.interactionSource)
1341     }
1342 
onAttachnull1343     final override fun onAttach() {
1344         onObservedReadsChanged()
1345         if (!lazilyCreateIndication) {
1346             initializeIndicationAndInteractionSourceIfNeeded()
1347         }
1348         if (enabled) {
1349             delegate(focusableNode)
1350         }
1351     }
1352 
onObservedReadsChangednull1353     override fun onObservedReadsChanged() {
1354         if (useLocalIndication) {
1355             observeReads {
1356                 val indication = currentValueOf(LocalIndication)
1357                 requirePrecondition(indication is IndicationNodeFactory) {
1358                     unsupportedIndicationExceptionMessage(indication)
1359                 }
1360                 val previousFactory = localIndicationNodeFactory
1361                 localIndicationNodeFactory = indication
1362                 // If we are changing from a non-null factory to a different factory, recreate
1363                 // indication if needed
1364                 if (previousFactory != null && localIndicationNodeFactory != previousFactory) {
1365                     recreateIndicationIfNeeded()
1366                 }
1367             }
1368         }
1369     }
1370 
onDetachnull1371     final override fun onDetach() {
1372         disposeInteractions()
1373         // If we lazily created an interaction source, reset it in case we are reused / moved. Note
1374         // that we need to do it here instead of onReset() - since onReset won't be called in the
1375         // movableContent case but we still want to dispose for that case
1376         if (userProvidedInteractionSource == null) {
1377             interactionSource = null
1378         }
1379         // Remove indication in case we are reused / moved - we will create a new node when needed
1380         indicationNode?.let { undelegate(it) }
1381         indicationNode = null
1382     }
1383 
disposeInteractionsnull1384     protected fun disposeInteractions() {
1385         interactionSource?.let { interactionSource ->
1386             pressInteraction?.let { oldValue ->
1387                 val interaction = PressInteraction.Cancel(oldValue)
1388                 interactionSource.tryEmit(interaction)
1389             }
1390             hoverInteraction?.let { oldValue ->
1391                 val interaction = HoverInteraction.Exit(oldValue)
1392                 interactionSource.tryEmit(interaction)
1393             }
1394             currentKeyPressInteractions.forEachValue {
1395                 interactionSource.tryEmit(PressInteraction.Cancel(it))
1396             }
1397         }
1398         pressInteraction = null
1399         hoverInteraction = null
1400         currentKeyPressInteractions.clear()
1401     }
1402 
onFocusChangenull1403     private fun onFocusChange(isFocused: Boolean) {
1404         if (isFocused) {
1405             initializeIndicationAndInteractionSourceIfNeeded()
1406         } else {
1407             // If we are no longer focused while we are tracking existing key presses, we need to
1408             // clear them and cancel the presses.
1409             if (interactionSource != null) {
1410                 currentKeyPressInteractions.forEachValue {
1411                     coroutineScope.launch { interactionSource?.emit(PressInteraction.Cancel(it)) }
1412                 }
1413             }
1414             currentKeyPressInteractions.clear()
1415             onCancelKeyInput()
1416         }
1417     }
1418 
recreateIndicationIfNeedednull1419     private fun recreateIndicationIfNeeded() {
1420         // If we already created a node lazily, or we are not lazily creating the node, create
1421         if (indicationNode != null || !lazilyCreateIndication) {
1422             indicationNode?.let { undelegate(it) }
1423             indicationNode = null
1424             initializeIndicationAndInteractionSourceIfNeeded()
1425         }
1426     }
1427 
initializeIndicationAndInteractionSourceIfNeedednull1428     private fun initializeIndicationAndInteractionSourceIfNeeded() {
1429         // We have already created the node, no need to do any work
1430         if (indicationNode != null) return
1431         val indicationFactory =
1432             if (useLocalIndication) localIndicationNodeFactory else indicationNodeFactory
1433         indicationFactory?.let { factory ->
1434             if (interactionSource == null) {
1435                 interactionSource = MutableInteractionSource()
1436             }
1437             focusableNode.update(interactionSource)
1438             val node = factory.create(interactionSource!!)
1439             delegate(node)
1440             indicationNode = node
1441         }
1442     }
1443 
onPointerEventnull1444     final override fun onPointerEvent(
1445         pointerEvent: PointerEvent,
1446         pass: PointerEventPass,
1447         bounds: IntSize
1448     ) {
1449         centerOffset = bounds.center.toOffset()
1450         initializeIndicationAndInteractionSourceIfNeeded()
1451         if (enabled) {
1452             if (pass == PointerEventPass.Main) {
1453                 when (pointerEvent.type) {
1454                     PointerEventType.Enter -> coroutineScope.launch { emitHoverEnter() }
1455                     PointerEventType.Exit -> coroutineScope.launch { emitHoverExit() }
1456                 }
1457             }
1458         }
1459         if (pointerInputNode == null) {
1460             pointerInputNode = delegate(SuspendingPointerInputModifierNode { clickPointerInput() })
1461         }
1462         pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
1463     }
1464 
onCancelPointerInputnull1465     final override fun onCancelPointerInput() {
1466         // Press cancellation is handled as part of detecting presses
1467         interactionSource?.let { interactionSource ->
1468             hoverInteraction?.let { oldValue ->
1469                 val interaction = HoverInteraction.Exit(oldValue)
1470                 interactionSource.tryEmit(interaction)
1471             }
1472         }
1473         hoverInteraction = null
1474         pointerInputNode?.onCancelPointerInput()
1475     }
1476 
onKeyEventnull1477     final override fun onKeyEvent(event: KeyEvent): Boolean {
1478         // Key events usually require focus, but if a focused child does not handle the KeyEvent,
1479         // the event can bubble up without this clickable ever being focused, and hence without
1480         // this being initialized through the focus path
1481         initializeIndicationAndInteractionSourceIfNeeded()
1482         val keyCode = event.key.keyCode
1483         return when {
1484             enabled && event.isPress -> {
1485                 // If the key already exists in the map, keyEvent is a repeat event.
1486                 // We ignore it as we only want to emit an interaction for the initial key press.
1487                 var wasInteractionHandled = false
1488                 if (!currentKeyPressInteractions.containsKey(keyCode)) {
1489                     val press = PressInteraction.Press(centerOffset)
1490                     currentKeyPressInteractions[keyCode] = press
1491                     // Even if the interactionSource is null, we still want to intercept the presses
1492                     // so we always track them above, and return true
1493                     if (interactionSource != null) {
1494                         coroutineScope.launch { interactionSource?.emit(press) }
1495                     }
1496                     wasInteractionHandled = true
1497                 }
1498                 onClickKeyDownEvent(event) || wasInteractionHandled
1499             }
1500             enabled && event.isClick -> {
1501                 val press = currentKeyPressInteractions.remove(keyCode)
1502                 if (press != null) {
1503                     if (interactionSource != null) {
1504                         coroutineScope.launch {
1505                             interactionSource?.emit(PressInteraction.Release(press))
1506                         }
1507                     }
1508                     // Don't invoke onClick if we were not pressed - this could happen if we became
1509                     // focused after the down event, or if the node was reused after the down event.
1510                     onClickKeyUpEvent(event)
1511                 }
1512                 // Only consume if we were previously pressed for this key event
1513                 press != null
1514             }
1515             else -> false
1516         }
1517     }
1518 
onClickKeyDownEventnull1519     protected abstract fun onClickKeyDownEvent(event: KeyEvent): Boolean
1520 
1521     protected abstract fun onClickKeyUpEvent(event: KeyEvent): Boolean
1522 
1523     /**
1524      * Called when focus is lost, to allow cleaning up and resetting the state for ongoing key
1525      * presses
1526      */
1527     protected open fun onCancelKeyInput() {}
1528 
onPreKeyEventnull1529     final override fun onPreKeyEvent(event: KeyEvent) = false
1530 
1531     final override val shouldMergeDescendantSemantics: Boolean
1532         get() = true
1533 
1534     final override fun SemanticsPropertyReceiver.applySemantics() {
1535         if (this@AbstractClickableNode.role != null) {
1536             role = this@AbstractClickableNode.role!!
1537         }
1538         onClick(
1539             action = {
1540                 onClick()
1541                 true
1542             },
1543             label = onClickLabel
1544         )
1545         if (enabled) {
1546             with(focusableNode) { applySemantics() }
1547         } else {
1548             disabled()
1549         }
1550         applyAdditionalSemantics()
1551     }
1552 
resetPointerInputHandlernull1553     protected fun resetPointerInputHandler() = pointerInputNode?.resetPointerInputHandler()
1554 
1555     protected suspend fun PressGestureScope.handlePressInteraction(offset: Offset) {
1556         interactionSource?.let { interactionSource ->
1557             coroutineScope {
1558                 val delayJob = launch {
1559                     if (delayPressInteraction()) {
1560                         delay(TapIndicationDelay)
1561                     }
1562                     val press = PressInteraction.Press(offset)
1563                     interactionSource.emit(press)
1564                     pressInteraction = press
1565                 }
1566                 val success = tryAwaitRelease()
1567                 if (delayJob.isActive) {
1568                     delayJob.cancelAndJoin()
1569                     // The press released successfully, before the timeout duration - emit the press
1570                     // interaction instantly. No else branch - if the press was cancelled before the
1571                     // timeout, we don't want to emit a press interaction.
1572                     if (success) {
1573                         val press = PressInteraction.Press(offset)
1574                         val release = PressInteraction.Release(press)
1575                         interactionSource.emit(press)
1576                         interactionSource.emit(release)
1577                     }
1578                 } else {
1579                     pressInteraction?.let { pressInteraction ->
1580                         val endInteraction =
1581                             if (success) {
1582                                 PressInteraction.Release(pressInteraction)
1583                             } else {
1584                                 PressInteraction.Cancel(pressInteraction)
1585                             }
1586                         interactionSource.emit(endInteraction)
1587                     }
1588                 }
1589                 pressInteraction = null
1590             }
1591         }
1592     }
1593 
delayPressInteractionnull1594     private fun delayPressInteraction(): Boolean =
1595         hasScrollableContainer() || isComposeRootInScrollableContainer()
1596 
1597     private fun emitHoverEnter() {
1598         if (hoverInteraction == null) {
1599             val interaction = HoverInteraction.Enter()
1600             interactionSource?.let { interactionSource ->
1601                 coroutineScope.launch { interactionSource.emit(interaction) }
1602             }
1603             hoverInteraction = interaction
1604         }
1605     }
1606 
emitHoverExitnull1607     private fun emitHoverExit() {
1608         hoverInteraction?.let { oldValue ->
1609             val interaction = HoverInteraction.Exit(oldValue)
1610             interactionSource?.let { interactionSource ->
1611                 coroutineScope.launch { interactionSource.emit(interaction) }
1612             }
1613             hoverInteraction = null
1614         }
1615     }
1616 
1617     override val traverseKey: Any = TraverseKey
1618 
1619     companion object TraverseKey
1620 }
1621 
hasScrollableContainernull1622 internal fun TraversableNode.hasScrollableContainer(): Boolean {
1623     var hasScrollable = false
1624     traverseAncestors(ScrollableContainerNode.TraverseKey) { node ->
1625         hasScrollable = hasScrollable || (node as ScrollableContainerNode).enabled
1626         !hasScrollable
1627     }
1628     return hasScrollable
1629 }
1630 
unsupportedIndicationExceptionMessagenull1631 private fun unsupportedIndicationExceptionMessage(indication: Indication): String {
1632     return "clickable only supports IndicationNodeFactory instances provided to LocalIndication, " +
1633         "but Indication was provided instead. Either migrate the Indication implementation to " +
1634         "implement IndicationNodeFactory, or use the other clickable overload that takes an " +
1635         "Indication parameter, and explicitly pass LocalIndication.current there. You can also " +
1636         "use ComposeFoundationFlags.isNonComposedClickableEnabled to temporarily opt-out; note " +
1637         "that this flag will be removed in a future release and is only intended to be a " +
1638         "temporary migration aid. The Indication instance provided here was: $indication"
1639 }
1640