feat(companion): menu gesture is now a three-finger hold

Two-finger hold collided with two-finger scrolling — any scroll longer
than 500ms popped the NESMenu. Three fingers hold for the menu; two
fingers scroll, on the trackpad, both controllers and the gamepad.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-07-23 20:51:49 +01:00
parent 265196bc2c
commit 1df075f7b1
5 changed files with 36 additions and 27 deletions

View File

@ -38,7 +38,7 @@ import com.archipelago.app.ui.theme.neoRaised
@Composable
fun GamepadLayout(
onKey: (String) -> Unit,
onTwoFingerHold: () -> Unit,
onThreeFingerHold: () -> Unit,
modifier: Modifier = Modifier,
) {
val surface = Neo.surface()
@ -54,9 +54,9 @@ fun GamepadLayout(
do {
val ev = awaitPointerEvent()
val a = ev.changes.filter { !it.changedToUp() }
if (a.size >= 2 && t == 0L) t = System.currentTimeMillis()
if (a.size >= 2 && !fired && t > 0 && System.currentTimeMillis() - t > 500) { fired = true; onTwoFingerHold() }
if (a.size < 2) t = 0L
if (a.size >= 3 && t == 0L) t = System.currentTimeMillis()
if (a.size >= 3 && !fired && t > 0 && System.currentTimeMillis() - t > 500) { fired = true; onThreeFingerHold() }
if (a.size < 3) t = 0L
} while (ev.changes.any { it.pressed })
}
}

View File

@ -116,7 +116,7 @@ fun NESController(
Box(
modifier = modifier
.fillMaxSize()
.twoFingerHold(onMenu)
.threeFingerHold(onMenu)
.padding(horizontal = 40.dp, vertical = 24.dp),
contentAlignment = Alignment.Center,
) {
@ -451,17 +451,17 @@ fun PlayerPill(c: NESPalette, playerId: Int, onToggle: () -> Unit) {
}
}
/** Two-finger hold gesture modifier */
fun Modifier.twoFingerHold(onHold: () -> Unit) = this.pointerInput(Unit) {
/** Three-finger hold gesture modifier (two fingers stay free for scrolling) */
fun Modifier.threeFingerHold(onHold: () -> Unit) = this.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false)
var t = 0L; var fired = false
do {
val ev = awaitPointerEvent()
val a = ev.changes.filter { !it.changedToUp() }
if (a.size >= 2 && t == 0L) t = System.currentTimeMillis()
if (a.size >= 2 && !fired && t > 0 && System.currentTimeMillis() - t > 500) { fired = true; onHold() }
if (a.size < 2) t = 0L
if (a.size >= 3 && t == 0L) t = System.currentTimeMillis()
if (a.size >= 3 && !fired && t > 0 && System.currentTimeMillis() - t > 500) { fired = true; onHold() }
if (a.size < 3) t = 0L
} while (ev.changes.any { it.pressed })
}
}

View File

@ -50,7 +50,7 @@ fun NESPortraitController(
Box(
Modifier
.fillMaxSize()
.twoFingerHold(onMenu)
.threeFingerHold(onMenu)
.padding(horizontal = 40.dp, vertical = 24.dp),
contentAlignment = Alignment.Center,
) {
@ -87,7 +87,7 @@ fun NESPortraitController(
onMove = { dx, dy -> onMouseMove(dx, dy) },
onClick = { onMouseClick(it) },
onScroll = { dy -> onMouseScroll(dy) },
onTwoFingerHold = onMenu,
onThreeFingerHold = onMenu,
modifier = Modifier
.fillMaxWidth()
.weight(1f),

View File

@ -32,7 +32,7 @@ fun Trackpad(
onMove: (dx: Int, dy: Int) -> Unit,
onClick: (button: Int) -> Unit,
onScroll: (dy: Int) -> Unit,
onTwoFingerHold: () -> Unit,
onThreeFingerHold: () -> Unit,
modifier: Modifier = Modifier,
) {
var fingers by remember { mutableIntStateOf(0) }
@ -53,7 +53,7 @@ fun Trackpad(
val t0 = System.currentTimeMillis()
var maxPtrs = 1
var holdFired = false
var twoStart = 0L
var threeStart = 0L
var scrollAcc = 0f
fingers = 1
@ -64,19 +64,24 @@ fun Trackpad(
fingers = active.size
when {
active.size >= 2 -> {
if (twoStart == 0L) twoStart = System.currentTimeMillis()
if (!holdFired && System.currentTimeMillis() - twoStart > 500) {
// Three fingers = hold for menu; two = scroll. Kept
// on separate counts so a long two-finger scroll can
// never fire the menu mid-gesture.
active.size >= 3 -> {
if (threeStart == 0L) threeStart = System.currentTimeMillis()
if (!holdFired && System.currentTimeMillis() - threeStart > 500) {
holdFired = true
onTwoFingerHold()
onThreeFingerHold()
}
if (!holdFired) {
val dy = active.map { it.positionChange().y }.average().toFloat()
scrollAcc += dy
if (kotlin.math.abs(scrollAcc) > 12f) {
onScroll(if (scrollAcc > 0) 1 else -1)
scrollAcc = 0f
}
ev.changes.forEach { it.consume() }
}
active.size == 2 -> {
threeStart = 0L
val dy = active.map { it.positionChange().y }.average().toFloat()
scrollAcc += dy
if (kotlin.math.abs(scrollAcc) > 12f) {
onScroll(if (scrollAcc > 0) 1 else -1)
scrollAcc = 0f
}
ev.changes.forEach { it.consume() }
}
@ -99,7 +104,11 @@ fun Trackpad(
contentAlignment = Alignment.Center,
) {
Text(
text = if (fingers >= 2) "hold for menu" else "",
text = when {
fingers >= 3 -> "hold for menu"
fingers == 2 -> "scroll"
else -> ""
},
style = MaterialTheme.typography.labelSmall,
color = muted.copy(alpha = 0.4f),
)

View File

@ -172,7 +172,7 @@ fun RemoteInputScreen(onBack: () -> Unit) {
onMove = { dx, dy -> ws.sendMouseMove(dx, dy) },
onClick = { ws.sendClick(it) },
onScroll = { ws.sendScroll(it) },
onTwoFingerHold = { showModal = true },
onThreeFingerHold = { showModal = true },
modifier = Modifier.fillMaxWidth().weight(1f)
.padding(horizontal = 16.dp, vertical = 8.dp),
)