Add scrollbar.showScrollbar

Fixes #5676
This commit is contained in:
Daniel Imms
2026-02-05 07:20:23 -08:00
parent 6f163beb97
commit c2db7d4c8a
14 changed files with 115 additions and 32 deletions
+3 -2
View File
@@ -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'));
+1 -1
View File
@@ -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
@@ -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' }
+10 -2
View File
@@ -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));
}
});
+10 -2
View File
@@ -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
};
}
@@ -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);
}
+2 -1
View File
@@ -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;
}
/**
@@ -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;
}
@@ -128,8 +128,10 @@ export interface IScrollableElementChangeOptions {
scrollPredominantAxis?: boolean;
horizontal?: ScrollbarVisibility;
horizontalScrollbarSize?: number;
horizontalHasArrows?: boolean;
vertical?: ScrollbarVisibility;
verticalScrollbarSize?: number;
verticalHasArrows?: boolean;
scrollByPage?: boolean;
}
+9 -1
View File
@@ -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);
}
+56 -21
View File
@@ -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);
+1 -1
View File
@@ -32,7 +32,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
logLevel: 'info',
logger: null,
scrollback: 1000,
scrollbar: {},
scrollbar: { showScrollbar: true },
scrollOnEraseInDisplay: false,
scrollOnUserInput: true,
scrollSensitivity: 1,
+1
View File
@@ -311,6 +311,7 @@ export interface ITerminalQuirks {
}
export interface IScrollbarOptions {
showScrollbar?: boolean;
showArrows?: boolean;
}
+6
View File
@@ -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.