debug: synthesize wheel events for mobile terminal touch scrolling
Replace manual scrollLines/SGR injection with synthetic WheelEvent instances dispatched into xterm.js's .xterm-viewport. Keep the debug overlay so we can verify whether the gesture is recognized and whether the wheel event reaches xterm.js. Quality gates: typecheck, lint clean.
This commit is contained in:
@@ -363,34 +363,32 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
let startY = 0;
|
||||
let startX = 0;
|
||||
let isScrolling = false;
|
||||
let scrollPending = 0;
|
||||
const WHEEL_DISTANCE = 12;
|
||||
|
||||
const flushScroll = (force = false) => {
|
||||
if (scrollPending === 0) return;
|
||||
if (!force && Math.abs(scrollPending) < WHEEL_DISTANCE) return;
|
||||
const direction = Math.sign(scrollPending);
|
||||
const steps = Math.max(
|
||||
1,
|
||||
Math.floor(Math.abs(scrollPending) / WHEEL_DISTANCE),
|
||||
);
|
||||
const ws = wsRef.current;
|
||||
if (ws?.readyState === WebSocket.OPEN && termRef.current) {
|
||||
const buf = termRef.current.buffer.active;
|
||||
const col = buf.cursorX + 1;
|
||||
const row = buf.cursorY + 1;
|
||||
const btn = direction > 0 ? 64 : 65;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
ws.send(`\x1b[<${btn};${col};${row}M`);
|
||||
}
|
||||
let debugOverlay: HTMLDivElement | null = null;
|
||||
const showDebug = (msg: string) => {
|
||||
if (!debugOverlay) {
|
||||
debugOverlay = document.createElement("div");
|
||||
debugOverlay.style.position = "fixed";
|
||||
debugOverlay.style.bottom = "120px";
|
||||
debugOverlay.style.left = "8px";
|
||||
debugOverlay.style.right = "8px";
|
||||
debugOverlay.style.zIndex = "9999";
|
||||
debugOverlay.style.background = "rgba(0,0,0,0.8)";
|
||||
debugOverlay.style.color = "#0f0";
|
||||
debugOverlay.style.fontFamily = "monospace";
|
||||
debugOverlay.style.fontSize = "11px";
|
||||
debugOverlay.style.padding = "8px";
|
||||
debugOverlay.style.borderRadius = "6px";
|
||||
debugOverlay.style.pointerEvents = "none";
|
||||
debugOverlay.style.whiteSpace = "pre-wrap";
|
||||
debugOverlay.style.maxHeight = "120px";
|
||||
debugOverlay.style.overflow = "auto";
|
||||
document.body.appendChild(debugOverlay);
|
||||
}
|
||||
scrollPending = 0;
|
||||
};
|
||||
|
||||
const isAlternateScreen = () => {
|
||||
const term = termRef.current;
|
||||
if (!term) return false;
|
||||
return term.buffer.active === term.buffer.alternate;
|
||||
const line = `${new Date().toLocaleTimeString()} ${msg}`;
|
||||
debugOverlay.textContent =
|
||||
line +
|
||||
"\n" +
|
||||
debugOverlay.textContent?.split("\n").slice(0, 8).join("\n");
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
@@ -398,7 +396,6 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
startY = e.touches[0].clientY;
|
||||
startX = e.touches[0].clientX;
|
||||
isScrolling = false;
|
||||
scrollPending = 0;
|
||||
}
|
||||
};
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
@@ -406,6 +403,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
const touch = e.touches[0];
|
||||
const deltaY = startY - touch.clientY;
|
||||
const deltaX = Math.abs(startX - touch.clientX);
|
||||
showDebug(`move dy=${deltaY.toFixed(0)} dx=${deltaX.toFixed(0)} scrolling=${isScrolling}`);
|
||||
|
||||
// Decide early whether this is a vertical scroll gesture.
|
||||
if (!isScrolling) {
|
||||
@@ -415,32 +413,34 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
}
|
||||
if (!isScrolling) return;
|
||||
|
||||
// Take over the gesture so the page/toolbar never scrolls.
|
||||
// Take over the gesture so the browser never scrolls the
|
||||
// page or the terminal canvas. We synthesize wheel events
|
||||
// and dispatch them into xterm.js, which already handles
|
||||
// both normal buffer scrolling and alternate-screen apps
|
||||
// (tmux/vim) correctly.
|
||||
e.preventDefault();
|
||||
|
||||
if (isAlternateScreen()) {
|
||||
// Alternate screen (tmux/vim): accumulate the swipe and
|
||||
// send SGR 1006 mouse-wheel events in steps.
|
||||
scrollPending += deltaY;
|
||||
flushScroll();
|
||||
} else if (termRef.current) {
|
||||
// Normal buffer: let xterm.js scroll its own viewport by
|
||||
// the number of lines corresponding to the swipe distance.
|
||||
const lineHeight =
|
||||
termRef.current.options.fontSize != null
|
||||
? (termRef.current.options.fontSize as number) * 1.2
|
||||
: 10;
|
||||
const lines = Math.round(deltaY / lineHeight);
|
||||
if (lines !== 0) {
|
||||
termRef.current.scrollLines(lines);
|
||||
}
|
||||
const target = container.querySelector(".xterm-viewport") as
|
||||
| HTMLElement
|
||||
| null;
|
||||
if (target) {
|
||||
showDebug(`dispatch wheel deltaY=${deltaY.toFixed(0)}`);
|
||||
const wheel = new WheelEvent("wheel", {
|
||||
deltaY,
|
||||
deltaX: 0,
|
||||
deltaZ: 0,
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
screenX: touch.screenX,
|
||||
screenY: touch.screenY,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
target.dispatchEvent(wheel);
|
||||
}
|
||||
startY = touch.clientY;
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
if (isScrolling) {
|
||||
flushScroll(true);
|
||||
}
|
||||
isScrolling = false;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user