Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RichText intercepts click events #169

Merged
merged 1 commit into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.mohamedrejeb.richeditor.gesture

import androidx.compose.foundation.gestures.*
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.platform.ViewConfiguration
import com.mohamedrejeb.richeditor.utils.fastAny
import com.mohamedrejeb.richeditor.utils.fastForEach
import kotlinx.coroutines.coroutineScope

/**
* Consumes all pointer events until nothing is pressed and then returns. This method assumes
* that something is currently pressed.
*/
private suspend fun AwaitPointerEventScope.consumeUntilUp() {
do {
val event = awaitPointerEvent()
event.changes.fastForEach { it.consume() }
} while (event.changes.fastAny { it.pressed })
}

/**
* Waits for [ViewConfiguration.doubleTapTimeoutMillis] for a second press event. If a
* second press event is received before the time out, it is returned or `null` is returned
* if no second press is received.
*/
private suspend fun AwaitPointerEventScope.awaitSecondDown(
firstUp: PointerInputChange
): PointerInputChange? = withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis
var change: PointerInputChange
// The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
do {
change = awaitFirstDown()
} while (change.uptimeMillis < minUptime)
change
}

internal suspend fun PointerInputScope.detectTapGestures(
onDoubleTap: ((Offset) -> Unit)? = null,
onLongPress: ((Offset) -> Unit)? = null,
onTap: ((Offset) -> Unit)? = null,
consumeDown: (Offset) -> Boolean,
) = coroutineScope {
awaitEachGesture {
val down = awaitFirstDown()
if (!consumeDown(down.position)) return@awaitEachGesture
down.consume()
val longPressTimeout = onLongPress?.let {
viewConfiguration.longPressTimeoutMillis
} ?: (Long.MAX_VALUE / 2)
var upOrCancel: PointerInputChange? = null
try {
// wait for first tap up or long press
upOrCancel = withTimeout(longPressTimeout) {
waitForUpOrCancellation()
}
upOrCancel?.consume()
} catch (_: PointerEventTimeoutCancellationException) {
onLongPress?.invoke(down.position)
consumeUntilUp()
}

if (upOrCancel != null) {
// tap was successful.
if (onDoubleTap == null) {
onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
} else {
// check for second tap
val secondDown = awaitSecondDown(upOrCancel)

if (secondDown == null) {
onTap?.invoke(upOrCancel.position) // no valid second tap started
} else {
try {
// Might have a long second press as the second tap
withTimeout(longPressTimeout) {
val secondUp = waitForUpOrCancellation()
if (secondUp != null) {
secondUp.consume()
onDoubleTap(secondUp.position)
} else {
onTap?.invoke(upOrCancel.position)
}
}
} catch (e: PointerEventTimeoutCancellationException) {
// The first tap was valid, but the second tap is a long press.
// notify for the first tap
onTap?.invoke(upOrCancel.position)

// notify for the long press
onLongPress?.invoke(secondDown.position)
consumeUntilUp()
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.mohamedrejeb.richeditor.ui

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.runtime.Composable
Expand All @@ -16,6 +15,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import com.mohamedrejeb.richeditor.gesture.detectTapGestures
import com.mohamedrejeb.richeditor.model.RichTextState

@Composable
Expand Down Expand Up @@ -63,7 +63,10 @@ fun BasicRichText(
e.printStackTrace()
}
}
}
},
consumeDown = { offset ->
state.isLink(offset)
},
)
},
style = style,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,18 @@ internal inline fun <T, R> List<T>.fastMap(
}

/**
* Copied from [androidx](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TempListUtils.kt;l=107;drc=ceaa7640c065146360515e598a3d09f6f66553dd).
* Returns `true` if at least one element matches the given [predicate].
*
* **Do not use for collections that come from public APIs**, since they may not support random
* access in an efficient way, and this method may actually be a lot slower. Only use for
* collections that are created by code we control and are known to support random access.
*/
@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
internal inline fun <T, R> List<T>.fastFold(initial: R, operation: (acc: R, T) -> R): R {
contract { callsInPlace(operation) }
var accumulator = initial
fastForEach { e ->
accumulator = operation(accumulator, e)
}
return accumulator
internal inline fun <T> List<T>.fastAny(predicate: (T) -> Boolean): Boolean {
contract { callsInPlace(predicate) }
fastForEach { if (predicate(it)) return true }
return false
}

@OptIn(ExperimentalContracts::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ fun SlackDemoContent() {
topBar = {
Column(
modifier = Modifier

) {
TopAppBar(
title = { Text("Slack Demo") },
Expand Down