From 55c627572c187cc171a819a88dd27f4698f85ad2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:32:55 -0700 Subject: [PATCH] Support smooth scroll --- src/browser/CoreBrowserTerminal.ts | 31 ++++++++++++++++-- src/browser/Viewport.ts | 52 ++++++++++++++++++++++++------ src/common/CoreTerminal.ts | 7 +--- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index 7944502c..23e4a560 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -71,6 +71,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { public linkifier: ILinkifier2 | undefined; private _overviewRulerRenderer: OverviewRulerRenderer | undefined; + private _viewport: Viewport | undefined; public browser: IBrowser = Browser as any; @@ -505,8 +506,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { this.register(this.onBlur(() => this._renderService!.handleBlur())); this.register(this.onFocus(() => this._renderService!.handleFocus())); - const viewport = this.register(this._instantiationService.createInstance(Viewport, this.element, this.screenElement)); - this.register(viewport.onRequestScrollLines(e => this.scrollLines(e, false))); + this._viewport = this.register(this._instantiationService.createInstance(Viewport, this.element, this.screenElement)); + this.register(this._viewport.onRequestScrollLines(e => super.scrollLines(e, false))); this._selectionService = this.register(this._instantiationService.createInstance(SelectionService, this.element, @@ -870,10 +871,34 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } public scrollLines(disp: number, suppressScrollEvent?: boolean): void { - super.scrollLines(disp, suppressScrollEvent); + // All scrollLines methods need to go via the viewport in order to support smooth scroll + if (this._viewport) { + this._viewport.scrollLines(disp); + } else { + super.scrollLines(disp, suppressScrollEvent); + } this.refresh(0, this.rows - 1); } + public scrollPages(pageCount: number): void { + this.scrollLines(pageCount * (this.rows - 1)); + } + + public scrollToTop(): void { + this.scrollLines(-this._bufferService.buffer.ydisp); + } + + public scrollToBottom(): void { + this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this._bufferService.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } + public paste(data: string): void { paste(data, this.textarea!, this.coreService, this.optionsService); } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index c247dab8..f1c031e4 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -7,9 +7,10 @@ import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/serv import { EventEmitter, runAndSubscribe } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, IOptionsService } from 'common/services/Services'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; -import { ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable'; +import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable'; const enum Constants { DEFAULT_SCROLL_BAR_WIDTH = 14 @@ -20,7 +21,7 @@ export class Viewport extends Disposable { protected _onRequestScrollLines = this.register(new EventEmitter()); public readonly onRequestScrollLines = this._onRequestScrollLines.event; - private _scrollableElement: DomScrollableElement; + private _scrollableElement: SmoothScrollableElement; private _styleElement: HTMLStyleElement; private _queuedAnimationFrame?: number; @@ -42,13 +43,23 @@ export class Viewport extends Disposable { // TODO: Support smooth scroll - this._scrollableElement = this.register(new DomScrollableElement(screenElement, { + const scrollable = this.register(new Scrollable({ + forceIntegerValues: false, + smoothScrollDuration: this._optionsService.rawOptions.smoothScrollDuration, + // This is used over `IRenderService.addRefreshCallback` since it can be canceled + scheduleAtNextAnimationFrame: cb => scheduleAtNextAnimationFrame(coreBrowserService.window, cb) + })); + this.register(this._optionsService.onSpecificOptionChange('smoothScrollDuration', () => { + scrollable.setSmoothScrollDuration(this._optionsService.rawOptions.smoothScrollDuration); + })); + + this._scrollableElement = this.register(new SmoothScrollableElement(screenElement, { vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Hidden, useShadows: false, mouseWheelSmoothScroll: true, ...this._getMutableOptions() - })); + }, scrollable)); this.register(this._optionsService.onMultipleOptionChange([ 'scrollSensitivity', 'fastScrollSensitivity', @@ -80,11 +91,29 @@ export class Viewport extends Disposable { })); this.register(this._bufferService.onResize(() => this._queueSync())); - this.register(this._bufferService.onScroll(ydisp => this._queueSync(ydisp))); + this.register(this._bufferService.onScroll(() => this._sync())); this.register(this._scrollableElement.onScroll(e => this._handleScroll(e))); } + public scrollLines(disp: number): void { + const pos = this._scrollableElement.getScrollPosition(); + this._scrollableElement.setScrollPosition({ + reuseAnimation: true, + scrollTop: pos.scrollTop + disp * this._renderService.dimensions.css.cell.height + }); + } + + public scrollToLine(line: number, disableSmoothScroll?: boolean): void { + if (!disableSmoothScroll) { + this._latestYDisp = line; + } + this._scrollableElement.setScrollPosition({ + reuseAnimation: !disableSmoothScroll, + scrollTop: line * this._renderService.dimensions.css.cell.height + }); + } + private _getMutableOptions(): ScrollableElementChangeOptions { return { mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity, @@ -123,9 +152,13 @@ export class Viewport extends Disposable { }); this._suppressOnScrollHandler = false; - this._scrollableElement.setScrollPosition({ - scrollTop: ydisp * this._renderService.dimensions.css.cell.height - }); + // If ydisp has been changed by some other copmonent (input/buffer), then stop animating smooth + // scroll and scroll there immediately. + if (ydisp !== this._latestYDisp) { + this._scrollableElement.setScrollPosition({ + scrollTop: ydisp * this._renderService.dimensions.css.cell.height + }); + } this._isSyncing = false; } @@ -141,6 +174,7 @@ export class Viewport extends Disposable { const newRow = Math.round(e.scrollTop / this._renderService.dimensions.css.cell.height); const diff = newRow - this._bufferService.buffer.ydisp; if (diff !== 0) { + this._latestYDisp = newRow; this._onRequestScrollLines.fire(diff); } this._isHandlingScroll = false; diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 470cdb3d..10f3bc18 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -133,15 +133,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.register(this.coreService.onRequestScrollToBottom(() => this.scrollToBottom())); this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput())); this.register(this.optionsService.onMultipleOptionChange(['windowsMode', 'windowsPty'], () => this._handleWindowsPtyOptionChange())); - this.register(this._bufferService.onScroll(event => { + this.register(this._bufferService.onScroll(() => { this._onScroll.fire({ position: this._bufferService.buffer.ydisp }); this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); - this.register(this._inputHandler.onScroll(event => { - this._onScroll.fire({ position: this._bufferService.buffer.ydisp }); - this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); - })); - // Setup WriteBuffer this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult))); this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));