diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 1eb9dc4e..6cff1f98 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -13,6 +13,12 @@ import { IRenderDimensions } from 'browser/renderer/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; +interface ISmoothScrollState { + startTime: number; + origin: number; + target: number; +} + /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. * Logic for the virtual scroll bar is included in this object. @@ -36,6 +42,11 @@ export class Viewport extends Disposable implements IViewport { private _refreshAnimationFrame: number | null = null; private _ignoreNextScrollEvent: boolean = false; + private _smoothScrollState: ISmoothScrollState = { + startTime: 0, + origin: -1, + target: -1 + }; constructor( private readonly _scrollLines: (amount: number) => void, @@ -168,6 +179,37 @@ export class Viewport extends Disposable implements IViewport { this._scrollLines(diff); } + private _smoothScroll(): void { + // Check valid state + if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) { + return; + } + + // Calculate position complete + const percent = this._smoothScrollPercent(); + this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin)); + + // Continue or finish smooth scroll + if (percent < 1) { + window.requestAnimationFrame(() => this._smoothScroll()); + } else { + this._clearSmoothScrollState(); + } + } + + private _smoothScrollPercent(): number { + if (!this._optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollState.startTime) { + return 1; + } + return Math.max(Math.min((Date.now() - this._smoothScrollState.startTime) / this._optionsService.rawOptions.smoothScrollDuration, 1), 0); + } + + private _clearSmoothScrollState(): void { + this._smoothScrollState.startTime = 0; + this._smoothScrollState.origin = -1; + this._smoothScrollState.target = -1; + } + /** * Handles bubbling of scroll event in case the viewport has reached top or bottom * @param ev The scroll event. @@ -196,7 +238,23 @@ export class Viewport extends Disposable implements IViewport { if (amount === 0) { return false; } - this._viewportElement.scrollTop += amount; + if (!this._optionsService.rawOptions.smoothScrollDuration) { + this._viewportElement.scrollTop += amount; + } else { + this._smoothScrollState.startTime = Date.now(); + if (this._smoothScrollPercent() < 1) { + this._smoothScrollState.origin = this._viewportElement.scrollTop; + if (this._smoothScrollState.target === -1) { + this._smoothScrollState.target = this._viewportElement.scrollTop + amount; + } else { + this._smoothScrollState.target += amount; + } + this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0); + this._smoothScroll(); + } else { + this._clearSmoothScrollState(); + } + } return this._bubbleScroll(ev, amount); } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 550adb31..ab9edfbf 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -28,6 +28,7 @@ export const DEFAULT_OPTIONS: Readonly = { scrollback: 1000, scrollSensitivity: 1, screenReaderMode: false, + smoothScrollDuration: 0, macOptionIsMeta: false, macOptionClickForcesSelection: false, minimumContrastRatio: 1, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 709e171f..248d5644 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -232,6 +232,7 @@ export interface ITerminalOptions { screenReaderMode: boolean; scrollback: number; scrollSensitivity: number; + smoothScrollDuration: number; tabStopWidth: number; theme: ITheme; windowsMode: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 76527e37..666d45c3 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -169,6 +169,12 @@ declare module 'xterm-headless' { */ 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. + */ + smoothScrollDuration?: number; + /** * The size of tab stops in the terminal. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 43e52ee6..4890b4dd 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -201,6 +201,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. + */ + smoothScrollDuration?: number; + /** * The size of tab stops in the terminal. */