mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into fix-caps-lock-ime
This commit is contained in:
@@ -63,6 +63,9 @@ export class FitAddon implements ITerminalAddon {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollbarWidth = this._terminal.options.scrollback === 0 ?
|
||||
0 : core.viewport.scrollBarWidth;
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
@@ -76,7 +79,7 @@ export class FitAddon implements ITerminalAddon {
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
||||
|
||||
@@ -77,7 +77,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._onDataDisposable = this._terminal.onData(() => this._updateMatches());
|
||||
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
|
||||
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
|
||||
this._highlightTimeout = setTimeout(() => {
|
||||
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
this._resultIndex = this._searchResults ? this._searchResults.size -1 : -1;
|
||||
this._onDidChangeResults.fire({ resultIndex: this._searchResults ? this._searchResults.size - 1 : -1, resultCount: this._searchResults ? this._searchResults.size : -1 });
|
||||
}, 200);
|
||||
}
|
||||
@@ -679,7 +680,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
color: options.activeMatchColorOverviewRuler
|
||||
}
|
||||
});
|
||||
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder));
|
||||
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
|
||||
this._selectedDecoration?.onDispose(() => marker.dispose());
|
||||
}
|
||||
}
|
||||
@@ -702,7 +703,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* @param borderColor the border color to apply
|
||||
* @returns
|
||||
*/
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined): void {
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
|
||||
if (element.clientWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -712,6 +713,9 @@ export class SearchAddon implements ITerminalAddon {
|
||||
element.style.outline = `1px solid ${borderColor}`;
|
||||
}
|
||||
}
|
||||
if (isActiveResult) {
|
||||
element.classList.add('xterm-find-active-result-decoration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -736,7 +740,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
position: 'center'
|
||||
}
|
||||
});
|
||||
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder));
|
||||
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
|
||||
findResultDecoration?.onDispose(() => marker.dispose());
|
||||
return findResultDecoration;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -167,7 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this._rectangleRenderer.onResize();
|
||||
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
|
||||
@@ -398,6 +397,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// Apply the selection color if needed
|
||||
if (this._isCellSelected(x, y)) {
|
||||
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
|
||||
if (this._colors.selectionForeground) {
|
||||
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
|
||||
@@ -23,6 +23,7 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number
|
||||
cursorAccent: NULL_COLOR,
|
||||
selectionTransparent: NULL_COLOR,
|
||||
selectionOpaque: NULL_COLOR,
|
||||
selectionForeground: NULL_COLOR,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice(),
|
||||
|
||||
@@ -875,6 +875,25 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
|
||||
itWebgl('transparent background inverse', async () => {
|
||||
const theme: ITheme = {
|
||||
selectionForeground: '#ff0000'
|
||||
};
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await page.evaluate(`window.term.selectAll()`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decoration color overrides', async () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
|
||||
@@ -183,3 +183,8 @@
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"nyc": "^15.1.0",
|
||||
"playwright": "^1.16.2",
|
||||
"playwright": "^1.22.1",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-loader": "^9.1.2",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user