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

View File

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

View File

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

View File

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

View File

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