fix(terminal): touchpad scrolls 1:1 with the finger
xterm.js sends one wheel report per event whatever its size and damps a touchpad's pixel deltas x0.3, so a swipe crawled. With mouse tracking on, TerminalView now sends the SGR reports itself (lib/wheelReports.ts): one per 5 rows of travel (what one report scrolls in a Claude pane), the remainder carried between events, at least one per wheel notch, at the pointer's cell with xterm's modifier bits.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file wheelReports.test.ts
|
||||
* @description A touchpad scrolls 1:1 with the finger (one report per
|
||||
* LINES_PER_REPORT rows, remainder carried), a wheel notch always sends one.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { LINES_PER_REPORT, wheelReports, type WheelState } from "../wheelReports";
|
||||
|
||||
const ROW = 20;
|
||||
const px = (deltaY: number) => ({ deltaY, deltaMode: 0 });
|
||||
|
||||
describe("wheelReports", () => {
|
||||
it("adds small touchpad steps up to one report per LINES_PER_REPORT rows", () => {
|
||||
const s: WheelState = { partial: 0 };
|
||||
let sent = 0;
|
||||
// 30 steps of 10px = 300px = 15 rows.
|
||||
for (let i = 0; i < 30; i++) sent += wheelReports(s, px(10), ROW, 40);
|
||||
expect(sent).toBe(15 / LINES_PER_REPORT);
|
||||
});
|
||||
|
||||
it("drops the carried remainder when the swipe turns around", () => {
|
||||
const s: WheelState = { partial: 0 };
|
||||
wheelReports(s, px(40), ROW, 40); // 2 rows down, nothing sent yet
|
||||
let sent = 0;
|
||||
for (let i = 0; i < 5; i++) sent += wheelReports(s, px(-20), ROW, 40); // 5 rows up
|
||||
expect(sent).toBe(-1);
|
||||
});
|
||||
|
||||
it("sends at least one report for a wheel notch, in either delta mode", () => {
|
||||
expect(wheelReports({ partial: 0 }, px(100), ROW, 40)).toBe(1);
|
||||
expect(wheelReports({ partial: 0 }, px(-100), ROW, 40)).toBe(-1);
|
||||
expect(wheelReports({ partial: 0 }, { deltaY: 3, deltaMode: 1 }, ROW, 40)).toBe(1);
|
||||
expect(wheelReports({ partial: 0 }, { deltaY: 1, deltaMode: 2 }, ROW, 40)).toBe(8);
|
||||
});
|
||||
|
||||
it("sends nothing for a horizontal-only event or an unmeasured terminal", () => {
|
||||
expect(wheelReports({ partial: 0 }, px(0), ROW, 40)).toBe(0);
|
||||
expect(wheelReports({ partial: 0 }, px(10), 0, 40)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @file wheelReports.ts — turns a browser wheel event into the number of SGR
|
||||
* wheel reports a terminal pane gets, in proportion to how far the finger or
|
||||
* the wheel moved, the way a native terminal does.
|
||||
*
|
||||
* xterm.js sends ONE report per wheel event whatever its size, and damps a
|
||||
* touchpad's pixel deltas ×0.3 before it counts even that one — a touchpad
|
||||
* swipe crawled. In our panes every report moves a fixed stretch (Claude's
|
||||
* `CLAUDE_CODE_SCROLL_SPEED`, tmux's copy-mode `WHEEL_LINES`), so a report is
|
||||
* sent per `LINES_PER_REPORT` rows of travel: a touchpad scrolls 1:1 with the
|
||||
* finger, and a wheel notch always sends at least one.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
/** Rows one report scrolls in a Claude pane (server/lib/pty-run.js CLAUDE_SCROLL_SPEED). */
|
||||
export const LINES_PER_REPORT = 5;
|
||||
|
||||
/** A pixel delta this large or larger is a wheel notch, not a touchpad step (xterm.js's own cut). */
|
||||
const NOTCH_PX = 50;
|
||||
|
||||
const DOM_DELTA_PIXEL = 0;
|
||||
const DOM_DELTA_LINE = 1;
|
||||
const DOM_DELTA_PAGE = 2;
|
||||
|
||||
export interface WheelState {
|
||||
/** Touchpad travel not yet sent, in rows (signed). */
|
||||
partial: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signed report count for `ev` — negative scrolls up. `rowPx` is one row's
|
||||
* height on screen (CSS scale included), `rows` the terminal's height.
|
||||
*/
|
||||
export function wheelReports(
|
||||
state: WheelState,
|
||||
ev: Pick<WheelEvent, "deltaY" | "deltaMode">,
|
||||
rowPx: number,
|
||||
rows: number
|
||||
): number {
|
||||
if (!ev.deltaY || !(rowPx > 0)) return 0;
|
||||
const sign = ev.deltaY < 0 ? -1 : 1;
|
||||
if (ev.deltaMode === DOM_DELTA_PIXEL && Math.abs(ev.deltaY) < NOTCH_PX) {
|
||||
// Touchpad: carry the remainder, so slow swipes still add up.
|
||||
if (Math.sign(state.partial) === -sign) state.partial = 0; // turned around
|
||||
state.partial += ev.deltaY / rowPx;
|
||||
const n = Math.trunc(state.partial / LINES_PER_REPORT);
|
||||
state.partial -= n * LINES_PER_REPORT;
|
||||
return n;
|
||||
}
|
||||
// A notch (or a fast flick): at least one report, more for a long one.
|
||||
state.partial = 0;
|
||||
const lines =
|
||||
ev.deltaMode === DOM_DELTA_LINE
|
||||
? ev.deltaY
|
||||
: ev.deltaMode === DOM_DELTA_PAGE
|
||||
? ev.deltaY * rows
|
||||
: ev.deltaY / rowPx;
|
||||
return sign * Math.max(1, Math.round(Math.abs(lines) / LINES_PER_REPORT));
|
||||
}
|
||||
Reference in New Issue
Block a user