Skip to content
2 changes: 2 additions & 0 deletions packages/web-core/src/shared/lib/terminalMobileState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ describe('terminal mobile state store', () => {
ctrlLatched: false,
selectMode: false,
dpadActive: false,
flingCatch: false,
scrollOwned: false,
});
});

Expand Down
6 changes: 6 additions & 0 deletions packages/web-core/src/shared/lib/terminalMobileState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ export interface TerminalMobileState {
selectMode: boolean;
/** A long-press D-pad gesture is running; the scroll bridge stands down. */
dpadActive: boolean;
/** The current touch sequence started by stopping live scroll momentum. */
flingCatch: boolean;
/** The scroll bridge committed vertical ownership during this sequence. */
scrollOwned: boolean;
}

const EMPTY_STATE: TerminalMobileState = {
ctrlLatched: false,
selectMode: false,
dpadActive: false,
flingCatch: false,
scrollOwned: false,
};

interface Entry {
Expand Down
44 changes: 44 additions & 0 deletions packages/web-core/src/shared/lib/terminalTouchGestures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,50 @@ describe('double-tap = Tab', () => {
ctrl.onTouchEnd(0, LONG_PRESS_MS + 90);
expect(events).toEqual([]);
});

it('does not count a fling catch toward the double-tap sequence', () => {
const { ctrl, events } = harness();
ctrl.onTouchStart(p(100, 100), 0);
ctrl.onTouchEnd(0, 40, true);

ctrl.onTouchStart(p(100, 100), 100);
ctrl.onTouchEnd(0, 140);
expect(events).toEqual([]);

ctrl.onTouchStart(p(100, 100), 200);
ctrl.onTouchEnd(0, 240);
expect(events).toEqual(['tab']);
});

it('clears prior double-tap history when a fling catch interrupts it', () => {
const { ctrl, events } = harness();
ctrl.onTouchStart(p(100, 100), 0);
ctrl.onTouchEnd(0, 40);

ctrl.onTouchStart(p(100, 100), 100);
ctrl.onTouchEnd(0, 140, true);

ctrl.onTouchStart(p(100, 100), 200);
ctrl.onTouchEnd(0, 240);
expect(events).toEqual([]);
});

it('does not let scroll-owned sequences seed or complete a double-tap', () => {
const { ctrl, events } = harness();
ctrl.onTouchStart(p(100, 100), 0);
ctrl.onTouchEnd(0, 40, false, true);

ctrl.onTouchStart(p(100, 100), 100);
ctrl.onTouchEnd(0, 140);
expect(events).toEqual([]);

ctrl.onTouchStart(p(100, 100), 200);
ctrl.onTouchEnd(0, 240, false, true);

ctrl.onTouchStart(p(100, 100), 300);
ctrl.onTouchEnd(0, 340);
expect(events).toEqual([]);
});
});

describe('three-finger tap = paste', () => {
Expand Down
21 changes: 18 additions & 3 deletions packages/web-core/src/shared/lib/terminalTouchGestures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,12 @@ export function createTouchGestureController(deps: GestureDeps) {
return { prevent: true };
},

onTouchEnd(remainingTouches: number, now: number): void {
onTouchEnd(
remainingTouches: number,
now: number,
flingCatch = false,
scrollOwned = false
): void {
if (remainingTouches > 0) return; // wait for the last finger
const wasPhase =
phase === 'dpad' &&
Expand Down Expand Up @@ -459,6 +464,10 @@ export function createTouchGestureController(deps: GestureDeps) {
lastTap = null;
return;
}
if (flingCatch || scrollOwned) {
lastTap = null;
return;
}
// A clean quick tap. Second one in time + place = Tab.
if (
lastTap &&
Expand Down Expand Up @@ -777,6 +786,7 @@ export function installTerminalTouchGestures(
};
const onEnd = (e: TouchEvent) => {
if (primaryTouchIdentifier === null) return;
const { flingCatch, scrollOwned } = getTerminalMobileState(terminal);
const primaryStillOwned = touchWithIdentifier(
e.targetTouches,
primaryTouchIdentifier
Expand All @@ -786,10 +796,15 @@ export function installTerminalTouchGestures(
// contain contacts that began on nav/key bars and will never emit an
// event on this terminal, so waiting for the global last finger strands
// D-pad suppression and its timer indefinitely.
controller.onTouchEnd(0, e.timeStamp);
controller.onTouchEnd(0, e.timeStamp, flingCatch, scrollOwned);
releaseTouchOwnership();
} else {
controller.onTouchEnd(e.targetTouches.length, e.timeStamp);
controller.onTouchEnd(
e.targetTouches.length,
e.timeStamp,
flingCatch,
scrollOwned
);
}
reschedule();
};
Expand Down
Loading
Loading