mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-30 09:40:15 +00:00
multiplatform: fix command clicks lost on quick successive clicks (serialize gesture press scope, don't restart pointerInput on recomposition)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
bc0c6b80bc
commit
6cf93ddfaa
+15
-7
@@ -403,31 +403,39 @@ fun ClickableText(
|
||||
shouldConsumeEvent: (Int) -> Boolean
|
||||
) {
|
||||
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||
val pressIndicator = Modifier.pointerInput(onClick, onLongClick) {
|
||||
// pointerInput keyed on these lambdas restarts on every recomposition (they are new
|
||||
// instances each time), and a restart mid-gesture swallows the click/hover in flight;
|
||||
// key on Unit and read the latest handlers via rememberUpdatedState instead
|
||||
val currentOnClick = rememberUpdatedState(onClick)
|
||||
val currentOnLongClick = rememberUpdatedState(onLongClick)
|
||||
val currentOnHover = rememberUpdatedState(onHover)
|
||||
val currentOnHoverExit = rememberUpdatedState(onHoverExit)
|
||||
val currentShouldConsumeEvent = rememberUpdatedState(shouldConsumeEvent)
|
||||
val pressIndicator = Modifier.pointerInput(Unit) {
|
||||
detectGesture(onLongPress = { pos ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
onLongClick(layoutResult.getOffsetForPosition(pos))
|
||||
currentOnLongClick.value(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
}, onPress = { pos ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
val res = tryAwaitRelease()
|
||||
if (res) {
|
||||
onClick(layoutResult.getOffsetForPosition(pos))
|
||||
currentOnClick.value(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
}
|
||||
}, shouldConsumeEvent = { pos ->
|
||||
var consume = false
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos))
|
||||
consume = currentShouldConsumeEvent.value(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
consume
|
||||
}
|
||||
)
|
||||
}.pointerInput(onHover, onHoverExit) {
|
||||
}.pointerInput(Unit) {
|
||||
if (appPlatform.isDesktop) {
|
||||
detectCursorMove(onExit = onHoverExit) { pos ->
|
||||
detectCursorMove(onExit = { currentOnHoverExit.value() }) { pos ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
onHover(layoutResult.getOffsetForPosition(pos))
|
||||
currentOnHover.value(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-9
@@ -57,8 +57,13 @@ suspend fun PointerInputScope.detectGesture(
|
||||
val shouldConsume = shouldConsumeEvent(down.position)
|
||||
if (shouldConsume)
|
||||
down.consumeDownChange()
|
||||
pressScope.reset()
|
||||
// reset() suspends until the previous gesture's press handler released the mutex,
|
||||
// and release/cancel join resetJob, so flag writes can't be reordered across gestures
|
||||
// (a fast second click would otherwise drop the first click's onClick).
|
||||
// Mirrors detectTapGestures in Compose 1.8.2.
|
||||
val resetJob = launch { pressScope.reset() }
|
||||
if (onPress !== NoPressGesture) launch {
|
||||
resetJob.join()
|
||||
pressScope.onPress(down.position)
|
||||
}
|
||||
val longPressTimeout = onLongPress?.let {
|
||||
@@ -70,22 +75,22 @@ suspend fun PointerInputScope.detectGesture(
|
||||
waitForUpOrCancellation()
|
||||
}
|
||||
if (upOrCancel == null) {
|
||||
pressScope.cancel()
|
||||
launch { resetJob.join(); pressScope.cancel() }
|
||||
} else {
|
||||
if (shouldConsume)
|
||||
upOrCancel.consumeDownChange()
|
||||
pressScope.release()
|
||||
launch { resetJob.join(); pressScope.release() }
|
||||
}
|
||||
} catch (_: PointerEventTimeoutCancellationException) {
|
||||
if (onLongPress != null) {
|
||||
onLongPress(down.position)
|
||||
if (shouldConsume)
|
||||
consumeUntilUp()
|
||||
pressScope.cancel()
|
||||
launch { resetJob.join(); pressScope.cancel() }
|
||||
} else {
|
||||
if (shouldConsume)
|
||||
consumeUntilUp()
|
||||
pressScope.release()
|
||||
launch { resetJob.join(); pressScope.release() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,16 +172,18 @@ private class PressGestureScopeImpl(
|
||||
|
||||
fun cancel() {
|
||||
isCanceled = true
|
||||
mutex.unlock()
|
||||
if (mutex.isLocked) mutex.unlock()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
isReleased = true
|
||||
mutex.unlock()
|
||||
if (mutex.isLocked) mutex.unlock()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
mutex.tryLock()
|
||||
// suspends until the previous gesture's tryAwaitRelease finished (or was never started):
|
||||
// the mutex is the serialization token between consecutive gestures
|
||||
suspend fun reset() {
|
||||
mutex.lock()
|
||||
isReleased = false
|
||||
isCanceled = false
|
||||
}
|
||||
@@ -184,6 +191,7 @@ private class PressGestureScopeImpl(
|
||||
override suspend fun tryAwaitRelease(): Boolean {
|
||||
if (!isReleased && !isCanceled) {
|
||||
mutex.lock()
|
||||
mutex.unlock()
|
||||
}
|
||||
return isReleased && !isCanceled
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user