Merge branch 'master' into fix-caps-lock-ime
This commit is contained in:
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
|
||||
describe('constructor', () => {
|
||||
it('should fill all colors with values', () => {
|
||||
for (const key of Object.keys(cm.colors)) {
|
||||
if (key !== 'ansi' && key !== 'contrastCache') {
|
||||
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok((cm.colors as any)[key].css.length >= 7);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ export class ColorManager implements IColorManager {
|
||||
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||
selectionTransparent: DEFAULT_SELECTION,
|
||||
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
selectionForeground: undefined,
|
||||
ansi: DEFAULT_ANSI_COLORS.slice(),
|
||||
contrastCache: this._contrastCache
|
||||
};
|
||||
@@ -128,6 +129,15 @@ export class ColorManager implements IColorManager {
|
||||
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
|
||||
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
|
||||
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
|
||||
const nullColor: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
|
||||
if (this.colors.selectionForeground === nullColor) {
|
||||
this.colors.selectionForeground = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If selection color is opaque, blend it with background with 0.3 opacity
|
||||
* Issue #2737
|
||||
|
||||
@@ -18,6 +18,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||
protected _currentLink: ILinkWithState | undefined;
|
||||
private _mouseDownLink: ILinkWithState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
private _linkCacheDisposables: IDisposable[] = [];
|
||||
private _lastBufferCell: IBufferCellPosition | undefined;
|
||||
@@ -61,7 +62,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
this._clearCurrentLink();
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
|
||||
}
|
||||
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
@@ -129,7 +131,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
let linkProvided = false;
|
||||
|
||||
// There is no link cached, so ask for one
|
||||
this._linkProviders.forEach((linkProvider, i) => {
|
||||
for (const [i, linkProvider] of this._linkProviders.entries()) {
|
||||
if (useLineCache) {
|
||||
const existingReply = this._activeProviderReplies?.get(i);
|
||||
// If there isn't a reply, the provider hasn't responded yet.
|
||||
@@ -156,7 +158,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _removeIntersectingLinks(y: number, replies: Map<Number, ILinkWithState[] | undefined>): void {
|
||||
@@ -222,18 +224,21 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
return linkProvided;
|
||||
}
|
||||
|
||||
private _onClick(event: MouseEvent): void {
|
||||
private _handleMouseDown(): void {
|
||||
this._mouseDownLink = this._currentLink;
|
||||
}
|
||||
|
||||
private _handleMouseUp(event: MouseEvent): void {
|
||||
if (!this._element || !this._mouseService || !this._currentLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._linkAtPosition(this._currentLink.link, position)) {
|
||||
if (this._mouseDownLink === this._currentLink && this._linkAtPosition(this._currentLink.link, position)) {
|
||||
this._currentLink.link.activate(event, this._currentLink.link.text);
|
||||
}
|
||||
}
|
||||
@@ -303,7 +308,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
|
||||
// Add listener for rerendering
|
||||
if (this._renderService) {
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => {
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
|
||||
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
|
||||
// cleared.
|
||||
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDebouncer } from 'browser/Types';
|
||||
import { IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
*/
|
||||
export class RenderDebouncer implements IRenderDebouncer {
|
||||
export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
private _rowStart: number | undefined;
|
||||
private _rowEnd: number | undefined;
|
||||
private _rowCount: number | undefined;
|
||||
private _animationFrame: number | undefined;
|
||||
private _refreshCallbacks: FrameRequestCallback[] = [];
|
||||
|
||||
constructor(
|
||||
private _renderCallback: (start: number, end: number) => void
|
||||
@@ -26,6 +27,14 @@ export class RenderDebouncer implements IRenderDebouncer {
|
||||
}
|
||||
}
|
||||
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
this._refreshCallbacks.push(callback);
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
return this._animationFrame;
|
||||
}
|
||||
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
@@ -43,8 +52,11 @@ export class RenderDebouncer implements IRenderDebouncer {
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
this._animationFrame = undefined;
|
||||
|
||||
// Make sure values are set
|
||||
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
|
||||
this._runRefreshCallbacks();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,9 +67,16 @@ export class RenderDebouncer implements IRenderDebouncer {
|
||||
// Reset debouncer (this happens before render callback as the render could trigger it again)
|
||||
this._rowStart = undefined;
|
||||
this._rowEnd = undefined;
|
||||
this._animationFrame = undefined;
|
||||
|
||||
// Run render callback
|
||||
this._renderCallback(start, end);
|
||||
this._runRefreshCallbacks();
|
||||
}
|
||||
|
||||
private _runRefreshCallbacks(): void {
|
||||
for (const callback of this._refreshCallbacks) {
|
||||
callback(0);
|
||||
}
|
||||
this._refreshCallbacks = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
const renderer = this._createRenderer();
|
||||
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
|
||||
this._instantiationService.setService(IRenderService, this._renderService);
|
||||
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
|
||||
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
|
||||
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
|
||||
|
||||
this._compositionView = document.createElement('div');
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOpt
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
@@ -16,6 +16,7 @@ import { Terminal } from 'browser/Terminal';
|
||||
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
|
||||
export class TestTerminal extends Terminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
@@ -30,6 +31,7 @@ export class MockTerminal implements ITerminal {
|
||||
public onBlur!: IEvent<void>;
|
||||
public onFocus!: IEvent<void>;
|
||||
public onA11yChar!: IEvent<string>;
|
||||
public onWriteParsed!: IEvent<void>;
|
||||
public onA11yTab!: IEvent<number>;
|
||||
public onCursorMove!: IEvent<void>;
|
||||
public onLineFeed!: IEvent<void>;
|
||||
@@ -370,7 +372,7 @@ export class MockMouseService implements IMouseService {
|
||||
export class MockRenderService implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
public onDimensionsChange: IEvent<IRenderDimensions> = new EventEmitter<IRenderDimensions>().event;
|
||||
public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public dimensions: IRenderDimensions = {
|
||||
@@ -390,15 +392,15 @@ export class MockRenderService implements IRenderService {
|
||||
public refreshRows(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearTextureAtlas(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public resize(cols: number, rows: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public changeOptions(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setRenderer(renderer: IRenderer): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -449,3 +451,54 @@ export class MockCharacterJoinerService implements ICharacterJoinerService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class MockSelectionService implements ISelectionService {
|
||||
public serviceBrand: undefined;
|
||||
public selectionText: string = '';
|
||||
public hasSelection: boolean = false;
|
||||
public selectionStart: [number, number] | undefined;
|
||||
public selectionEnd: [number, number] | undefined;
|
||||
public onLinuxMouseSelection = new EventEmitter<string>().event;
|
||||
public onRequestRedraw = new EventEmitter<ISelectionRedrawRequestEvent>().event;
|
||||
public onRequestScrollLines = new EventEmitter<ISelectionRequestScrollLinesEvent>().event;
|
||||
public onSelectionChange = new EventEmitter<void>().event;
|
||||
public disable(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public enable(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public reset(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setSelection(row: number, col: number, length: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public selectAll(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public selectLines(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearSelection(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public rightClickSelect(event: MouseEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public shouldColumnSelect(event: MouseEvent | KeyboardEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public shouldForceSelection(event: MouseEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public isCellInSelection(x: number, y: number): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -44,6 +44,7 @@ export interface IPublicTerminal extends IDisposable {
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onWriteParsed: IEvent<void>;
|
||||
onTitleChange: IEvent<string>;
|
||||
onBell: IEvent<void>;
|
||||
blur(): void;
|
||||
@@ -120,6 +121,7 @@ export interface IColorSet {
|
||||
selectionTransparent: IColor;
|
||||
/** The selection blended on top of background. */
|
||||
selectionOpaque: IColor;
|
||||
selectionForeground: IColor | undefined;
|
||||
ansi: IColor[];
|
||||
contrastCache: IColorContrastCache;
|
||||
}
|
||||
@@ -309,3 +311,7 @@ export interface ICharacterJoiner {
|
||||
export interface IRenderDebouncer extends IDisposable {
|
||||
refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;
|
||||
}
|
||||
|
||||
export interface IRenderDebouncerWithCallback extends IRenderDebouncer {
|
||||
addRefreshCallback(callback: FrameRequestCallback): number;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
private _lastTouchY: number = 0;
|
||||
private _lastScrollTop: number = 0;
|
||||
private _lastHadScrollBar: boolean = false;
|
||||
private _activeBuffer: IBuffer;
|
||||
private _renderDimensions: IRenderDimensions;
|
||||
|
||||
@@ -54,7 +53,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this._lastHadScrollBar = true;
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
|
||||
// Track properties used in performance critical code manually to avoid using slow getters
|
||||
@@ -109,17 +107,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
// Update scroll bar width
|
||||
if (this._optionsService.rawOptions.scrollback === 0) {
|
||||
this.scrollBarWidth = 0;
|
||||
} else {
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
}
|
||||
this._lastHadScrollBar = this.scrollBarWidth > 0;
|
||||
|
||||
const elementStyle = window.getComputedStyle(this._element);
|
||||
const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight);
|
||||
this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px';
|
||||
this._refreshAnimationFrame = null;
|
||||
}
|
||||
|
||||
@@ -151,11 +138,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the scroll bar visibility changed
|
||||
if (this._lastHadScrollBar !== (this._optionsService.rawOptions.scrollback > 0)) {
|
||||
this._refresh(immediate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
|
||||
private _animationFrame: number | undefined;
|
||||
private _altBufferIsActive: boolean = false;
|
||||
private _dimensionsChanged: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _screenElement: HTMLElement,
|
||||
@@ -27,8 +28,11 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
this._container.classList.add('xterm-decoration-container');
|
||||
this._screenElement.appendChild(this._container);
|
||||
|
||||
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this._queueRefresh()));
|
||||
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
|
||||
this.register(this._renderService.onDimensionsChange(() => {
|
||||
this._dimensionsChanged = true;
|
||||
this._queueRefresh();
|
||||
}));
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh()));
|
||||
this.register(this._bufferService.buffers.onBufferActivate(() => {
|
||||
this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt;
|
||||
@@ -47,7 +51,7 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
if (this._animationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._renderService.addRefreshCallback(() => {
|
||||
this.refreshDecorations();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
@@ -57,10 +61,14 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
for (const decoration of this._decorationService.decorations) {
|
||||
this._renderDecoration(decoration);
|
||||
}
|
||||
this._dimensionsChanged = false;
|
||||
}
|
||||
|
||||
private _renderDecoration(decoration: IInternalDecoration): void {
|
||||
this._refreshStyle(decoration);
|
||||
if (this._dimensionsChanged) {
|
||||
this._refreshXPosition(decoration);
|
||||
}
|
||||
}
|
||||
|
||||
private _createElement(decoration: IInternalDecoration): HTMLElement {
|
||||
@@ -76,11 +84,7 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
// exceeded the container width, so hide
|
||||
element.style.display = 'none';
|
||||
}
|
||||
if ((decoration.options.anchor || 'left') === 'right') {
|
||||
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
} else {
|
||||
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
}
|
||||
this._refreshXPosition(decoration, element);
|
||||
|
||||
return element;
|
||||
}
|
||||
@@ -108,6 +112,18 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshXPosition(decoration: IInternalDecoration, element: HTMLElement | undefined = decoration.element): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const x = decoration.options.x ?? 0;
|
||||
if ((decoration.options.anchor || 'left') === 'right') {
|
||||
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
} else {
|
||||
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
}
|
||||
}
|
||||
|
||||
private _removeDecoration(decoration: IInternalDecoration): void {
|
||||
this._decorationElements.get(decoration)?.remove();
|
||||
this._decorationElements.delete(decoration);
|
||||
|
||||
@@ -82,7 +82,7 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
* and hide the canvas if the alt buffer is active
|
||||
*/
|
||||
private _registerBufferChangeListeners(): void {
|
||||
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
|
||||
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
|
||||
this.register(this._bufferService.buffers.onBufferActivate(() => {
|
||||
this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
|
||||
}));
|
||||
|
||||
@@ -11,30 +11,38 @@ const CHAR_WIDTH = 10;
|
||||
const CHAR_HEIGHT = 20;
|
||||
|
||||
describe('Mouse getCoords', () => {
|
||||
let windowOverride: Pick<Window, 'getComputedStyle'>;
|
||||
let document: Document;
|
||||
|
||||
beforeEach(() => {
|
||||
windowOverride = {
|
||||
getComputedStyle(): any {
|
||||
return {
|
||||
getPropertyValue: () => '0px'
|
||||
} as Pick<CSSStyleDeclaration, 'getPropertyValue'>;
|
||||
}
|
||||
};
|
||||
document = new jsdom.JSDOM('').window.document;
|
||||
});
|
||||
|
||||
it('should return the cell that was clicked', () => {
|
||||
let coords: [number, number] | undefined;
|
||||
coords = getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 2]);
|
||||
coords = getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [2, 1]);
|
||||
});
|
||||
|
||||
it('should ensure the coordinates are returned within the terminal bounds', () => {
|
||||
let coords: [number, number] | undefined;
|
||||
coords = getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
// Event are double the cols/rows
|
||||
coords = getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
|
||||
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return [event.clientX - rect.left, event.clientY - rect.top];
|
||||
const elementStyle = window.getComputedStyle(element);
|
||||
const leftPadding = parseInt(elementStyle.getPropertyValue('padding-left'));
|
||||
const topPadding = parseInt(elementStyle.getPropertyValue('padding-top'));
|
||||
return [
|
||||
event.clientX - rect.left - leftPadding,
|
||||
event.clientY - rect.top - topPadding
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,13 +26,13 @@ export function getCoordsRelativeToElement(event: {clientX: number, clientY: num
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
// Coordinates cannot be measured if there are no valid
|
||||
if (!hasValidCharSize) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const coords = getCoordsRelativeToElement(event, element);
|
||||
const coords = getCoordsRelativeToElement(window, event, element);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export class Terminal implements ITerminalApi {
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get parser(): IParser {
|
||||
|
||||
@@ -29,6 +29,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
|
||||
private _selectionStart: [number, number] | undefined;
|
||||
private _selectionEnd: [number, number] | undefined;
|
||||
private _columnSelectMode: boolean = false;
|
||||
|
||||
protected _charAtlas: BaseCharAtlas | undefined;
|
||||
|
||||
/**
|
||||
@@ -80,7 +84,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
public onFocus(): void {}
|
||||
public onCursorMove(): void {}
|
||||
public onGridChanged(startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._selectionStart = start;
|
||||
this._selectionEnd = end;
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public setColors(colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(colorSet);
|
||||
@@ -457,6 +466,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
|
||||
fgOverride = this._colors.selectionForeground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -546,5 +562,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
return this._colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._selectionStart;
|
||||
const end = this._selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
if (this._columnSelectMode) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y < end[1];
|
||||
}
|
||||
return (y > start[1] && y < end[1]) ||
|
||||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === start[1] && x >= start[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,10 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
|
||||
// Selection foreground requires a full re-render
|
||||
if (this._colors.selectionForeground) {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
|
||||
@@ -54,6 +54,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
super.onSelectionChanged(start, end, columnSelectMode);
|
||||
|
||||
// Selection has not changed
|
||||
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
|
||||
return;
|
||||
|
||||
@@ -281,6 +281,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
|
||||
this.renderRows(0, this._bufferService.rows - 1);
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
return;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { css } from 'common/Color';
|
||||
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
|
||||
import { MockCharacterJoinerService, MockSelectionService } from 'browser/TestUtils.test';
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
|
||||
@@ -7,10 +7,10 @@ import { IBufferLine, ICellData, IColor } from 'common/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { color, rgba } from 'common/Color';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
|
||||
@@ -28,6 +28,10 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
|
||||
export class DomRendererRowFactory {
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _selectionStart: [number, number] | undefined;
|
||||
private _selectionEnd: [number, number] | undefined;
|
||||
private _columnSelectMode: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
private _colors: IColorSet,
|
||||
@@ -42,6 +46,12 @@ export class DomRendererRowFactory {
|
||||
this._colors = colors;
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
this._selectionStart = start;
|
||||
this._selectionEnd = end;
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
|
||||
@@ -195,6 +205,20 @@ export class DomRendererRowFactory {
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && this._isCellInSelection(x, row)) {
|
||||
fgColorMode = Attributes.CM_RGB;
|
||||
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
fgOverride = this._colors.selectionForeground;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a top decoration, render above the selection
|
||||
if (isTop) {
|
||||
charElement.classList.add(`xterm-decoration-top`);
|
||||
}
|
||||
|
||||
// Foreground
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
@@ -278,6 +302,22 @@ export class DomRendererRowFactory {
|
||||
private _addStyle(element: HTMLElement, style: string): void {
|
||||
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
|
||||
}
|
||||
|
||||
private _isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._selectionStart;
|
||||
const end = this._selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
if (this._columnSelectMode) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y < end[1];
|
||||
}
|
||||
return (y > start[1] && y < end[1]) ||
|
||||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === start[1] && x >= start[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function padStart(text: string, padChar: string, length: number): string {
|
||||
|
||||
@@ -17,6 +17,7 @@ export class MouseService implements IMouseService {
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IRenderDebouncer } from 'browser/Types';
|
||||
import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
@@ -22,7 +22,7 @@ interface ISelectionState {
|
||||
export class RenderService extends Disposable implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _renderDebouncer: IRenderDebouncer;
|
||||
private _renderDebouncer: IRenderDebouncerWithCallback;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _isPaused: boolean = false;
|
||||
@@ -39,8 +39,8 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
private _onDimensionsChange = new EventEmitter<IRenderDimensions>();
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; }
|
||||
private _onRenderedBufferChange = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRenderedBufferChange.event; }
|
||||
private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
|
||||
@@ -70,7 +70,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
this.register(bufferService.onResize(() => this._fullRefresh()));
|
||||
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
|
||||
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
|
||||
|
||||
// Do a full refresh whenever any decoration is added or removed. This may not actually result
|
||||
@@ -131,7 +131,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
// Fire render event only if it was not a redraw
|
||||
if (!this._isNextRenderRedrawOnly) {
|
||||
this._onRenderedBufferChange.fire({ start, end });
|
||||
this._onRenderedViewportChange.fire({ start, end });
|
||||
}
|
||||
this._onRender.fire({ start, end });
|
||||
this._isNextRenderRedrawOnly = true;
|
||||
@@ -142,7 +142,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._fireOnCanvasResize();
|
||||
}
|
||||
|
||||
public changeOptions(): void {
|
||||
private _handleOptionsChanged(): void {
|
||||
this._renderer.onOptionsChanged();
|
||||
this.refreshRows(0, this._rowCount - 1);
|
||||
this._fireOnCanvasResize();
|
||||
@@ -171,6 +171,10 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this._fullRefresh();
|
||||
}
|
||||
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
return this._renderDebouncer.addRefreshCallback(callback);
|
||||
}
|
||||
|
||||
private _fullRefresh(): void {
|
||||
if (this._isPaused) {
|
||||
this._needsFullRefresh = true;
|
||||
|
||||
@@ -308,6 +308,15 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
return this._areCoordsInSelection(coords, start, end);
|
||||
}
|
||||
|
||||
public isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return this._areCoordsInSelection([x, y], start, end);
|
||||
}
|
||||
|
||||
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
|
||||
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
||||
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
|
||||
@@ -393,7 +402,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = getCoordsRelativeToElement(event, this._screenElement)[1];
|
||||
let offset = getCoordsRelativeToElement(window, event, this._screenElement)[1];
|
||||
const terminalHeight = this._renderService.dimensions.canvasHeight;
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
@@ -771,6 +780,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._model.selectionStart = [col, row];
|
||||
this._model.selectionStartLength = length;
|
||||
this.refresh();
|
||||
this._fireEventIfSelectionChanged();
|
||||
}
|
||||
|
||||
public rightClickSelect(ev: MouseEvent): void {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, IRenderDebouncer } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { IDisposable } from 'common/Types';
|
||||
@@ -49,7 +49,7 @@ export interface IRenderService extends IDisposable {
|
||||
* Fires when buffer changes are rendered. This does not fire when only cursor
|
||||
* or selections are rendered.
|
||||
*/
|
||||
onRenderedBufferChange: IEvent<{ start: number, end: number }>;
|
||||
onRenderedViewportChange: IEvent<{ start: number, end: number }>;
|
||||
/**
|
||||
* Fires on render
|
||||
*/
|
||||
@@ -58,10 +58,11 @@ export interface IRenderService extends IDisposable {
|
||||
|
||||
dimensions: IRenderDimensions;
|
||||
|
||||
addRefreshCallback(callback: FrameRequestCallback): number;
|
||||
|
||||
refreshRows(start: number, end: number): void;
|
||||
clearTextureAtlas(): void;
|
||||
resize(cols: number, rows: number): void;
|
||||
changeOptions(): void;
|
||||
setRenderer(renderer: IRenderer): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
@@ -101,6 +102,7 @@ export interface ISelectionService {
|
||||
shouldForceSelection(event: MouseEvent): boolean;
|
||||
refresh(isLinuxMouseSelection?: boolean): void;
|
||||
onMouseDown(event: MouseEvent): void;
|
||||
isCellInSelection(x: number, y: number): boolean;
|
||||
}
|
||||
|
||||
export const ISoundService = createDecorator<ISoundService>('SoundService');
|
||||
|
||||
@@ -153,6 +153,44 @@ describe('Color', () => {
|
||||
|
||||
describe('css', () => {
|
||||
describe('toColor', () => {
|
||||
it('should convert the #rgb format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('#000'), { css: '#000000', rgba: 0x000000FF });
|
||||
assert.deepEqual(css.toColor('#111'), { css: '#111111', rgba: 0x111111FF });
|
||||
assert.deepEqual(css.toColor('#222'), { css: '#222222', rgba: 0x222222FF });
|
||||
assert.deepEqual(css.toColor('#333'), { css: '#333333', rgba: 0x333333FF });
|
||||
assert.deepEqual(css.toColor('#444'), { css: '#444444', rgba: 0x444444FF });
|
||||
assert.deepEqual(css.toColor('#555'), { css: '#555555', rgba: 0x555555FF });
|
||||
assert.deepEqual(css.toColor('#666'), { css: '#666666', rgba: 0x666666FF });
|
||||
assert.deepEqual(css.toColor('#777'), { css: '#777777', rgba: 0x777777FF });
|
||||
assert.deepEqual(css.toColor('#888'), { css: '#888888', rgba: 0x888888FF });
|
||||
assert.deepEqual(css.toColor('#999'), { css: '#999999', rgba: 0x999999FF });
|
||||
assert.deepEqual(css.toColor('#aaa'), { css: '#aaaaaa', rgba: 0xaaaaaaFF });
|
||||
assert.deepEqual(css.toColor('#bbb'), { css: '#bbbbbb', rgba: 0xbbbbbbFF });
|
||||
assert.deepEqual(css.toColor('#ccc'), { css: '#cccccc', rgba: 0xccccccFF });
|
||||
assert.deepEqual(css.toColor('#ddd'), { css: '#dddddd', rgba: 0xddddddFF });
|
||||
assert.deepEqual(css.toColor('#eee'), { css: '#eeeeee', rgba: 0xeeeeeeFF });
|
||||
assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF });
|
||||
assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF });
|
||||
});
|
||||
it('should convert the #rgb format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('#0000'), { css: '#00000000', rgba: 0x00000000 });
|
||||
assert.deepEqual(css.toColor('#1111'), { css: '#11111111', rgba: 0x11111111 });
|
||||
assert.deepEqual(css.toColor('#2222'), { css: '#22222222', rgba: 0x22222222 });
|
||||
assert.deepEqual(css.toColor('#3333'), { css: '#33333333', rgba: 0x33333333 });
|
||||
assert.deepEqual(css.toColor('#4444'), { css: '#44444444', rgba: 0x44444444 });
|
||||
assert.deepEqual(css.toColor('#5555'), { css: '#55555555', rgba: 0x55555555 });
|
||||
assert.deepEqual(css.toColor('#6666'), { css: '#66666666', rgba: 0x66666666 });
|
||||
assert.deepEqual(css.toColor('#7777'), { css: '#77777777', rgba: 0x77777777 });
|
||||
assert.deepEqual(css.toColor('#8888'), { css: '#88888888', rgba: 0x88888888 });
|
||||
assert.deepEqual(css.toColor('#9999'), { css: '#99999999', rgba: 0x99999999 });
|
||||
assert.deepEqual(css.toColor('#aaaa'), { css: '#aaaaaaaa', rgba: 0xaaaaaaaa });
|
||||
assert.deepEqual(css.toColor('#bbbb'), { css: '#bbbbbbbb', rgba: 0xbbbbbbbb });
|
||||
assert.deepEqual(css.toColor('#cccc'), { css: '#cccccccc', rgba: 0xcccccccc });
|
||||
assert.deepEqual(css.toColor('#dddd'), { css: '#dddddddd', rgba: 0xdddddddd });
|
||||
assert.deepEqual(css.toColor('#eeee'), { css: '#eeeeeeee', rgba: 0xeeeeeeee });
|
||||
assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
it('should convert the #rrggbb format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF });
|
||||
assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF });
|
||||
@@ -191,6 +229,20 @@ describe('Color', () => {
|
||||
assert.deepEqual(css.toColor('#f0f0f0f0'), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
|
||||
assert.deepEqual(css.toColor('#ffffffff'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
it('should convert the rgb() format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('rgb(0, 0, 0)'), { css: '#000000ff', rgba: 0x000000ff });
|
||||
assert.deepEqual(css.toColor('rgb(80, 0, 0)'), { css: '#500000ff', rgba: 0x500000ff });
|
||||
assert.deepEqual(css.toColor('rgb(0, 80, 0)'), { css: '#005000ff', rgba: 0x005000ff });
|
||||
assert.deepEqual(css.toColor('rgb(0, 0, 80)'), { css: '#000050ff', rgba: 0x000050ff });
|
||||
assert.deepEqual(css.toColor('rgb(255, 255, 255)'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
it('should convert the rgba() format to an IColor', () => {
|
||||
assert.deepEqual(css.toColor('rgba(0, 0, 0, 0)'), { css: '#00000000', rgba: 0x00000000 });
|
||||
assert.deepEqual(css.toColor('rgba(80, 0, 0, 0.5)'), { css: '#50000080', rgba: 0x50000080 });
|
||||
assert.deepEqual(css.toColor('rgba(0, 80, 0, 0.5)'), { css: '#00500080', rgba: 0x00500080 });
|
||||
assert.deepEqual(css.toColor('rgba(0, 0, 80, 0.5)'), { css: '#00005080', rgba: 0x00005080 });
|
||||
assert.deepEqual(css.toColor('rgba(255, 255, 255, 1)'), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+66
-16
@@ -94,17 +94,40 @@ export namespace color {
|
||||
*/
|
||||
export namespace css {
|
||||
export function toColor(css: string): IColor {
|
||||
switch (css.length) {
|
||||
case 7: // #rrggbb
|
||||
return {
|
||||
css,
|
||||
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
|
||||
};
|
||||
case 9: // #rrggbbaa
|
||||
return {
|
||||
css,
|
||||
rgba: parseInt(css.slice(1), 16) >>> 0
|
||||
};
|
||||
if (css.match(/#[0-9a-f]{3,8}/i)) {
|
||||
switch (css.length) {
|
||||
case 4: { // #rgb
|
||||
const r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
const g = parseInt(css.slice(2, 3).repeat(2), 16);
|
||||
const b = parseInt(css.slice(3, 4).repeat(2), 16);
|
||||
return rgba.toColor(r, g, b);
|
||||
}
|
||||
case 5: { // #rgba
|
||||
const r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
const g = parseInt(css.slice(2, 3).repeat(2), 16);
|
||||
const b = parseInt(css.slice(3, 4).repeat(2), 16);
|
||||
const a = parseInt(css.slice(4, 5).repeat(2), 16);
|
||||
return rgba.toColor(r, g, b, a);
|
||||
}
|
||||
case 7: // #rrggbb
|
||||
return {
|
||||
css,
|
||||
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
|
||||
};
|
||||
case 9: // #rrggbbaa
|
||||
return {
|
||||
css,
|
||||
rgba: parseInt(css.slice(1), 16) >>> 0
|
||||
};
|
||||
}
|
||||
}
|
||||
const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);
|
||||
if (rgbaMatch) { // rgb() or rgba()
|
||||
const r = parseInt(rgbaMatch[1]);
|
||||
const g = parseInt(rgbaMatch[2]);
|
||||
const b = parseInt(rgbaMatch[3]);
|
||||
const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
|
||||
return rgba.toColor(r, g, b, a);
|
||||
}
|
||||
throw new Error('css.toColor: Unsupported css format');
|
||||
}
|
||||
@@ -150,15 +173,42 @@ export namespace rgb {
|
||||
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
|
||||
*/
|
||||
export namespace rgba {
|
||||
/**
|
||||
* Given a foreground color and a background color, either increase or reduce the luminance of the
|
||||
* foreground color until the specified contrast ratio is met. If pure white or black is hit
|
||||
* without the contrast ratio being met, go the other direction using the background color as the
|
||||
* foreground color and take either the first or second result depending on which has the higher
|
||||
* contrast ratio.
|
||||
*
|
||||
* `undefined` will be returned if the contrast ratio is already met.
|
||||
*
|
||||
* @param bgRgba The background color in rgba format.
|
||||
* @param fgRgba The foreground color in rgba format.
|
||||
* @param ratio The contrast ratio to achieve.
|
||||
*/
|
||||
export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
|
||||
const bgL = rgb.relativeLuminance(bgRgba >> 8);
|
||||
const fgL = rgb.relativeLuminance(fgRgba >> 8);
|
||||
const cr = contrastRatio(bgL, fgL);
|
||||
if (cr < ratio) {
|
||||
if (fgL < bgL) {
|
||||
return reduceLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultA = reduceLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
|
||||
if (resultARatio < ratio) {
|
||||
const resultB = increaseLuminance(bgRgba, bgRgba, ratio);
|
||||
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
|
||||
return resultARatio > resultBRatio ? resultA : resultB;
|
||||
}
|
||||
return resultA;
|
||||
}
|
||||
return increaseLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultA = increaseLuminance(bgRgba, fgRgba, ratio);
|
||||
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
|
||||
if (resultARatio < ratio) {
|
||||
const resultB = reduceLuminance(bgRgba, bgRgba, ratio);
|
||||
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
|
||||
return resultARatio > resultBRatio ? resultA : resultB;
|
||||
}
|
||||
return resultA;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -208,10 +258,10 @@ export namespace rgba {
|
||||
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
|
||||
}
|
||||
|
||||
export function toColor(r: number, g: number, b: number): IColor {
|
||||
export function toColor(r: number, g: number, b: number, a?: number): IColor {
|
||||
return {
|
||||
css: channels.toCss(r, g, b),
|
||||
rgba: channels.toRgba(r, g, b)
|
||||
css: channels.toCss(r, g, b, a),
|
||||
rgba: channels.toRgba(r, g, b, a)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
protected _onScroll = new EventEmitter<IScrollEvent, void>();
|
||||
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
|
||||
protected _onWriteParsed = new EventEmitter<void>();
|
||||
/**
|
||||
* Internally we track the source of the scroll but this is meaningless outside the library so
|
||||
* it's filtered out.
|
||||
@@ -138,6 +140,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
|
||||
// Setup WriteBuffer
|
||||
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
|
||||
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
|
||||
declare const setTimeout: (handler: () => void, timeout?: number) => void;
|
||||
|
||||
/**
|
||||
@@ -44,6 +46,8 @@ export class WriteBuffer {
|
||||
private _bufferOffset = 0;
|
||||
private _isSyncWriting = false;
|
||||
private _syncCalls = 0;
|
||||
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
|
||||
private _onWriteParsed = new EventEmitter<void>();
|
||||
|
||||
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
|
||||
|
||||
@@ -220,5 +224,6 @@ export class WriteBuffer {
|
||||
this._pendingData = 0;
|
||||
this._bufferOffset = 0;
|
||||
}
|
||||
this._onWriteParsed.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,7 @@ export interface ITheme {
|
||||
cursor?: string;
|
||||
cursorAccent?: string;
|
||||
selection?: string;
|
||||
selectionForeground?: string;
|
||||
black?: string;
|
||||
red?: string;
|
||||
green?: string;
|
||||
|
||||
Reference in New Issue
Block a user