From b7d73f63cd730bd8aeb48121ee29d9f8f1c7dc2e Mon Sep 17 00:00:00 2001 From: tisilent Date: Wed, 19 Apr 2023 15:48:12 +0800 Subject: [PATCH 1/6] Add smoothScroll to scrollLines --- src/browser/Terminal.ts | 76 +++++++++++++++++++++++++++++++++++++++-- src/browser/Types.d.ts | 10 ++++++ src/browser/Viewport.ts | 8 +---- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index fe19f954..37a4eca1 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 } from 'browser/Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange, IBufferElementProvider, ISmoothScrollProgressState } from 'browser/Types'; import { IRenderer } from 'browser/renderer/shared/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; @@ -122,6 +122,13 @@ 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 }>()); @@ -869,11 +876,76 @@ export class Terminal extends CoreTerminal implements ITerminal { } } - public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { + 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); + } 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(); + } + } + } + } + + 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/Types.d.ts b/src/browser/Types.d.ts index 5f2ed899..93236e69 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -150,6 +150,16 @@ export interface IViewport extends IDisposable { handleTouchMove(ev: TouchEvent): boolean; } +export interface ISmoothScrollState { + startTime: number; + origin: number; + target: number; +} + +export interface ISmoothScrollProgressState extends ISmoothScrollState { + progress: number; +} + export interface ILinkifierEvent { x1: number; y1: number; diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 25757bca..b5fa722d 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, IViewport, ReadonlyColorSet } from 'browser/Types'; +import { IColorSet, ISmoothScrollState, 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,12 +13,6 @@ import { IRenderDimensions } from 'browser/renderer/shared/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. From ce2d11508787f79781d35ff046d921a453855f8e Mon Sep 17 00:00:00 2001 From: tisilent Date: Tue, 6 Jun 2023 20:14:24 +0800 Subject: [PATCH 2/6] Move logic to Viewport --- src/browser/Terminal.ts | 78 +++-------------------------------- src/browser/TestUtils.test.ts | 3 ++ src/browser/Types.d.ts | 11 +---- src/browser/Viewport.ts | 56 +++++++++++++++++++++---- 4 files changed, 57 insertions(+), 91 deletions(-) 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) { From 74c5256d6d7373f4b12a415a35809feae0253ff3 Mon Sep 17 00:00:00 2001 From: tisilent Date: Fri, 30 Jun 2023 11:44:06 +0800 Subject: [PATCH 3/6] using _smoothScroll --- src/browser/Viewport.ts | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 9009439e..ecb8c8c7 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -19,12 +19,6 @@ interface ISmoothScrollState { 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. @@ -190,7 +184,7 @@ export class Viewport extends Disposable implements IViewport { this._scrollLines(diff, true); } - private _smoothScroll(handle: (context: ISmoothScrollCalculateContext) => void, lastPosition: number = 0): void { + private _smoothScroll(): void { // Check valid state if (this._isDisposed || this._smoothScrollState.origin === -1 || this._smoothScrollState.target === -1) { return; @@ -198,29 +192,16 @@ export class Viewport extends Disposable implements IViewport { // Calculate position complete const percent = this._smoothScrollPercent(); - const position = Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin)); - handle({ percent, position, lastPosition }); + this._viewportElement.scrollTop = this._smoothScrollState.origin + Math.round(percent * (this._smoothScrollState.target - this._smoothScrollState.origin)); // Continue or finish smooth scroll if (percent < 1) { - this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll(handle, position)); + this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll()); } 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; @@ -274,7 +255,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._wheelSmoothScroll(); + this._smoothScroll(); } else { this._clearSmoothScrollState(); } @@ -286,17 +267,19 @@ export class Viewport extends Disposable implements IViewport { if (!this._optionsService.rawOptions.smoothScrollDuration) { this._scrollLines(disp, false); } else { + const amount = disp * this._currentRowHeight; this._smoothScrollState.startTime = Date.now(); if (this._smoothScrollPercent() < 1) { - this._smoothScrollState.origin = 0; - this._smoothScrollState.target = disp; - this._linesSmoothScroll(); + this._smoothScrollState.origin = this._viewportElement.scrollTop; + this._smoothScrollState.target = this._smoothScrollState.origin + amount; + this._smoothScrollState.target = Math.max(Math.min(this._smoothScrollState.target, this._viewportElement.scrollHeight), 0); + this._smoothScroll(); } else { this._clearSmoothScrollState(); } } } - + private _getPixelsScrolled(ev: WheelEvent): number { // Do nothing if it's not a vertical scroll event if (ev.deltaY === 0 || ev.shiftKey) { From 1fb9f0185a517781671e101f633d8aef45b1e71f Mon Sep 17 00:00:00 2001 From: tisilent Date: Fri, 30 Jun 2023 11:45:50 +0800 Subject: [PATCH 4/6] format --- src/browser/Viewport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index ecb8c8c7..4be1323e 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -279,7 +279,7 @@ export class Viewport extends Disposable implements IViewport { } } } - + private _getPixelsScrolled(ev: WheelEvent): number { // Do nothing if it's not a vertical scroll event if (ev.deltaY === 0 || ev.shiftKey) { From e7a78428080934d10644adc71eeded6c35a83dd6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 1 Aug 2023 05:11:19 -0700 Subject: [PATCH 5/6] Use viewport source in tests --- src/browser/Terminal.test.ts | 20 ++++++++++---------- src/browser/TestUtils.test.ts | 4 +--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 745d759a..787d1670 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -8,7 +8,7 @@ import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { MockUnicodeService } from 'common/TestUtils.test'; -import { IMarker } from 'common/Types'; +import { IMarker, ScrollSource } from 'common/Types'; import { ICoreService } from 'common/services/Services'; const INIT_COLS = 80; @@ -258,27 +258,27 @@ describe('Terminal', () => { }); it('should scroll a single line', () => { assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-1); + term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, startYDisp - 1); - term.scrollLines(1); + term.scrollLines(1, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, startYDisp); }); it('should scroll multiple lines', () => { assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-5); + term.scrollLines(-5, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, startYDisp - 5); - term.scrollLines(5); + term.scrollLines(5, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, startYDisp); }); it('should not scroll beyond the bounds of the buffer', () => { assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(1); + term.scrollLines(1, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, startYDisp); for (let i = 0; i < startYDisp; i++) { - term.scrollLines(-1); + term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); } assert.equal(term.buffer.ydisp, 0); - term.scrollLines(-1); + term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, 0); }); }); @@ -329,7 +329,7 @@ describe('Terminal', () => { startYDisp = (term.rows * 2) + 1; }); it('should scroll to the bottom', () => { - term.scrollLines(-1); + term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); term.scrollToBottom(); assert.equal(term.buffer.ydisp, startYDisp); term.scrollPages(-1); @@ -398,7 +398,7 @@ describe('Terminal', () => { }); assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-1); + term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); assert.equal(term.buffer.ydisp, startYDisp - 1); term.keyPress({ keyCode: 0 }); assert.equal(term.buffer.ydisp, startYDisp - 1); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 0002dfe2..b27cef5a 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -319,9 +319,7 @@ 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.'); - } + public scrollLines(disp: number): void { } } export class MockCompositionHelper implements ICompositionHelper { From 1067ec19a8f323c953c904e8c458ffbc07c10ce2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 1 Aug 2023 05:31:30 -0700 Subject: [PATCH 6/6] Implement smooth scroll for pages/top/bottom, add viewport request event --- src/browser/Terminal.test.ts | 19 +++++++++--------- src/browser/Terminal.ts | 9 +++------ src/browser/TestUtils.test.ts | 6 +++++- src/browser/Types.d.ts | 19 ++++++++++++++++++ src/browser/Viewport.ts | 11 +++++++---- src/common/CoreTerminal.ts | 28 +++++++++++---------------- src/common/services/BufferService.ts | 29 ---------------------------- src/common/services/Services.ts | 4 ---- 8 files changed, 55 insertions(+), 70 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 787d1670..6c4c9326 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -29,6 +29,7 @@ describe('Terminal', () => { term.refresh = () => { }; (term as any).renderer = new MockRenderer(); term.viewport = new MockViewport(); + term.viewport.onRequestScrollLines(e => term.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)); (term as any)._compositionHelper = new MockCompositionHelper(); (term as any).element = { classList: { @@ -258,27 +259,27 @@ describe('Terminal', () => { }); it('should scroll a single line', () => { assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(-1); assert.equal(term.buffer.ydisp, startYDisp - 1); - term.scrollLines(1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(1); assert.equal(term.buffer.ydisp, startYDisp); }); it('should scroll multiple lines', () => { assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-5, undefined, ScrollSource.VIEWPORT); + term.scrollLines(-5); assert.equal(term.buffer.ydisp, startYDisp - 5); - term.scrollLines(5, undefined, ScrollSource.VIEWPORT); + term.scrollLines(5); assert.equal(term.buffer.ydisp, startYDisp); }); it('should not scroll beyond the bounds of the buffer', () => { assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(1); assert.equal(term.buffer.ydisp, startYDisp); for (let i = 0; i < startYDisp; i++) { - term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(-1); } assert.equal(term.buffer.ydisp, 0); - term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(-1); assert.equal(term.buffer.ydisp, 0); }); }); @@ -329,7 +330,7 @@ describe('Terminal', () => { startYDisp = (term.rows * 2) + 1; }); it('should scroll to the bottom', () => { - term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(-1); term.scrollToBottom(); assert.equal(term.buffer.ydisp, startYDisp); term.scrollPages(-1); @@ -398,7 +399,7 @@ describe('Terminal', () => { }); assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-1, undefined, ScrollSource.VIEWPORT); + term.scrollLines(-1); assert.equal(term.buffer.ydisp, startYDisp - 1); term.keyPress({ keyCode: 0 }); assert.equal(term.buffer.ydisp, startYDisp - 1); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 7db4771c..8f32c6cf 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -497,11 +497,8 @@ export class Terminal extends CoreTerminal implements ITerminal { this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); - this.viewport = this._instantiationService.createInstance(Viewport, - (amount: number, suppressScrollEvent: boolean) => this.scrollLines(amount, suppressScrollEvent, ScrollSource.VIEWPORT), - this._viewportElement, - this._viewportScrollArea - ); + this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea); + this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)), this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea())); this.register(this.viewport); @@ -1007,7 +1004,7 @@ export class Terminal extends CoreTerminal implements ITerminal { if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) { if (this.options.scrollOnUserInput && this.buffer.ybase !== this.buffer.ydisp) { - this._bufferService.scrollToBottom(); + this.scrollToBottom(); } return false; } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index b27cef5a..c04c9af2 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -296,6 +296,8 @@ export class MockRenderer implements IRenderer { } export class MockViewport implements IViewport { + private readonly _onRequestScrollLines = new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>(); + public readonly onRequestScrollLines = this._onRequestScrollLines.event; public dispose(): void { throw new Error('Method not implemented.'); } @@ -319,7 +321,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 { } + public scrollLines(disp: number): void { + this._onRequestScrollLines.fire({ amount: disp, suppressScrollEvent: false }); + } } export class MockCompositionHelper implements ICompositionHelper { diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 919c1d32..42d56c06 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -69,10 +69,28 @@ export interface IPublicTerminal extends IDisposable { selectAll(): void; selectLines(start: number, end: number): void; dispose(): void; + /** + * Scroll the display of the terminal + * @param amount The number of lines to scroll down (negative scroll up). + */ scrollLines(amount: number): void; + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ scrollPages(pageCount: number): void; + /** + * Scrolls the display of the terminal to the top. + */ scrollToTop(): void; + /** + * Scrolls the display of the terminal to the bottom. + */ scrollToBottom(): void; + /** + * Scrolls to a line within the buffer. + * @param line The 0-based line index to scroll to. + */ scrollToLine(line: number): void; clear(): void; write(data: string | Uint8Array, callback?: () => void): void; @@ -142,6 +160,7 @@ export interface IPartialColorSet { export interface IViewport extends IDisposable { scrollBarWidth: number; + readonly onRequestScrollLines: IEvent<{ amount: number, suppressScrollEvent: boolean }>; syncScrollArea(immediate?: boolean): void; getLinesScrolled(ev: WheelEvent): number; getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement }; diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 4be1323e..a30228a4 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -10,6 +10,7 @@ import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; +import { EventEmitter } from 'common/EventEmitter'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -48,8 +49,10 @@ export class Viewport extends Disposable implements IViewport { target: -1 }; + private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>()); + public readonly onRequestScrollLines = this._onRequestScrollLines.event; + constructor( - private readonly _scrollLines: (amount: number, suppressScrollEvent: boolean) => void, private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @@ -175,13 +178,13 @@ export class Viewport extends Disposable implements IViewport { if (this._ignoreNextScrollEvent) { this._ignoreNextScrollEvent = false; // Still trigger the scroll so lines get refreshed - this._scrollLines(0, true); + this._onRequestScrollLines.fire({ amount: 0, suppressScrollEvent: true }); return; } const newRow = Math.round(this._lastScrollTop / this._currentRowHeight); const diff = newRow - this._bufferService.buffer.ydisp; - this._scrollLines(diff, true); + this._onRequestScrollLines.fire({ amount: diff, suppressScrollEvent: true }); } private _smoothScroll(): void { @@ -265,7 +268,7 @@ export class Viewport extends Disposable implements IViewport { public scrollLines(disp: number): void { if (!this._optionsService.rawOptions.smoothScrollDuration) { - this._scrollLines(disp, false); + this._onRequestScrollLines.fire({ amount: disp, suppressScrollEvent: false }); } else { const amount = disp * this._currentRowHeight; this._smoothScrollState.startTime = Date.now(); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 8ff9b988..89d9f5e5 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -195,38 +195,32 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { /** * Scroll the display of the terminal * @param disp The number of lines to scroll down (negative scroll up). - * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used - * to avoid unwanted events being handled by the viewport when the event was triggered from the - * viewport originally. + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used to avoid + * unwanted events being handled by the viewport when the event was triggered from the viewport + * originally. + * @param source Which component the event came from. */ public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { this._bufferService.scrollLines(disp, suppressScrollEvent, source); } - /** - * Scroll the display of the terminal by a number of pages. - * @param pageCount The number of pages to scroll (negative scrolls up). - */ public scrollPages(pageCount: number): void { - this._bufferService.scrollPages(pageCount); + this.scrollLines(pageCount * (this.rows - 1)); } - /** - * Scrolls the display of the terminal to the top. - */ public scrollToTop(): void { - this._bufferService.scrollToTop(); + this.scrollLines(-this._bufferService.buffer.ydisp); } - /** - * Scrolls the display of the terminal to the bottom. - */ public scrollToBottom(): void { - this._bufferService.scrollToBottom(); + this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); } public scrollToLine(line: number): void { - this._bufferService.scrollToLine(line); + const scrollAmount = line - this._bufferService.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 528d2674..7b02cb7d 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -147,33 +147,4 @@ export class BufferService extends Disposable implements IBufferService { this._onScroll.fire(buffer.ydisp); } } - - /** - * Scroll the display of the terminal by a number of pages. - * @param pageCount The number of pages to scroll (negative scrolls up). - */ - public scrollPages(pageCount: number): void { - this.scrollLines(pageCount * (this.rows - 1)); - } - - /** - * Scrolls the display of the terminal to the top. - */ - public scrollToTop(): void { - this.scrollLines(-this.buffer.ydisp); - } - - /** - * Scrolls the display of the terminal to the bottom. - */ - public scrollToBottom(): void { - this.scrollLines(this.buffer.ybase - this.buffer.ydisp); - } - - public scrollToLine(line: number): void { - const scrollAmount = line - this.buffer.ydisp; - if (scrollAmount !== 0) { - this.scrollLines(scrollAmount); - } - } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index b4021e45..3a1e5d8d 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -21,11 +21,7 @@ export interface IBufferService { onResize: IEvent<{ cols: number, rows: number }>; onScroll: IEvent; scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; - scrollToBottom(): void; - scrollToTop(): void; - scrollToLine(line: number): void; scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void; - scrollPages(pageCount: number): void; resize(cols: number, rows: number): void; reset(): void; }