1 /*
2  * Copyright (C) 2024 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 com.android.systemui.communal.ui.compose.extensions
18 
19 import androidx.compose.foundation.gestures.awaitEachGesture
20 import androidx.compose.foundation.gestures.awaitFirstDown
21 import androidx.compose.foundation.gestures.waitForUpOrCancellation
22 import androidx.compose.ui.geometry.Offset
23 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
24 import androidx.compose.ui.input.pointer.PointerEventPass
25 import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
26 import androidx.compose.ui.input.pointer.PointerInputChange
27 import androidx.compose.ui.input.pointer.PointerInputScope
28 import androidx.compose.ui.util.fastAny
29 import androidx.compose.ui.util.fastForEach
30 import kotlinx.coroutines.coroutineScope
31 
32 /**
33  * Observe taps without consuming them by default, so child elements can still respond to them. Long
34  * presses are excluded.
35  */
observeTapsnull36 suspend fun PointerInputScope.observeTaps(
37     pass: PointerEventPass = PointerEventPass.Initial,
38     shouldConsume: Boolean = false,
39     onTap: ((Offset) -> Unit)? = null,
40 ) = coroutineScope {
41     if (onTap == null) return@coroutineScope
42     awaitEachGesture {
43         val down = awaitFirstDown(pass = pass)
44         if (shouldConsume) down.consume()
45         val tapTimeout = viewConfiguration.longPressTimeoutMillis
46         val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) }
47         if (up != null) {
48             onTap(up.position)
49         }
50     }
51 }
52 
53 /**
54  * Detect long press gesture and calls onLongPress when detected. The callback parameter receives an
55  * Offset representing the position relative to the containing element.
56  */
detectLongPressGesturenull57 suspend fun PointerInputScope.detectLongPressGesture(
58     pass: PointerEventPass = PointerEventPass.Initial,
59     onLongPress: ((Offset) -> Unit),
60 ) = coroutineScope {
61     awaitEachGesture {
62         val down = awaitFirstDown(pass = pass)
63         val longPressTimeout = viewConfiguration.longPressTimeoutMillis
64         // wait for first tap up or long press
65         try {
66             withTimeout(longPressTimeout) { waitForUpOrCancellation(pass = pass) }
67         } catch (_: PointerEventTimeoutCancellationException) {
68             // withTimeout throws exception if timeout has passed before block completes
69             onLongPress.invoke(down.position)
70             consumeUntilUp(pass)
71         }
72     }
73 }
74 
75 /**
76  * Consumes all pointer events until nothing is pressed and then returns. This method assumes that
77  * something is currently pressed.
78  */
consumeUntilUpnull79 private suspend fun AwaitPointerEventScope.consumeUntilUp(
80     pass: PointerEventPass = PointerEventPass.Initial
81 ) {
82     do {
83         val event = awaitPointerEvent(pass = pass)
84         event.changes.fastForEach { it.consume() }
85     } while (event.changes.fastAny { it.pressed })
86 }
87 
88 /** Consume all gestures on the initial pass so that child elements do not receive them. */
<lambda>null89 suspend fun PointerInputScope.consumeAllGestures() = coroutineScope {
90     awaitEachGesture {
91         awaitPointerEvent(pass = PointerEventPass.Initial)
92             .changes
93             .forEach(PointerInputChange::consume)
94     }
95 }
96