From 38913e4ae15289227b79da4f9004e1ccd608071c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:09:03 -0800 Subject: [PATCH] Fix scrolling with touch Part of #5377 --- src/browser/CoreBrowserTerminal.ts | 73 ++++++++++++++++++++++++++++++ src/browser/Viewport.ts | 12 ++--- src/browser/scrollable/touch.ts | 4 ++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 8ad8409f4..607b12199 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -57,6 +57,7 @@ import { AccessibilityManager } from './AccessibilityManager'; import { Linkifier } from './Linkifier'; import { Emitter, EventUtils, type IEvent } from 'common/Event'; import { addDisposableListener } from 'browser/Dom'; +import { Gesture, EventType as GestureEventType, type IGestureEvent } from 'browser/scrollable/touch'; import { MutableDisposable, toDisposable } from 'common/Lifecycle'; export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { @@ -78,6 +79,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { private _customKeyEventHandler: CustomKeyEventHandler | undefined; private _customWheelEventHandler: CustomWheelEventHandler | undefined; + private _touchScrollAccumulator: number = 0; // Browser services private readonly _decorationService: DecorationService; @@ -914,9 +916,80 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { return false; } }, { passive: false })); + + // Touch/gesture scrolling support + this._register(Gesture.addTarget(this.screenElement!)); + this._register(addDisposableListener(this.screenElement!, GestureEventType.START, () => { + this._touchScrollAccumulator = 0; + })); + this._register(addDisposableListener(this.screenElement!, GestureEventType.CHANGE, (e: IGestureEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // When mouse protocol has wheel events active, send as mouse wheel events + if (requestedEvents.wheel) { + this._handleTouchScrollAsWheel(e, sendEvent); + return; + } + + // When in alt buffer (no scrollback), send up/down key sequences + if (!this.buffer.hasScrollback) { + this._handleTouchScrollAsKeys(e); + return; + } + + // Normal scrollback: delegate to viewport + this._viewport?.handleTouchScroll(e.translationY); + })); } + private _handleTouchScrollAsKeys(e: IGestureEvent): void { + const cellHeight = this._renderService?.dimensions.css.cell.height; + if (!cellHeight) return; + + this._touchScrollAccumulator -= e.translationY; + const lines = Math.trunc(this._touchScrollAccumulator / cellHeight); + if (lines === 0) return; + + this._touchScrollAccumulator -= lines * cellHeight; + const sequence = C0.ESC + + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + + (lines < 0 ? 'A' : 'B'); + for (let i = 0; i < Math.abs(lines); i++) { + this.coreService.triggerDataEvent(sequence, true); + } + } + + private _handleTouchScrollAsWheel(e: IGestureEvent, sendEvent: (ev: MouseEvent | WheelEvent) => boolean): void { + const cellHeight = this._renderService?.dimensions.css.cell.height; + if (!cellHeight) return; + + this._touchScrollAccumulator -= e.translationY; + const lines = Math.trunc(this._touchScrollAccumulator / cellHeight); + if (lines === 0) return; + + this._touchScrollAccumulator -= lines * cellHeight; + + // Get coordinates from gesture event + const pos = this._mouseService?.getMouseReportCoords(e, this.screenElement!); + if (!pos) return; + + for (let i = 0; i < Math.abs(lines); i++) { + this.coreMouseService.triggerMouseEvent({ + col: pos.col, + row: pos.row, + x: pos.x, + y: pos.y, + button: CoreMouseButton.WHEEL, + action: lines < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN, + ctrl: false, + alt: false, + shift: false + }); + } + } + /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 09864f7ac..fd98ab7cc 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -8,12 +8,11 @@ import { ViewportConstants } from 'browser/shared/Constants'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services'; import { CoreMouseEventType } from 'common/Types'; -import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom'; +import { scheduleAtNextAnimationFrame } from 'browser/Dom'; import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement'; import type { IScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions'; import { Emitter, EventUtils } from 'common/Event'; import { Scrollable, ScrollbarVisibility, type IScrollEvent } from 'browser/scrollable/scrollable'; -import { Gesture, EventType as GestureEventType, type IGestureEvent } from 'browser/scrollable/touch'; export class Viewport extends Disposable { @@ -107,9 +106,6 @@ export class Viewport extends Disposable { this._register(this._scrollableElement.onScroll(e => this._handleScroll(e))); - // Touch/gesture scrolling support - this._register(Gesture.addTarget(screenElement)); - this._register(addDisposableListener(screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleGestureChange(e))); } public scrollLines(disp: number): void { @@ -204,12 +200,10 @@ export class Viewport extends Disposable { this._isHandlingScroll = false; } - private _handleGestureChange(e: IGestureEvent): void { - e.preventDefault(); - e.stopPropagation(); + public handleTouchScroll(translationY: number): void { const pos = this._scrollableElement.getScrollPosition(); this._scrollableElement.setScrollPosition({ - scrollTop: pos.scrollTop - e.translationY + scrollTop: pos.scrollTop - translationY }); } } diff --git a/src/browser/scrollable/touch.ts b/src/browser/scrollable/touch.ts index cba4e5b57..8ffdfe699 100644 --- a/src/browser/scrollable/touch.ts +++ b/src/browser/scrollable/touch.ts @@ -153,6 +153,8 @@ export interface IGestureEvent extends MouseEvent { translationY: number; pageX: number; pageY: number; + clientX: number; + clientY: number; tapCount: number; } @@ -459,6 +461,8 @@ export class Gesture extends Disposable { evt.translationY = touch.pageY - tail(data.rollingPageY)!; evt.pageX = touch.pageX; evt.pageY = touch.pageY; + evt.clientX = touch.clientX; + evt.clientY = touch.clientY; this._dispatchEvent(evt); if (data.rollingPageX.length > 3) {