diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index cedf889b4..1b18a17ee 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -653,7 +653,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this._mouseService.bindMouse({ element: this.element!, screenElement: this.screenElement!, - document: this._document! + document: this._document!, + handleTouchScroll: amount => this._viewport?.handleTouchScroll(amount) }, disposable => this._register(disposable), () => this.focus()); } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index c627c916e..b120105bf 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, IMouseStateService, 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) { diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index ddb457365..d701796c2 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -9,6 +9,7 @@ import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, import { C0 } from 'common/data/EscapeSequences'; import { toDisposable } from 'common/Lifecycle'; import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services'; +import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch'; type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>; @@ -23,6 +24,7 @@ export class MouseService implements IMouseService { private _lastEvent: ICoreMouseEvent | null = null; private _wheelPartialScroll: number = 0; + private _touchScrollAccumulator: number = 0; constructor( @IRenderService private readonly _renderService: IRenderService, @@ -82,6 +84,9 @@ export class MouseService implements IMouseService { */ register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev))); register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false })); + register(Gesture.addTarget(target.screenElement)); + register(addDisposableListener(target.screenElement, GestureEventType.START, () => this._handleTouchStart())); + register(addDisposableListener(target.screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleTouchChange(ctx, e))); } private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean { @@ -264,9 +269,88 @@ export class MouseService implements IMouseService { } } + private _handleTouchStart(): void { + this._touchScrollAccumulator = 0; + } + + private _handleTouchChange(ctx: IMouseBindContext, e: IGestureEvent): void { + e.preventDefault(); + e.stopPropagation(); + + // When mouse protocol has wheel events active, send as mouse wheel events. + if (ctx.requestedEvents.wheel) { + this._handleTouchScrollAsWheel(ctx, e); + return; + } + + // When in alt buffer (no scrollback), send up/down key sequences. + if (!this._bufferService.buffer.hasScrollback) { + this._handleTouchScrollAsKeys(e); + return; + } + + // Normal scrollback: delegate to viewport scrolling when available. + ctx.target.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(ctx: IMouseBindContext, 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 pos = this._mouseCoordsService.getMouseReportCoords(e, ctx.target.screenElement); + if (!pos) { + return; + } + + for (let i = 0; i < Math.abs(lines); i++) { + this._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 + }); + } + } + public reset(): void { this._lastEvent = null; this._wheelPartialScroll = 0; + this._touchScrollAccumulator = 0; } private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void { diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 47c46ec66..72659cafe 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -68,6 +68,7 @@ export interface IMouseServiceTarget { element: HTMLElement; screenElement: HTMLElement; document: Document; + handleTouchScroll?(amount: number): void; } export const IRenderService = createDecorator('RenderService');