diff --git a/demo/client.ts b/demo/client.ts index ea8c36c3..bcaabdd1 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -296,8 +296,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a function updateTerminalSize(): void { const cols = parseInt((document.getElementById(`opt-cols`)).value, 10); const rows = parseInt((document.getElementById(`opt-rows`)).value, 10); - const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; - const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px'; + const width = (cols * term._core._renderCoordinator.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; + const height = (rows * term._core._renderCoordinator.dimensions.actualCellHeight).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; term.fit(); diff --git a/demo/index.html b/demo/index.html index a7ab0f0c..4e9a75d4 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,6 +2,7 @@ xterm.js demo + diff --git a/demo/logo.png b/demo/logo.png new file mode 100644 index 00000000..ed702570 Binary files /dev/null and b/demo/logo.png differ diff --git a/demo/server.js b/demo/server.js index e3473400..c7c9fc9b 100644 --- a/demo/server.js +++ b/demo/server.js @@ -18,6 +18,7 @@ function startServer() { logs = {}; app.use('/src', express.static(__dirname + '/../src')); + app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png')); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 4351a336..7f1d8eaa 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -9,6 +9,8 @@ import { isMac } from './common/Platform'; import { RenderDebouncer } from './ui/RenderDebouncer'; import { addDisposableDomListener } from './ui/Lifecycle'; import { Disposable } from './common/Lifecycle'; +import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; +import { IRenderDimensions } from './renderer/Types'; const MAX_ROWS_TO_READ = 20; @@ -25,6 +27,7 @@ export class AccessibilityManager extends Disposable { private _liveRegionLineCount: number = 0; private _renderRowsDebouncer: RenderDebouncer; + private _screenDprMonitor: ScreenDprMonitor; private _topBoundaryFocusListener: (e: FocusEvent) => void; private _bottomBoundaryFocusListener: (e: FocusEvent) => void; @@ -40,7 +43,10 @@ export class AccessibilityManager extends Disposable { */ private _charsToConsume: string[] = []; - constructor(private _terminal: ITerminal) { + constructor( + private _terminal: ITerminal, + private _dimensions: IRenderDimensions + ) { super(); this._accessibilityTreeRoot = document.createElement('div'); this._accessibilityTreeRoot.classList.add('xterm-accessibility'); @@ -81,13 +87,12 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); this.register(this._terminal.onKey(e => this._onKey(e.key))); this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); - // TODO: Maybe renderer should fire an event on terminal when the characters change and that - // should be listened to instead? That would mean that the order of events are always - // guarenteed - this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions())); - this.register(this._terminal.renderer.onCanvasResize(() => this._refreshRowsDimensions())); + + this._screenDprMonitor = new ScreenDprMonitor(); + this.register(this._screenDprMonitor); + this._screenDprMonitor.setListener(() => this._refreshRowsDimensions()); // This shouldn't be needed on modern browsers but is present in case the - // media query that drives the dprchange event isn't supported + // media query that drives the ScreenDprMonitor isn't supported this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); } @@ -258,7 +263,7 @@ export class AccessibilityManager extends Disposable { } private _refreshRowsDimensions(): void { - if (!this._terminal.renderer.dimensions.actualCellHeight) { + if (!this._dimensions.actualCellHeight) { return; } if (this._rowElements.length !== this._terminal.rows) { @@ -269,8 +274,13 @@ export class AccessibilityManager extends Disposable { } } + public setDimensions(dimensions: IRenderDimensions): void { + this._dimensions = dimensions; + this._refreshRowsDimensions(); + } + private _refreshRowDimensions(element: HTMLElement): void { - element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`; + element.style.height = `${this._dimensions.actualCellHeight}px`; } private _announceCharacter(char: string): void { diff --git a/src/MouseHelper.test.ts b/src/MouseHelper.test.ts index 26888bf7..0925d49f 100644 --- a/src/MouseHelper.test.ts +++ b/src/MouseHelper.test.ts @@ -31,7 +31,7 @@ describe('MouseHelper.getCoords', () => { actualCellWidth: CHAR_WIDTH, actualCellHeight: CHAR_HEIGHT }; - mouseHelper = new MouseHelper(renderer); + mouseHelper = new MouseHelper(renderer as any); }); describe('when charMeasure is not initialized', () => { diff --git a/src/MouseHelper.ts b/src/MouseHelper.ts index fb91c9ef..12905222 100644 --- a/src/MouseHelper.ts +++ b/src/MouseHelper.ts @@ -4,13 +4,12 @@ */ import { ICharMeasure, IMouseHelper } from './Types'; -import { IRenderer } from './renderer/Types'; +import { RenderCoordinator } from './renderer/RenderCoordinator'; export class MouseHelper implements IMouseHelper { - constructor(private _renderer: IRenderer) {} - - public setRenderer(renderer: IRenderer): void { - this._renderer = renderer; + constructor( + private _renderCoordinator: RenderCoordinator + ) { } public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] { @@ -42,8 +41,8 @@ export class MouseHelper implements IMouseHelper { return null; } - coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth); - coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight); + coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderCoordinator.dimensions.actualCellWidth / 2 : 0)) / this._renderCoordinator.dimensions.actualCellWidth); + coords[1] = Math.ceil(coords[1] / this._renderCoordinator.dimensions.actualCellHeight); // Ensure coordinates are within the terminal viewport. Note that selections // need an addition point of precision to cover the end point (as characters diff --git a/src/Terminal.ts b/src/Terminal.ts index 7e774a6a..7083c0f0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -42,7 +42,6 @@ import { MouseHelper } from './MouseHelper'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; -import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; @@ -55,6 +54,7 @@ import { EventEmitter2, IEvent } from './common/EventEmitter2'; import { Attributes, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from './ui/ColorManager'; +import { RenderCoordinator } from './renderer/RenderCoordinator'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -204,7 +204,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _inputHandler: InputHandler; public soundManager: SoundManager; - public renderer: IRenderer; + private _renderCoordinator: RenderCoordinator; public selectionManager: SelectionManager; public linkifier: ILinkifier; public buffers: BufferSet; @@ -215,7 +215,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public mouseHelper: MouseHelper; private _accessibilityManager: AccessibilityManager; private _colorManager: ColorManager; - private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; private _windowsMode: IDisposable | undefined; @@ -357,8 +356,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); - // Reuse renderer if the Terminal is being recreated via a reset call. - this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; this.linkifier = this.linkifier || new Linkifier(this); this._mouseZoneManager = this._mouseZoneManager || null; @@ -470,12 +467,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'theme': - // If open has been called we do not want to set options.theme as the - // source of truth is owned by the renderer. - if (this.renderer) { - this._setTheme(value); - return; - } + this._setTheme(value); break; case 'scrollback': value = Math.min(value, MAX_BUFFER_SIZE); @@ -504,8 +496,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'fontFamily': case 'fontSize': // When the font changes the size of the cells may change which requires a renderer clear - if (this.renderer) { - this.renderer.clear(); + if (this._renderCoordinator) { + this._renderCoordinator.clear(); this.charMeasure.measure(this.options); } break; @@ -517,21 +509,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'fontWeight': case 'fontWeightBold': // When the font changes the size of the cells may change which requires a renderer clear - if (this.renderer) { - this.renderer.clear(); - this.renderer.onResize(this.cols, this.rows); + if (this._renderCoordinator) { + this._renderCoordinator.clear(); + this._renderCoordinator.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); } break; case 'rendererType': - if (this.renderer) { - this.unregister(this.renderer); - this.renderer.dispose(); - this.renderer = null; + if (this._renderCoordinator) { + this._renderCoordinator.setRenderer(this._createRenderer()); } - this._setupRenderer(); - this.renderer.onCharSizeChanged(); - this.mouseHelper.setRenderer(this.renderer); break; case 'scrollback': this.buffers.resize(this.cols, this.rows); @@ -541,8 +528,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II break; case 'screenReaderMode': if (value) { - if (!this._accessibilityManager) { - this._accessibilityManager = new AccessibilityManager(this); + if (!this._accessibilityManager && this._renderCoordinator) { + this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions); } } else { if (this._accessibilityManager) { @@ -566,8 +553,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II break; } // Inform renderer of changes - if (this.renderer) { - this.renderer.onOptionsChanged(); + if (this._renderCoordinator) { + this._renderCoordinator.onOptionsChanged(); } } @@ -706,10 +693,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._context = this._parent.ownerDocument.defaultView; this._document = this._parent.ownerDocument; - this._screenDprMonitor = new ScreenDprMonitor(); - this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio)); - this.register(this._screenDprMonitor); - // Create main element container this.element = this._document.createElement('div'); this.element.dir = 'ltr'; // xterm.css assumes LTR @@ -769,27 +752,27 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.options.theme = null; this._colorManager = new ColorManager(document, this.options.allowTransparency); this._colorManager.setTheme(this._theme); - this._setupRenderer(); - this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); + const renderer = this._createRenderer(); + this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement); + this._renderCoordinator.onRender(e => this._onRender.fire(e)); + this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows)); + + this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure, this._renderCoordinator.dimensions); this.viewport.onThemeChange(this._colorManager.colors); this.register(this.viewport); - this.register(this.onCursorMove(() => this.renderer.onCursorMove())); - this.register(this.onResize(() => this.renderer.onResize(this.cols, this.rows))); - this.register(this.addDisposableListener('blur', () => this.renderer.onBlur())); - this.register(this.addDisposableListener('focus', () => this.renderer.onFocus())); - this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio))); - // dprchange should handle this case, we need this as well for browsers that don't support the - // matchMedia query. - this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); - this.register(this.charMeasure.onCharSizeChanged(() => this.renderer.onCharSizeChanged())); - this.register(this.renderer.onCanvasResize(() => this.viewport.syncScrollArea())); + this.register(this.onCursorMove(() => this._renderCoordinator.onCursorMove())); + this.register(this.onResize(() => this._renderCoordinator.onResize(this.cols, this.rows))); + this.register(this.addDisposableListener('blur', () => this._renderCoordinator.onBlur())); + this.register(this.addDisposableListener('focus', () => this._renderCoordinator.onFocus())); + this.register(this.charMeasure.onCharSizeChanged(() => this._renderCoordinator.onCharSizeChanged())); + this.register(this._renderCoordinator.onDimensionsChange(() => this.viewport.syncScrollArea())); this.selectionManager = new SelectionManager(this, this.charMeasure); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); - this.register(this.selectionManager.onRedrawRequest(e => this.renderer.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this.selectionManager.onRedrawRequest(e => this._renderCoordinator.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this.selectionManager.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired @@ -804,7 +787,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II })); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); - this.mouseHelper = new MouseHelper(this.renderer); + this.mouseHelper = new MouseHelper(this._renderCoordinator); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { @@ -816,7 +799,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to // ensure the correct order of the dprchange event - this._accessibilityManager = new AccessibilityManager(this); + this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions); + this._accessibilityManager.register(this._renderCoordinator.onDimensionsChange(e => this._accessibilityManager.setDimensions(e))); } // Measure the character size @@ -834,15 +818,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } - private _setupRenderer(): void { + private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': this.renderer = new Renderer(this, this._colorManager.colors); break; - case 'dom': this.renderer = new DomRenderer(this, this._colorManager.colors); break; - case 'webgl': this.renderer = new WebglRenderer(this, this._colorManager.colors); break; + case 'canvas': return new Renderer(this, this._colorManager.colors); break; + case 'dom': return new DomRenderer(this, this._colorManager.colors); break; + case 'webgl': return new WebglRenderer(this, this._colorManager.colors); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } - this.renderer.onRender(e => this._onRender.fire(e)); - this.register(this.renderer); } /** @@ -852,8 +834,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _setTheme(theme: ITheme): void { this._theme = theme; this._colorManager.setTheme(theme); - if (this.renderer) { - this.renderer.onThemeChange(this._colorManager.colors); + if (this._renderCoordinator) { + this._renderCoordinator.setColors(this._colorManager.colors); } if (this.viewport) { this.viewport.onThemeChange(this._colorManager.colors); @@ -1209,8 +1191,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param end The row to end at (between start and this.rows - 1). */ public refresh(start: number, end: number): void { - if (this.renderer) { - this.renderer.refreshRows(start, end); + if (this._renderCoordinator) { + this._renderCoordinator.refreshRows(start, end); } } @@ -1595,13 +1577,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - const joinerId = this.renderer.registerCharacterJoiner(handler); + const joinerId = this._renderCoordinator.registerCharacterJoiner(handler); this.refresh(0, this.rows - 1); return joinerId; } public deregisterCharacterJoiner(joinerId: number): void { - if (this.renderer.deregisterCharacterJoiner(joinerId)) { + if (this._renderCoordinator.deregisterCharacterJoiner(joinerId)) { this.refresh(0, this.rows - 1); } } diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index d3ed93a3..f83e7d22 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -384,7 +384,7 @@ export class MockRenderer implements IRenderer { throw new Error('Method not implemented.'); } dimensions: IRenderDimensions; - onThemeChange(colors: IColorSet): void { + setColors(colors: IColorSet): void { throw new Error('Method not implemented.'); } onResize(cols: number, rows: number): void {} @@ -394,9 +394,9 @@ export class MockRenderer implements IRenderer { onSelectionChanged(start: [number, number], end: [number, number]): void {} onCursorMove(): void {} onOptionsChanged(): void {} - onWindowResize(devicePixelRatio: number): void {} + onDevicePixelRatioChange(): void {} clear(): void {} - refreshRows(start: number, end: number): void {} + renderRows(start: number, end: number): void {} registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } deregisterCharacterJoiner(): boolean { return true; } } diff --git a/src/Types.ts b/src/Types.ts index bb3e8023..91bee283 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -4,7 +4,6 @@ */ import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm'; -import { IRenderer } from './renderer/Types'; import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types'; import { ICircularList } from './common/Types'; import { IEvent } from './common/EventEmitter2'; @@ -201,7 +200,6 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc screenElement: HTMLElement; selectionManager: ISelectionManager; charMeasure: ICharMeasure; - renderer: IRenderer; browser: IBrowser; writeBuffer: string[]; cursorHidden: boolean; diff --git a/src/Viewport.ts b/src/Viewport.ts index e32113e1..e53c99ed 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -8,6 +8,7 @@ import { CharMeasure } from './CharMeasure'; import { Disposable } from './common/Lifecycle'; import { addDisposableDomListener } from './ui/Lifecycle'; import { IColorSet } from './ui/Types'; +import { IRenderDimensions } from './renderer/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -43,7 +44,8 @@ export class Viewport extends Disposable implements IViewport { private _terminal: ITerminal, private _viewportElement: HTMLElement, private _scrollArea: HTMLElement, - private _charMeasure: CharMeasure + private _charMeasure: CharMeasure, + private _dimensions: IRenderDimensions ) { super(); @@ -57,6 +59,10 @@ export class Viewport extends Disposable implements IViewport { setTimeout(() => this.syncScrollArea(), 0); } + public onDimensionsChance(dimensions: IRenderDimensions): void { + this._dimensions = dimensions; + } + public onThemeChange(colors: IColorSet): void { this._viewportElement.style.backgroundColor = colors.background.css; } @@ -73,9 +79,9 @@ export class Viewport extends Disposable implements IViewport { private _innerRefresh(): void { if (this._charMeasure.height > 0) { - this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; + this._currentRowHeight = this._dimensions.scaledCellHeight / window.devicePixelRatio; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; - const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight); + const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._dimensions.canvasHeight); if (this._lastRecordedBufferHeight !== newBufferHeight) { this._lastRecordedBufferHeight = newBufferHeight; this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; @@ -106,7 +112,7 @@ export class Viewport extends Disposable implements IViewport { } // If viewport height changed - if (this._lastRecordedViewportHeight !== (this._terminal).renderer.dimensions.canvasHeight) { + if (this._lastRecordedViewportHeight !== this._dimensions.canvasHeight) { this._refresh(); return; } @@ -125,7 +131,7 @@ export class Viewport extends Disposable implements IViewport { } // If row height changed - if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { + if (this._dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { this._refresh(); return; } diff --git a/src/addons/fit/fit.ts b/src/addons/fit/fit.ts index ac978807..26f8752c 100644 --- a/src/addons/fit/fit.ts +++ b/src/addons/fit/fit.ts @@ -39,8 +39,8 @@ export function proposeGeometry(term: Terminal): IGeometry { const availableHeight = parentElementHeight - elementPaddingVer; const availableWidth = parentElementWidth - elementPaddingHor - (term)._core.viewport.scrollBarWidth; const geometry = { - cols: Math.floor(availableWidth / (term)._core.renderer.dimensions.actualCellWidth), - rows: Math.floor(availableHeight / (term)._core.renderer.dimensions.actualCellHeight) + cols: Math.floor(availableWidth / (term)._core._renderCoordinator.dimensions.actualCellWidth), + rows: Math.floor(availableHeight / (term)._core._renderCoordinator.dimensions.actualCellHeight) }; return geometry; } @@ -50,7 +50,7 @@ export function fit(term: Terminal): void { if (geometry) { // Force a full render if (term.rows !== geometry.rows || term.cols !== geometry.cols) { - (term)._core.renderer.clear(); + (term)._core._renderCoordinator.clear(); term.resize(geometry.cols, geometry.rows); } } diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 2ad5313e..d8325229 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -104,6 +104,58 @@ describe('API Integration Tests', () => { assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'dom'); }); + describe('renderer', () => { + it('foreground', async function(): Promise { + this.timeout(10000); + await openTerminal({ rendererType: 'dom' }); + await page.evaluate(`window.term.write('\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7')`); + assert.deepEqual(await page.evaluate(` + [ + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className + ] + `), [ + 'xterm-fg-0', + 'xterm-fg-1', + 'xterm-fg-2', + 'xterm-fg-3', + 'xterm-fg-4', + 'xterm-fg-5', + 'xterm-fg-6' + ]); + }); + + it('background', async function(): Promise { + this.timeout(10000); + await openTerminal({ rendererType: 'dom' }); + await page.evaluate(`window.term.write('\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7')`); + assert.deepEqual(await page.evaluate(` + [ + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className + ] + `), [ + 'xterm-bg-0', + 'xterm-bg-1', + 'xterm-bg-2', + 'xterm-bg-3', + 'xterm-bg-4', + 'xterm-bg-5', + 'xterm-bg-6' + ]); + }); + }); + it('selection', async function(): Promise { this.timeout(10000); await openTerminal({ rows: 5, cols: 5 }); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3901a860..bae96d72 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -74,7 +74,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {} public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {} - public onThemeChange(terminal: ITerminal, colorSet: IColorSet): void { + public setColors(terminal: ITerminal, colorSet: IColorSet): void { this._refreshCharAtlas(terminal, colorSet); } diff --git a/src/renderer/RenderCoordinator.ts b/src/renderer/RenderCoordinator.ts new file mode 100644 index 00000000..251af38f --- /dev/null +++ b/src/renderer/RenderCoordinator.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderer, IRenderDimensions } from './Types'; +import { RenderDebouncer } from '../ui/RenderDebouncer'; +import { EventEmitter2, IEvent } from '../common/EventEmitter2'; +import { Disposable } from '../common/Lifecycle'; +import { ScreenDprMonitor } from '../ui/ScreenDprMonitor'; +import { addDisposableDomListener } from '../ui/Lifecycle'; +import { IColorSet } from '..//ui/Types'; +import { CharacterJoinerHandler } from '../Types'; + +export class RenderCoordinator extends Disposable { + private _renderDebouncer: RenderDebouncer; + private _screenDprMonitor: ScreenDprMonitor; + + private _isPaused: boolean = false; + private _needsFullRefresh: boolean = false; + private _canvasWidth: number = 0; + private _canvasHeight: number = 0; + + private _onDimensionsChange = new EventEmitter2(); + public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } + private _onRender = new EventEmitter2<{ start: number, end: number }>(); + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + private _onRefreshRequest = new EventEmitter2<{ start: number, end: number }>(); + public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } + + public get dimensions(): IRenderDimensions { return this._renderer.dimensions; } + + constructor( + private _renderer: IRenderer, + private _rowCount: number, + screenElement: HTMLElement + ) { + super(); + this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); + this.register(this._renderDebouncer); + + this._screenDprMonitor = new ScreenDprMonitor(); + this._screenDprMonitor.setListener(() => this._renderer.onDevicePixelRatioChange()); + this.register(this._screenDprMonitor); + + // dprchange should handle this case, we need this as well for browsers that don't support the + // matchMedia query. + this.register(addDisposableDomListener(window, 'resize', () => this._renderer.onDevicePixelRatioChange())); + + // Detect whether IntersectionObserver is detected and enable renderer pause + // and resume based on terminal visibility if so + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 }); + observer.observe(screenElement); + this.register({ dispose: () => observer.disconnect() }); + } + } + + private _onIntersectionChange(entry: IntersectionObserverEntry): void { + this._isPaused = entry.intersectionRatio === 0; + if (!this._isPaused && this._needsFullRefresh) { + this.refreshRows(0, this._rowCount - 1); + this._needsFullRefresh = false; + } + } + + public refreshRows(start: number, end: number): void { + if (this._isPaused) { + this._needsFullRefresh = true; + return; + } + this._renderDebouncer.refresh(start, end, this._rowCount); + } + + private _renderRows(start: number, end: number): void { + this._renderer.renderRows(start, end); + this._onRender.fire({ start, end }); + } + + public resize(cols: number, rows: number): void { + this._rowCount = rows; + this._fireOnCanvasResize(); + } + + public changeOptions(): void { + this._renderer.onOptionsChanged(); + this._fireOnCanvasResize(); + } + + private _fireOnCanvasResize(): void { + // Don't fire the event if the dimensions haven't changed + if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) { + return; + } + this._onDimensionsChange.fire(this._renderer.dimensions); + } + + public setRenderer(renderer: IRenderer): void { + // TODO: RenderCoordinator should be the only one to dispose the renderer + this._renderer.dispose(); + this._renderer = renderer; + } + + private _fullRefresh(): void { + if (this._isPaused) { + this._needsFullRefresh = true; + } else { + this.refreshRows(0, this._rowCount); + } + } + + public setColors(colors: IColorSet): void { + this._renderer.setColors(colors); + this._fullRefresh(); + } + + public onDevicePixelRatioChange(): void { + this._renderer.onDevicePixelRatioChange(); + } + + public onResize(cols: number, rows: number): void { + this._renderer.onResize(cols, rows); + this._fullRefresh(); + } + + // TODO: Is this useful when we have onResize? + public onCharSizeChanged(): void { + this._renderer.onCharSizeChanged(); + } + + public onBlur(): void { + this._renderer.onBlur(); + } + + public onFocus(): void { + this._renderer.onFocus(); + } + + public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void { + this._renderer.onSelectionChanged(start, end, columnSelectMode); + } + + public onCursorMove(): void { + this._renderer.onCursorMove(); + } + + public onOptionsChanged(): void { + this._renderer.onOptionsChanged(); + } + + public clear(): void { + this._renderer.clear(); + } + + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { + return this._renderer.registerCharacterJoiner(handler); + } + + public deregisterCharacterJoiner(joinerId: number): boolean { + return this._renderer.deregisterCharacterJoiner(joinerId); + } +} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 527c5966..b3b93ca7 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -9,30 +9,17 @@ import { CursorRenderLayer } from './CursorRenderLayer'; import { IRenderLayer, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { ITerminal, CharacterJoinerHandler } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; -import { RenderDebouncer } from '../ui/RenderDebouncer'; -import { ScreenDprMonitor } from '../ui/ScreenDprMonitor'; import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry'; -import { EventEmitter2, IEvent } from '../common/EventEmitter2'; import { Disposable } from '../common/Lifecycle'; import { IColorSet } from '../ui/Types'; export class Renderer extends Disposable implements IRenderer { - private _renderDebouncer: RenderDebouncer; - private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; - private _screenDprMonitor: ScreenDprMonitor; - private _isPaused: boolean = false; - private _needsFullRefresh: boolean = false; private _characterJoinerRegistry: ICharacterJoinerRegistry; public dimensions: IRenderDimensions; - private _onCanvasResize = new EventEmitter2<{ width: number, height: number }>(); - public get onCanvasResize(): IEvent<{ width: number, height: number }> { return this._onCanvasResize.event; } - private _onRender = new EventEmitter2<{ start: number, end: number }>(); - public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - constructor( private _terminal: ITerminal, private _colors: IColorSet @@ -64,19 +51,6 @@ export class Renderer extends Disposable implements IRenderer { this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); this.onOptionsChanged(); - - this._renderDebouncer = new RenderDebouncer(this._renderRows.bind(this)); - this._screenDprMonitor = new ScreenDprMonitor(); - this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio)); - this.register(this._screenDprMonitor); - - // Detect whether IntersectionObserver is detected and enable renderer pause - // and resume based on terminal visibility if so - if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver(e => this.onIntersectionChange(e[e.length - 1]), { threshold: 0 }); - observer.observe(this._terminal.element); - this.register({ dispose: () => observer.disconnect() }); - } } public dispose(): void { @@ -84,35 +58,23 @@ export class Renderer extends Disposable implements IRenderer { this._renderLayers.forEach(l => l.dispose()); } - public onIntersectionChange(entry: IntersectionObserverEntry): void { - this._isPaused = entry.intersectionRatio === 0; - if (!this._isPaused && this._needsFullRefresh) { - this._terminal.refresh(0, this._terminal.rows - 1); - this._needsFullRefresh = false; - } - } - - public onWindowResize(devicePixelRatio: number): void { + public onDevicePixelRatioChange(): void { // If the device pixel ratio changed, the char atlas needs to be regenerated // and the terminal needs to refreshed - if (this._devicePixelRatio !== devicePixelRatio) { - this._devicePixelRatio = devicePixelRatio; + if (this._devicePixelRatio !== window.devicePixelRatio) { + this._devicePixelRatio = window.devicePixelRatio; this.onResize(this._terminal.cols, this._terminal.rows); } } - public onThemeChange(colors: IColorSet): void { + public setColors(colors: IColorSet): void { + this._colors = colors; + // Clear layers and force a full render this._renderLayers.forEach(l => { - l.onThemeChange(this._terminal, this._colors); + l.setColors(this._terminal, this._colors); l.reset(this._terminal); }); - - if (this._isPaused) { - this._needsFullRefresh = true; - } else { - this._terminal.refresh(0, this._terminal.rows - 1); - } } public onResize(cols: number, rows: number): void { @@ -122,21 +84,9 @@ export class Renderer extends Disposable implements IRenderer { // Resize all render layers this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions)); - // Force a refresh - if (this._isPaused) { - this._needsFullRefresh = true; - } else { - this._terminal.refresh(0, this._terminal.rows - 1); - } - // Resize the screen this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; - - this._onCanvasResize.fire({ - width: this.dimensions.canvasWidth, - height: this.dimensions.canvasHeight - }); } public onCharSizeChanged(): void { @@ -168,34 +118,15 @@ export class Renderer extends Disposable implements IRenderer { } private _runOperation(operation: (layer: IRenderLayer) => void): void { - if (this._isPaused) { - this._needsFullRefresh = true; - } else { - this._renderLayers.forEach(l => operation(l)); - } - } - - /** - * Queues a refresh between two rows (inclusive), to be done on next animation - * frame. - * @param start The start row. - * @param end The end row. - */ - public refreshRows(start: number, end: number): void { - if (this._isPaused) { - this._needsFullRefresh = true; - return; - } - this._renderDebouncer.refresh(start, end, this._terminal.rows); + this._renderLayers.forEach(l => operation(l)); } /** * Performs the refresh loop callback, calling refresh only if a refresh is * necessary before queueing up the next one. */ - private _renderRows(start: number, end: number): void { + public renderRows(start: number, end: number): void { this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); - this._onRender.fire({ start, end }); } /** diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 590df0e0..4a4afbf0 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -5,7 +5,6 @@ import { ITerminal, CharacterJoinerHandler } from '../Types'; import { IDisposable } from 'xterm'; -import { IEvent } from '../common/EventEmitter2'; import { IColorSet } from '../ui/Types'; /** @@ -26,14 +25,11 @@ export const enum FLAGS { * rendering rows to the screen. */ export interface IRenderer extends IDisposable { - dimensions: IRenderDimensions; - - onCanvasResize: IEvent<{ width: number, height: number }>; - onRender: IEvent<{ start: number, end: number }>; + readonly dimensions: IRenderDimensions; dispose(): void; - onThemeChange(colors: IColorSet): void; - onWindowResize(devicePixelRatio: number): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; onResize(cols: number, rows: number): void; onCharSizeChanged(): void; onBlur(): void; @@ -42,7 +38,7 @@ export interface IRenderer extends IDisposable { onCursorMove(): void; onOptionsChanged(): void; clear(): void; - refreshRows(start: number, end: number): void; + renderRows(start: number, end: number): void; registerCharacterJoiner(handler: CharacterJoinerHandler): number; deregisterCharacterJoiner(joinerId: number): boolean; } @@ -86,7 +82,7 @@ export interface IRenderLayer extends IDisposable { /** * Called when the theme changes. */ - onThemeChange(terminal: ITerminal, colorSet: IColorSet): void; + setColors(terminal: ITerminal, colorSet: IColorSet): void; /** * Called when the data in the grid has changed (or needs to be rendered diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 6f46f0ec..2797639d 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -5,10 +5,8 @@ import { IRenderer, IRenderDimensions } from '../Types'; import { ILinkifierEvent, ITerminal, CharacterJoinerHandler } from '../../Types'; -import { RenderDebouncer } from '../../ui/RenderDebouncer'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { EventEmitter2, IEvent } from '../../common/EventEmitter2'; import { Disposable } from '../../common/Lifecycle'; import { IColorSet } from '../../ui/Types'; @@ -30,7 +28,6 @@ let nextTerminalId = 1; * canvas is not an option. */ export class DomRenderer extends Disposable implements IRenderer { - private _renderDebouncer: RenderDebouncer; private _rowFactory: DomRendererRowFactory; private _terminalClass: number = nextTerminalId++; @@ -42,11 +39,6 @@ export class DomRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; - private _onCanvasResize = new EventEmitter2<{ width: number, height: number }>(); - public get onCanvasResize(): IEvent<{ width: number, height: number }> { return this._onCanvasResize.event; } - private _onRender = new EventEmitter2<{ start: number, end: number }>(); - public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - constructor( private _terminal: ITerminal, private _colors: IColorSet @@ -78,7 +70,6 @@ export class DomRenderer extends Disposable implements IRenderer { }; this._updateDimensions(); - this._renderDebouncer = new RenderDebouncer(this._renderRows.bind(this)); this._rowFactory = new DomRendererRowFactory(_terminal.options, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); @@ -140,7 +131,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; } - public onThemeChange(colors: IColorSet): void { + public setColors(colors: IColorSet): void { this._colors = colors; this._injectCss(); } @@ -222,7 +213,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._themeStyleElement.innerHTML = styles; } - public onWindowResize(devicePixelRatio: number): void { + public onDevicePixelRatioChange(): void { this._updateDimensions(); } @@ -242,10 +233,6 @@ export class DomRenderer extends Disposable implements IRenderer { public onResize(cols: number, rows: number): void { this._refreshRowElements(cols, rows); this._updateDimensions(); - this._onCanvasResize.fire({ - width: this.dimensions.canvasWidth, - height: this.dimensions.canvasHeight - }); } public onCharSizeChanged(): void { @@ -337,11 +324,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._rowElements.forEach(e => e.innerHTML = ''); } - public refreshRows(start: number, end: number): void { - this._renderDebouncer.refresh(start, end, this._terminal.rows); - } - - private _renderRows(start: number, end: number): void { + public renderRows(start: number, end: number): void { const terminal = this._terminal; const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; @@ -357,8 +340,6 @@ export class DomRenderer extends Disposable implements IRenderer { const cursorStyle = terminal.options.cursorStyle; rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); } - - this._onRender.fire({ start, end }); } private get _terminalSelector(): string { diff --git a/src/renderer/webgl/WebglRenderer.ts b/src/renderer/webgl/WebglRenderer.ts index f7ad0e58..e3425173 100644 --- a/src/renderer/webgl/WebglRenderer.ts +++ b/src/renderer/webgl/WebglRenderer.ts @@ -136,12 +136,12 @@ export class WebglRenderer extends Disposable implements IRenderer { } } - public onThemeChange(colors: IColorSet): void { + public setColors(colors: IColorSet): void { this._applyBgLuminanceBasedSelection(); // Clear layers and force a full render this._renderLayers.forEach(l => { - l.onThemeChange(this._terminal, this._colors); + l.setColors(this._terminal, this._colors); l.reset(this._terminal); }); diff --git a/src/ui/ColorManager.ts b/src/ui/ColorManager.ts index 6d68c58d..923b2823 100644 --- a/src/ui/ColorManager.ts +++ b/src/ui/ColorManager.ts @@ -3,8 +3,7 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet } from './Types'; -import { ITheme } from 'xterm'; +import { IColorManager, IColor, IColorSet, ITheme } from './Types'; const DEFAULT_FOREGROUND = fromHex('#ffffff'); const DEFAULT_BACKGROUND = fromHex('#000000'); @@ -90,7 +89,11 @@ export class ColorManager implements IColorManager { const canvas = document.createElement('canvas'); canvas.width = 1; canvas.height = 1; - this._ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Could not get rendering context'); + } + this._ctx = ctx; this._ctx.globalCompositeOperation = 'copy'; this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); this.colors = { @@ -133,11 +136,11 @@ export class ColorManager implements IColorManager { } private _parseColor( - css: string, + css: string | undefined, fallback: IColor, allowTransparency: boolean = this.allowTransparency ): IColor { - if (!css) { + if (css === undefined) { return fallback; } diff --git a/src/ui/Types.ts b/src/ui/Types.ts index ef725ba6..b7b0ac21 100644 --- a/src/ui/Types.ts +++ b/src/ui/Types.ts @@ -20,3 +20,27 @@ export interface IColorSet { selection: IColor; ansi: IColor[]; } + +export interface ITheme { + foreground?: string; + background?: string; + cursor?: string; + cursorAccent?: string; + selection?: string; + black?: string; + red?: string; + green?: string; + yellow?: string; + blue?: string; + magenta?: string; + cyan?: string; + white?: string; + brightBlack?: string; + brightRed?: string; + brightGreen?: string; + brightYellow?: string; + brightBlue?: string; + brightMagenta?: string; + brightCyan?: string; + brightWhite?: string; +} diff --git a/src/ui/tsconfig.json b/src/ui/tsconfig.json index b3613eef..e07bd9c3 100644 --- a/src/ui/tsconfig.json +++ b/src/ui/tsconfig.json @@ -10,12 +10,8 @@ "../../node_modules/@types/mocha" ] }, - "include": [ - "./Lifecycle.ts", - "./RenderDebouncer.ts", - "./ScreenDprMonitor.ts" - ], + "include": [ "./**/*" ], "references": [ { "path": "../common" } ] -} \ No newline at end of file +}