Fix scrolling with touch

Part of #5377
This commit is contained in:
Daniel Imms
2026-02-07 16:09:03 -08:00
parent e14746e6e9
commit 38913e4ae1
3 changed files with 80 additions and 9 deletions
+73
View File
@@ -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.
+3 -9
View File
@@ -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
});
}
}
+4
View File
@@ -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) {