diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 37a4eca1..d99861eb 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange, IBufferElementProvider, ISmoothScrollProgressState } from 'browser/Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange, IBufferElementProvider } from 'browser/Types'; import { IRenderer } from 'browser/renderer/shared/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; @@ -122,13 +122,6 @@ export class Terminal extends CoreTerminal implements ITerminal { private _compositionHelper: ICompositionHelper | undefined; private _accessibilityManager: AccessibilityManager | undefined; - private _smoothScrollProgressState: ISmoothScrollProgressState = { - startTime: 0, - origin: 0, - target: 0, - progress: 0 - }; - private readonly _onCursorMove = this.register(new EventEmitter()); public readonly onCursorMove = this._onCursorMove.event; private readonly _onKey = this.register(new EventEmitter<{ key: string, domEvent: KeyboardEvent }>()); @@ -505,7 +498,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._instantiationService.setService(IMouseService, this._mouseService); this.viewport = this._instantiationService.createInstance(Viewport, - (amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT), + (amount: number, suppressScrollEvent: boolean) => this.scrollLines(amount, suppressScrollEvent, ScrollSource.VIEWPORT), this._viewportElement, this._viewportScrollArea ); @@ -876,76 +869,15 @@ export class Terminal extends CoreTerminal implements ITerminal { } } - private _scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { - super.scrollLines(disp, suppressScrollEvent, source); - this.refresh(0, this.rows - 1); - } - public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { if (source === ScrollSource.VIEWPORT) { - this._scrollLines(disp, suppressScrollEvent, source); + super.scrollLines(disp, suppressScrollEvent, source); + this.refresh(0, this.rows - 1); } else { - if (!this.optionsService.rawOptions.smoothScrollDuration) { - this._scrollLines(disp, suppressScrollEvent, source); - } else { - this._smoothScrollProgressState.startTime = Date.now(); - if (this._smoothScrollPercent() < 1) { - this._smoothScrollProgressState.origin = 0; - this._smoothScrollProgressState.target = disp; - this._smoothScrollProgressState.progress = 0; - this._smoothScroll(suppressScrollEvent, source); - } else { - this._clearSmoothScrollState(); - } - } + this.viewport?.scrollLines(disp); } } - private _smoothScrollPercent(): number { - if (!this.optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollProgressState.startTime) { - return 1; - } - return Math.max(Math.min((Date.now() - this._smoothScrollProgressState.startTime) / this.optionsService.rawOptions.smoothScrollDuration, 1), 0); - } - - private _isSmoothScrollEnd(): boolean { - if (this._smoothScrollProgressState.target < 0) { - if (this._smoothScrollProgressState.progress > this._smoothScrollProgressState.target) { - return false; - } - } else if (this._smoothScrollProgressState.target > 0) { - if (this._smoothScrollProgressState.progress < this._smoothScrollProgressState.target) { - return false; - } - } - return true; - } - - private _smoothScroll(suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { - if (this._smoothScrollProgressState.startTime === 0 || this._isSmoothScrollEnd()) { - return; - } - - const percent = this._smoothScrollPercent(); - const step = Math.round(percent * (this._smoothScrollProgressState.target - this._smoothScrollProgressState.origin)) - this._smoothScrollProgressState.progress; - this._smoothScrollProgressState.progress += step; - this._scrollLines(step, suppressScrollEvent, source); - - if (this._isSmoothScrollEnd()) { - this._clearSmoothScrollState(); - return; - } - - this._coreBrowserService?.window.requestAnimationFrame(() => this._smoothScroll(suppressScrollEvent, source)); - } - - private _clearSmoothScrollState(): void { - this._smoothScrollProgressState.origin = 0; - this._smoothScrollProgressState.target = 0; - this._smoothScrollProgressState.progress = 0; - this._smoothScrollProgressState.startTime = 0; - } - public paste(data: string): void { paste(data, this.textarea!, this.coreService); } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 89494755..0002dfe2 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -319,6 +319,9 @@ export class MockViewport implements IViewport { public getBufferElements(startLine: number, endLine?: number | undefined): { bufferElements: HTMLElement[], cursorElement?: HTMLElement | undefined } { throw new Error('Method not implemented.'); } + public scrollLines(disp: number): void { + throw new Error('Method not implemented.'); + } } export class MockCompositionHelper implements ICompositionHelper { diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 93236e69..919c1d32 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -148,16 +148,7 @@ export interface IViewport extends IDisposable { handleWheel(ev: WheelEvent): boolean; handleTouchStart(ev: TouchEvent): void; handleTouchMove(ev: TouchEvent): boolean; -} - -export interface ISmoothScrollState { - startTime: number; - origin: number; - target: number; -} - -export interface ISmoothScrollProgressState extends ISmoothScrollState { - progress: number; + scrollLines(disp: number): void; // todo api name? } export interface ILinkifierEvent { diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index b5fa722d..9009439e 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -5,7 +5,7 @@ import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, ISmoothScrollState, IViewport, ReadonlyColorSet } from 'browser/Types'; +import { IColorSet, IViewport, ReadonlyColorSet } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBuffer } from 'common/buffer/Types'; @@ -13,6 +13,18 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; +interface ISmoothScrollState { + startTime: number; + origin: number; + target: number; +} + +interface ISmoothScrollCalculateContext { + percent: number; + position: number; + lastPosition: 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. @@ -43,7 +55,7 @@ export class Viewport extends Disposable implements IViewport { }; constructor( - private readonly _scrollLines: (amount: number) => void, + private readonly _scrollLines: (amount: number, suppressScrollEvent: boolean) => void, private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @@ -169,16 +181,16 @@ export class Viewport extends Disposable implements IViewport { if (this._ignoreNextScrollEvent) { this._ignoreNextScrollEvent = false; // Still trigger the scroll so lines get refreshed - this._scrollLines(0); + this._scrollLines(0, true); return; } const newRow = Math.round(this._lastScrollTop / this._currentRowHeight); const diff = newRow - this._bufferService.buffer.ydisp; - this._scrollLines(diff); + this._scrollLines(diff, true); } - private _smoothScroll(): void { + private _smoothScroll(handle: (context: ISmoothScrollCalculateContext) => void, lastPosition: number = 0): void { // Check valid state if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) { return; @@ -186,16 +198,29 @@ export class Viewport extends Disposable implements IViewport { // Calculate position complete const percent = this._smoothScrollPercent(); - this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin)); + const position = Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin)); + handle({ percent, position, lastPosition }); // Continue or finish smooth scroll if (percent < 1) { - this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll()); + this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll(handle, position)); } else { this._clearSmoothScrollState(); } } + private _wheelSmoothScroll(): void { + this._smoothScroll(({ position }) => { + this._viewportElement.scrollTop = this._smoothScrollState.origin + position; + }); + } + + private _linesSmoothScroll(): void { + this._smoothScroll(({ position, lastPosition }) => { + this._scrollLines(position - lastPosition, false); + }); + } + private _smoothScrollPercent(): number { if (!this._optionsService.rawOptions.smoothScrollDuration || !this._smoothScrollState.startTime) { return 1; @@ -249,7 +274,7 @@ export class Viewport extends Disposable implements IViewport { this._smoothScrollState.target += amount; } this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0); - this._smoothScroll(); + this._wheelSmoothScroll(); } else { this._clearSmoothScrollState(); } @@ -257,6 +282,21 @@ export class Viewport extends Disposable implements IViewport { return this._bubbleScroll(ev, amount); } + public scrollLines(disp: number): void { + if (!this._optionsService.rawOptions.smoothScrollDuration) { + this._scrollLines(disp, false); + } else { + this._smoothScrollState.startTime = Date.now(); + if (this._smoothScrollPercent() < 1) { + this._smoothScrollState.origin = 0; + this._smoothScrollState.target = disp; + this._linesSmoothScroll(); + } else { + this._clearSmoothScrollState(); + } + } + } + private _getPixelsScrolled(ev: WheelEvent): number { // Do nothing if it's not a vertical scroll event if (ev.deltaY === 0 || ev.shiftKey) {