From c2db7d4c8a777762f9388e81139dc99da1744e73 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:20:23 -0800 Subject: [PATCH] Add scrollbar.showScrollbar Fixes #5676 --- addons/addon-fit/src/FitAddon.ts | 5 +- demo/client/client.ts | 2 +- .../client/components/window/optionsWindow.ts | 2 + src/browser/CoreBrowserTerminal.ts | 12 ++- src/browser/Viewport.ts | 12 ++- .../decorations/OverviewRulerRenderer.ts | 7 +- src/browser/scrollable/abstractScrollbar.ts | 3 +- src/browser/scrollable/scrollableElement.ts | 6 ++ .../scrollable/scrollableElementOptions.ts | 2 + src/browser/scrollable/scrollbarState.ts | 10 ++- src/browser/scrollable/verticalScrollbar.ts | 77 ++++++++++++++----- src/common/services/OptionsService.ts | 2 +- src/common/services/Services.ts | 1 + typings/xterm.d.ts | 6 ++ 14 files changed, 115 insertions(+), 32 deletions(-) diff --git a/addons/addon-fit/src/FitAddon.ts b/addons/addon-fit/src/FitAddon.ts index 3b06fb73..c51cd63d 100644 --- a/addons/addon-fit/src/FitAddon.ts +++ b/addons/addon-fit/src/FitAddon.ts @@ -69,9 +69,10 @@ export class FitAddon implements ITerminalAddon, IFitApi { return undefined; } - const scrollbarWidth = (this._terminal.options.scrollback === 0 + const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true; + const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar ? 0 - : (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH)); + : (this._terminal.options.overviewRuler?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH)); const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement); const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); diff --git a/demo/client/client.ts b/demo/client/client.ts index 1fcc6db0..fa249a9e 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -284,7 +284,7 @@ function createTerminal(): Terminal { const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ - scrollbar: { showArrows: true }, + scrollbar: { showScrollbar: true }, allowProposedApi: true, windowsPty: isWindows ? { // In a real scenario, these values should be verified on the backend diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts index b97204d2..dfea4aed 100644 --- a/demo/client/components/window/optionsWindow.ts +++ b/demo/client/components/window/optionsWindow.ts @@ -123,6 +123,8 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { 'windowsPty', ]; const nestedBooleanOptions: { label: string, parent: string, prop: string }[] = [ + { label: 'scrollbar.showScrollbar', parent: 'scrollbar', prop: 'showScrollbar' }, + { label: 'scrollbar.showArrows', parent: 'scrollbar', prop: 'showArrows' }, { label: 'vtExtensions.kittyKeyboard', parent: 'vtExtensions', prop: 'kittyKeyboard' }, { label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' }, { label: 'vtExtensions.win32InputMode', parent: 'vtExtensions', prop: 'win32InputMode' } diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index b7958570..902f5d35 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -616,11 +616,19 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { } this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e))); - if (this.options.overviewRuler.width) { + const showScrollbar = this.options.scrollbar?.showScrollbar ?? true; + if (showScrollbar && this.options.overviewRuler.width) { this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); } this.optionsService.onSpecificOptionChange('overviewRuler', value => { - if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) { + const shouldShow = (this.options.scrollbar?.showScrollbar ?? true) && !!value?.width; + if (!this._overviewRulerRenderer && shouldShow && this._viewportElement && this.screenElement) { + this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); + } + }); + this.optionsService.onSpecificOptionChange('scrollbar', value => { + const shouldShow = (value?.showScrollbar ?? true) && !!this.options.overviewRuler.width; + if (!this._overviewRulerRenderer && shouldShow && this._viewportElement && this.screenElement) { this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); } }); diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index f036f619..49e744cf 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -62,7 +62,8 @@ export class Viewport extends Disposable { this._register(this._optionsService.onMultipleOptionChange([ 'scrollSensitivity', 'fastScrollSensitivity', - 'overviewRuler' + 'overviewRuler', + 'scrollbar' ], () => this._scrollableElement.updateOptions(this._getChangeOptions()))); // Don't handle mouse wheel if wheel events are supported by the current mouse prototcol this._register(coreMouseService.onProtocolChange(type => { @@ -131,10 +132,17 @@ export class Viewport extends Disposable { } private _getChangeOptions(): IScrollableElementChangeOptions { + const showScrollbar = this._optionsService.rawOptions.scrollbar?.showScrollbar ?? true; + const showArrows = this._optionsService.rawOptions.scrollbar?.showArrows ?? false; + const verticalScrollbarSize = showScrollbar + ? (this._optionsService.rawOptions.overviewRuler?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH) + : 0; return { mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity, fastScrollSensitivity: this._optionsService.rawOptions.fastScrollSensitivity, - verticalScrollbarSize: this._optionsService.rawOptions.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH + vertical: showScrollbar ? ScrollbarVisibility.AUTO : ScrollbarVisibility.HIDDEN, + verticalScrollbarSize, + verticalHasArrows: showArrows }; } diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index ea36cf76..576cdf6f 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -38,7 +38,11 @@ export class OverviewRulerRenderer extends Disposable { private readonly _ctx: CanvasRenderingContext2D; private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore(); private get _width(): number { - return this._optionsService.options.overviewRuler?.width || 0; + const showScrollbar = this._optionsService.rawOptions.scrollbar?.showScrollbar ?? true; + if (!showScrollbar) { + return 0; + } + return this._optionsService.rawOptions.overviewRuler?.width ?? 0; } private _animationFrame: number | undefined; @@ -96,6 +100,7 @@ export class OverviewRulerRenderer extends Disposable { this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true))); this._register(this._optionsService.onSpecificOptionChange('overviewRuler', () => this._queueRefresh(true))); + this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true))); this._register(this._themeService.onChangeColors(() => this._queueRefresh())); this._queueRefresh(true); } diff --git a/src/browser/scrollable/abstractScrollbar.ts b/src/browser/scrollable/abstractScrollbar.ts index 4870f665..64c25d9d 100644 --- a/src/browser/scrollable/abstractScrollbar.ts +++ b/src/browser/scrollable/abstractScrollbar.ts @@ -82,10 +82,11 @@ export abstract class AbstractScrollbar extends Widget { /** * Creates the dom node for an arrow & adds it to the container */ - protected _createArrow(opts: IScrollbarArrowOptions): void { + protected _createArrow(opts: IScrollbarArrowOptions): ScrollbarArrow { const arrow = this._register(new ScrollbarArrow(opts)); this.domNode.domNode.appendChild(arrow.bgDomNode); this.domNode.domNode.appendChild(arrow.domNode); + return arrow; } /** diff --git a/src/browser/scrollable/scrollableElement.ts b/src/browser/scrollable/scrollableElement.ts index 7014c845..e30b36fc 100644 --- a/src/browser/scrollable/scrollableElement.ts +++ b/src/browser/scrollable/scrollableElement.ts @@ -314,6 +314,12 @@ export class SmoothScrollableElement extends Widget { if (typeof newOptions.vertical !== 'undefined') { this._options.vertical = newOptions.vertical; } + if (typeof newOptions.horizontalHasArrows !== 'undefined') { + this._options.horizontalHasArrows = newOptions.horizontalHasArrows; + } + if (typeof newOptions.verticalHasArrows !== 'undefined') { + this._options.verticalHasArrows = newOptions.verticalHasArrows; + } if (typeof newOptions.horizontalScrollbarSize !== 'undefined') { this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize; } diff --git a/src/browser/scrollable/scrollableElementOptions.ts b/src/browser/scrollable/scrollableElementOptions.ts index 8b241a60..037cd5c1 100644 --- a/src/browser/scrollable/scrollableElementOptions.ts +++ b/src/browser/scrollable/scrollableElementOptions.ts @@ -128,8 +128,10 @@ export interface IScrollableElementChangeOptions { scrollPredominantAxis?: boolean; horizontal?: ScrollbarVisibility; horizontalScrollbarSize?: number; + horizontalHasArrows?: boolean; vertical?: ScrollbarVisibility; verticalScrollbarSize?: number; + verticalHasArrows?: boolean; scrollByPage?: boolean; } diff --git a/src/browser/scrollable/scrollbarState.ts b/src/browser/scrollable/scrollbarState.ts index 9af66401..f9129f1b 100644 --- a/src/browser/scrollable/scrollbarState.ts +++ b/src/browser/scrollable/scrollbarState.ts @@ -35,7 +35,7 @@ export class ScrollbarState { * For the vertical scrollbar: the height of the scrollbar's arrows. * For the horizontal scrollbar: the width of the scrollbar's arrows. */ - private readonly _arrowSize: number; + private _arrowSize: number; // --- variables /** @@ -127,6 +127,14 @@ export class ScrollbarState { this._scrollbarSize = Math.round(scrollbarSize); } + public setArrowSize(arrowSize: number): void { + const iArrowSize = Math.round(arrowSize); + if (this._arrowSize !== iArrowSize) { + this._arrowSize = iArrowSize; + this._refreshComputedValues(); + } + } + public setOppositeScrollbarSize(oppositeScrollbarSize: number): void { this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize); } diff --git a/src/browser/scrollable/verticalScrollbar.ts b/src/browser/scrollable/verticalScrollbar.ts index 9efbe4d8..615f443a 100644 --- a/src/browser/scrollable/verticalScrollbar.ts +++ b/src/browser/scrollable/verticalScrollbar.ts @@ -7,17 +7,22 @@ import { AbstractScrollbar, ISimplifiedPointerEvent, IScrollbarHost } from './ab import { IScrollableElementResolvedOptions } from './scrollableElementOptions'; import { ScrollbarState } from './scrollbarState'; import { INewScrollPosition, Scrollable, ScrollbarVisibility, IScrollEvent } from './scrollable'; +import type { ScrollbarArrow } from './scrollbarArrow'; export class VerticalScrollbar extends AbstractScrollbar { + private _arrowUp: ScrollbarArrow | undefined; + private _arrowDown: ScrollbarArrow | undefined; + private _arrowScrollDelta: number = 0; constructor(scrollable: Scrollable, options: IScrollableElementResolvedOptions, host: IScrollbarHost) { const scrollDimensions = scrollable.getScrollDimensions(); const scrollPosition = scrollable.getCurrentScrollPosition(); + const hasArrows = options.verticalHasArrows; super({ lazyRender: options.lazyRender, host: host, scrollbarState: new ScrollbarState( - (options.verticalHasArrows ? options.verticalScrollbarSize : 0), + (hasArrows ? options.verticalScrollbarSize : 0), (options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize), 0, scrollDimensions.height, @@ -30,26 +35,7 @@ export class VerticalScrollbar extends AbstractScrollbar { scrollByPage: options.scrollByPage }); - if (options.verticalHasArrows) { - const arrowSize = options.verticalScrollbarSize; - const arrowDelta = 0; - this._createArrow({ - className: 'xterm-scra xterm-arrow-up', - top: arrowDelta, - left: arrowDelta, - bgWidth: options.verticalScrollbarSize, - bgHeight: arrowSize, - handleActivate: () => this._arrowScroll(-arrowSize) - }); - this._createArrow({ - className: 'xterm-scra xterm-arrow-down', - bottom: arrowDelta, - left: arrowDelta, - bgWidth: options.verticalScrollbarSize, - bgHeight: arrowSize, - handleActivate: () => this._arrowScroll(arrowSize) - }); - } + this._setArrows(hasArrows, options.verticalScrollbarSize); this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined); } @@ -98,7 +84,56 @@ export class VerticalScrollbar extends AbstractScrollbar { this._scrollable.setScrollPositionNow({ scrollTop: currentPosition.scrollTop + delta }); } + private _setArrows(showArrows: boolean, size: number): void { + this._arrowScrollDelta = size; + if (!this._arrowUp || !this._arrowDown) { + const arrowDelta = 0; + this._arrowUp = this._createArrow({ + className: 'xterm-scra xterm-arrow-up', + top: arrowDelta, + left: arrowDelta, + bgWidth: size, + bgHeight: size, + handleActivate: () => this._arrowScroll(-this._arrowScrollDelta) + }); + this._arrowDown = this._createArrow({ + className: 'xterm-scra xterm-arrow-down', + bottom: arrowDelta, + left: arrowDelta, + bgWidth: size, + bgHeight: size, + handleActivate: () => this._arrowScroll(this._arrowScrollDelta) + }); + } + + this._updateArrowSize(this._arrowUp, size); + this._updateArrowSize(this._arrowDown, size); + + if (!this._arrowUp || !this._arrowDown) { + return; + } + + const display = showArrows ? '' : 'none'; + this._arrowUp.bgDomNode.style.display = display; + this._arrowUp.domNode.style.display = display; + this._arrowDown.bgDomNode.style.display = display; + this._arrowDown.domNode.style.display = display; + } + + private _updateArrowSize(arrow: ScrollbarArrow | undefined, size: number): void { + if (!arrow) { + return; + } + arrow.bgDomNode.style.width = `${size}px`; + arrow.bgDomNode.style.height = `${size}px`; + arrow.domNode.style.width = `${size}px`; + arrow.domNode.style.height = `${size}px`; + } + public updateOptions(options: IScrollableElementResolvedOptions): void { + const arrowSize = options.verticalHasArrows ? options.verticalScrollbarSize : 0; + this._scrollbarState.setArrowSize(arrowSize); + this._setArrows(options.verticalHasArrows, options.verticalScrollbarSize); this.updateScrollbarSize(options.vertical === ScrollbarVisibility.HIDDEN ? 0 : options.verticalScrollbarSize); this._scrollbarState.setOppositeScrollbarSize(0); this._visibilityController.setVisibility(options.vertical); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 23c523d4..397825e9 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -32,7 +32,7 @@ export const DEFAULT_OPTIONS: Readonly> = { logLevel: 'info', logger: null, scrollback: 1000, - scrollbar: {}, + scrollbar: { showScrollbar: true }, scrollOnEraseInDisplay: false, scrollOnUserInput: true, scrollSensitivity: 1, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 6a240560..85819960 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -311,6 +311,7 @@ export interface ITerminalQuirks { } export interface IScrollbarOptions { + showScrollbar?: boolean; showArrows?: boolean; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 39b0d592..28f02b68 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -716,6 +716,7 @@ declare module '@xterm/xterm' { /** * When defined, renders decorations in the overview ruler to the right of * the terminal. This must be set in order to see the overview ruler. + * This is ignored when {@link IScrollbarOptions.showScrollbar} is false. * @param color The color of the decoration. * @param position The position of the decoration. */ @@ -738,6 +739,11 @@ declare module '@xterm/xterm' { * Options for configuring the scrollbar. */ export interface IScrollbarOptions { + /** + * Whether to show the scrollbar. When false, this supersedes + * {@link IOverviewRulerOptions.width}. Defaults to true. + */ + showScrollbar?: boolean; /** * Whether to show arrows at the top and bottom of the scrollbar. Defaults * to false.