Merge branch 'master' into 731_screen_reader
This commit is contained in:
+4
-1
@@ -213,7 +213,10 @@ export class Buffer implements IBuffer {
|
||||
// needed here because some chars are 0 characters long (eg. after wide
|
||||
// chars) and some chars are longer than 1 characters long (eg. emojis).
|
||||
let startIndex = startCol;
|
||||
endCol = endCol || line.length;
|
||||
// Only set endCol to the line length when it is null. 0 is a valid column.
|
||||
if (endCol === null) {
|
||||
endCol = line.length;
|
||||
}
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
|
||||
+22
-12
@@ -63,28 +63,33 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
let min = 0;
|
||||
let max = data.length - 1;
|
||||
let mid;
|
||||
if (ucs < data[0][0] || ucs > data[max][1])
|
||||
if (ucs < data[0][0] || ucs > data[max][1]) {
|
||||
return false;
|
||||
}
|
||||
while (max >= min) {
|
||||
mid = (min + max) >> 1;
|
||||
if (ucs > data[mid][1])
|
||||
if (ucs > data[mid][1]) {
|
||||
min = mid + 1;
|
||||
else if (ucs < data[mid][0])
|
||||
} else if (ucs < data[mid][0]) {
|
||||
max = mid - 1;
|
||||
else
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function wcwidthBMP(ucs: number): number {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0)
|
||||
if (ucs === 0) {
|
||||
return opts.nul;
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
|
||||
}
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
|
||||
return opts.control;
|
||||
}
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs, COMBINING_BMP))
|
||||
if (bisearch(ucs, COMBINING_BMP)) {
|
||||
return 0;
|
||||
}
|
||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||
if (isWideBMP(ucs)) {
|
||||
return 2;
|
||||
@@ -106,8 +111,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6)));
|
||||
}
|
||||
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
|
||||
if (bisearch(ucs, COMBINING_HIGH))
|
||||
if (bisearch(ucs, COMBINING_HIGH)) {
|
||||
return 0;
|
||||
}
|
||||
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
||||
return 2;
|
||||
}
|
||||
@@ -128,8 +134,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
for (let i = 0; i < CONTAINERSIZE; ++i) {
|
||||
let num = 0;
|
||||
let pos = CODEPOINTS_PER_ITEM;
|
||||
while (pos--)
|
||||
while (pos--) {
|
||||
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
|
||||
}
|
||||
table[i] = num;
|
||||
}
|
||||
return table;
|
||||
@@ -148,13 +155,16 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
|
||||
return function (num: number): number {
|
||||
num = num | 0; // get asm.js like optimization under V8
|
||||
if (num < 32)
|
||||
if (num < 32) {
|
||||
return control | 0;
|
||||
if (num < 127)
|
||||
}
|
||||
if (num < 127) {
|
||||
return 1;
|
||||
}
|
||||
let t = table || init_table();
|
||||
if (num < 65536)
|
||||
if (num < 65536) {
|
||||
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
||||
}
|
||||
// do a full search for high codepoints
|
||||
return wcwidthHigh(num);
|
||||
};
|
||||
|
||||
+2
-1
@@ -73,8 +73,9 @@ export class InputHandler implements IInputHandler {
|
||||
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
|
||||
}
|
||||
} else {
|
||||
if (chWidth === 2) // FIXME: check for xterm behavior
|
||||
if (chWidth === 2) { // FIXME: check for xterm behavior
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
|
||||
@@ -150,6 +150,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
validationCallback: options.validationCallback,
|
||||
hoverTooltipCallback: options.tooltipCallback,
|
||||
hoverLeaveCallback: options.leaveCallback,
|
||||
willLinkActivate: options.willLinkActivate,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
@@ -290,6 +291,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
},
|
||||
e => {
|
||||
if (matcher.willLinkActivate) {
|
||||
return matcher.willLinkActivate(e, uri);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
+2
-1
@@ -224,8 +224,9 @@ export class Parser {
|
||||
ch += data.charAt(this._position + 1);
|
||||
}
|
||||
// surrogate low - already handled above
|
||||
if (0xDC00 <= code && code <= 0xDFFF)
|
||||
if (0xDC00 <= code && code <= 0xDFFF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (this._state) {
|
||||
case ParserState.NORMAL:
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestSelectionManager extends SelectionManager {
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
||||
|
||||
// Disable DOM interaction
|
||||
public enable(): void {}
|
||||
|
||||
+59
-8
@@ -11,6 +11,7 @@ import { CircularList } from './utils/CircularList';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
@@ -28,6 +29,12 @@ const DRAG_SCROLL_MAX_SPEED = 15;
|
||||
*/
|
||||
const DRAG_SCROLL_INTERVAL = 50;
|
||||
|
||||
/**
|
||||
* The maximum amount of time that can have elapsed for an alt click to move the
|
||||
* cursor.
|
||||
*/
|
||||
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
@@ -96,6 +103,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: XtermListener;
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _charMeasure: CharMeasure
|
||||
@@ -244,6 +253,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current click was inside the current selection
|
||||
* @param event The mouse event
|
||||
*/
|
||||
public isClickInSelection(event: MouseEvent): boolean {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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]) ||
|
||||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects word at the current mouse event coordinates.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
public selectWordAtCursor(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._selectWordAt(coords, false);
|
||||
this._model.selectionEnd = null;
|
||||
this.refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all text within the terminal.
|
||||
*/
|
||||
@@ -269,7 +309,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
|
||||
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
@@ -289,7 +329,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
|
||||
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
|
||||
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
@@ -317,6 +357,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mousedown event.
|
||||
*/
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
this._mouseDownTimeStamp = event.timeStamp;
|
||||
// If we have selection, we want the context menu on right click even if the
|
||||
// terminal is in mouse mode.
|
||||
if (event.button === 2 && this.hasSelection) {
|
||||
@@ -435,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords);
|
||||
this._selectWordAt(coords, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,10 +577,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent): void {
|
||||
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
||||
|
||||
this._removeMouseDownListeners();
|
||||
|
||||
if (this.hasSelection)
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
|
||||
(new AltClickHandler(event, this._terminal)).move();
|
||||
} else if (this.hasSelection) {
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
}
|
||||
|
||||
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
|
||||
@@ -587,7 +633,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
||||
const bufferLine = this._buffer.lines.get(coords[1]);
|
||||
if (!bufferLine) {
|
||||
return null;
|
||||
@@ -693,15 +739,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
|
||||
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
|
||||
|
||||
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the word at the coordinates specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
* @param allowWhitespaceOnlySelection If whitespace should be selected
|
||||
*/
|
||||
protected _selectWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||
if (wordPosition) {
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
@@ -713,7 +764,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
const wordPosition = this._getWordAt(coords, true);
|
||||
if (wordPosition) {
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
}
|
||||
|
||||
+27
-21
@@ -89,8 +89,10 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
cancelEvents: false,
|
||||
disableStdin: false,
|
||||
useFlowControl: false,
|
||||
allowTransparency: false,
|
||||
tabStopWidth: 8,
|
||||
theme: null
|
||||
theme: null,
|
||||
rightClickSelectsWord: Browser.isMac
|
||||
// programFeatures: false,
|
||||
// focusKeys: false,
|
||||
};
|
||||
@@ -98,6 +100,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
|
||||
public textarea: HTMLTextAreaElement;
|
||||
public element: HTMLElement;
|
||||
public screenElement: HTMLElement;
|
||||
|
||||
/**
|
||||
* The HTMLElement that the terminal is created in, set by Terminal.open.
|
||||
@@ -530,12 +533,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// Firefox doesn't appear to fire the contextmenu event on right click
|
||||
on(this.element, 'mousedown', (event: MouseEvent) => {
|
||||
if (event.button === 2) {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
on(this.element, 'contextmenu', (event: MouseEvent) => {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -627,15 +630,18 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.viewportScrollArea.classList.add('xterm-scroll-area');
|
||||
this.viewportElement.appendChild(this.viewportScrollArea);
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this);
|
||||
this.on('scroll', () => this._mouseZoneManager.clearAll());
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
this.screenElement = document.createElement('div');
|
||||
this.screenElement.classList.add('xterm-screen');
|
||||
// Create the container that will hold helpers like the textarea for
|
||||
// capturing DOM Events. Then produce the helpers.
|
||||
this.helperContainer = document.createElement('div');
|
||||
this.helperContainer.classList.add('xterm-helpers');
|
||||
fragment.appendChild(this.helperContainer);
|
||||
this.screenElement.appendChild(this.helperContainer);
|
||||
fragment.appendChild(this.screenElement);
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this);
|
||||
this.on('scroll', () => this._mouseZoneManager.clearAll());
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
this.textarea = document.createElement('textarea');
|
||||
this.textarea.classList.add('xterm-helper-textarea');
|
||||
@@ -766,7 +772,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
button = getButton(ev);
|
||||
|
||||
// get mouse coordinates
|
||||
pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
sendEvent(button, pos);
|
||||
@@ -792,7 +798,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
|
||||
function sendMove(ev: MouseEvent): void {
|
||||
let button = pressed;
|
||||
let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
// buttons marked as motions
|
||||
@@ -1613,28 +1619,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
break;
|
||||
case 36:
|
||||
// home
|
||||
if (modifiers)
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
|
||||
else if (this.applicationCursor)
|
||||
} else if (this.applicationCursor) {
|
||||
result.key = C0.ESC + 'OH';
|
||||
else
|
||||
} else {
|
||||
result.key = C0.ESC + '[H';
|
||||
}
|
||||
break;
|
||||
case 35:
|
||||
// end
|
||||
if (modifiers)
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
|
||||
else if (this.applicationCursor)
|
||||
} else if (this.applicationCursor) {
|
||||
result.key = C0.ESC + 'OF';
|
||||
else
|
||||
} else {
|
||||
result.key = C0.ESC + '[F';
|
||||
}
|
||||
break;
|
||||
case 33:
|
||||
// page up
|
||||
if (ev.shiftKey) {
|
||||
result.scrollLines = -(this.rows - 1);
|
||||
} else if (modifiers) {
|
||||
result.key = C0.ESC + '[5;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[5~';
|
||||
}
|
||||
@@ -1643,8 +1649,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
// page down
|
||||
if (ev.shiftKey) {
|
||||
result.scrollLines = this.rows - 1;
|
||||
} else if (modifiers) {
|
||||
result.key = C0.ESC + '[6;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[6~';
|
||||
}
|
||||
@@ -1918,7 +1922,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.rows = y;
|
||||
this.buffers.setupTabStops(this.cols);
|
||||
|
||||
this.charMeasure.measure(this.options);
|
||||
if (this.charMeasure) {
|
||||
this.charMeasure.measure(this.options);
|
||||
}
|
||||
|
||||
this.refresh(0, this.rows - 1);
|
||||
this.emit('resize', {cols: x, rows: y});
|
||||
|
||||
@@ -87,6 +87,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
}
|
||||
|
||||
export interface IViewport {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(): void;
|
||||
onWheel(ev: WheelEvent): void;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
@@ -163,6 +164,7 @@ export interface ILinkMatcher {
|
||||
matchIndex?: number;
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
priority?: number;
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ICharset {
|
||||
@@ -176,6 +178,7 @@ export interface ILinkHoverEvent {
|
||||
}
|
||||
|
||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
screenElement: HTMLElement;
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
renderer: IRenderer;
|
||||
@@ -189,13 +192,16 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
buffers: IBufferSet;
|
||||
isFocused: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
viewport: IViewport;
|
||||
bracketedPasteMode: boolean;
|
||||
applicationCursor: boolean;
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
handler(data: string): void;
|
||||
send(data: string): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
log(text: string): void;
|
||||
@@ -285,6 +291,8 @@ export interface ISelectionManager {
|
||||
disable(): void;
|
||||
enable(): void;
|
||||
setSelection(row: number, col: number, length: number): void;
|
||||
isClickInSelection(event: MouseEvent): boolean;
|
||||
selectWordAtCursor(event: MouseEvent): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
@@ -321,6 +329,13 @@ export interface ILinkMatcherOptions {
|
||||
* default value is 0.
|
||||
*/
|
||||
priority?: number;
|
||||
/**
|
||||
* A callback that fires when the mousedown and click events occur that
|
||||
* determines whether a link will be activated upon click. This enables
|
||||
* only activating a link when a certain modifier is held down, if not the
|
||||
* mouse event will continue propagation (eg. double click to select word).
|
||||
*/
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface IBrowser {
|
||||
|
||||
+15
-7
@@ -7,11 +7,14 @@ import { IColorSet } from './renderer/Types';
|
||||
import { ITerminal, IViewport } from './Types';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
/**
|
||||
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
||||
* Logic for the virtual scroll bar is included in this object.
|
||||
*/
|
||||
export class Viewport implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private currentRowHeight: number = 0;
|
||||
private lastRecordedBufferLength: number = 0;
|
||||
private lastRecordedViewportHeight: number = 0;
|
||||
@@ -31,6 +34,10 @@ export class Viewport implements IViewport {
|
||||
private scrollArea: HTMLElement,
|
||||
private charMeasure: CharMeasure
|
||||
) {
|
||||
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
|
||||
// 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.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
@@ -48,13 +55,8 @@ export class Viewport implements IViewport {
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
|
||||
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
|
||||
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
|
||||
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
|
||||
}
|
||||
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
|
||||
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight);
|
||||
if (this.lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this.lastRecordedBufferHeight = newBufferHeight;
|
||||
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
|
||||
@@ -93,6 +95,12 @@ export class Viewport implements IViewport {
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private onScroll(ev: Event): void {
|
||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||
// which causes the terminal to scroll the buffer to the top
|
||||
if (!this.viewportElement.offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||
const diff = newRow - this.terminal.buffer.ydisp;
|
||||
this.terminal.scrollLines(diff, true);
|
||||
|
||||
+10
-5
@@ -28,17 +28,22 @@ export function proposeGeometry(term: Terminal): IGeometry {
|
||||
}
|
||||
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(term.element);
|
||||
const elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
|
||||
const elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
|
||||
const geometry = {
|
||||
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, LineData } from '../Types';
|
||||
import { C0 } from '../EscapeSequences';
|
||||
|
||||
enum Direction {
|
||||
Up = 'A',
|
||||
Down = 'B',
|
||||
Right = 'C',
|
||||
Left = 'D'
|
||||
}
|
||||
|
||||
export class AltClickHandler {
|
||||
private _startRow: number;
|
||||
private _startCol: number;
|
||||
private _endRow: number;
|
||||
private _endCol: number;
|
||||
private _lines: ICircularList<LineData>;
|
||||
|
||||
constructor(
|
||||
private _mouseEvent: MouseEvent,
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this._lines = this._terminal.buffer.lines;
|
||||
this._startCol = this._terminal.buffer.x;
|
||||
this._startRow = this._terminal.buffer.y;
|
||||
|
||||
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
|
||||
this._mouseEvent,
|
||||
this._terminal.element,
|
||||
this._terminal.charMeasure,
|
||||
this._terminal.options.lineHeight,
|
||||
this._terminal.cols,
|
||||
this._terminal.rows,
|
||||
false
|
||||
).map((coordinate: number) => {
|
||||
return coordinate - 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the escape sequences of arrows to the terminal
|
||||
*/
|
||||
public move(): void {
|
||||
if (this._mouseEvent.altKey) {
|
||||
this._terminal.send(this._arrowSequences());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates all the arrow sequences together.
|
||||
* Resets the starting row to an unwrapped row, moves to the requested row,
|
||||
* then moves to requested col.
|
||||
*/
|
||||
private _arrowSequences(): string {
|
||||
return this._resetStartingRow() +
|
||||
this._moveToRequestedRow() +
|
||||
this._moveToRequestedCol();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the initial position of the cursor is on a row that is wrapped, move the
|
||||
* cursor up to the first row that is not wrapped to have accurate vertical
|
||||
* positioning.
|
||||
*/
|
||||
private _resetStartingRow(): string {
|
||||
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
let endRow = this._endRow;
|
||||
|
||||
if (this._moveToRequestedRow().length === 0) {
|
||||
return '';
|
||||
} else {
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, this._startRow, this._startCol,
|
||||
this._startRow - this._wrappedRowsForRow(this._startRow), false
|
||||
).length, this._sequence(Direction.Left));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the reset starting and ending row, move to the requested row,
|
||||
* ignoring wrapped rows
|
||||
*/
|
||||
private _moveToRequestedRow(): string {
|
||||
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
|
||||
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
|
||||
|
||||
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the requested col on the ending row
|
||||
*/
|
||||
private _moveToRequestedCol(): string {
|
||||
let startRow;
|
||||
if (this._moveToRequestedRow().length > 0) {
|
||||
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
} else {
|
||||
startRow = this._startRow;
|
||||
}
|
||||
|
||||
let endRow = this._endRow;
|
||||
let direction = this._horizontalDirection();
|
||||
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, startRow, this._endCol, endRow,
|
||||
direction === Direction.Right
|
||||
).length, this._sequence(direction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates the number of wrapped rows between the unwrapped starting and
|
||||
* ending rows. These rows need to ignored since the cursor skips over them.
|
||||
*/
|
||||
private _wrappedRowsCount(): number {
|
||||
let wrappedRows = 0;
|
||||
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
let direction = this._verticalDirection() === Direction.Up ? -1 : 1;
|
||||
|
||||
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
|
||||
wrappedRows++;
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of wrapped rows that make up a given row.
|
||||
* @param currentRow The row to determine how many wrapped rows make it up
|
||||
*/
|
||||
private _wrappedRowsForRow(currentRow: number): number {
|
||||
let rowCount = 0;
|
||||
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
|
||||
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
|
||||
rowCount++;
|
||||
currentRow--;
|
||||
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direction determiners
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if the right or left arrow is needed
|
||||
*/
|
||||
private _horizontalDirection(): Direction {
|
||||
let startRow;
|
||||
if (this._moveToRequestedRow().length > 0) {
|
||||
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
} else {
|
||||
startRow = this._startRow;
|
||||
}
|
||||
|
||||
if ((this._startCol < this._endCol &&
|
||||
startRow <= this._endRow) || // down/right or same y/right
|
||||
(this._startCol >= this._endCol &&
|
||||
startRow < this._endRow)) { // down/left or same y/left
|
||||
return Direction.Right;
|
||||
} else {
|
||||
return Direction.Left;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the up or down arrow is needed
|
||||
*/
|
||||
private _verticalDirection(): Direction {
|
||||
if (this._startRow > this._endRow) {
|
||||
return Direction.Up;
|
||||
} else {
|
||||
return Direction.Down;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the string of chars in the buffer from a starting row and col
|
||||
* to an ending row and col
|
||||
* @param startCol The starting column position
|
||||
* @param startRow The starting row position
|
||||
* @param endCol The ending column position
|
||||
* @param endRow The ending row position
|
||||
* @param forward Direction to move
|
||||
*/
|
||||
private _bufferLine(
|
||||
startCol: number,
|
||||
startRow: number,
|
||||
endCol: number,
|
||||
endRow: number,
|
||||
forward: boolean
|
||||
): string {
|
||||
let currentCol = startCol;
|
||||
let currentRow = startRow;
|
||||
let bufferStr = '';
|
||||
|
||||
while (currentCol !== endCol || currentRow !== endRow) {
|
||||
currentCol += forward ? 1 : -1;
|
||||
|
||||
if (forward && currentCol > this._terminal.cols - 1) {
|
||||
bufferStr += this._terminal.buffer.translateBufferLineToString(
|
||||
currentRow, false, startCol, currentCol
|
||||
);
|
||||
currentCol = 0;
|
||||
startCol = 0;
|
||||
currentRow++;
|
||||
} else if (!forward && currentCol < 0) {
|
||||
bufferStr += this._terminal.buffer.translateBufferLineToString(
|
||||
currentRow, false, 0, startCol + 1
|
||||
);
|
||||
currentCol = this._terminal.cols - 1;
|
||||
startCol = currentCol;
|
||||
currentRow--;
|
||||
}
|
||||
}
|
||||
|
||||
return bufferStr + this._terminal.buffer.translateBufferLineToString(
|
||||
currentRow, false, startCol, currentCol
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the escape sequence for clicking an arrow
|
||||
* @param direction The direction to move
|
||||
*/
|
||||
private _sequence(direction: Direction): string {
|
||||
const mod = this._terminal.applicationCursor ? 'O' : '[';
|
||||
return C0.ESC + mod + direction;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string repeated a given number of times
|
||||
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
|
||||
* @param count The number of times to repeat the string
|
||||
* @param string The string that is to be repeated
|
||||
*/
|
||||
function repeat(count: number, str: string): string {
|
||||
count = Math.floor(count);
|
||||
let rpt = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
rpt += str;
|
||||
}
|
||||
return rpt;
|
||||
}
|
||||
@@ -100,6 +100,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
// Timeout needs to be long enough for click event to be handled.
|
||||
setTimeout(() => {
|
||||
textarea.style.position = null;
|
||||
textarea.style.width = null;
|
||||
@@ -107,7 +108,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
textarea.style.zIndex = null;
|
||||
}, 4);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +116,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
|
||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||
selectionManager.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
|
||||
@@ -151,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
// components from handling the mouse event.
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone) {
|
||||
// TODO: When link modifier support is added, the event should only be
|
||||
// cancelled when the modifier is held (see #1021)
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (zone.willLinkActivate(e)) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
private _findZoneEventAt(e: MouseEvent): IMouseZone {
|
||||
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
|
||||
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
@@ -189,9 +189,10 @@ export class MouseZone implements IMouseZone {
|
||||
public x2: number,
|
||||
public y: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverCallback?: (e: MouseEvent) => any,
|
||||
public tooltipCallback?: (e: MouseEvent) => any,
|
||||
public leaveCallback?: () => void
|
||||
public hoverCallback: (e: MouseEvent) => any,
|
||||
public tooltipCallback: (e: MouseEvent) => any,
|
||||
public leaveCallback: () => void,
|
||||
public willLinkActivate: (e: MouseEvent) => boolean
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -13,7 +13,8 @@ export interface IMouseZone {
|
||||
x2: number;
|
||||
y: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback?: (e: MouseEvent) => any;
|
||||
tooltipCallback?: (e: MouseEvent) => any;
|
||||
leaveCallback?: () => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
|
||||
@@ -24,22 +24,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.id = `xterm-${id}-layer`;
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
|
||||
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!_alpha) {
|
||||
if (!this._alpha) {
|
||||
this.clearAll();
|
||||
}
|
||||
container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: ITerminal): void {}
|
||||
@@ -53,6 +56,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
}
|
||||
|
||||
protected setTransparency(terminal: ITerminal, alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
if (alpha === this._alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new canvas and replace old one
|
||||
const oldCanvas = this._canvas;
|
||||
this._alpha = alpha;
|
||||
// Cloning preserves properties
|
||||
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
|
||||
this._initCanvas();
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this.onGridChanged(terminal, 0, terminal.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
@@ -63,7 +85,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = null;
|
||||
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
|
||||
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
if (result instanceof HTMLCanvasElement) {
|
||||
this._charAtlas = result;
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ICharAtlasConfig {
|
||||
fontWeightBold: string;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
colors: IColorSet;
|
||||
}
|
||||
|
||||
@@ -83,7 +84,8 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
|
||||
background: colors.background,
|
||||
foreground: colors.foreground,
|
||||
ansiColors: colors.ansi,
|
||||
devicePixelRatio: window.devicePixelRatio
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
allowTransparency: terminal.options.allowTransparency
|
||||
};
|
||||
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
@@ -111,6 +113,7 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi
|
||||
fontSize: terminal.options.fontSize,
|
||||
fontWeight: terminal.options.fontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold,
|
||||
allowTransparency: terminal.options.allowTransparency,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
@@ -125,6 +128,7 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
|
||||
@@ -86,7 +86,7 @@ export class ColorManager implements IColorManager {
|
||||
*/
|
||||
public setTheme(theme: ITheme): void {
|
||||
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
|
||||
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
|
||||
this.colors.background = theme.background || DEFAULT_BACKGROUND;
|
||||
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
|
||||
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
|
||||
this.colors.selection = theme.selection || DEFAULT_SELECTION;
|
||||
@@ -107,20 +107,4 @@ export class ColorManager implements IColorManager {
|
||||
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
|
||||
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
|
||||
}
|
||||
|
||||
private _validateColor(color: string, fallback: string): string {
|
||||
if (!color) {
|
||||
return fallback;
|
||||
}
|
||||
if (color.length === 7 && color.charAt(0) === '#') {
|
||||
return color;
|
||||
}
|
||||
if (color.length === 4 && color.charAt(0) === '#') {
|
||||
const r = color.charAt(1);
|
||||
const g = color.charAt(2);
|
||||
const b = color.charAt(3);
|
||||
return `#${r}${r}${g}${g}${b}${b}`;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,12 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
if (theme) {
|
||||
this.colorManager.setTheme(theme);
|
||||
}
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
|
||||
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: null,
|
||||
@@ -119,6 +120,10 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
}
|
||||
|
||||
// Resize the screen
|
||||
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth + this._terminal.viewport.scrollBarWidth}px`;
|
||||
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this.emit('resize', {
|
||||
width: this.dimensions.canvasWidth,
|
||||
height: this.dimensions.canvasHeight
|
||||
|
||||
@@ -22,8 +22,8 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _characterFont: string;
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'text', zIndex, false, colors);
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
|
||||
super(container, 'text', zIndex, alpha, colors);
|
||||
this._state = new GridCache<CharData>();
|
||||
}
|
||||
|
||||
@@ -190,6 +190,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: ITerminal): void {
|
||||
this.setTransparency(terminal, terminal.options.allowTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a character is overlapping to the next cell.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface ICharAtlasRequest {
|
||||
foreground: string;
|
||||
ansiColors: string[];
|
||||
devicePixelRatio: number;
|
||||
allowTransparency: boolean;
|
||||
}
|
||||
|
||||
export const CHAR_ATLAS_CELL_SPACING = 1;
|
||||
@@ -43,7 +44,7 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number
|
||||
/*255 ascii chars*/255 * cellWidth,
|
||||
(/*default+default bold*/2 + /*0-15*/16) * cellHeight
|
||||
);
|
||||
const ctx = canvas.getContext('2d', {alpha: false});
|
||||
const ctx = canvas.getContext('2d', {alpha: request.allowTransparency});
|
||||
|
||||
ctx.fillStyle = request.background;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
@@ -71,6 +71,9 @@ export class MockTerminal implements ITerminal {
|
||||
write(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
send(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
bracketedPasteMode: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
renderer: IRenderer;
|
||||
@@ -78,6 +81,7 @@ export class MockTerminal implements ITerminal {
|
||||
isFocused: boolean;
|
||||
options: ITerminalOptions = {};
|
||||
element: HTMLElement;
|
||||
screenElement: HTMLElement;
|
||||
rowContainer: HTMLElement;
|
||||
selectionContainer: HTMLElement;
|
||||
selectionManager: ISelectionManager;
|
||||
@@ -94,6 +98,8 @@ export class MockTerminal implements ITerminal {
|
||||
scrollback: number;
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
viewport: IViewport;
|
||||
applicationCursor: boolean;
|
||||
handler(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -317,6 +323,7 @@ export class MockRenderer implements IRenderer {
|
||||
}
|
||||
|
||||
export class MockViewport implements IViewport {
|
||||
scrollBarWidth: number = 0;
|
||||
onThemeChanged(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
+10
-1
@@ -96,9 +96,18 @@
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm canvas {
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
Reference in New Issue
Block a user