Move all impl to scrollable/
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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' : ''));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user