Skip to content

Commit 322fde1

Browse files
committed
fix(touch): stabilize long-press selection activation on mobile
1 parent 156d8fb commit 322fde1

1 file changed

Lines changed: 72 additions & 19 deletions

File tree

src/app/index.ts

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -384,16 +384,22 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
384384
const touchSelectionState: {
385385
pendingPointerId: number | null;
386386
activePointerId: number | null;
387+
panPointerId: number | null;
387388
pendingCell: { row: number; col: number } | null;
389+
pendingStartedAt: number;
388390
pendingStartX: number;
389391
pendingStartY: number;
392+
panLastY: number;
390393
pendingTimer: number;
391394
} = {
392395
pendingPointerId: null,
393396
activePointerId: null,
397+
panPointerId: null,
394398
pendingCell: null,
399+
pendingStartedAt: 0,
395400
pendingStartX: 0,
396401
pendingStartY: 0,
402+
panLastY: 0,
397403
pendingTimer: 0,
398404
};
399405

@@ -431,6 +437,21 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
431437
}
432438
touchSelectionState.pendingPointerId = null;
433439
touchSelectionState.pendingCell = null;
440+
touchSelectionState.pendingStartedAt = 0;
441+
}
442+
443+
function tryActivatePendingTouchSelection(pointerId: number) {
444+
if (touchSelectionMode !== "long-press") return false;
445+
if (touchSelectionState.pendingPointerId !== pointerId || !touchSelectionState.pendingCell) {
446+
return false;
447+
}
448+
if (performance.now() - touchSelectionState.pendingStartedAt < touchSelectionLongPressMs) {
449+
return false;
450+
}
451+
const pendingCell = touchSelectionState.pendingCell;
452+
clearPendingTouchSelection();
453+
beginSelectionDrag(pendingCell, pointerId);
454+
return true;
434455
}
435456

436457
function beginSelectionDrag(cell: { row: number; col: number }, pointerId: number) {
@@ -439,6 +460,7 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
439460
selectionState.anchor = cell;
440461
selectionState.focus = cell;
441462
touchSelectionState.activePointerId = pointerId;
463+
touchSelectionState.panPointerId = null;
442464
canvas.setPointerCapture?.(pointerId);
443465
updateCanvasCursor();
444466
needsRender = true;
@@ -448,6 +470,18 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
448470
scrollbarState.lastInputAt = performance.now();
449471
}
450472

473+
function scrollViewportByLines(lines: number) {
474+
if (!wasmReady || !wasmHandle || !gridState.cellH) return;
475+
scrollRemainder += lines;
476+
const delta = Math.trunc(scrollRemainder);
477+
scrollRemainder -= delta;
478+
if (!delta) return;
479+
wasm.scrollViewport(wasmHandle, delta);
480+
wasm.renderUpdate(wasmHandle);
481+
needsRender = true;
482+
noteScrollActivity();
483+
}
484+
451485
function releaseKittyImage(entry: KittyDecodedImage | undefined) {
452486
const source = entry?.source as ImageBitmap | undefined;
453487
if (source && typeof source.close === "function") {
@@ -1337,7 +1371,10 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
13371371

13381372
function bindCanvasEvents() {
13391373
if (!attachCanvasEvents) return;
1340-
canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
1374+
canvas.style.touchAction =
1375+
touchSelectionMode === "long-press" || touchSelectionMode === "drag"
1376+
? "none"
1377+
: "pan-y pinch-zoom";
13411378
const onPointerDown = (event: PointerEvent) => {
13421379
if (inputHandler.sendMouseEvent("down", event)) {
13431380
event.preventDefault();
@@ -1348,6 +1385,7 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
13481385
if (event.button !== 0) return;
13491386
const cell = normalizeSelectionCell(positionToCell(event));
13501387
touchSelectionState.activePointerId = null;
1388+
touchSelectionState.panPointerId = null;
13511389

13521390
if (touchSelectionMode === "off") return;
13531391
if (touchSelectionMode === "drag") {
@@ -1359,18 +1397,13 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
13591397
clearPendingTouchSelection();
13601398
touchSelectionState.pendingPointerId = event.pointerId;
13611399
touchSelectionState.pendingCell = cell;
1400+
touchSelectionState.pendingStartedAt = performance.now();
13621401
touchSelectionState.pendingStartX = event.clientX;
13631402
touchSelectionState.pendingStartY = event.clientY;
1403+
touchSelectionState.panPointerId = event.pointerId;
1404+
touchSelectionState.panLastY = event.clientY;
13641405
touchSelectionState.pendingTimer = setTimeout(() => {
1365-
if (
1366-
touchSelectionState.pendingPointerId !== event.pointerId ||
1367-
!touchSelectionState.pendingCell
1368-
) {
1369-
return;
1370-
}
1371-
const pendingCell = touchSelectionState.pendingCell;
1372-
clearPendingTouchSelection();
1373-
beginSelectionDrag(pendingCell, event.pointerId);
1406+
tryActivatePendingTouchSelection(event.pointerId);
13741407
}, touchSelectionLongPressMs);
13751408
return;
13761409
}
@@ -1387,12 +1420,22 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
13871420
return;
13881421
}
13891422
if (isTouchPointer(event)) {
1423+
tryActivatePendingTouchSelection(event.pointerId);
13901424
if (touchSelectionState.pendingPointerId === event.pointerId) {
13911425
const dx = event.clientX - touchSelectionState.pendingStartX;
13921426
const dy = event.clientY - touchSelectionState.pendingStartY;
13931427
if (dx * dx + dy * dy >= touchSelectionMoveThresholdPx * touchSelectionMoveThresholdPx) {
13941428
clearPendingTouchSelection();
13951429
}
1430+
if (
1431+
touchSelectionMode === "long-press" &&
1432+
touchSelectionState.panPointerId === event.pointerId
1433+
) {
1434+
const deltaPx = touchSelectionState.panLastY - event.clientY;
1435+
touchSelectionState.panLastY = event.clientY;
1436+
scrollViewportByLines((deltaPx / Math.max(1, gridState.cellH)) * 1.5);
1437+
event.preventDefault();
1438+
}
13961439
return;
13971440
}
13981441
if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
@@ -1402,6 +1445,16 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
14021445
updateLinkHover(null);
14031446
updateCanvasCursor();
14041447
needsRender = true;
1448+
return;
1449+
}
1450+
if (
1451+
touchSelectionMode === "long-press" &&
1452+
touchSelectionState.panPointerId === event.pointerId
1453+
) {
1454+
const deltaPx = touchSelectionState.panLastY - event.clientY;
1455+
touchSelectionState.panLastY = event.clientY;
1456+
scrollViewportByLines((deltaPx / Math.max(1, gridState.cellH)) * 1.5);
1457+
event.preventDefault();
14051458
}
14061459
return;
14071460
}
@@ -1426,6 +1479,7 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
14261479
if (touchSelectionState.pendingPointerId === event.pointerId) {
14271480
clearPendingTouchSelection();
14281481
touchSelectionState.activePointerId = null;
1482+
touchSelectionState.panPointerId = null;
14291483
return;
14301484
}
14311485
if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
@@ -1445,6 +1499,10 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
14451499
updateCanvasCursor();
14461500
needsRender = true;
14471501
}
1502+
return;
1503+
}
1504+
if (touchSelectionState.panPointerId === event.pointerId) {
1505+
touchSelectionState.panPointerId = null;
14481506
}
14491507
return;
14501508
}
@@ -1482,6 +1540,9 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
14821540
if (touchSelectionState.pendingPointerId === event.pointerId) {
14831541
clearPendingTouchSelection();
14841542
}
1543+
if (touchSelectionState.panPointerId === event.pointerId) {
1544+
touchSelectionState.panPointerId = null;
1545+
}
14851546
if (touchSelectionState.activePointerId === event.pointerId) {
14861547
touchSelectionState.activePointerId = null;
14871548
if (selectionState.dragging) {
@@ -1512,15 +1573,7 @@ export function createResttyApp(options: ResttyAppOptions): ResttyApp {
15121573
} else {
15131574
lines = event.deltaY / gridState.cellH;
15141575
}
1515-
lines *= speed;
1516-
scrollRemainder += lines;
1517-
const delta = Math.trunc(scrollRemainder);
1518-
scrollRemainder -= delta;
1519-
if (!delta) return;
1520-
wasm.scrollViewport(wasmHandle, delta);
1521-
wasm.renderUpdate(wasmHandle);
1522-
needsRender = true;
1523-
noteScrollActivity();
1576+
scrollViewportByLines(lines * speed);
15241577
event.preventDefault();
15251578
};
15261579

0 commit comments

Comments
 (0)