mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 23:00:08 +00:00
multiplatform: don't cancel command click when chat list shifts under the pointer
A press is cancelled when it goes out of the node's bounds, but when a sent message inserts into the chat the node moves out from under a stationary pointer, which is not a drag-away. Exempt out-of-bounds cancellation when the pointer did not move in window coordinates (within touch slop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
15d2547713
commit
b2a3edbc84
+7
-2
@@ -11,8 +11,10 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.AnnotatedString.Range
|
||||
@@ -411,6 +413,8 @@ fun ClickableText(
|
||||
val currentOnHover = rememberUpdatedState(onHover)
|
||||
val currentOnHoverExit = rememberUpdatedState(onHoverExit)
|
||||
val currentShouldConsumeEvent = rememberUpdatedState(shouldConsumeEvent)
|
||||
// to tell a moved pointer from text that shifted under a stationary pointer (see waitForUpOrCancellation)
|
||||
val textCoordinates = remember { mutableStateOf<LayoutCoordinates?>(null) }
|
||||
val pressIndicator = Modifier.pointerInput(Unit) {
|
||||
detectGesture(onLongPress = { pos ->
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
@@ -423,7 +427,7 @@ fun ClickableText(
|
||||
currentOnClick.value(layoutResult.getOffsetForPosition(pos))
|
||||
}
|
||||
}
|
||||
}, shouldConsumeEvent = { pos ->
|
||||
}, positionInWindow = { textCoordinates.value?.positionInWindow() ?: Offset.Zero }, shouldConsumeEvent = { pos ->
|
||||
var consume = false
|
||||
layoutResult.value?.let { layoutResult ->
|
||||
consume = currentShouldConsumeEvent.value(layoutResult.getOffsetForPosition(pos))
|
||||
@@ -443,7 +447,8 @@ fun ClickableText(
|
||||
|
||||
BasicText(
|
||||
text = text,
|
||||
modifier = modifier.then(selectionHighlight(selectionRange, text.length, layoutResult)).then(pressIndicator),
|
||||
modifier = modifier.then(selectionHighlight(selectionRange, text.length, layoutResult)).then(pressIndicator)
|
||||
.onGloballyPositioned { textCoordinates.value = it },
|
||||
style = style,
|
||||
softWrap = softWrap,
|
||||
overflow = overflow,
|
||||
|
||||
+14
-3
@@ -46,6 +46,7 @@ private val NoPressGesture: suspend PressGestureScope.(Offset) -> Unit = { }
|
||||
suspend fun PointerInputScope.detectGesture(
|
||||
onLongPress: ((Offset) -> Unit)? = null,
|
||||
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
|
||||
positionInWindow: (() -> Offset)? = null,
|
||||
shouldConsumeEvent: (Offset) -> Boolean
|
||||
) = coroutineScope {
|
||||
val pressScope = PressGestureScopeImpl(this@detectGesture)
|
||||
@@ -57,6 +58,7 @@ suspend fun PointerInputScope.detectGesture(
|
||||
val shouldConsume = shouldConsumeEvent(down.position)
|
||||
if (shouldConsume)
|
||||
down.consumeDownChange()
|
||||
val downPositionInWindow = positionInWindow?.let { it() + down.position }
|
||||
// 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).
|
||||
@@ -72,7 +74,7 @@ suspend fun PointerInputScope.detectGesture(
|
||||
|
||||
try {
|
||||
val upOrCancel: PointerInputChange? = withTimeout(longPressTimeout) {
|
||||
waitForUpOrCancellation()
|
||||
waitForUpOrCancellation(downPositionInWindow, positionInWindow)
|
||||
}
|
||||
if (upOrCancel == null) {
|
||||
launch { resetJob.join(); pressScope.cancel() }
|
||||
@@ -143,7 +145,16 @@ internal suspend fun AwaitPointerEventScope.awaitFirstDownOnPass(
|
||||
return event.changes[0]
|
||||
}
|
||||
|
||||
suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? {
|
||||
suspend fun AwaitPointerEventScope.waitForUpOrCancellation(
|
||||
downPositionInWindow: Offset? = null,
|
||||
positionInWindow: (() -> Offset)? = null
|
||||
): PointerInputChange? {
|
||||
// out of bounds in local coordinates while stationary in window coordinates is the node
|
||||
// moving under the pointer (chat list shifted after a sent message), not the pointer
|
||||
// leaving the node — such a press stays valid
|
||||
fun stationaryInWindow(change: PointerInputChange): Boolean =
|
||||
downPositionInWindow != null && positionInWindow != null &&
|
||||
(positionInWindow() + change.position - downPositionInWindow).getDistance() <= viewConfiguration.touchSlop
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(PointerEventPass.Main)
|
||||
if (event.changes.all { it.changedToUp() }) {
|
||||
@@ -151,7 +162,7 @@ suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange
|
||||
}
|
||||
|
||||
if (event.changes.any {
|
||||
it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding)
|
||||
it.consumed.downChange || (it.isOutOfBounds(size, extendedTouchPadding) && !stationaryInWindow(it))
|
||||
}
|
||||
) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user