From a55728dd28f45f3da643aa38196a616e1334fcb1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:41:00 -0700 Subject: [PATCH] Add duration-based smooth scroll Fixes #1140 --- src/browser/Viewport.ts | 50 ++++++++++++++++++++++++++- src/common/services/OptionsService.ts | 1 + src/common/services/Services.ts | 1 + typings/xterm-headless.d.ts | 9 ++++- typings/xterm.d.ts | 6 ++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 1eb9dc4e21..65ccc333cd 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -36,6 +36,9 @@ export class Viewport extends Disposable implements IViewport { private _refreshAnimationFrame: number | null = null; private _ignoreNextScrollEvent: boolean = false; + private _lastSmoothScrollOrigin?: number = undefined; + private _lastSmoothScrollTarget?: number = undefined; + private _lastSmoothScrollStartTime?: number = undefined; constructor( private readonly _scrollLines: (amount: number) => void, @@ -168,6 +171,33 @@ export class Viewport extends Disposable implements IViewport { this._scrollLines(diff); } + private _smoothScroll(): void { + // Check valid state + if (this._isDisposed || this._lastSmoothScrollOrigin === undefined || this._lastSmoothScrollTarget === undefined) { + return; + } + + // Calculate position complete + const percent = this._smoothScrollPercent(); + this._viewportElement.scrollTop = this._lastSmoothScrollOrigin + Math.round(percent * (this._lastSmoothScrollTarget - this._lastSmoothScrollOrigin)); + + // Continue or finish smooth scroll + if (percent < 1) { + window.requestAnimationFrame(() => this._smoothScroll()); + } else { + this._lastSmoothScrollStartTime = undefined; + this._lastSmoothScrollOrigin = undefined; + this._lastSmoothScrollTarget = undefined; + } + } + + private _smoothScrollPercent(): number { + if (!this._optionsService.rawOptions.smoothScrollingDuration || !this._lastSmoothScrollStartTime) { + return 1; + } + return Math.max(Math.min((Date.now() - this._lastSmoothScrollStartTime) / this._optionsService.rawOptions.smoothScrollingDuration, 1), 0); + } + /** * Handles bubbling of scroll event in case the viewport has reached top or bottom * @param ev The scroll event. @@ -196,7 +226,25 @@ export class Viewport extends Disposable implements IViewport { if (amount === 0) { return false; } - this._viewportElement.scrollTop += amount; + if (!this._optionsService.rawOptions.smoothScrollingDuration) { + this._viewportElement.scrollTop += amount; + } else { + this._lastSmoothScrollStartTime = Date.now(); + if (this._smoothScrollPercent() < 1) { + this._lastSmoothScrollOrigin = this._viewportElement.scrollTop; + if (this._lastSmoothScrollTarget === undefined) { + this._lastSmoothScrollTarget = this._viewportElement.scrollTop + amount; + } else { + this._lastSmoothScrollTarget += amount; + } + this._lastSmoothScrollTarget = Math.max(Math.min(this._lastSmoothScrollTarget, this._viewportElement.scrollHeight), 0); + this._smoothScroll(); + } else { + this._lastSmoothScrollStartTime = undefined; + this._lastSmoothScrollOrigin = undefined; + this._lastSmoothScrollTarget = undefined; + } + } return this._bubbleScroll(ev, amount); } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 4f9600a4c4..32358bdd51 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,6 +37,7 @@ export const DEFAULT_OPTIONS: Readonly = { scrollback: 1000, scrollSensitivity: 1, screenReaderMode: false, + smoothScrollingDuration: 125, macOptionIsMeta: false, macOptionClickForcesSelection: false, minimumContrastRatio: 1, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index fab8435a56..72418ee2ad 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -241,6 +241,7 @@ export interface ITerminalOptions { screenReaderMode: boolean; scrollback: number; scrollSensitivity: number; + smoothScrollingDuration: number; tabStopWidth: number; theme: ITheme; windowsMode: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 26a01e4c2b..cc08039e06 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -182,10 +182,17 @@ declare module 'xterm-headless' { scrollback?: number; /** - * The scrolling speed multiplier used for adjusting normal scrolling speed. + * The duration to smoothly scroll between the origin and the target in + * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. */ scrollSensitivity?: number; + /** + * The duration to smoothly scroll between the origin and the target. Set + * this to 0 to disable smooth scrolling and scroll instantly. + */ + smoothScrollingDuration?: number; + /** * The size of tab stops in the terminal. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a3f47300a5..afa138d7ec 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -233,6 +233,12 @@ declare module 'xterm' { */ scrollSensitivity?: number; + /** + * The duration to smoothly scroll between the origin and the target in + * milliseconds. Set to 0 to disable smooth scrolling and scroll instantly. + */ + smoothScrollingDuration?: number; + /** * The size of tab stops in the terminal. */