diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 745d759a..6c4c9326 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; @@ -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: { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 7b407957..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) => this.scrollLines(amount, true, 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); @@ -870,8 +867,12 @@ export class Terminal extends CoreTerminal implements ITerminal { } public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { - super.scrollLines(disp, suppressScrollEvent, source); - this.refresh(0, this.rows - 1); + if (source === ScrollSource.VIEWPORT) { + super.scrollLines(disp, suppressScrollEvent, source); + this.refresh(0, this.rows - 1); + } else { + this.viewport?.scrollLines(disp); + } } public paste(data: string): void { @@ -1003,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 89494755..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,6 +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 { + 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 5f2ed899..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,12 +160,14 @@ 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 }; handleWheel(ev: WheelEvent): boolean; handleTouchStart(ev: TouchEvent): void; handleTouchMove(ev: TouchEvent): boolean; + scrollLines(disp: number): void; // todo api name? } export interface ILinkifierEvent { diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 25757bca..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) => 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); + 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); + this._onRequestScrollLines.fire({ amount: diff, suppressScrollEvent: true }); } private _smoothScroll(): void { @@ -263,6 +266,23 @@ export class Viewport extends Disposable implements IViewport { return this._bubbleScroll(ev, amount); } + public scrollLines(disp: number): void { + if (!this._optionsService.rawOptions.smoothScrollDuration) { + this._onRequestScrollLines.fire({ amount: disp, suppressScrollEvent: false }); + } else { + const amount = disp * this._currentRowHeight; + this._smoothScrollState.startTime = Date.now(); + if (this._smoothScrollPercent() < 1) { + 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) { 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; }