Move all impl to scrollable/

This commit is contained in:
Daniel Imms
2026-02-01 06:25:12 -08:00
parent eea03cf0a9
commit f666135881
36 changed files with 5164 additions and 4 deletions
+1
View File
@@ -127,6 +127,7 @@ if (config.addon) {
} else if (config.isDemoClient) {
bundleConfig = {
...bundleConfig,
sourcemap: false,
entryPoints: [`demo/client/client.ts`],
outfile: 'demo/dist/client-bundle.js',
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
+4 -4
View File
@@ -9,11 +9,11 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
import { CoreMouseEventType } from 'common/Types';
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom';
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
import type { ScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
import { Emitter, EventUtils } from 'common/Event';
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable';
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch';
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'browser/scrollable/scrollable';
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'browser/scrollable/touch';
export class Viewport extends Disposable {
+299
View File
@@ -0,0 +1,299 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from './dom';
import { createFastDomNode, FastDomNode } from './fastDomNode';
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { StandardWheelEvent } from './mouseEvent';
import { ScrollbarArrow, ScrollbarArrowOptions } from './scrollbarArrow';
import { ScrollbarState } from './scrollbarState';
import { ScrollbarVisibilityController } from './scrollbarVisibilityController';
import { Widget } from './widget';
import * as platform from './platform';
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from './scrollable';
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
*/
const POINTER_DRAG_RESET_DISTANCE = 140;
export interface ISimplifiedPointerEvent {
buttons: number;
pageX: number;
pageY: number;
}
export interface ScrollbarHost {
onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
onDragStart(): void;
onDragEnd(): void;
}
export interface AbstractScrollbarOptions {
lazyRender: boolean;
host: ScrollbarHost;
scrollbarState: ScrollbarState;
visibility: ScrollbarVisibility;
extraScrollbarClassName: string;
scrollable: Scrollable;
scrollByPage: boolean;
}
export abstract class AbstractScrollbar extends Widget {
protected _host: ScrollbarHost;
protected _scrollable: Scrollable;
protected _scrollByPage: boolean;
private _lazyRender: boolean;
protected _scrollbarState: ScrollbarState;
protected _visibilityController: ScrollbarVisibilityController;
private _pointerMoveMonitor: GlobalPointerMoveMonitor;
public domNode: FastDomNode<HTMLElement>;
public slider!: FastDomNode<HTMLElement>;
protected _shouldRender: boolean;
constructor(opts: AbstractScrollbarOptions) {
super();
this._lazyRender = opts.lazyRender;
this._host = opts.host;
this._scrollable = opts.scrollable;
this._scrollByPage = opts.scrollByPage;
this._scrollbarState = opts.scrollbarState;
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._shouldRender = true;
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this._visibilityController.setDomNode(this.domNode);
this.domNode.setPosition('absolute');
this._register(dom.addDisposableListener(this.domNode.domNode, dom.EventType.POINTER_DOWN, (e: PointerEvent) => this._domNodePointerDown(e)));
}
// ----------------- creation
/**
* Creates the dom node for an arrow & adds it to the container
*/
protected _createArrow(opts: ScrollbarArrowOptions): void {
const arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
}
/**
* Creates the slider dom node, adds it to the container & hooks up the events
*/
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
this.slider = createFastDomNode(document.createElement('div'));
this.slider.setClassName('slider');
this.slider.setPosition('absolute');
this.slider.setTop(top);
this.slider.setLeft(left);
if (typeof width === 'number') {
this.slider.setWidth(width);
}
if (typeof height === 'number') {
this.slider.setHeight(height);
}
this.slider.setLayerHinting(true);
this.slider.setContain('strict');
this.domNode.domNode.appendChild(this.slider.domNode);
this._register(dom.addDisposableListener(
this.slider.domNode,
dom.EventType.POINTER_DOWN,
(e: PointerEvent) => {
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
}
));
this.onclick(this.slider.domNode, e => {
if (e.leftButton) {
e.stopPropagation();
}
});
}
// ----------------- Update state
protected _onElementSize(visibleSize: number): boolean {
if (this._scrollbarState.setVisibleSize(visibleSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
protected _onElementScrollSize(elementScrollSize: number): boolean {
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
protected _onElementScrollPosition(elementScrollPosition: number): boolean {
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
// ----------------- rendering
public beginReveal(): void {
this._visibilityController.setShouldBeVisible(true);
}
public beginHide(): void {
this._visibilityController.setShouldBeVisible(false);
}
public render(): void {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
}
// ----------------- DOM events
private _domNodePointerDown(e: PointerEvent): void {
if (e.target !== this.domNode.domNode) {
return;
}
this._onPointerDown(e);
}
public delegatePointerDown(e: PointerEvent): void {
const domTop = this.domNode.domNode.getClientRects()[0].top;
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
const pointerPos = this._sliderPointerPosition(e);
if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
} else {
this._onPointerDown(e);
}
}
private _onPointerDown(e: PointerEvent): void {
let offsetX: number;
let offsetY: number;
if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
offsetX = e.offsetX;
offsetY = e.offsetY;
} else {
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
offsetX = e.pageX - domNodePosition.left;
offsetY = e.pageY - domNodePosition.top;
}
const offset = this._pointerDownRelativePosition(offsetX, offsetY);
this._setDesiredScrollPositionNow(
this._scrollByPage
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset)
);
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
}
private _sliderPointerDown(e: PointerEvent): void {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const initialPointerPosition = this._sliderPointerPosition(e);
const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
const initialScrollbarState = this._scrollbarState.clone();
this.slider.toggleClassName('active', true);
this._pointerMoveMonitor.startMonitoring(
e.target,
e.pointerId,
e.buttons,
(pointerMoveData: PointerEvent) => {
const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
return;
}
const pointerPosition = this._sliderPointerPosition(pointerMoveData);
const pointerDelta = pointerPosition - initialPointerPosition;
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
},
() => {
this.slider.toggleClassName('active', false);
this._host.onDragEnd();
}
);
this._host.onDragStart();
}
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
const desiredScrollPosition: INewScrollPosition = {};
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
public updateScrollbarSize(scrollbarSize: number): void {
this._updateScrollbarSize(scrollbarSize);
this._scrollbarState.setScrollbarSize(scrollbarSize);
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
public isNeeded(): boolean {
return this._scrollbarState.isNeeded();
}
// ----------------- Overwrite these
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
protected abstract _pointerDownRelativePosition(offsetX: number, offsetY: number): number;
protected abstract _sliderPointerPosition(e: ISimplifiedPointerEvent): number;
protected abstract _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number;
protected abstract _updateScrollbarSize(size: number): void;
public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
}
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function tail<T>(array: ArrayLike<T>, n: number = 0): T | undefined {
return array[array.length - (1 + n)];
}
export function tail2<T>(arr: T[]): [T[], T] {
if (arr.length === 0) {
throw new Error('Invalid tail call');
}
return [arr.slice(0, arr.length - 1), arr[arr.length - 1]];
}
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from './lifecycle';
export class TimeoutTimer implements IDisposable {
private _token: any = -1;
private _isDisposed = false;
dispose(): void {
this.cancel();
this._isDisposed = true;
}
cancel(): void {
if (this._token !== -1) {
clearTimeout(this._token);
this._token = -1;
}
}
cancelAndSet(runner: () => void, timeout: number): void {
if (this._isDisposed) {
throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');
}
this.cancel();
this._token = setTimeout(() => {
this._token = -1;
runner();
}, timeout);
}
setIfNotSet(runner: () => void, timeout: number): void {
if (this._isDisposed) {
throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');
}
if (this._token !== -1) {
return;
}
this._token = setTimeout(() => {
this._token = -1;
runner();
}, timeout);
}
}
export class IntervalTimer implements IDisposable {
private _disposable: IDisposable | undefined;
private _isDisposed = false;
cancel(): void {
this._disposable?.dispose();
this._disposable = undefined;
}
cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {
if (this._isDisposed) {
throw new Error('Calling cancelAndSet on a disposed IntervalTimer');
}
this.cancel();
const handle = context.setInterval(() => {
runner();
}, interval);
this._disposable = {
dispose: () => {
context.clearInterval(handle as any);
this._disposable = undefined;
}
};
}
dispose(): void {
this.cancel();
this._isDisposed = true;
}
}
+141
View File
@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CodeWindow, mainWindow } from './window';
import { Emitter } from './event';
class WindowManager {
static readonly INSTANCE = new WindowManager();
// --- Zoom Level
private readonly mapWindowIdToZoomLevel = new Map<number, number>();
private readonly _onDidChangeZoomLevel = new Emitter<number>();
readonly onDidChangeZoomLevel = this._onDidChangeZoomLevel.event;
getZoomLevel(targetWindow: Window): number {
return this.mapWindowIdToZoomLevel.get(this.getWindowId(targetWindow)) ?? 0;
}
setZoomLevel(zoomLevel: number, targetWindow: Window): void {
if (this.getZoomLevel(targetWindow) === zoomLevel) {
return;
}
const targetWindowId = this.getWindowId(targetWindow);
this.mapWindowIdToZoomLevel.set(targetWindowId, zoomLevel);
this._onDidChangeZoomLevel.fire(targetWindowId);
}
// --- Zoom Factor
private readonly mapWindowIdToZoomFactor = new Map<number, number>();
getZoomFactor(targetWindow: Window): number {
return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
}
setZoomFactor(zoomFactor: number, targetWindow: Window): void {
this.mapWindowIdToZoomFactor.set(this.getWindowId(targetWindow), zoomFactor);
}
// --- Fullscreen
private readonly _onDidChangeFullscreen = new Emitter<number>();
readonly onDidChangeFullscreen = this._onDidChangeFullscreen.event;
private readonly mapWindowIdToFullScreen = new Map<number, boolean>();
setFullscreen(fullscreen: boolean, targetWindow: Window): void {
if (this.isFullscreen(targetWindow) === fullscreen) {
return;
}
const windowId = this.getWindowId(targetWindow);
this.mapWindowIdToFullScreen.set(windowId, fullscreen);
this._onDidChangeFullscreen.fire(windowId);
}
isFullscreen(targetWindow: Window): boolean {
return !!this.mapWindowIdToFullScreen.get(this.getWindowId(targetWindow));
}
private getWindowId(targetWindow: Window): number {
return (targetWindow as CodeWindow).vscodeWindowId;
}
}
export function addMatchMediaChangeListener(targetWindow: Window, query: string | MediaQueryList, callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
if (typeof query === 'string') {
query = targetWindow.matchMedia(query);
}
query.addEventListener('change', callback);
}
/** A zoom index, e.g. 1, 2, 3 */
export function setZoomLevel(zoomLevel: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomLevel(zoomLevel, targetWindow);
}
export function getZoomLevel(targetWindow: Window): number {
return WindowManager.INSTANCE.getZoomLevel(targetWindow);
}
export const onDidChangeZoomLevel = WindowManager.INSTANCE.onDidChangeZoomLevel;
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
export function getZoomFactor(targetWindow: Window): number {
return WindowManager.INSTANCE.getZoomFactor(targetWindow);
}
export function setZoomFactor(zoomFactor: number, targetWindow: Window): void {
WindowManager.INSTANCE.setZoomFactor(zoomFactor, targetWindow);
}
export function setFullscreen(fullscreen: boolean, targetWindow: Window): void {
WindowManager.INSTANCE.setFullscreen(fullscreen, targetWindow);
}
export function isFullscreen(targetWindow: Window): boolean {
return WindowManager.INSTANCE.isFullscreen(targetWindow);
}
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
const userAgent = typeof navigator === 'object' ? navigator.userAgent : '';
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isAndroid = (userAgent.indexOf('Android') >= 0);
let standalone = false;
if (typeof mainWindow.matchMedia === 'function') {
const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
standalone = standaloneMatchMedia.matches;
addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
// entering fullscreen would change standaloneMatchMedia.matches to false
// if standalone is true (running as PWA) and entering fullscreen, skip this change
if (standalone && fullScreenMatchMedia.matches) {
return;
}
// otherwise update standalone (browser to PWA or PWA to browser)
standalone = matches;
});
}
export function isStandalone(): boolean {
return standalone;
}
// Visible means that the feature is enabled, not necessarily being rendered
// e.g. visible is true even in fullscreen mode where the controls are hidden
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/visible
export function isWCOEnabled(): boolean {
return (navigator as any)?.windowControlsOverlay?.visible;
}
// Returns the bounding rect of the titlebar area if it is supported and defined
// See docs at https://developer.mozilla.org/en-US/docs/Web/API/WindowControlsOverlay/getTitlebarAreaRect
export function getWCOBoundingRect(): DOMRect | undefined {
return (navigator as any)?.windowControlsOverlay?.getTitlebarAreaRect();
}
+140
View File
@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* An interface for a JavaScript object that
* acts a dictionary. The keys are strings.
*/
export type IStringDictionary<V> = Record<string, V>;
/**
* An interface for a JavaScript object that
* acts a dictionary. The keys are numbers.
*/
export type INumberDictionary<V> = Record<number, V>;
/**
* Groups the collection into a dictionary based on the provided
* group function.
*/
export function groupBy<K extends string | number | symbol, V>(data: V[], groupFn: (element: V) => K): Record<K, V[]> {
const result: Record<K, V[]> = Object.create(null);
for (const element of data) {
const key = groupFn(element);
let target = result[key];
if (!target) {
target = result[key] = [];
}
target.push(element);
}
return result;
}
export function diffSets<T>(before: Set<T>, after: Set<T>): { removed: T[]; added: T[] } {
const removed: T[] = [];
const added: T[] = [];
for (const element of before) {
if (!after.has(element)) {
removed.push(element);
}
}
for (const element of after) {
if (!before.has(element)) {
added.push(element);
}
}
return { removed, added };
}
export function diffMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[]; added: V[] } {
const removed: V[] = [];
const added: V[] = [];
for (const [index, value] of before) {
if (!after.has(index)) {
removed.push(value);
}
}
for (const [index, value] of after) {
if (!before.has(index)) {
added.push(value);
}
}
return { removed, added };
}
/**
* Computes the intersection of two sets.
*
* @param setA - The first set.
* @param setB - The second iterable.
* @returns A new set containing the elements that are in both `setA` and `setB`.
*/
export function intersection<T>(setA: Set<T>, setB: Iterable<T>): Set<T> {
const result = new Set<T>();
for (const elem of setB) {
if (setA.has(elem)) {
result.add(elem);
}
}
return result;
}
export class SetWithKey<T> implements Set<T> {
private _map = new Map<any, T>();
constructor(values: T[], private toKey: (t: T) => any) {
for (const value of values) {
this.add(value);
}
}
get size(): number {
return this._map.size;
}
add(value: T): this {
const key = this.toKey(value);
this._map.set(key, value);
return this;
}
delete(value: T): boolean {
return this._map.delete(this.toKey(value));
}
has(value: T): boolean {
return this._map.has(this.toKey(value));
}
*entries(): IterableIterator<[T, T]> {
for (const entry of this._map.values()) {
yield [entry, entry];
}
}
keys(): IterableIterator<T> {
return this.values();
}
*values(): IterableIterator<T> {
for (const entry of this._map.values()) {
yield entry;
}
}
clear(): void {
this._map.clear();
}
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this));
}
[Symbol.iterator](): IterableIterator<T> {
return this.values();
}
[Symbol.toStringTag]: string = 'SetWithKey';
}
+130
View File
@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function createDecorator(mapFn: (fn: Function, key: string) => Function): Function {
return (target: any, key: string, descriptor: any) => {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn) {
throw new Error('not supported');
}
descriptor[fnKey!] = mapFn(fn, key);
};
}
export function memoize(_target: any, key: string, descriptor: any) {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
if (fn!.length !== 0) {
console.warn('Memoize should only be used in functions with zero parameters');
}
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn) {
throw new Error('not supported');
}
const memoizeKey = `$memoize$${key}`;
descriptor[fnKey!] = function (...args: any[]) {
if (!this.hasOwnProperty(memoizeKey)) {
Object.defineProperty(this, memoizeKey, {
configurable: false,
enumerable: false,
writable: false,
value: fn.apply(this, args)
});
}
return this[memoizeKey];
};
}
export interface IDebounceReducer<T> {
(previousValue: T, ...args: any[]): T;
}
export function debounce<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
return createDecorator((fn, key) => {
const timerKey = `$debounce$${key}`;
const resultKey = `$debounce$result$${key}`;
return function (this: any, ...args: any[]) {
if (!this[resultKey]) {
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
}
clearTimeout(this[timerKey]);
if (reducer) {
this[resultKey] = reducer(this[resultKey], ...args);
args = [this[resultKey]];
}
this[timerKey] = setTimeout(() => {
fn.apply(this, args);
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
}, delay);
};
});
}
export function throttle<T>(delay: number, reducer?: IDebounceReducer<T>, initialValueProvider?: () => T): Function {
return createDecorator((fn, key) => {
const timerKey = `$throttle$timer$${key}`;
const resultKey = `$throttle$result$${key}`;
const lastRunKey = `$throttle$lastRun$${key}`;
const pendingKey = `$throttle$pending$${key}`;
return function (this: any, ...args: any[]) {
if (!this[resultKey]) {
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
}
if (this[lastRunKey] === null || this[lastRunKey] === undefined) {
this[lastRunKey] = -Number.MAX_VALUE;
}
if (reducer) {
this[resultKey] = reducer(this[resultKey], ...args);
}
if (this[pendingKey]) {
return;
}
const nextTime = this[lastRunKey] + delay;
if (nextTime <= Date.now()) {
this[lastRunKey] = Date.now();
fn.apply(this, [this[resultKey]]);
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
} else {
this[pendingKey] = true;
this[timerKey] = setTimeout(() => {
this[pendingKey] = false;
this[lastRunKey] = Date.now();
fn.apply(this, [this[resultKey]]);
this[resultKey] = initialValueProvider ? initialValueProvider() : undefined;
}, nextTime - Date.now());
}
};
});
}
+198
View File
@@ -0,0 +1,198 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IntervalTimer } from './async';
import { Emitter, Event } from './event';
import { DisposableStore, IDisposable } from './lifecycle';
export interface IRegisteredWindow {
readonly window: Window;
readonly disposables: DisposableStore;
}
const _onDidRegisterWindow = new Emitter<IRegisteredWindow>();
export const onDidRegisterWindow: Event<IRegisteredWindow> = _onDidRegisterWindow.event;
export function registerWindow(window: Window): IDisposable {
const disposables = new DisposableStore();
_onDidRegisterWindow.fire({ window, disposables });
return disposables;
}
export function getWindow(e: Node | UIEvent | undefined | null): Window {
const candidateNode = e as Node | undefined | null;
if (candidateNode?.ownerDocument?.defaultView) {
return candidateNode.ownerDocument.defaultView;
}
const candidateEvent = e as UIEvent | undefined | null;
if (candidateEvent?.view) {
return candidateEvent.view;
}
return window;
}
class DomListener implements IDisposable {
private _handler: ((e: any) => void) | null;
private _node: EventTarget | null;
private readonly _type: string;
private readonly _options: boolean | AddEventListenerOptions | undefined;
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
this._node = node;
this._type = type;
this._handler = handler;
this._options = options;
node.addEventListener(type, handler, options);
}
public dispose(): void {
if (!this._node || !this._handler) {
return;
}
this._node.removeEventListener(this._type, this._handler, this._options);
this._node = null;
this._handler = null;
}
}
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
return new DomListener(node, type, handler, useCaptureOrOptions);
}
export function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
return addDisposableListener(node, type, handler, useCapture);
}
export const EventType = {
CLICK: 'click',
MOUSE_DOWN: 'mousedown',
MOUSE_OVER: 'mouseover',
MOUSE_LEAVE: 'mouseleave',
KEY_DOWN: 'keydown',
KEY_UP: 'keyup',
INPUT: 'input',
BLUR: 'blur',
FOCUS: 'focus',
CHANGE: 'change',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
POINTER_UP: 'pointerup',
MOUSE_WHEEL: 'wheel',
WHEEL: 'wheel'
} as const;
export interface IDomNodePagePosition {
left: number;
top: number;
width: number;
height: number;
}
export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePosition {
const bb = domNode.getBoundingClientRect();
const win = getWindow(domNode);
return {
left: bb.left + win.scrollX,
top: bb.top + win.scrollY,
width: bb.width,
height: bb.height
};
}
class AnimationFrameQueueItem implements IDisposable {
private _canceled = false;
constructor(private readonly _runner: () => void, public priority: number) {
}
public dispose(): void {
this._canceled = true;
}
public execute(): void {
if (this._canceled) {
return;
}
try {
this._runner();
} catch (e) {
console.error(e);
}
}
public static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
return b.priority - a.priority;
}
}
interface IWindowAnimationFrameState {
next: AnimationFrameQueueItem[];
current: AnimationFrameQueueItem[];
animFrameRequested: boolean;
inAnimationFrameRunner: boolean;
}
const animationFrameState = new Map<Window, IWindowAnimationFrameState>();
function getAnimationFrameState(targetWindow: Window): IWindowAnimationFrameState {
let state = animationFrameState.get(targetWindow);
if (!state) {
state = {
next: [],
current: [],
animFrameRequested: false,
inAnimationFrameRunner: false
};
animationFrameState.set(targetWindow, state);
}
return state;
}
function animationFrameRunner(targetWindow: Window): void {
const state = getAnimationFrameState(targetWindow);
state.animFrameRequested = false;
state.current = state.next;
state.next = [];
state.inAnimationFrameRunner = true;
while (state.current.length > 0) {
state.current.sort(AnimationFrameQueueItem.sort);
const top = state.current.shift()!;
top.execute();
}
state.inAnimationFrameRunner = false;
}
export function scheduleAtNextAnimationFrame(targetWindow: Window, runner: () => void, priority: number = 0): IDisposable {
const state = getAnimationFrameState(targetWindow);
const item = new AnimationFrameQueueItem(runner, priority);
state.next.push(item);
if (!state.animFrameRequested) {
state.animFrameRequested = true;
targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindow));
}
return item;
}
export class WindowIntervalTimer extends IntervalTimer {
private readonly _defaultTarget?: Window;
constructor(node?: Node) {
super();
this._defaultTarget = node ? getWindow(node) : undefined;
}
public cancelAndSet(runner: () => void, interval: number, targetWindow?: Window): void {
super.cancelAndSet(runner, interval, targetWindow ?? this._defaultTarget ?? window);
}
}
+85
View File
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*
* Minimal event utilities for scrollable components.
*/
import { Disposable, DisposableStore, IDisposable, toDisposable } from './lifecycle';
export interface Event<T> {
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;
}
export class Emitter<T> {
private _listeners: { fn: (e: T) => any; thisArgs: any }[] = [];
private _disposed = false;
private _event: Event<T> | undefined;
public get event(): Event<T> {
if (this._event) {
return this._event;
}
this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {
if (this._disposed) {
return Disposable.None;
}
const entry = { fn: listener, thisArgs };
this._listeners.push(entry);
const result = toDisposable(() => {
const idx = this._listeners.indexOf(entry);
if (idx !== -1) {
this._listeners.splice(idx, 1);
}
});
if (disposables) {
if (Array.isArray(disposables)) {
disposables.push(result);
} else {
disposables.add(result);
}
}
return result;
};
return this._event;
}
public fire(event: T): void {
if (this._disposed || this._listeners.length === 0) {
return;
}
if (this._listeners.length === 1) {
const { fn, thisArgs } = this._listeners[0];
fn.call(thisArgs, event);
return;
}
const listeners = this._listeners.slice();
for (const { fn, thisArgs } of listeners) {
fn.call(thisArgs, event);
}
}
public dispose(): void {
if (this._disposed) {
return;
}
this._disposed = true;
this._listeners.length = 0;
}
}
export namespace Event {
export const None: Event<any> = () => Disposable.None;
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T) => void, initial: T): IDisposable;
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => void): IDisposable;
export function runAndSubscribe<T>(event: Event<T>, handler: (e: T | undefined) => void, initial?: T): IDisposable {
handler(initial);
return event(e => handler(e));
}
}
+320
View File
@@ -0,0 +1,320 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class FastDomNode<T extends HTMLElement> {
private _maxWidth: string = '';
private _width: string = '';
private _height: string = '';
private _top: string = '';
private _left: string = '';
private _bottom: string = '';
private _right: string = '';
private _paddingTop: string = '';
private _paddingLeft: string = '';
private _paddingBottom: string = '';
private _paddingRight: string = '';
private _fontFamily: string = '';
private _fontWeight: string = '';
private _fontSize: string = '';
private _fontStyle: string = '';
private _fontFeatureSettings: string = '';
private _fontVariationSettings: string = '';
private _textDecoration: string = '';
private _lineHeight: string = '';
private _letterSpacing: string = '';
private _className: string = '';
private _display: string = '';
private _position: string = '';
private _visibility: string = '';
private _color: string = '';
private _backgroundColor: string = '';
private _layerHint: boolean = false;
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint' = 'none';
private _boxShadow: string = '';
constructor(
public readonly domNode: T
) { }
public setMaxWidth(_maxWidth: number | string): void {
const maxWidth = numberAsPixels(_maxWidth);
if (this._maxWidth === maxWidth) {
return;
}
this._maxWidth = maxWidth;
this.domNode.style.maxWidth = this._maxWidth;
}
public setWidth(_width: number | string): void {
const width = numberAsPixels(_width);
if (this._width === width) {
return;
}
this._width = width;
this.domNode.style.width = this._width;
}
public setHeight(_height: number | string): void {
const height = numberAsPixels(_height);
if (this._height === height) {
return;
}
this._height = height;
this.domNode.style.height = this._height;
}
public setTop(_top: number | string): void {
const top = numberAsPixels(_top);
if (this._top === top) {
return;
}
this._top = top;
this.domNode.style.top = this._top;
}
public setLeft(_left: number | string): void {
const left = numberAsPixels(_left);
if (this._left === left) {
return;
}
this._left = left;
this.domNode.style.left = this._left;
}
public setBottom(_bottom: number | string): void {
const bottom = numberAsPixels(_bottom);
if (this._bottom === bottom) {
return;
}
this._bottom = bottom;
this.domNode.style.bottom = this._bottom;
}
public setRight(_right: number | string): void {
const right = numberAsPixels(_right);
if (this._right === right) {
return;
}
this._right = right;
this.domNode.style.right = this._right;
}
public setPaddingTop(_paddingTop: number | string): void {
const paddingTop = numberAsPixels(_paddingTop);
if (this._paddingTop === paddingTop) {
return;
}
this._paddingTop = paddingTop;
this.domNode.style.paddingTop = this._paddingTop;
}
public setPaddingLeft(_paddingLeft: number | string): void {
const paddingLeft = numberAsPixels(_paddingLeft);
if (this._paddingLeft === paddingLeft) {
return;
}
this._paddingLeft = paddingLeft;
this.domNode.style.paddingLeft = this._paddingLeft;
}
public setPaddingBottom(_paddingBottom: number | string): void {
const paddingBottom = numberAsPixels(_paddingBottom);
if (this._paddingBottom === paddingBottom) {
return;
}
this._paddingBottom = paddingBottom;
this.domNode.style.paddingBottom = this._paddingBottom;
}
public setPaddingRight(_paddingRight: number | string): void {
const paddingRight = numberAsPixels(_paddingRight);
if (this._paddingRight === paddingRight) {
return;
}
this._paddingRight = paddingRight;
this.domNode.style.paddingRight = this._paddingRight;
}
public setFontFamily(fontFamily: string): void {
if (this._fontFamily === fontFamily) {
return;
}
this._fontFamily = fontFamily;
this.domNode.style.fontFamily = this._fontFamily;
}
public setFontWeight(fontWeight: string): void {
if (this._fontWeight === fontWeight) {
return;
}
this._fontWeight = fontWeight;
this.domNode.style.fontWeight = this._fontWeight;
}
public setFontSize(_fontSize: number | string): void {
const fontSize = numberAsPixels(_fontSize);
if (this._fontSize === fontSize) {
return;
}
this._fontSize = fontSize;
this.domNode.style.fontSize = this._fontSize;
}
public setFontStyle(fontStyle: string): void {
if (this._fontStyle === fontStyle) {
return;
}
this._fontStyle = fontStyle;
this.domNode.style.fontStyle = this._fontStyle;
}
public setFontFeatureSettings(fontFeatureSettings: string): void {
if (this._fontFeatureSettings === fontFeatureSettings) {
return;
}
this._fontFeatureSettings = fontFeatureSettings;
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
}
public setFontVariationSettings(fontVariationSettings: string): void {
if (this._fontVariationSettings === fontVariationSettings) {
return;
}
this._fontVariationSettings = fontVariationSettings;
this.domNode.style.fontVariationSettings = this._fontVariationSettings;
}
public setTextDecoration(textDecoration: string): void {
if (this._textDecoration === textDecoration) {
return;
}
this._textDecoration = textDecoration;
this.domNode.style.textDecoration = this._textDecoration;
}
public setLineHeight(_lineHeight: number | string): void {
const lineHeight = numberAsPixels(_lineHeight);
if (this._lineHeight === lineHeight) {
return;
}
this._lineHeight = lineHeight;
this.domNode.style.lineHeight = this._lineHeight;
}
public setLetterSpacing(_letterSpacing: number | string): void {
const letterSpacing = numberAsPixels(_letterSpacing);
if (this._letterSpacing === letterSpacing) {
return;
}
this._letterSpacing = letterSpacing;
this.domNode.style.letterSpacing = this._letterSpacing;
}
public setClassName(className: string): void {
if (this._className === className) {
return;
}
this._className = className;
this.domNode.className = this._className;
}
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
this.domNode.classList.toggle(className, shouldHaveIt);
this._className = this.domNode.className;
}
public setDisplay(display: string): void {
if (this._display === display) {
return;
}
this._display = display;
this.domNode.style.display = this._display;
}
public setPosition(position: string): void {
if (this._position === position) {
return;
}
this._position = position;
this.domNode.style.position = this._position;
}
public setVisibility(visibility: string): void {
if (this._visibility === visibility) {
return;
}
this._visibility = visibility;
this.domNode.style.visibility = this._visibility;
}
public setColor(color: string): void {
if (this._color === color) {
return;
}
this._color = color;
this.domNode.style.color = this._color;
}
public setBackgroundColor(backgroundColor: string): void {
if (this._backgroundColor === backgroundColor) {
return;
}
this._backgroundColor = backgroundColor;
this.domNode.style.backgroundColor = this._backgroundColor;
}
public setLayerHinting(layerHint: boolean): void {
if (this._layerHint === layerHint) {
return;
}
this._layerHint = layerHint;
if (layerHint) {
this.domNode.style.transform = 'translate3d(0px, 0px, 0px)';
} else {
this.domNode.style.transform = '';
}
}
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
if (this._contain === contain) {
return;
}
this._contain = contain;
this.domNode.style.contain = this._contain;
}
public setBoxShadow(boxShadow: string): void {
if (this._boxShadow === boxShadow) {
return;
}
this._boxShadow = boxShadow;
this.domNode.style.boxShadow = this._boxShadow;
}
public setAttribute(name: string, value: string): void {
this.domNode.setAttribute(name, value);
}
public removeAttribute(name: string): void {
this.domNode.removeAttribute(name);
}
public appendChild(child: FastDomNode<T>): void {
this.domNode.appendChild(child.domNode);
}
public removeChild(child: FastDomNode<T>): void {
this.domNode.removeChild(child.domNode);
}
}
export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
return new FastDomNode(domNode);
}
function numberAsPixels(value: number | string): string {
return (typeof value === 'number' ? `${value}px` : value);
}
+32
View File
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Given a function, returns a function that is only calling that function once.
*/
export function createSingleCallFunction<T extends Function>(this: unknown, fn: T, fnDidRunCallback?: () => void): T {
const _this = this;
let didCall = false;
let result: unknown;
return function () {
if (didCall) {
return result;
}
didCall = true;
if (fnDidRunCallback) {
try {
result = fn.apply(_this, arguments);
} finally {
fnDidRunCallback();
}
} else {
result = fn.apply(_this, arguments);
}
return result;
} as unknown as T;
}
@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from './dom';
import { DisposableStore, IDisposable, toDisposable } from './lifecycle';
export interface IPointerMoveCallback {
(event: PointerEvent): void;
}
export interface IOnStopCallback {
(browserEvent?: PointerEvent | KeyboardEvent): void;
}
export class GlobalPointerMoveMonitor implements IDisposable {
private readonly _hooks = new DisposableStore();
private _pointerMoveCallback: IPointerMoveCallback | null = null;
private _onStopCallback: IOnStopCallback | null = null;
public dispose(): void {
this.stopMonitoring(false);
this._hooks.dispose();
}
public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void {
if (!this.isMonitoring()) {
return;
}
this._hooks.clear();
this._pointerMoveCallback = null;
const onStopCallback = this._onStopCallback;
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback(browserEvent);
}
}
public isMonitoring(): boolean {
return !!this._pointerMoveCallback;
}
public startMonitoring(
initialElement: Element,
pointerId: number,
initialButtons: number,
pointerMoveCallback: IPointerMoveCallback,
onStopCallback: IOnStopCallback
): void {
if (this.isMonitoring()) {
this.stopMonitoring(false);
}
this._pointerMoveCallback = pointerMoveCallback;
this._onStopCallback = onStopCallback;
let eventSource: Element | Window = initialElement;
try {
initialElement.setPointerCapture(pointerId);
this._hooks.add(toDisposable(() => {
try {
initialElement.releasePointerCapture(pointerId);
} catch (err) {
// ignore
}
}));
} catch (err) {
eventSource = dom.getWindow(initialElement);
}
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.EventType.POINTER_MOVE,
(e) => {
if (e.buttons !== initialButtons) {
this.stopMonitoring(true);
return;
}
e.preventDefault();
this._pointerMoveCallback!(e);
}
));
this._hooks.add(dom.addDisposableListener(
eventSource,
dom.EventType.POINTER_UP,
(e: PointerEvent) => this.stopMonitoring(true)
));
}
}
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from './abstractScrollbar';
import { ScrollableElementResolvedOptions } from './scrollableElementOptions';
import { ScrollbarState } from './scrollbarState';
import { INewScrollPosition, Scrollable, ScrollbarVisibility, ScrollEvent } from './scrollable';
export class HorizontalScrollbar extends AbstractScrollbar {
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(options.horizontalHasArrows ? options.arrowSize : 0),
(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize),
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
scrollDimensions.width,
scrollDimensions.scrollWidth,
scrollPosition.scrollLeft
),
visibility: options.horizontal,
extraScrollbarClassName: 'horizontal',
scrollable: scrollable,
scrollByPage: options.scrollByPage
});
if (options.horizontalHasArrows) {
throw new Error('horizontalHasArrows is not supported in xterm.js');
}
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
this.slider.setWidth(sliderSize);
this.slider.setLeft(sliderPosition);
}
protected _renderDomNode(largeSize: number, smallSize: number): void {
this.domNode.setWidth(largeSize);
this.domNode.setHeight(smallSize);
this.domNode.setLeft(0);
this.domNode.setBottom(0);
}
public onDidScroll(e: ScrollEvent): boolean {
this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
return this._shouldRender;
}
protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
return offsetX;
}
protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageX;
}
protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageY;
}
protected _updateScrollbarSize(size: number): void {
this.slider.setHeight(size);
}
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
target.scrollLeft = scrollPosition;
}
public updateOptions(options: ScrollableElementResolvedOptions): void {
this.updateScrollbarSize(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize);
this._visibilityController.setVisibility(options.horizontal);
this._scrollByPage = options.scrollByPage;
}
}
+135
View File
@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a window in a possible chain of iframes
*/
interface IWindowChainElement {
/**
* The window object for it
*/
readonly window: WeakRef<Window>;
/**
* The iframe element inside the window.parent corresponding to window
*/
readonly iframeElement: Element | null;
}
const sameOriginWindowChainCache = new WeakMap<Window, IWindowChainElement[] | null>();
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
// Cannot really tell if we have access to the parent window unless we try to access something in it
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
} catch (e) {
return null;
}
return w.parent;
}
export class IframeUtils {
/**
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
*/
private static getSameOriginWindowChain(targetWindow: Window): IWindowChainElement[] {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w: Window | null = targetWindow;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement || null
});
} else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
/**
* Returns the position of `childWindow` relative to `ancestorWindow`
*/
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0, left = 0;
const windowChain = this.getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
/**
* Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32
*/
export async function parentOriginHash(parentOrigin: string, salt: string): Promise<string> {
// This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`
if (!crypto.subtle) {
throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`);
}
const strData = JSON.stringify({ parentOrigin, salt });
const encoder = new TextEncoder();
const arrData = encoder.encode(strData);
const hash = await crypto.subtle.digest('sha-256', arrData);
return sha256AsBase32(hash);
}
function sha256AsBase32(bytes: ArrayBuffer): string {
const array = Array.from(new Uint8Array(bytes));
const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join('');
// sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32
return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0');
}
+159
View File
@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export namespace Iterable {
export function is<T = any>(thing: any): thing is Iterable<T> {
return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function';
}
const _empty: Iterable<any> = Object.freeze([]);
export function empty<T = any>(): Iterable<T> {
return _empty;
}
export function* single<T>(element: T): Iterable<T> {
yield element;
}
export function wrap<T>(iterableOrElement: Iterable<T> | T): Iterable<T> {
if (is(iterableOrElement)) {
return iterableOrElement;
} else {
return single(iterableOrElement);
}
}
export function from<T>(iterable: Iterable<T> | undefined | null): Iterable<T> {
return iterable || _empty;
}
export function* reverse<T>(array: Array<T>): Iterable<T> {
for (let i = array.length - 1; i >= 0; i--) {
yield array[i];
}
}
export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean {
return !iterable || iterable[Symbol.iterator]().next().done === true;
}
export function first<T>(iterable: Iterable<T>): T | undefined {
return iterable[Symbol.iterator]().next().value;
}
export function some<T>(iterable: Iterable<T>, predicate: (t: T, i: number) => unknown): boolean {
let i = 0;
for (const element of iterable) {
if (predicate(element, i++)) {
return true;
}
}
return false;
}
export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): R | undefined;
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined;
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined {
for (const element of iterable) {
if (predicate(element)) {
return element;
}
}
return undefined;
}
export function filter<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): Iterable<R>;
export function filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T>;
export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
for (const element of iterable) {
if (predicate(element)) {
yield element;
}
}
}
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => R): Iterable<R> {
let index = 0;
for (const element of iterable) {
yield fn(element, index++);
}
}
export function* flatMap<T, R>(iterable: Iterable<T>, fn: (t: T, index: number) => Iterable<R>): Iterable<R> {
let index = 0;
for (const element of iterable) {
yield* fn(element, index++);
}
}
export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
for (const iterable of iterables) {
yield* iterable;
}
}
export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
let value = initialValue;
for (const element of iterable) {
value = reducer(value, element);
}
return value;
}
/**
* Returns an iterable slice of the array, with the same semantics as `array.slice()`.
*/
export function* slice<T>(arr: ReadonlyArray<T>, from: number, to = arr.length): Iterable<T> {
if (from < 0) {
from += arr.length;
}
if (to < 0) {
to += arr.length;
} else if (to > arr.length) {
to = arr.length;
}
for (; from < to; from++) {
yield arr[from];
}
}
/**
* Consumes `atMost` elements from iterable and returns the consumed elements,
* and an iterable for the rest of the elements.
*/
export function consume<T>(iterable: Iterable<T>, atMost: number = Number.POSITIVE_INFINITY): [T[], Iterable<T>] {
const consumed: T[] = [];
if (atMost === 0) {
return [consumed, iterable];
}
const iterator = iterable[Symbol.iterator]();
for (let i = 0; i < atMost; i++) {
const next = iterator.next();
if (next.done) {
return [consumed, Iterable.empty()];
}
consumed.push(next.value);
}
return [consumed, { [Symbol.iterator]() { return iterator; } }];
}
export async function asyncToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
const result: T[] = [];
for await (const item of iterable) {
result.push(item);
}
return Promise.resolve(result);
}
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface IKeyboardEvent {
readonly browserEvent: KeyboardEvent;
readonly keyCode: number;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly key: string;
preventDefault(): void;
stopPropagation(): void;
}
export class StandardKeyboardEvent implements IKeyboardEvent {
public readonly browserEvent: KeyboardEvent;
public readonly keyCode: number;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly key: string;
constructor(e: KeyboardEvent) {
this.browserEvent = e;
this.keyCode = (e.keyCode || (e as any).which || 0) as number;
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
this.key = e.key || '';
}
public preventDefault(): void {
this.browserEvent.preventDefault();
}
public stopPropagation(): void {
this.browserEvent.stopPropagation();
}
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*
* Minimal lifecycle utilities for scrollable components.
*/
export interface IDisposable {
dispose(): void;
}
export function toDisposable(fn: () => void): IDisposable {
return { dispose: fn };
}
export function dispose<T extends IDisposable>(disposable: T): T;
export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined;
export function dispose<T extends IDisposable>(disposables: T[]): T[];
export function dispose<T extends IDisposable>(arg: T | T[] | undefined): T | T[] | undefined {
if (!arg) {
return arg;
}
if (Array.isArray(arg)) {
for (const d of arg) {
d.dispose();
}
return [];
}
arg.dispose();
return arg;
}
export function combinedDisposable(...disposables: IDisposable[]): IDisposable {
return toDisposable(() => dispose(disposables));
}
export class DisposableStore implements IDisposable {
private readonly _disposables = new Set<IDisposable>();
private _isDisposed = false;
public add<T extends IDisposable>(o: T): T {
if (this._isDisposed) {
o.dispose();
} else {
this._disposables.add(o);
}
return o;
}
public dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
for (const d of this._disposables) {
d.dispose();
}
this._disposables.clear();
}
public clear(): void {
for (const d of this._disposables) {
d.dispose();
}
this._disposables.clear();
}
}
export abstract class Disposable implements IDisposable {
static readonly None: IDisposable = Object.freeze({ dispose() { } });
protected readonly _store = new DisposableStore();
public dispose(): void {
this._store.dispose();
}
protected _register<T extends IDisposable>(o: T): T {
return this._store.add(o);
}
}
export function markAsSingleton<T extends IDisposable>(singleton: T): T {
return singleton;
}
export class MutableDisposable<T extends IDisposable> implements IDisposable {
private _value: T | undefined;
private _isDisposed = false;
public get value(): T | undefined {
return this._isDisposed ? undefined : this._value;
}
public set value(value: T | undefined) {
if (this._isDisposed || value === this._value) {
return;
}
this._value?.dispose();
this._value = value;
}
public clear(): void {
this.value = undefined;
}
public dispose(): void {
this._isDisposed = true;
this._value?.dispose();
this._value = undefined;
}
}
+142
View File
@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class Node<E> {
static readonly Undefined = new Node<any>(undefined);
element: E;
next: Node<E>;
prev: Node<E>;
constructor(element: E) {
this.element = element;
this.next = Node.Undefined;
this.prev = Node.Undefined;
}
}
export class LinkedList<E> {
private _first: Node<E> = Node.Undefined;
private _last: Node<E> = Node.Undefined;
private _size: number = 0;
get size(): number {
return this._size;
}
isEmpty(): boolean {
return this._first === Node.Undefined;
}
clear(): void {
let node = this._first;
while (node !== Node.Undefined) {
const next = node.next;
node.prev = Node.Undefined;
node.next = Node.Undefined;
node = next;
}
this._first = Node.Undefined;
this._last = Node.Undefined;
this._size = 0;
}
unshift(element: E): () => void {
return this._insert(element, false);
}
push(element: E): () => void {
return this._insert(element, true);
}
private _insert(element: E, atTheEnd: boolean): () => void {
const newNode = new Node(element);
if (this._first === Node.Undefined) {
this._first = newNode;
this._last = newNode;
} else if (atTheEnd) {
// push
const oldLast = this._last;
this._last = newNode;
newNode.prev = oldLast;
oldLast.next = newNode;
} else {
// unshift
const oldFirst = this._first;
this._first = newNode;
newNode.next = oldFirst;
oldFirst.prev = newNode;
}
this._size += 1;
let didRemove = false;
return () => {
if (!didRemove) {
didRemove = true;
this._remove(newNode);
}
};
}
shift(): E | undefined {
if (this._first === Node.Undefined) {
return undefined;
} else {
const res = this._first.element;
this._remove(this._first);
return res;
}
}
pop(): E | undefined {
if (this._last === Node.Undefined) {
return undefined;
} else {
const res = this._last.element;
this._remove(this._last);
return res;
}
}
private _remove(node: Node<E>): void {
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
// middle
const anchor = node.prev;
anchor.next = node.next;
node.next.prev = anchor;
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
// only node
this._first = Node.Undefined;
this._last = Node.Undefined;
} else if (node.next === Node.Undefined) {
// last
this._last = this._last.prev!;
this._last.next = Node.Undefined;
} else if (node.prev === Node.Undefined) {
// first
this._first = this._first.next!;
this._first.prev = Node.Undefined;
}
// done
this._size -= 1;
}
*[Symbol.iterator](): Iterator<E> {
let node = this._first;
while (node !== Node.Undefined) {
yield node.element;
node = node.next;
}
}
}
+202
View File
@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function getOrSet<K, V>(map: Map<K, V>, key: K, value: V): V {
let result = map.get(key);
if (result === undefined) {
result = value;
map.set(key, result);
}
return result;
}
export function mapToString<K, V>(map: Map<K, V>): string {
const entries: string[] = [];
map.forEach((value, key) => {
entries.push(`${key} => ${value}`);
});
return `Map(${map.size}) {${entries.join(', ')}}`;
}
export function setToString<K>(set: Set<K>): string {
const entries: K[] = [];
set.forEach(value => {
entries.push(value);
});
return `Set(${set.size}) {${entries.join(', ')}}`;
}
export const enum Touch {
None = 0,
AsOld = 1,
AsNew = 2
}
export class CounterSet<T> {
private map = new Map<T, number>();
add(value: T): CounterSet<T> {
this.map.set(value, (this.map.get(value) || 0) + 1);
return this;
}
delete(value: T): boolean {
let counter = this.map.get(value) || 0;
if (counter === 0) {
return false;
}
counter--;
if (counter === 0) {
this.map.delete(value);
} else {
this.map.set(value, counter);
}
return true;
}
has(value: T): boolean {
return this.map.has(value);
}
}
/**
* A map that allows access both by keys and values.
* **NOTE**: values need to be unique.
*/
export class BidirectionalMap<K, V> {
private readonly _m1 = new Map<K, V>();
private readonly _m2 = new Map<V, K>();
constructor(entries?: readonly (readonly [K, V])[]) {
if (entries) {
for (const [key, value] of entries) {
this.set(key, value);
}
}
}
clear(): void {
this._m1.clear();
this._m2.clear();
}
set(key: K, value: V): void {
this._m1.set(key, value);
this._m2.set(value, key);
}
get(key: K): V | undefined {
return this._m1.get(key);
}
getKey(value: V): K | undefined {
return this._m2.get(value);
}
delete(key: K): boolean {
const value = this._m1.get(key);
if (value === undefined) {
return false;
}
this._m1.delete(key);
this._m2.delete(value);
return true;
}
forEach(callbackfn: (value: V, key: K, map: BidirectionalMap<K, V>) => void, thisArg?: any): void {
this._m1.forEach((value, key) => {
callbackfn.call(thisArg, value, key, this);
});
}
keys(): IterableIterator<K> {
return this._m1.keys();
}
values(): IterableIterator<V> {
return this._m1.values();
}
}
export class SetMap<K, V> {
private map = new Map<K, Set<V>>();
add(key: K, value: V): void {
let values = this.map.get(key);
if (!values) {
values = new Set<V>();
this.map.set(key, values);
}
values.add(value);
}
delete(key: K, value: V): void {
const values = this.map.get(key);
if (!values) {
return;
}
values.delete(value);
if (values.size === 0) {
this.map.delete(key);
}
}
forEach(key: K, fn: (value: V) => void): void {
const values = this.map.get(key);
if (!values) {
return;
}
values.forEach(fn);
}
get(key: K): ReadonlySet<V> {
const values = this.map.get(key);
if (!values) {
return new Set<V>();
}
return values;
}
}
export function mapsStrictEqualIgnoreOrder(a: Map<unknown, unknown>, b: Map<unknown, unknown>): boolean {
if (a === b) {
return true;
}
if (a.size !== b.size) {
return false;
}
for (const [key, value] of a) {
if (!b.has(key) || b.get(key) !== value) {
return false;
}
}
for (const [key] of b) {
if (!a.has(key)) {
return false;
}
}
return true;
}
+202
View File
@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from './browser';
import { IframeUtils } from './iframe';
import * as platform from './platform';
export interface IMouseEvent {
readonly browserEvent: MouseEvent;
readonly leftButton: boolean;
readonly middleButton: boolean;
readonly rightButton: boolean;
readonly buttons: number;
readonly target: HTMLElement;
readonly detail: number;
readonly posx: number;
readonly posy: number;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly timestamp: number;
preventDefault(): void;
stopPropagation(): void;
}
export class StandardMouseEvent implements IMouseEvent {
public readonly browserEvent: MouseEvent;
public readonly leftButton: boolean;
public readonly middleButton: boolean;
public readonly rightButton: boolean;
public readonly buttons: number;
public readonly target: HTMLElement;
public detail: number;
public readonly posx: number;
public readonly posy: number;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly timestamp: number;
constructor(targetWindow: Window, e: MouseEvent) {
this.timestamp = Date.now();
this.browserEvent = e;
this.leftButton = e.button === 0;
this.middleButton = e.button === 1;
this.rightButton = e.button === 2;
this.buttons = e.buttons;
this.target = e.target as HTMLElement;
this.detail = e.detail || 1;
if (e.type === 'dblclick') {
this.detail = 2;
}
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
if (typeof e.pageX === 'number') {
this.posx = e.pageX;
this.posy = e.pageY;
} else {
this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
}
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
this.posx -= iframeOffsets.left;
this.posy -= iframeOffsets.top;
}
public preventDefault(): void {
this.browserEvent.preventDefault();
}
public stopPropagation(): void {
this.browserEvent.stopPropagation();
}
}
export interface IMouseWheelEvent extends MouseEvent {
readonly wheelDelta: number;
readonly wheelDeltaX: number;
readonly wheelDeltaY: number;
readonly deltaX: number;
readonly deltaY: number;
readonly deltaZ: number;
readonly deltaMode: number;
}
interface IWebKitMouseWheelEvent {
wheelDeltaY: number;
wheelDeltaX: number;
}
interface IGeckoMouseWheelEvent {
HORIZONTAL_AXIS: number;
VERTICAL_AXIS: number;
axis: number;
detail: number;
}
export class StandardWheelEvent {
public readonly browserEvent: IMouseWheelEvent | null;
public readonly deltaY: number;
public readonly deltaX: number;
public readonly target: Node | null;
constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {
this.browserEvent = e || null;
this.target = e ? (e.target || (e as any).targetNode || e.srcElement) : null;
this.deltaY = deltaY;
this.deltaX = deltaX;
let shouldFactorDPR: boolean = false;
if (browser.isChrome) {
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
shouldFactorDPR = chromeMajorVersion <= 122;
}
if (e) {
const e1 = e as IWebKitMouseWheelEvent as any;
const e2 = e as unknown as IGeckoMouseWheelEvent;
const devicePixelRatio = e.view?.devicePixelRatio || 1;
if (typeof e1.wheelDeltaY !== 'undefined') {
if (shouldFactorDPR) {
this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
} else {
this.deltaY = e1.wheelDeltaY / 120;
}
} else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
this.deltaY = -e2.detail / 3;
} else if (e.type === 'wheel') {
const ev = e as unknown as WheelEvent;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
if (browser.isFirefox && !platform.isMacintosh) {
this.deltaY = -e.deltaY / 3;
} else {
this.deltaY = -e.deltaY;
}
} else {
this.deltaY = -e.deltaY / 40;
}
}
if (typeof e1.wheelDeltaX !== 'undefined') {
if (browser.isSafari && platform.isWindows) {
this.deltaX = -(e1.wheelDeltaX / 120);
} else if (shouldFactorDPR) {
this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
} else {
this.deltaX = e1.wheelDeltaX / 120;
}
} else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
this.deltaX = -e.detail / 3;
} else if (e.type === 'wheel') {
const ev = e as unknown as WheelEvent;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
if (browser.isFirefox && !platform.isMacintosh) {
this.deltaX = -e.deltaX / 3;
} else {
this.deltaX = -e.deltaX;
}
} else {
this.deltaX = -e.deltaX / 40;
}
}
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
if (shouldFactorDPR) {
this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
} else {
this.deltaY = e.wheelDelta / 120;
}
}
}
}
public preventDefault(): void {
this.browserEvent?.preventDefault();
}
public stopPropagation(): void {
this.browserEvent?.stopPropagation();
}
}
+90
View File
@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ILocalizeInfo {
key: string;
comment: string[];
}
export function localize(info: ILocalizeInfo | string, message: string, ...args: (string | number | boolean | undefined | null)[]): string {
return message;
}
export interface INLSLanguagePackConfiguration {
/**
* The path to the translations config file that contains pointers to
* all message bundles for `main` and extensions.
*/
readonly translationsConfigFile: string;
/**
* The path to the file containing the translations for this language
* pack as flat string array.
*/
readonly messagesFile: string;
/**
* The path to the file that can be used to signal a corrupt language
* pack, for example when reading the `messagesFile` fails. This will
* instruct the application to re-create the cache on next startup.
*/
readonly corruptMarkerFile: string;
}
export interface INLSConfiguration {
/**
* Locale as defined in `argv.json` or `app.getLocale()`.
*/
readonly userLocale: string;
/**
* Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`).
*/
readonly osLocale: string;
/**
* The actual language of the UI that ends up being used considering `userLocale`
* and `osLocale`.
*/
readonly resolvedLanguage: string;
/**
* Defined if a language pack is used that is not the
* default english language pack. This requires a language
* pack to be installed as extension.
*/
readonly languagePack?: INLSLanguagePackConfiguration;
/**
* The path to the file containing the default english messages
* as flat string array. The file is only present in built
* versions of the application.
*/
readonly defaultMessagesFile: string;
/**
* Below properties are deprecated and only there to continue support
* for `vscode-nls` module that depends on them.
* Refs https://github.com/microsoft/vscode-nls/blob/main/src/node/main.ts#L36-L46
*/
/** @deprecated */
readonly locale: string;
/** @deprecated */
readonly availableLanguages: Record<string, string>;
/** @deprecated */
readonly _languagePackSupport?: boolean;
/** @deprecated */
readonly _languagePackId?: string;
/** @deprecated */
readonly _translationsConfigFile?: string;
/** @deprecated */
readonly _cacheRoot?: string;
/** @deprecated */
readonly _resolvedLanguagePackCoreLocation?: string;
/** @deprecated */
readonly _corruptedFile?: string;
}
+98
View File
@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
export function rot(index: number, modulo: number): number {
return (modulo + (index % modulo)) % modulo;
}
export class Counter {
private _next = 0;
getNext(): number {
return this._next++;
}
}
export class MovingAverage {
private _n = 1;
private _val = 0;
update(value: number): number {
this._val = this._val + (value - this._val) / this._n;
this._n += 1;
return this._val;
}
get value(): number {
return this._val;
}
}
export class SlidingWindowAverage {
private _n: number = 0;
private _val = 0;
private readonly _values: number[] = [];
private _index: number = 0;
private _sum = 0;
constructor(size: number) {
this._values = new Array(size);
this._values.fill(0, 0, size);
}
update(value: number): number {
const oldValue = this._values[this._index];
this._values[this._index] = value;
this._index = (this._index + 1) % this._values.length;
this._sum -= oldValue;
this._sum += value;
if (this._n < this._values.length) {
this._n += 1;
}
this._val = this._sum / this._n;
return this._val;
}
get value(): number {
return this._val;
}
}
/** Returns whether the point is within the triangle formed by the following 6 x/y point pairs */
export function isPointWithinTriangle(
x: number, y: number,
ax: number, ay: number,
bx: number, by: number,
cx: number, cy: number
) {
const v0x = cx - ax;
const v0y = cy - ay;
const v1x = bx - ax;
const v1y = by - ay;
const v2x = x - ax;
const v2y = y - ay;
const dot00 = v0x * v0x + v0y * v0y;
const dot01 = v0x * v1x + v0y * v1y;
const dot02 = v0x * v2x + v0y * v2y;
const dot11 = v1x * v1x + v1y * v1y;
const dot12 = v1x * v2x + v1y * v2y;
const invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return u >= 0 && v >= 0 && u + v < 1;
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
const userAgent = typeof navigator === 'object' ? navigator.userAgent : '';
const platform = typeof navigator === 'object' ? navigator.platform : '';
export const isMacintosh = platform.indexOf('Mac') >= 0;
export const isWindows = platform.indexOf('Win') >= 0;
export const isLinux = platform.indexOf('Linux') >= 0;
export const isIOS = /iPad|iPhone|iPod/.test(userAgent);
+490
View File
@@ -0,0 +1,490 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from './event';
import { Disposable, IDisposable } from './lifecycle';
export const enum ScrollbarVisibility {
Auto = 1,
Hidden = 2,
Visible = 3
}
export interface ScrollEvent {
inSmoothScrolling: boolean;
oldWidth: number;
oldScrollWidth: number;
oldScrollLeft: number;
width: number;
scrollWidth: number;
scrollLeft: number;
oldHeight: number;
oldScrollHeight: number;
oldScrollTop: number;
height: number;
scrollHeight: number;
scrollTop: number;
widthChanged: boolean;
scrollWidthChanged: boolean;
scrollLeftChanged: boolean;
heightChanged: boolean;
scrollHeightChanged: boolean;
scrollTopChanged: boolean;
}
export class ScrollState implements IScrollDimensions, IScrollPosition {
_scrollStateBrand: void = undefined;
public readonly rawScrollLeft: number;
public readonly rawScrollTop: number;
public readonly width: number;
public readonly scrollWidth: number;
public readonly scrollLeft: number;
public readonly height: number;
public readonly scrollHeight: number;
public readonly scrollTop: number;
constructor(
private readonly _forceIntegerValues: boolean,
width: number,
scrollWidth: number,
scrollLeft: number,
height: number,
scrollHeight: number,
scrollTop: number
) {
if (this._forceIntegerValues) {
width = width | 0;
scrollWidth = scrollWidth | 0;
scrollLeft = scrollLeft | 0;
height = height | 0;
scrollHeight = scrollHeight | 0;
scrollTop = scrollTop | 0;
}
this.rawScrollLeft = scrollLeft;
this.rawScrollTop = scrollTop;
if (width < 0) {
width = 0;
}
if (scrollLeft + width > scrollWidth) {
scrollLeft = scrollWidth - width;
}
if (scrollLeft < 0) {
scrollLeft = 0;
}
if (height < 0) {
height = 0;
}
if (scrollTop + height > scrollHeight) {
scrollTop = scrollHeight - height;
}
if (scrollTop < 0) {
scrollTop = 0;
}
this.width = width;
this.scrollWidth = scrollWidth;
this.scrollLeft = scrollLeft;
this.height = height;
this.scrollHeight = scrollHeight;
this.scrollTop = scrollTop;
}
public equals(other: ScrollState): boolean {
return (
this.rawScrollLeft === other.rawScrollLeft
&& this.rawScrollTop === other.rawScrollTop
&& this.width === other.width
&& this.scrollWidth === other.scrollWidth
&& this.scrollLeft === other.scrollLeft
&& this.height === other.height
&& this.scrollHeight === other.scrollHeight
&& this.scrollTop === other.scrollTop
);
}
public withScrollDimensions(update: INewScrollDimensions, useRawScrollPositions: boolean): ScrollState {
return new ScrollState(
this._forceIntegerValues,
(typeof update.width !== 'undefined' ? update.width : this.width),
(typeof update.scrollWidth !== 'undefined' ? update.scrollWidth : this.scrollWidth),
useRawScrollPositions ? this.rawScrollLeft : this.scrollLeft,
(typeof update.height !== 'undefined' ? update.height : this.height),
(typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : this.scrollHeight),
useRawScrollPositions ? this.rawScrollTop : this.scrollTop
);
}
public withScrollPosition(update: INewScrollPosition): ScrollState {
return new ScrollState(
this._forceIntegerValues,
this.width,
this.scrollWidth,
(typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : this.rawScrollLeft),
this.height,
this.scrollHeight,
(typeof update.scrollTop !== 'undefined' ? update.scrollTop : this.rawScrollTop)
);
}
public createScrollEvent(previous: ScrollState, inSmoothScrolling: boolean): ScrollEvent {
const widthChanged = (this.width !== previous.width);
const scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
const scrollLeftChanged = (this.scrollLeft !== previous.scrollLeft);
const heightChanged = (this.height !== previous.height);
const scrollHeightChanged = (this.scrollHeight !== previous.scrollHeight);
const scrollTopChanged = (this.scrollTop !== previous.scrollTop);
return {
inSmoothScrolling: inSmoothScrolling,
oldWidth: previous.width,
oldScrollWidth: previous.scrollWidth,
oldScrollLeft: previous.scrollLeft,
width: this.width,
scrollWidth: this.scrollWidth,
scrollLeft: this.scrollLeft,
oldHeight: previous.height,
oldScrollHeight: previous.scrollHeight,
oldScrollTop: previous.scrollTop,
height: this.height,
scrollHeight: this.scrollHeight,
scrollTop: this.scrollTop,
widthChanged: widthChanged,
scrollWidthChanged: scrollWidthChanged,
scrollLeftChanged: scrollLeftChanged,
heightChanged: heightChanged,
scrollHeightChanged: scrollHeightChanged,
scrollTopChanged: scrollTopChanged,
};
}
}
export interface IScrollDimensions {
readonly width: number;
readonly scrollWidth: number;
readonly height: number;
readonly scrollHeight: number;
}
export interface INewScrollDimensions {
width?: number;
scrollWidth?: number;
height?: number;
scrollHeight?: number;
}
export interface IScrollPosition {
readonly scrollLeft: number;
readonly scrollTop: number;
}
export interface ISmoothScrollPosition {
readonly scrollLeft: number;
readonly scrollTop: number;
readonly width: number;
readonly height: number;
}
export interface INewScrollPosition {
scrollLeft?: number;
scrollTop?: number;
}
export interface IScrollableOptions {
forceIntegerValues: boolean;
smoothScrollDuration: number;
scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
}
export class Scrollable extends Disposable {
_scrollableBrand: void = undefined;
private _smoothScrollDuration: number;
private readonly _scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable;
private _state: ScrollState;
private _smoothScrolling: SmoothScrollingOperation | null;
private _onScroll = this._register(new Emitter<ScrollEvent>());
public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
constructor(options: IScrollableOptions) {
super();
this._smoothScrollDuration = options.smoothScrollDuration;
this._scheduleAtNextAnimationFrame = options.scheduleAtNextAnimationFrame;
this._state = new ScrollState(options.forceIntegerValues, 0, 0, 0, 0, 0, 0);
this._smoothScrolling = null;
}
public override dispose(): void {
if (this._smoothScrolling) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
}
super.dispose();
}
public setSmoothScrollDuration(smoothScrollDuration: number): void {
this._smoothScrollDuration = smoothScrollDuration;
}
public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition {
return this._state.withScrollPosition(scrollPosition);
}
public getScrollDimensions(): IScrollDimensions {
return this._state;
}
public setScrollDimensions(dimensions: INewScrollDimensions, useRawScrollPositions: boolean): void {
const newState = this._state.withScrollDimensions(dimensions, useRawScrollPositions);
this._setState(newState, Boolean(this._smoothScrolling));
this._smoothScrolling?.acceptScrollDimensions(this._state);
}
public getFutureScrollPosition(): IScrollPosition {
if (this._smoothScrolling) {
return this._smoothScrolling.to;
}
return this._state;
}
public getCurrentScrollPosition(): IScrollPosition {
return this._state;
}
public setScrollPositionNow(update: INewScrollPosition): void {
const newState = this._state.withScrollPosition(update);
if (this._smoothScrolling) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
}
this._setState(newState, false);
}
public setScrollPositionSmooth(update: INewScrollPosition, reuseAnimation?: boolean): void {
if (this._smoothScrollDuration === 0) {
return this.setScrollPositionNow(update);
}
if (this._smoothScrolling) {
update = {
scrollLeft: (typeof update.scrollLeft === 'undefined' ? this._smoothScrolling.to.scrollLeft : update.scrollLeft),
scrollTop: (typeof update.scrollTop === 'undefined' ? this._smoothScrolling.to.scrollTop : update.scrollTop)
};
const validTarget = this._state.withScrollPosition(update);
if (this._smoothScrolling.to.scrollLeft === validTarget.scrollLeft && this._smoothScrolling.to.scrollTop === validTarget.scrollTop) {
return;
}
let newSmoothScrolling: SmoothScrollingOperation;
if (reuseAnimation) {
newSmoothScrolling = new SmoothScrollingOperation(this._smoothScrolling.from, validTarget, this._smoothScrolling.startTime, this._smoothScrolling.duration);
} else {
newSmoothScrolling = this._smoothScrolling.combine(this._state, validTarget, this._smoothScrollDuration);
}
this._smoothScrolling.dispose();
this._smoothScrolling = newSmoothScrolling;
} else {
const validTarget = this._state.withScrollPosition(update);
this._smoothScrolling = SmoothScrollingOperation.start(this._state, validTarget, this._smoothScrollDuration);
}
this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
if (!this._smoothScrolling) {
return;
}
this._smoothScrolling.animationFrameDisposable = null;
this._performSmoothScrolling();
});
}
public hasPendingScrollAnimation(): boolean {
return Boolean(this._smoothScrolling);
}
private _performSmoothScrolling(): void {
if (!this._smoothScrolling) {
return;
}
const update = this._smoothScrolling.tick();
const newState = this._state.withScrollPosition(update);
this._setState(newState, true);
if (!this._smoothScrolling) {
return;
}
if (update.isDone) {
this._smoothScrolling.dispose();
this._smoothScrolling = null;
return;
}
this._smoothScrolling.animationFrameDisposable = this._scheduleAtNextAnimationFrame(() => {
if (!this._smoothScrolling) {
return;
}
this._smoothScrolling.animationFrameDisposable = null;
this._performSmoothScrolling();
});
}
private _setState(newState: ScrollState, inSmoothScrolling: boolean): void {
const oldState = this._state;
if (oldState.equals(newState)) {
return;
}
this._state = newState;
this._onScroll.fire(this._state.createScrollEvent(oldState, inSmoothScrolling));
}
}
export class SmoothScrollingUpdate {
public readonly scrollLeft: number;
public readonly scrollTop: number;
public readonly isDone: boolean;
constructor(scrollLeft: number, scrollTop: number, isDone: boolean) {
this.scrollLeft = scrollLeft;
this.scrollTop = scrollTop;
this.isDone = isDone;
}
}
interface IAnimation {
(completion: number): number;
}
function createEaseOutCubic(from: number, to: number): IAnimation {
const delta = to - from;
return function (completion: number): number {
return from + delta * easeOutCubic(completion);
};
}
function createComposed(a: IAnimation, b: IAnimation, cut: number): IAnimation {
return function (completion: number): number {
if (completion < cut) {
return a(completion / cut);
}
return b((completion - cut) / (1 - cut));
};
}
export class SmoothScrollingOperation {
public readonly from: ISmoothScrollPosition;
public to: ISmoothScrollPosition;
public readonly duration: number;
public readonly startTime: number;
public animationFrameDisposable: IDisposable | null;
private scrollLeft!: IAnimation;
private scrollTop!: IAnimation;
constructor(from: ISmoothScrollPosition, to: ISmoothScrollPosition, startTime: number, duration: number) {
this.from = from;
this.to = to;
this.duration = duration;
this.startTime = startTime;
this.animationFrameDisposable = null;
this._initAnimations();
}
private _initAnimations(): void {
this.scrollLeft = this._initAnimation(this.from.scrollLeft, this.to.scrollLeft, this.to.width);
this.scrollTop = this._initAnimation(this.from.scrollTop, this.to.scrollTop, this.to.height);
}
private _initAnimation(from: number, to: number, viewportSize: number): IAnimation {
const delta = Math.abs(from - to);
if (delta > 2.5 * viewportSize) {
let stop1: number, stop2: number;
if (from < to) {
stop1 = from + 0.75 * viewportSize;
stop2 = to - 0.75 * viewportSize;
} else {
stop1 = from - 0.75 * viewportSize;
stop2 = to + 0.75 * viewportSize;
}
return createComposed(createEaseOutCubic(from, stop1), createEaseOutCubic(stop2, to), 0.33);
}
return createEaseOutCubic(from, to);
}
public dispose(): void {
if (this.animationFrameDisposable !== null) {
this.animationFrameDisposable.dispose();
this.animationFrameDisposable = null;
}
}
public acceptScrollDimensions(state: ScrollState): void {
this.to = state.withScrollPosition(this.to);
this._initAnimations();
}
public tick(): SmoothScrollingUpdate {
return this._tick(Date.now());
}
protected _tick(now: number): SmoothScrollingUpdate {
const completion = (now - this.startTime) / this.duration;
if (completion < 1) {
const newScrollLeft = this.scrollLeft(completion);
const newScrollTop = this.scrollTop(completion);
return new SmoothScrollingUpdate(newScrollLeft, newScrollTop, false);
}
return new SmoothScrollingUpdate(this.to.scrollLeft, this.to.scrollTop, true);
}
public combine(from: ISmoothScrollPosition, to: ISmoothScrollPosition, duration: number): SmoothScrollingOperation {
return SmoothScrollingOperation.start(from, to, duration);
}
public static start(from: ISmoothScrollPosition, to: ISmoothScrollPosition, duration: number): SmoothScrollingOperation {
duration = duration + 10;
const startTime = Date.now() - 10;
return new SmoothScrollingOperation(from, to, startTime, duration);
}
}
function easeInCubic(t: number) {
return Math.pow(t, 3);
}
function easeOutCubic(t: number) {
return 1 - easeInCubic(1 - t);
}
+665
View File
@@ -0,0 +1,665 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getZoomFactor, isChrome } from './browser';
import * as dom from './dom';
import { FastDomNode, createFastDomNode } from './fastDomNode';
import { IMouseEvent, IMouseWheelEvent, StandardWheelEvent } from './mouseEvent';
import { ScrollbarHost } from './abstractScrollbar';
import { HorizontalScrollbar } from './horizontalScrollbar';
import { ScrollableElementChangeOptions, ScrollableElementCreationOptions, ScrollableElementResolvedOptions } from './scrollableElementOptions';
import { VerticalScrollbar } from './verticalScrollbar';
import { Widget } from './widget';
import { TimeoutTimer } from './async';
import { Emitter, Event } from './event';
import { IDisposable, dispose } from './lifecycle';
import * as platform from './platform';
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from './scrollable';
// import 'vs/css!./media/scrollbars';
const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
export interface IOverviewRulerLayoutInfo {
parent: HTMLElement;
insertBefore: HTMLElement;
}
class MouseWheelClassifierItem {
public timestamp: number;
public deltaX: number;
public deltaY: number;
public score: number;
constructor(timestamp: number, deltaX: number, deltaY: number) {
this.timestamp = timestamp;
this.deltaX = deltaX;
this.deltaY = deltaY;
this.score = 0;
}
}
export class MouseWheelClassifier {
public static readonly INSTANCE = new MouseWheelClassifier();
private readonly _capacity: number;
private _memory: MouseWheelClassifierItem[];
private _front: number;
private _rear: number;
constructor() {
this._capacity = 5;
this._memory = [];
this._front = -1;
this._rear = -1;
}
public isPhysicalMouseWheel(): boolean {
if (this._front === -1 && this._rear === -1) {
return false;
}
let remainingInfluence = 1;
let score = 0;
let iteration = 1;
let index = this._rear;
do {
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
remainingInfluence -= influence;
score += this._memory[index].score * influence;
if (index === this._front) {
break;
}
index = (this._capacity + index - 1) % this._capacity;
iteration++;
} while (true);
return (score <= 0.5);
}
public acceptStandardWheelEvent(e: StandardWheelEvent): void {
if (isChrome) {
const targetWindow = dom.getWindow(e.browserEvent);
const pageZoomFactor = getZoomFactor(targetWindow);
this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
} else {
this.accept(Date.now(), e.deltaX, e.deltaY);
}
}
public accept(timestamp: number, deltaX: number, deltaY: number): void {
let previousItem = null;
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
if (this._front === -1 && this._rear === -1) {
this._memory[0] = item;
this._front = 0;
this._rear = 0;
} else {
previousItem = this._memory[this._rear];
this._rear = (this._rear + 1) % this._capacity;
if (this._rear === this._front) {
this._front = (this._front + 1) % this._capacity;
}
this._memory[this._rear] = item;
}
item.score = this._computeScore(item, previousItem);
}
private _computeScore(item: MouseWheelClassifierItem, previousItem: MouseWheelClassifierItem | null): number {
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
return 1;
}
let score: number = 0.5;
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
score += 0.25;
}
if (previousItem) {
const absDeltaX = Math.abs(item.deltaX);
const absDeltaY = Math.abs(item.deltaY);
const absPreviousDeltaX = Math.abs(previousItem.deltaX);
const absPreviousDeltaY = Math.abs(previousItem.deltaY);
const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1);
const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1);
const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX);
const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY);
const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0);
if (isSameModulo) {
score -= 0.5;
}
}
return Math.min(Math.max(score, 0), 1);
}
private _isAlmostInt(value: number): boolean {
const delta = Math.abs(Math.round(value) - value);
return (delta < 0.01);
}
}
export abstract class AbstractScrollableElement extends Widget {
private readonly _options: ScrollableElementResolvedOptions;
protected readonly _scrollable: Scrollable;
private readonly _verticalScrollbar: VerticalScrollbar;
private readonly _horizontalScrollbar: HorizontalScrollbar;
private readonly _domNode: HTMLElement;
private readonly _leftShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _topShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _topLeftShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _listenOnDomNode: HTMLElement;
private _mouseWheelToDispose: IDisposable[];
private _isDragging: boolean;
private _mouseIsOver: boolean;
private readonly _hideTimeout: TimeoutTimer;
private _shouldRender: boolean;
private _revealOnScroll: boolean;
private readonly _onScroll = this._register(new Emitter<ScrollEvent>());
public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
public readonly onWillScroll: Event<ScrollEvent> = this._onWillScroll.event;
public get options(): Readonly<ScrollableElementResolvedOptions> {
return this._options;
}
protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
super();
this._options = resolveOptions(options);
this._scrollable = scrollable;
this._register(this._scrollable.onScroll((e) => {
this._onWillScroll.fire(e);
this._onDidScroll(e);
this._onScroll.fire(e);
}));
const scrollbarHost: ScrollbarHost = {
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onDragStart: () => this._onDragStart(),
onDragEnd: () => this._onDragEnd(),
};
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
this._domNode = document.createElement('div');
this._domNode.className = 'xterm-scrollable-element ' + this._options.className;
this._domNode.setAttribute('role', 'presentation');
this._domNode.style.position = 'relative';
this._domNode.appendChild(element);
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
if (this._options.useShadows) {
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
this._leftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._leftShadowDomNode.domNode);
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
this._topShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topShadowDomNode.domNode);
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
this._topLeftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
} else {
this._leftShadowDomNode = null;
this._topShadowDomNode = null;
this._topLeftShadowDomNode = null;
}
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
this._mouseWheelToDispose = [];
this._setListeningToMouseWheel(this._options.handleMouseWheel);
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
this.onmouseleave(this._listenOnDomNode, (e) => this._onMouseLeave(e));
this._hideTimeout = this._register(new TimeoutTimer());
this._isDragging = false;
this._mouseIsOver = false;
this._shouldRender = true;
this._revealOnScroll = true;
}
public override dispose(): void {
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
super.dispose();
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
return {
parent: this._domNode,
insertBefore: this._verticalScrollbar.domNode.domNode,
};
}
public delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void {
this._verticalScrollbar.delegatePointerDown(browserEvent);
}
public getScrollDimensions(): IScrollDimensions {
return this._scrollable.getScrollDimensions();
}
public setScrollDimensions(dimensions: INewScrollDimensions): void {
this._scrollable.setScrollDimensions(dimensions, false);
}
public updateClassName(newClassName: string): void {
this._options.className = newClassName;
if (platform.isMacintosh) {
this._options.className += ' mac';
}
this._domNode.className = 'xterm-scrollable-element ' + this._options.className;
}
public updateOptions(newOptions: ScrollableElementChangeOptions): void {
if (typeof newOptions.handleMouseWheel !== 'undefined') {
this._options.handleMouseWheel = newOptions.handleMouseWheel;
this._setListeningToMouseWheel(this._options.handleMouseWheel);
}
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
}
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
}
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
}
if (typeof newOptions.horizontal !== 'undefined') {
this._options.horizontal = newOptions.horizontal;
}
if (typeof newOptions.vertical !== 'undefined') {
this._options.vertical = newOptions.vertical;
}
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
}
if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
}
if (typeof newOptions.scrollByPage !== 'undefined') {
this._options.scrollByPage = newOptions.scrollByPage;
}
this._horizontalScrollbar.updateOptions(this._options);
this._verticalScrollbar.updateOptions(this._options);
if (!this._options.lazyRender) {
this._render();
}
}
public setRevealOnScroll(value: boolean) {
this._revealOnScroll = value;
}
public delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
}
// -------------------- mouse wheel scrolling --------------------
private _setListeningToMouseWheel(shouldListen: boolean): void {
const isListening = (this._mouseWheelToDispose.length > 0);
if (isListening === shouldListen) {
return;
}
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
if (shouldListen) {
const onMouseWheel = (browserEvent: IMouseWheelEvent) => {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
};
this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
}
}
private _onMouseWheel(e: StandardWheelEvent): void {
if (e.browserEvent?.defaultPrevented) {
return;
}
const classifier = MouseWheelClassifier.INSTANCE;
if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
classifier.acceptStandardWheelEvent(e);
}
let didScroll = false;
if (e.deltaY || e.deltaX) {
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
if (this._options.scrollPredominantAxis) {
if (this._options.scrollYToX && deltaX + deltaY === 0) {
deltaX = deltaY = 0;
} else if (Math.abs(deltaY) >= Math.abs(deltaX)) {
deltaX = 0;
} else {
deltaY = 0;
}
}
if (this._options.flipAxes) {
[deltaY, deltaX] = [deltaX, deltaY];
}
const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
deltaX = deltaY;
deltaY = 0;
}
if (e.browserEvent && e.browserEvent.altKey) {
deltaX = deltaX * this._options.fastScrollSensitivity;
deltaY = deltaY * this._options.fastScrollSensitivity;
}
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
let desiredScrollPosition: INewScrollPosition = {};
if (deltaY) {
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
}
if (deltaX) {
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
}
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
const canPerformSmoothScroll = (
SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED
&& this._options.mouseWheelSmoothScroll
&& classifier.isPhysicalMouseWheel()
);
if (canPerformSmoothScroll) {
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
} else {
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
didScroll = true;
}
}
let consumeMouseWheel = didScroll;
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
consumeMouseWheel = true;
}
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
consumeMouseWheel = true;
}
if (consumeMouseWheel) {
e.preventDefault();
e.stopPropagation();
}
}
private _onDidScroll(e: ScrollEvent): void {
this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
if (this._options.useShadows) {
this._shouldRender = true;
}
if (this._revealOnScroll) {
this._reveal();
}
if (!this._options.lazyRender) {
this._render();
}
}
public renderNow(): void {
if (!this._options.lazyRender) {
throw new Error('Please use `lazyRender` together with `renderNow`!');
}
this._render();
}
private _render(): void {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._horizontalScrollbar.render();
this._verticalScrollbar.render();
if (this._options.useShadows) {
const scrollState = this._scrollable.getCurrentScrollPosition();
const enableTop = scrollState.scrollTop > 0;
const enableLeft = scrollState.scrollLeft > 0;
const leftClassName = (enableLeft ? ' left' : '');
const topClassName = (enableTop ? ' top' : '');
const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
this._leftShadowDomNode!.setClassName(`shadow${leftClassName}`);
this._topShadowDomNode!.setClassName(`shadow${topClassName}`);
this._topLeftShadowDomNode!.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
}
}
// -------------------- fade in / fade out --------------------
private _onDragStart(): void {
this._isDragging = true;
this._reveal();
}
private _onDragEnd(): void {
this._isDragging = false;
this._hide();
}
private _onMouseLeave(e: IMouseEvent): void {
this._mouseIsOver = false;
this._hide();
}
private _onMouseOver(e: IMouseEvent): void {
this._mouseIsOver = true;
this._reveal();
}
private _reveal(): void {
this._verticalScrollbar.beginReveal();
this._horizontalScrollbar.beginReveal();
this._scheduleHide();
}
private _hide(): void {
if (!this._mouseIsOver && !this._isDragging) {
this._verticalScrollbar.beginHide();
this._horizontalScrollbar.beginHide();
}
}
private _scheduleHide(): void {
if (!this._mouseIsOver && !this._isDragging) {
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
}
}
}
export class ScrollableElement extends AbstractScrollableElement {
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable({
forceIntegerValues: true,
smoothScrollDuration: 0,
scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
});
super(element, options, scrollable);
this._register(scrollable);
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
}
export class SmoothScrollableElement extends AbstractScrollableElement {
constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
super(element, options, scrollable);
}
public setScrollPosition(update: INewScrollPosition & { reuseAnimation?: boolean }): void {
if (update.reuseAnimation) {
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
} else {
this._scrollable.setScrollPositionNow(update);
}
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
}
export class DomScrollableElement extends AbstractScrollableElement {
private _element: HTMLElement;
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable({
forceIntegerValues: false,
smoothScrollDuration: 0,
scheduleAtNextAnimationFrame: (callback) => dom.scheduleAtNextAnimationFrame(dom.getWindow(element), callback)
});
super(element, options, scrollable);
this._register(scrollable);
this._element = element;
this._register(this.onScroll((e) => {
if (e.scrollTopChanged) {
this._element.scrollTop = e.scrollTop;
}
if (e.scrollLeftChanged) {
this._element.scrollLeft = e.scrollLeft;
}
}));
this.scanDomNode();
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
public scanDomNode(): void {
this.setScrollDimensions({
width: this._element.clientWidth,
scrollWidth: this._element.scrollWidth,
height: this._element.clientHeight,
scrollHeight: this._element.scrollHeight
});
this.setScrollPosition({
scrollLeft: this._element.scrollLeft,
scrollTop: this._element.scrollTop,
});
}
}
function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
const result: ScrollableElementResolvedOptions = {
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : ScrollbarVisibility.Auto),
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto),
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
};
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
if (platform.isMacintosh) {
result.className += ' mac';
}
return result;
}
@@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ScrollbarVisibility } from './scrollable';
export interface ScrollableElementCreationOptions {
/**
* The scrollable element should not do any DOM mutations until renderNow() is called.
* Defaults to false.
*/
lazyRender?: boolean;
/**
* CSS Class name for the scrollable element.
*/
className?: string;
/**
* Drop subtle horizontal and vertical shadows.
* Defaults to false.
*/
useShadows?: boolean;
/**
* Handle mouse wheel (listen to mouse wheel scrolling).
* Defaults to true
*/
handleMouseWheel?: boolean;
/**
* If mouse wheel is handled, make mouse wheel scrolling smooth.
* Defaults to true.
*/
mouseWheelSmoothScroll?: boolean;
/**
* Flip axes. Treat vertical scrolling like horizontal and vice-versa.
* Defaults to false.
*/
flipAxes?: boolean;
/**
* If enabled, will scroll horizontally when scrolling vertical.
* Defaults to false.
*/
scrollYToX?: boolean;
/**
* Consume all mouse wheel events if a scrollbar is needed (i.e. scrollSize > size).
* Defaults to false.
*/
consumeMouseWheelIfScrollbarIsNeeded?: boolean;
/**
* Always consume mouse wheel events, even when scrolling is no longer possible.
* Defaults to false.
*/
alwaysConsumeMouseWheel?: boolean;
/**
* A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.
* Defaults to 1.
*/
mouseWheelScrollSensitivity?: number;
/**
* FastScrolling mulitplier speed when pressing `Alt`
* Defaults to 5.
*/
fastScrollSensitivity?: number;
/**
* Whether the scrollable will only scroll along the predominant axis when scrolling both
* vertically and horizontally at the same time.
* Prevents horizontal drift when scrolling vertically on a trackpad.
* Defaults to true.
*/
scrollPredominantAxis?: boolean;
/**
* Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right).
* Defaults to 11.
*/
arrowSize?: number;
/**
* The dom node events should be bound to.
* If no listenOnDomNode is provided, the dom node passed to the constructor will be used for event listening.
*/
listenOnDomNode?: HTMLElement;
/**
* Control the visibility of the horizontal scrollbar.
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
* Defaults to 'auto'.
*/
horizontal?: ScrollbarVisibility;
/**
* Height (in px) of the horizontal scrollbar.
* Defaults to 10.
*/
horizontalScrollbarSize?: number;
/**
* Height (in px) of the horizontal scrollbar slider.
* Defaults to `horizontalScrollbarSize`
*/
horizontalSliderSize?: number;
/**
* Render arrows (left/right) for the horizontal scrollbar.
* Defaults to false.
*/
horizontalHasArrows?: boolean;
/**
* Control the visibility of the vertical scrollbar.
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
* Defaults to 'auto'.
*/
vertical?: ScrollbarVisibility;
/**
* Width (in px) of the vertical scrollbar.
* Defaults to 10.
*/
verticalScrollbarSize?: number;
/**
* Width (in px) of the vertical scrollbar slider.
* Defaults to `verticalScrollbarSize`
*/
verticalSliderSize?: number;
/**
* Render arrows (top/bottom) for the vertical scrollbar.
* Defaults to false.
*/
verticalHasArrows?: boolean;
/**
* Scroll gutter clicks move by page vs. jump to position.
* Defaults to false.
*/
scrollByPage?: boolean;
}
export interface ScrollableElementChangeOptions {
handleMouseWheel?: boolean;
mouseWheelScrollSensitivity?: number;
fastScrollSensitivity?: number;
scrollPredominantAxis?: boolean;
horizontal?: ScrollbarVisibility;
horizontalScrollbarSize?: number;
vertical?: ScrollbarVisibility;
verticalScrollbarSize?: number;
scrollByPage?: boolean;
}
export interface ScrollableElementResolvedOptions {
lazyRender: boolean;
className: string;
useShadows: boolean;
handleMouseWheel: boolean;
flipAxes: boolean;
scrollYToX: boolean;
consumeMouseWheelIfScrollbarIsNeeded: boolean;
alwaysConsumeMouseWheel: boolean;
mouseWheelScrollSensitivity: number;
fastScrollSensitivity: number;
scrollPredominantAxis: boolean;
mouseWheelSmoothScroll: boolean;
arrowSize: number;
listenOnDomNode: HTMLElement | null;
horizontal: ScrollbarVisibility;
horizontalScrollbarSize: number;
horizontalSliderSize: number;
horizontalHasArrows: boolean;
vertical: ScrollbarVisibility;
verticalScrollbarSize: number;
verticalSliderSize: number;
verticalHasArrows: boolean;
scrollByPage: boolean;
}
+114
View File
@@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { GlobalPointerMoveMonitor } from './globalPointerMoveMonitor';
import { Widget } from './widget';
import { TimeoutTimer } from './async';
import * as dom from './dom';
/**
* The arrow image size.
*/
export const ARROW_IMG_SIZE = 11;
export interface ScrollbarArrowOptions {
onActivate: () => void;
className: string;
// icon: ThemeIcon;
bgWidth: number;
bgHeight: number;
top?: number;
left?: number;
bottom?: number;
right?: number;
}
export class ScrollbarArrow extends Widget {
private _onActivate: () => void;
public bgDomNode: HTMLElement;
public domNode: HTMLElement;
private _pointerdownRepeatTimer: dom.WindowIntervalTimer;
private _pointerdownScheduleRepeatTimer: TimeoutTimer;
private _pointerMoveMonitor: GlobalPointerMoveMonitor;
constructor(opts: ScrollbarArrowOptions) {
super();
this._onActivate = opts.onActivate;
this.bgDomNode = document.createElement('div');
this.bgDomNode.className = 'arrow-background';
this.bgDomNode.style.position = 'absolute';
this.bgDomNode.style.width = opts.bgWidth + 'px';
this.bgDomNode.style.height = opts.bgHeight + 'px';
if (typeof opts.top !== 'undefined') {
this.bgDomNode.style.top = '0px';
}
if (typeof opts.left !== 'undefined') {
this.bgDomNode.style.left = '0px';
}
if (typeof opts.bottom !== 'undefined') {
this.bgDomNode.style.bottom = '0px';
}
if (typeof opts.right !== 'undefined') {
this.bgDomNode.style.right = '0px';
}
this.domNode = document.createElement('div');
this.domNode.className = opts.className;
// this.domNode.classList.add(...ThemeIcon.asClassNameArray(opts.icon));
this.domNode.style.position = 'absolute';
this.domNode.style.width = ARROW_IMG_SIZE + 'px';
this.domNode.style.height = ARROW_IMG_SIZE + 'px';
if (typeof opts.top !== 'undefined') {
this.domNode.style.top = opts.top + 'px';
}
if (typeof opts.left !== 'undefined') {
this.domNode.style.left = opts.left + 'px';
}
if (typeof opts.bottom !== 'undefined') {
this.domNode.style.bottom = opts.bottom + 'px';
}
if (typeof opts.right !== 'undefined') {
this.domNode.style.right = opts.right + 'px';
}
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._register(dom.addStandardDisposableListener(this.bgDomNode, dom.EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
this._register(dom.addStandardDisposableListener(this.domNode, dom.EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
this._pointerdownRepeatTimer = this._register(new dom.WindowIntervalTimer());
this._pointerdownScheduleRepeatTimer = this._register(new TimeoutTimer());
}
private _arrowPointerDown(e: PointerEvent): void {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const scheduleRepeater = () => {
this._pointerdownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24, dom.getWindow(e));
};
this._onActivate();
this._pointerdownRepeatTimer.cancel();
this._pointerdownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
this._pointerMoveMonitor.startMonitoring(
e.target,
e.pointerId,
e.buttons,
(pointerMoveData) => { /* Intentional empty */ },
() => {
this._pointerdownRepeatTimer.cancel();
this._pointerdownScheduleRepeatTimer.cancel();
}
);
e.preventDefault();
}
}
+223
View File
@@ -0,0 +1,223 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.
*/
const MINIMUM_SLIDER_SIZE = 20;
export class ScrollbarState {
/**
* For the vertical scrollbar: the width.
* For the horizontal scrollbar: the height.
*/
private _scrollbarSize: number;
/**
* For the vertical scrollbar: the height of the pair horizontal scrollbar.
* For the horizontal scrollbar: the width of the pair vertical scrollbar.
*/
private _oppositeScrollbarSize: number;
/**
* 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;
// --- variables
/**
* For the vertical scrollbar: the viewport height.
* For the horizontal scrollbar: the viewport width.
*/
private _visibleSize: number;
/**
* For the vertical scrollbar: the scroll height.
* For the horizontal scrollbar: the scroll width.
*/
private _scrollSize: number;
/**
* For the vertical scrollbar: the scroll top.
* For the horizontal scrollbar: the scroll left.
*/
private _scrollPosition: number;
// --- computed variables
/**
* `visibleSize` - `oppositeScrollbarSize`
*/
private _computedAvailableSize: number;
/**
* (`scrollSize` > 0 && `scrollSize` > `visibleSize`)
*/
private _computedIsNeeded: boolean;
private _computedSliderSize: number;
private _computedSliderRatio: number;
private _computedSliderPosition: number;
constructor(arrowSize: number, scrollbarSize: number, oppositeScrollbarSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
this._scrollbarSize = Math.round(scrollbarSize);
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
this._arrowSize = Math.round(arrowSize);
this._visibleSize = visibleSize;
this._scrollSize = scrollSize;
this._scrollPosition = scrollPosition;
this._computedAvailableSize = 0;
this._computedIsNeeded = false;
this._computedSliderSize = 0;
this._computedSliderRatio = 0;
this._computedSliderPosition = 0;
this._refreshComputedValues();
}
public clone(): ScrollbarState {
return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
}
public setVisibleSize(visibleSize: number): boolean {
const iVisibleSize = Math.round(visibleSize);
if (this._visibleSize !== iVisibleSize) {
this._visibleSize = iVisibleSize;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollSize(scrollSize: number): boolean {
const iScrollSize = Math.round(scrollSize);
if (this._scrollSize !== iScrollSize) {
this._scrollSize = iScrollSize;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollPosition(scrollPosition: number): boolean {
const iScrollPosition = Math.round(scrollPosition);
if (this._scrollPosition !== iScrollPosition) {
this._scrollPosition = iScrollPosition;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollbarSize(scrollbarSize: number): void {
this._scrollbarSize = Math.round(scrollbarSize);
}
public setOppositeScrollbarSize(oppositeScrollbarSize: number): void {
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
}
private static _computeValues(oppositeScrollbarSize: number, arrowSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
if (!computedIsNeeded) {
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedRepresentableSize),
computedSliderRatio: 0,
computedSliderPosition: 0,
};
}
const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
const computedSliderPosition = (scrollPosition * computedSliderRatio);
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedSliderSize),
computedSliderRatio: computedSliderRatio,
computedSliderPosition: Math.round(computedSliderPosition),
};
}
private _refreshComputedValues(): void {
const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
this._computedAvailableSize = r.computedAvailableSize;
this._computedIsNeeded = r.computedIsNeeded;
this._computedSliderSize = r.computedSliderSize;
this._computedSliderRatio = r.computedSliderRatio;
this._computedSliderPosition = r.computedSliderPosition;
}
public getArrowSize(): number {
return this._arrowSize;
}
public getScrollPosition(): number {
return this._scrollPosition;
}
public getRectangleLargeSize(): number {
return this._computedAvailableSize;
}
public getRectangleSmallSize(): number {
return this._scrollbarSize;
}
public isNeeded(): boolean {
return this._computedIsNeeded;
}
public getSliderSize(): number {
return this._computedSliderSize;
}
public getSliderPosition(): number {
return this._computedSliderPosition;
}
public getDesiredScrollPositionFromOffset(offset: number): number {
if (!this._computedIsNeeded) {
return 0;
}
const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
public getDesiredScrollPositionFromOffsetPaged(offset: number): number {
if (!this._computedIsNeeded) {
return 0;
}
const correctedOffset = offset - this._arrowSize;
let desiredScrollPosition = this._scrollPosition;
if (correctedOffset < this._computedSliderPosition) {
desiredScrollPosition -= this._visibleSize;
} else {
desiredScrollPosition += this._visibleSize;
}
return desiredScrollPosition;
}
public getDesiredScrollPositionFromDelta(delta: number): number {
if (!this._computedIsNeeded) {
return 0;
}
const desiredSliderPosition = this._computedSliderPosition + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
}
@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FastDomNode } from './fastDomNode';
import { TimeoutTimer } from './async';
import { Disposable } from './lifecycle';
import { ScrollbarVisibility } from './scrollable';
export class ScrollbarVisibilityController extends Disposable {
private _visibility: ScrollbarVisibility;
private _visibleClassName: string;
private _invisibleClassName: string;
private _domNode: FastDomNode<HTMLElement> | null;
private _rawShouldBeVisible: boolean;
private _shouldBeVisible: boolean;
private _isNeeded: boolean;
private _isVisible: boolean;
private _revealTimer: TimeoutTimer;
constructor(visibility: ScrollbarVisibility, visibleClassName: string, invisibleClassName: string) {
super();
this._visibility = visibility;
this._visibleClassName = visibleClassName;
this._invisibleClassName = invisibleClassName;
this._domNode = null;
this._isVisible = false;
this._isNeeded = false;
this._rawShouldBeVisible = false;
this._shouldBeVisible = false;
this._revealTimer = this._register(new TimeoutTimer());
}
public setVisibility(visibility: ScrollbarVisibility): void {
if (this._visibility !== visibility) {
this._visibility = visibility;
this._updateShouldBeVisible();
}
}
public setShouldBeVisible(rawShouldBeVisible: boolean): void {
this._rawShouldBeVisible = rawShouldBeVisible;
this._updateShouldBeVisible();
}
private _applyVisibilitySetting(): boolean {
if (this._visibility === ScrollbarVisibility.Hidden) {
return false;
}
if (this._visibility === ScrollbarVisibility.Visible) {
return true;
}
return this._rawShouldBeVisible;
}
private _updateShouldBeVisible(): void {
const shouldBeVisible = this._applyVisibilitySetting();
if (this._shouldBeVisible !== shouldBeVisible) {
this._shouldBeVisible = shouldBeVisible;
this.ensureVisibility();
}
}
public setIsNeeded(isNeeded: boolean): void {
if (this._isNeeded !== isNeeded) {
this._isNeeded = isNeeded;
this.ensureVisibility();
}
}
public setDomNode(domNode: FastDomNode<HTMLElement>): void {
this._domNode = domNode;
this._domNode.setClassName(this._invisibleClassName);
this.setShouldBeVisible(false);
}
public ensureVisibility(): void {
if (!this._isNeeded) {
this._hide(false);
return;
}
if (this._shouldBeVisible) {
this._reveal();
} else {
this._hide(true);
}
}
private _reveal(): void {
if (this._isVisible) {
return;
}
this._isVisible = true;
this._revealTimer.setIfNotSet(() => {
this._domNode?.setClassName(this._visibleClassName);
}, 0);
}
private _hide(withFadeAway: boolean): void {
this._revealTimer.cancel();
if (!this._isVisible) {
return;
}
this._isVisible = false;
this._domNode?.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
}
}
+43
View File
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// fake definition so that the valid layers check won't trip on this
declare const globalThis: { performance?: { now(): number } };
const hasPerformanceNow = (globalThis.performance && typeof globalThis.performance.now === 'function');
export class StopWatch {
private _startTime: number;
private _stopTime: number;
private readonly _now: () => number;
public static create(highResolution?: boolean): StopWatch {
return new StopWatch(highResolution);
}
constructor(highResolution?: boolean) {
this._now = hasPerformanceNow && highResolution === false ? Date.now : globalThis.performance!.now.bind(globalThis.performance);
this._startTime = this._now();
this._stopTime = -1;
}
public stop(): void {
this._stopTime = this._now();
}
public reset(): void {
this._startTime = this._now();
this._stopTime = -1;
}
public elapsed(): number {
if (this._stopTime !== -1) {
return this._stopTime - this._startTime;
}
return this._now() - this._startTime;
}
}
+9
View File
@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Can be passed into the Delayed to defer using a microtask
* */
export const MicrotaskDelay = Symbol('MicrotaskDelay');
+364
View File
@@ -0,0 +1,364 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DomUtils from './dom';
import { mainWindow } from './window';
import * as arrays from './arrays';
import { memoize } from './decorators';
import { Event as EventUtils } from './event';
import { Disposable, IDisposable, markAsSingleton, toDisposable } from './lifecycle';
import { LinkedList } from './linkedList';
export namespace EventType {
export const Tap = '-xterm-gesturetap';
export const Change = '-xterm-gesturechange';
export const Start = '-xterm-gesturestart';
export const End = '-xterm-gesturesend';
export const Contextmenu = '-xterm-gesturecontextmenu';
}
interface TouchData {
id: number;
initialTarget: EventTarget;
initialTimeStamp: number;
initialPageX: number;
initialPageY: number;
rollingTimestamps: number[];
rollingPageX: number[];
rollingPageY: number[];
}
export interface GestureEvent extends MouseEvent {
initialTarget: EventTarget | undefined;
translationX: number;
translationY: number;
pageX: number;
pageY: number;
tapCount: number;
}
interface Touch {
identifier: number;
screenX: number;
screenY: number;
clientX: number;
clientY: number;
pageX: number;
pageY: number;
radiusX: number;
radiusY: number;
rotationAngle: number;
force: number;
target: Element;
}
interface TouchList {
[i: number]: Touch;
length: number;
item(index: number): Touch;
identifiedTouch(id: number): Touch;
}
interface TouchEvent extends Event {
touches: TouchList;
targetTouches: TouchList;
changedTouches: TouchList;
}
export class Gesture extends Disposable {
private static readonly SCROLL_FRICTION = -0.005;
private static INSTANCE: Gesture;
private static readonly HOLD_DELAY = 700;
private dispatched = false;
private readonly targets = new LinkedList<HTMLElement>();
private readonly ignoreTargets = new LinkedList<HTMLElement>();
private handle: IDisposable | null;
private readonly activeTouches: { [id: number]: TouchData };
private _lastSetTapCountTime: number;
private static readonly CLEAR_TAP_COUNT_TIME = 400; // ms
private constructor() {
super();
this.activeTouches = {};
this.handle = null;
this._lastSetTapCountTime = 0;
this._register(EventUtils.runAndSubscribe(DomUtils.onDidRegisterWindow, ({ window, disposables }) => {
disposables.add(DomUtils.addDisposableListener(window.document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e), { passive: false }));
disposables.add(DomUtils.addDisposableListener(window.document, 'touchend', (e: TouchEvent) => this.onTouchEnd(window, e)));
disposables.add(DomUtils.addDisposableListener(window.document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e), { passive: false }));
}, { window: mainWindow, disposables: this._store }));
}
public static addTarget(element: HTMLElement): IDisposable {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = markAsSingleton(new Gesture());
}
const remove = Gesture.INSTANCE.targets.push(element);
return toDisposable(remove);
}
public static ignoreTarget(element: HTMLElement): IDisposable {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = markAsSingleton(new Gesture());
}
const remove = Gesture.INSTANCE.ignoreTargets.push(element);
return toDisposable(remove);
}
@memoize
static isTouchDevice(): boolean {
return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0;
}
public override dispose(): void {
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
super.dispose();
}
private onTouchStart(e: TouchEvent): void {
const timestamp = Date.now();
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
for (let i = 0, len = e.targetTouches.length; i < len; i++) {
const touch = e.targetTouches.item(i);
this.activeTouches[touch.identifier] = {
id: touch.identifier,
initialTarget: touch.target,
initialTimeStamp: timestamp,
initialPageX: touch.pageX,
initialPageY: touch.pageY,
rollingTimestamps: [timestamp],
rollingPageX: [touch.pageX],
rollingPageY: [touch.pageY]
};
const evt = this.newGestureEvent(EventType.Start, touch.target);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.dispatchEvent(evt);
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
private onTouchEnd(targetWindow: Window, e: TouchEvent): void {
const timestamp = Date.now();
const activeTouchCount = Object.keys(this.activeTouches).length;
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
const touch = e.changedTouches.item(i);
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('move of an UNKNOWN touch', touch);
continue;
}
const data = this.activeTouches[touch.identifier],
holdTime = Date.now() - data.initialTimeStamp;
if (holdTime < Gesture.HOLD_DELAY
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)!) < 30) {
const evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX)!;
evt.pageY = arrays.tail(data.rollingPageY)!;
this.dispatchEvent(evt);
} else if (holdTime >= Gesture.HOLD_DELAY
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)!) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)!) < 30) {
const evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX)!;
evt.pageY = arrays.tail(data.rollingPageY)!;
this.dispatchEvent(evt);
} else if (activeTouchCount === 1) {
const finalX = arrays.tail(data.rollingPageX)!;
const finalY = arrays.tail(data.rollingPageY)!;
const deltaT = arrays.tail(data.rollingTimestamps)! - data.rollingTimestamps[0];
const deltaX = finalX - data.rollingPageX[0];
const deltaY = finalY - data.rollingPageY[0];
const dispatchTo = [...this.targets].filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
this.inertia(targetWindow, dispatchTo, timestamp,
Math.abs(deltaX) / deltaT,
deltaX > 0 ? 1 : -1,
finalX,
Math.abs(deltaY) / deltaT,
deltaY > 0 ? 1 : -1,
finalY
);
}
this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
delete this.activeTouches[touch.identifier];
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent {
const event = document.createEvent('CustomEvent') as unknown as GestureEvent;
event.initEvent(type, false, true);
event.initialTarget = initialTarget;
event.tapCount = 0;
return event;
}
private dispatchEvent(event: GestureEvent): void {
if (event.type === EventType.Tap) {
const currentTime = (new Date()).getTime();
let setTapCount = 0;
if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
setTapCount = 1;
} else {
setTapCount = 2;
}
this._lastSetTapCountTime = currentTime;
event.tapCount = setTapCount;
} else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
this._lastSetTapCountTime = 0;
}
if (event.initialTarget instanceof Node) {
for (const ignoreTarget of this.ignoreTargets) {
if (ignoreTarget.contains(event.initialTarget)) {
return;
}
}
const targets: [number, HTMLElement][] = [];
for (const target of this.targets) {
if (target.contains(event.initialTarget)) {
let depth = 0;
let now: Node | null = event.initialTarget;
while (now && now !== target) {
depth++;
now = now.parentElement;
}
targets.push([depth, target]);
}
}
targets.sort((a, b) => a[0] - b[0]);
for (const [_, target] of targets) {
target.dispatchEvent(event);
this.dispatched = true;
}
}
}
private inertia(targetWindow: Window, dispatchTo: readonly EventTarget[], t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
this.handle = DomUtils.scheduleAtNextAnimationFrame(targetWindow, () => {
const now = Date.now();
const deltaT = now - t1;
let delta_pos_x = 0, delta_pos_y = 0;
let stopped = true;
vX += Gesture.SCROLL_FRICTION * deltaT;
vY += Gesture.SCROLL_FRICTION * deltaT;
if (vX > 0) {
stopped = false;
delta_pos_x = dirX * vX * deltaT;
}
if (vY > 0) {
stopped = false;
delta_pos_y = dirY * vY * deltaT;
}
const evt = this.newGestureEvent(EventType.Change);
evt.translationX = delta_pos_x;
evt.translationY = delta_pos_y;
dispatchTo.forEach(d => d.dispatchEvent(evt));
if (!stopped) {
this.inertia(targetWindow, dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
}
});
}
private onTouchMove(e: TouchEvent): void {
const timestamp = Date.now();
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
const touch = e.changedTouches.item(i);
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('end of an UNKNOWN touch', touch);
continue;
}
const data = this.activeTouches[touch.identifier];
const evt = this.newGestureEvent(EventType.Change, data.initialTarget);
evt.translationX = touch.pageX - arrays.tail(data.rollingPageX)!;
evt.translationY = touch.pageY - arrays.tail(data.rollingPageY)!;
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.dispatchEvent(evt);
if (data.rollingPageX.length > 3) {
data.rollingPageX.shift();
data.rollingPageY.shift();
data.rollingTimestamps.shift();
}
data.rollingPageX.push(touch.pageX);
data.rollingPageY.push(touch.pageY);
data.rollingTimestamps.push(timestamp);
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
}
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from './abstractScrollbar';
import { ScrollableElementResolvedOptions } from './scrollableElementOptions';
import { ScrollbarState } from './scrollbarState';
import { INewScrollPosition, Scrollable, ScrollbarVisibility, ScrollEvent } from './scrollable';
export class VerticalScrollbar extends AbstractScrollbar {
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(options.verticalHasArrows ? options.arrowSize : 0),
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
0,
scrollDimensions.height,
scrollDimensions.scrollHeight,
scrollPosition.scrollTop
),
visibility: options.vertical,
extraScrollbarClassName: 'vertical',
scrollable: scrollable,
scrollByPage: options.scrollByPage
});
if (options.verticalHasArrows) {
throw new Error('horizontalHasArrows is not supported in xterm.js');
}
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
this.slider.setHeight(sliderSize);
this.slider.setTop(sliderPosition);
}
protected _renderDomNode(largeSize: number, smallSize: number): void {
this.domNode.setWidth(smallSize);
this.domNode.setHeight(largeSize);
this.domNode.setRight(0);
this.domNode.setTop(0);
}
public onDidScroll(e: ScrollEvent): boolean {
this._shouldRender = this._onElementScrollSize(e.scrollHeight) || this._shouldRender;
this._shouldRender = this._onElementScrollPosition(e.scrollTop) || this._shouldRender;
this._shouldRender = this._onElementSize(e.height) || this._shouldRender;
return this._shouldRender;
}
protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number {
return offsetY;
}
protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageY;
}
protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number {
return e.pageX;
}
protected _updateScrollbarSize(size: number): void {
this.slider.setWidth(size);
}
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
target.scrollTop = scrollPosition;
}
public updateOptions(options: ScrollableElementResolvedOptions): void {
this.updateScrollbarSize(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(0);
this._visibilityController.setVisibility(options.vertical);
this._scrollByPage = options.scrollByPage;
}
}
+57
View File
@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from './dom';
import { IKeyboardEvent, StandardKeyboardEvent } from './keyboardEvent';
import { IMouseEvent, StandardMouseEvent } from './mouseEvent';
import { Gesture } from './touch';
import { Disposable, IDisposable } from './lifecycle';
export abstract class Widget extends Disposable {
protected onclick(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onmousedown(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onmouseover(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onmouseleave(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_LEAVE, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onkeydown(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
}
protected onkeyup(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_UP, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
}
protected oninput(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.INPUT, listener));
}
protected onblur(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.BLUR, listener));
}
protected onfocus(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.FOCUS, listener));
}
protected onchange(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.CHANGE, listener));
}
protected ignoreGesture(domNode: HTMLElement): IDisposable {
return Gesture.ignoreTarget(domNode);
}
}
+14
View File
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export type CodeWindow = Window & typeof globalThis & {
readonly vscodeWindowId: number;
};
export function ensureCodeWindow(targetWindow: Window, fallbackWindowId: number): asserts targetWindow is CodeWindow {
}
// eslint-disable-next-line no-restricted-globals
export const mainWindow = (typeof window === 'object' ? window : globalThis) as CodeWindow;