From 440ab765c458497fcb43d55b71c44afc5fe96803 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 09:22:05 -0700 Subject: [PATCH 01/11] Remove rendererType setting --- .../xterm-addon-attach/test/AttachAddon.api.ts | 4 ++-- .../test/SerializeAddon.api.ts | 2 +- .../test/WebLinksAddon.api.ts | 2 +- .../xterm-addon-webgl/test/WebglRenderer.api.ts | 8 ++++---- demo/client.ts | 1 - src/browser/Terminal.ts | 12 +----------- src/common/services/OptionsService.ts | 2 -- src/common/services/Services.ts | 3 --- test/api/Terminal.api.ts | 17 ++++++++--------- test/api/TestUtils.ts | 6 +----- typings/xterm.d.ts | 15 --------------- 11 files changed, 18 insertions(+), 54 deletions(-) diff --git a/addons/xterm-addon-attach/test/AttachAddon.api.ts b/addons/xterm-addon-attach/test/AttachAddon.api.ts index 8335cf0f..a4827656 100644 --- a/addons/xterm-addon-attach/test/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/test/AttachAddon.api.ts @@ -28,7 +28,7 @@ describe('AttachAddon', () => { beforeEach(async () => await page.goto(APP)); it('string', async function(): Promise { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); const port = 8080; const server = new WebSocket.Server({ port }); server.on('connection', socket => socket.send('foo')); @@ -38,7 +38,7 @@ describe('AttachAddon', () => { }); it('utf8', async function(): Promise { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); const port = 8080; const server = new WebSocket.Server({ port }); const data = new Uint8Array([102, 111, 111]); diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index b86b066b..157b7072 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -42,7 +42,7 @@ describe('SerializeAddon', () => { page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); - await openTerminal(page, { rows: 10, cols: 10, rendererType: 'dom' }); + await openTerminal(page, { rows: 10, cols: 10 }); await page.evaluate(` window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); diff --git a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts index fe44dc31..bc978085 100644 --- a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts @@ -38,7 +38,7 @@ describe('WebLinksAddon', () => { }); async function testHostName(hostname: string): Promise { - await openTerminal(page, { rendererType: 'dom', cols: 40 }); + await openTerminal(page, { cols: 40 }); await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); const data = ` http://${hostname} \\r\\n` + ` http://${hostname}/a~b#c~d?e~f \\r\\n` + diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 13a15cf3..797f0531 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -860,7 +860,7 @@ describe('WebGL Renderer Integration Tests', async () => { describe('allowTransparency', async () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true })); + before(async () => setupBrowser({ allowTransparency: true })); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -879,7 +879,7 @@ describe('WebGL Renderer Integration Tests', async () => { describe('selectionForeground', () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom' })); + before(async () => setupBrowser()); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -898,7 +898,7 @@ describe('WebGL Renderer Integration Tests', async () => { describe('decoration color overrides', async () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom' })); + before(async () => setupBrowser()); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -1014,7 +1014,7 @@ async function getCellPixels(col: number, row: number): Promise { return await page.evaluate(`Array.from(window.result)`); } -async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { +async function setupBrowser(options: ITerminalOptions = {}): Promise { browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); diff --git a/demo/client.ts b/demo/client.ts index b2468ec3..d3b5af9c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -331,7 +331,6 @@ function initOptions(term: TerminalType): void { fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], logLevel: ['debug', 'info', 'warn', 'error', 'off'], - rendererType: ['dom', 'canvas'], theme: ['default', 'xtermjs', 'sapphire', 'light'], wordSeparator: null }; diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 053071d0..91c4a920 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -296,12 +296,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.refresh(0, this.rows - 1); } break; - case 'rendererType': - if (this._renderService) { - this._renderService.setRenderer(this._createRenderer()); - this._renderService.onResize(this.cols, this.rows); - } - break; case 'scrollback': this.viewport?.syncScrollArea(); break; @@ -615,11 +609,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } private _createRenderer(): IRenderer { - switch (this.options.rendererType) { - case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier2); - case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); - default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); - } + return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier2); } /** diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 911cc9b2..550adb31 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,7 +37,6 @@ export const DEFAULT_OPTIONS: Readonly = { tabStopWidth: 8, theme: {}, rightClickSelectsWord: isMac, - rendererType: 'canvas', windowOptions: {}, windowsMode: false, wordSeparator: ' ()[]{}\',"`', @@ -120,7 +119,6 @@ export class OptionsService implements IOptionsService { } break; case 'cursorStyle': - case 'rendererType': case 'wordSeparator': if (!value) { value = DEFAULT_OPTIONS[key]; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 22bff06d..709e171f 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -203,8 +203,6 @@ export interface IOptionsService { export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; -export type RendererType = 'dom' | 'canvas'; - export interface ITerminalOptions { allowProposedApi: boolean; allowTransparency: boolean; @@ -229,7 +227,6 @@ export interface ITerminalOptions { macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; minimumContrastRatio: number; - rendererType: RendererType; rightClickSelectsWord: boolean; rows: number; screenReaderMode: boolean; diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 80e46be0..fa19b5b4 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -157,7 +157,6 @@ describe('API Integration Tests', function(): void { describe('options', () => { it('getter', async () => { await openTerminal(page); - assert.equal(await page.evaluate(`window.term.options.rendererType`), 'canvas'); assert.equal(await page.evaluate(`window.term.options.cols`), 80); assert.equal(await page.evaluate(`window.term.options.rows`), 24); }); @@ -190,7 +189,7 @@ describe('API Integration Tests', function(): void { describe('renderer', () => { it('foreground', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); await writeSync(page, '\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7'); await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); assert.deepEqual(await page.evaluate(` @@ -215,7 +214,7 @@ describe('API Integration Tests', function(): void { }); it('background', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); await writeSync(page, '\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7'); await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); assert.deepEqual(await page.evaluate(` @@ -784,7 +783,7 @@ describe('API Integration Tests', function(): void { describe('registerLinkProvider', () => { it('should fire provideLinks when hovering cells', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -806,7 +805,7 @@ describe('API Integration Tests', function(): void { }); it('should fire hover and leave events on the link', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -844,7 +843,7 @@ describe('API Integration Tests', function(): void { }); it('should work fine when hover and leave callbacks are not provided', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -887,7 +886,7 @@ describe('API Integration Tests', function(): void { }); it('should fire activate events when clicking the link', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -929,7 +928,7 @@ describe('API Integration Tests', function(): void { }); it('should work when multiple links are provided on the same line', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -978,7 +977,7 @@ describe('API Integration Tests', function(): void { }); it('should dispose links when hovering away', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts index e059c92c..2b4e96a4 100644 --- a/test/api/TestUtils.ts +++ b/test/api/TestUtils.ts @@ -46,11 +46,7 @@ export async function timeout(ms: number): Promise { export async function openTerminal(page: playwright.Page, options: ITerminalOptions = {}): Promise { await page.evaluate(`window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })})`); await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - if (options.rendererType === 'dom') { - await page.waitForSelector('.xterm-rows'); - } else { - await page.waitForSelector('.xterm-text-layer'); - } + await page.waitForSelector('.xterm-rows'); } export function getBrowserType(): playwright.BrowserType | playwright.BrowserType | playwright.BrowserType { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index eb90202c..f16ff970 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -20,11 +20,6 @@ declare module 'xterm' { */ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; - /** - * A string representing a renderer type. - */ - export type RendererType = 'dom' | 'canvas'; - /** * An object containing start up options for the terminal. */ @@ -176,16 +171,6 @@ declare module 'xterm' { */ minimumContrastRatio?: number; - /** - * The type of renderer to use, this allows using the fallback DOM renderer - * when canvas is too slow for the environment. The following features do - * not work when the DOM renderer is used: - * - * - Letter spacing - * - Cursor blink - */ - rendererType?: RendererType; - /** * Whether to select the word under the cursor on right click, this is * standard behavior in a lot of macOS applications. From ab7da06cc1bdeac48d9e679812ad1f3e7ef080ac Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 09:26:52 -0700 Subject: [PATCH 02/11] Stub canvas addon --- .eslintrc.json | 1 + addons/xterm-addon-canvas/.gitignore | 2 + addons/xterm-addon-canvas/.npmignore | 29 +++++++++++ addons/xterm-addon-canvas/LICENSE | 19 +++++++ addons/xterm-addon-canvas/README.md | 3 ++ addons/xterm-addon-canvas/package.json | 27 ++++++++++ addons/xterm-addon-canvas/src/CanvasAddon.ts | 49 +++++++++++++++++++ addons/xterm-addon-canvas/src/tsconfig.json | 40 +++++++++++++++ addons/xterm-addon-canvas/tsconfig.json | 7 +++ .../typings/xterm-addon-canvas.d.ts | 33 +++++++++++++ addons/xterm-addon-canvas/webpack.config.js | 39 +++++++++++++++ 11 files changed, 249 insertions(+) create mode 100644 addons/xterm-addon-canvas/.gitignore create mode 100644 addons/xterm-addon-canvas/.npmignore create mode 100644 addons/xterm-addon-canvas/LICENSE create mode 100644 addons/xterm-addon-canvas/README.md create mode 100644 addons/xterm-addon-canvas/package.json create mode 100644 addons/xterm-addon-canvas/src/CanvasAddon.ts create mode 100644 addons/xterm-addon-canvas/src/tsconfig.json create mode 100644 addons/xterm-addon-canvas/tsconfig.json create mode 100644 addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts create mode 100644 addons/xterm-addon-canvas/webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json index 427c7a16..3a60f5bc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "test/benchmark/tsconfig.json", "addons/xterm-addon-attach/src/tsconfig.json", "addons/xterm-addon-attach/test/tsconfig.json", + "addons/xterm-addon-canvas/src/tsconfig.json", "addons/xterm-addon-fit/src/tsconfig.json", "addons/xterm-addon-fit/test/tsconfig.json", "addons/xterm-addon-ligatures/src/tsconfig.json", diff --git a/addons/xterm-addon-canvas/.gitignore b/addons/xterm-addon-canvas/.gitignore new file mode 100644 index 00000000..a9f4ed54 --- /dev/null +++ b/addons/xterm-addon-canvas/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/addons/xterm-addon-canvas/.npmignore b/addons/xterm-addon-canvas/.npmignore new file mode 100644 index 00000000..b203232a --- /dev/null +++ b/addons/xterm-addon-canvas/.npmignore @@ -0,0 +1,29 @@ +# Blacklist - exclude everything except npm defaults such as LICENSE, etc +* +!*/ + +# Whitelist - lib/ +!lib/**/*.d.ts + +!lib/**/*.js +!lib/**/*.js.map + +!lib/**/*.css + +# Whitelist - src/ +!src/**/*.ts +!src/**/*.d.ts + +!src/**/*.js +!src/**/*.js.map + +!src/**/*.css + +# Blacklist - src/ test files +src/**/*.test.ts +src/**/*.test.d.ts +src/**/*.test.js +src/**/*.test.js.map + +# Whitelist - typings/ +!typings/*.d.ts diff --git a/addons/xterm-addon-canvas/LICENSE b/addons/xterm-addon-canvas/LICENSE new file mode 100644 index 00000000..b9dc26fe --- /dev/null +++ b/addons/xterm-addon-canvas/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-canvas/README.md b/addons/xterm-addon-canvas/README.md new file mode 100644 index 00000000..deec04c6 --- /dev/null +++ b/addons/xterm-addon-canvas/README.md @@ -0,0 +1,3 @@ +## xterm-addon-canvas + +TODO: Doc diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json new file mode 100644 index 00000000..3d08e9db --- /dev/null +++ b/addons/xterm-addon-canvas/package.json @@ -0,0 +1,27 @@ +{ + "name": "xterm-addon-canvas", + "version": "0.12.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-canvas.js", + "types": "typings/xterm-addon-canvas.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", + "license": "MIT", + "keywords": [ + "terminal", + "canvas", + "xterm", + "xterm.js" + ], + "scripts": { + "build": "../../node_modules/.bin/tsc -p .", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^4.0.0" + } +} diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts new file mode 100644 index 00000000..34fffb84 --- /dev/null +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminalAddon, Terminal } from 'xterm'; + +export class CanvasAddon implements ITerminalAddon { + private _terminal?: Terminal; + // private _renderer?: WebglRenderer; + // private _onContextLoss = new EventEmitter(); + // public get onContextLoss(): IEvent { return this._onContextLoss.event; } + + public activate(terminal: Terminal): void { + // if (!terminal.element) { + // throw new Error('Cannot activate WebglAddon before Terminal.open'); + // } + // if (isSafari) { + // throw new Error('Webgl is not currently supported on Safari'); + // } + this._terminal = terminal; + // const renderService: IRenderService = (terminal as any)._core._renderService; + // const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; + // const decorationService: IDecorationService = (terminal as any)._core._decorationService; + // const colors: IColorSet = (terminal as any)._core._colorManager.colors; + // this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer); + // this._renderer.onContextLoss(() => this._onContextLoss.fire()); + // renderService.setRenderer(this._renderer); + } + + public dispose(): void { + // if (!this._terminal) { + // throw new Error('Cannot dispose WebglAddon because it is activated'); + // } + // const renderService: IRenderService = (this._terminal as any)._core._renderService; + // renderService.setRenderer((this._terminal as any)._core._createRenderer()); + // renderService.onResize(this._terminal.cols, this._terminal.rows); + // this._renderer?.dispose(); + // this._renderer = undefined; + } + + // public get textureAtlas(): HTMLCanvasElement | undefined { + // return this._renderer?.textureAtlas; + // } + + // public clearTextureAtlas(): void { + // this._renderer?.clearCharAtlas(); + // } +} diff --git a/addons/xterm-addon-canvas/src/tsconfig.json b/addons/xterm-addon-canvas/src/tsconfig.json new file mode 100644 index 00000000..b0c9f6be --- /dev/null +++ b/addons/xterm-addon-canvas/src/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es6", + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "downlevelIteration": true, + "types": [ + "../../../node_modules/@types/mocha" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + } + ] +} diff --git a/addons/xterm-addon-canvas/tsconfig.json b/addons/xterm-addon-canvas/tsconfig.json new file mode 100644 index 00000000..b711f30a --- /dev/null +++ b/addons/xterm-addon-canvas/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" } + ] +} diff --git a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts new file mode 100644 index 00000000..7bbbd63d --- /dev/null +++ b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; + +declare module 'xterm-addon-canvas' { + /** + * An xterm.js addon that provides search functionality. + */ + export class CanvasAddon implements ITerminalAddon { + public textureAtlas?: HTMLCanvasElement; + + constructor(); + + /** + * Activates the addon. + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ + public dispose(): void; + + /** + * Clears the terminal's texture atlas and triggers a redraw. + */ + public clearTextureAtlas(): void; + } +} diff --git a/addons/xterm-addon-canvas/webpack.config.js b/addons/xterm-addon-canvas/webpack.config.js new file mode 100644 index 00000000..9c98760a --- /dev/null +++ b/addons/xterm-addon-canvas/webpack.config.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'CanvasAddon'; +const mainFile = 'xterm-addon-canvas.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + resolve: { + modules: ['./node_modules'], + extensions: [ '.js' ], + alias: { + common: path.resolve('../../out/common'), + browser: path.resolve('../../out/browser') + } + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; From 1c6ce99419d5f466cb5b6de7813a176520363973 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:26:02 -0700 Subject: [PATCH 03/11] Move canvas renderer contents into addon --- .../src}/BaseRenderLayer.ts | 13 +- .../src}/CursorRenderLayer.ts | 2 +- .../xterm-addon-canvas/src}/CustomGlyphs.ts | 0 .../xterm-addon-canvas/src}/GridCache.test.ts | 2 +- .../xterm-addon-canvas/src}/GridCache.ts | 0 .../src}/LinkRenderLayer.ts | 4 +- .../xterm-addon-canvas/src}/Renderer.ts | 13 +- .../src}/SelectionRenderLayer.ts | 2 +- .../src}/TextRenderLayer.ts | 4 +- addons/xterm-addon-canvas/src/Types.d.ts | 110 +++ .../src}/atlas/BaseCharAtlas.ts | 2 +- .../src}/atlas/CharAtlasCache.ts | 8 +- .../src}/atlas/CharAtlasUtils.ts | 2 +- .../src}/atlas/DynamicCharAtlas.ts | 8 +- .../src}/atlas/LRUMap.test.ts | 2 +- .../xterm-addon-canvas/src}/atlas/LRUMap.ts | 0 .../xterm-addon-canvas/src}/atlas/Types.d.ts | 0 addons/xterm-addon-canvas/src/tsconfig.json | 2 + addons/xterm-addon-webgl/src/CustomGlyphs.ts | 646 ++++++++++++++++++ .../src/atlas/WebglCharAtlas.ts | 6 +- .../src/renderLayer/BaseRenderLayer.ts | 2 +- .../src/renderLayer/LinkRenderLayer.ts | 2 +- addons/xterm-addon-webgl/src/tsconfig.json | 2 + demo/client.ts | 3 + src/browser/Terminal.ts | 3 +- src/browser/renderer/{atlas => }/Constants.ts | 3 +- src/browser/renderer/Types.d.ts | 53 -- src/browser/renderer/dom/DomRenderer.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 6 +- tsconfig.all.json | 1 + 30 files changed, 807 insertions(+), 96 deletions(-) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/BaseRenderLayer.ts (98%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/CursorRenderLayer.ts (99%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/CustomGlyphs.ts (100%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/GridCache.test.ts (96%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/GridCache.ts (100%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/LinkRenderLayer.ts (94%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/Renderer.ts (94%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/SelectionRenderLayer.ts (98%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/TextRenderLayer.ts (98%) create mode 100644 addons/xterm-addon-canvas/src/Types.d.ts rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/BaseCharAtlas.ts (96%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/CharAtlasCache.ts (89%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/CharAtlasUtils.ts (96%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/DynamicCharAtlas.ts (98%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/LRUMap.test.ts (96%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/LRUMap.ts (100%) rename {src/browser/renderer => addons/xterm-addon-canvas/src}/atlas/Types.d.ts (100%) create mode 100644 addons/xterm-addon-webgl/src/CustomGlyphs.ts rename src/browser/renderer/{atlas => }/Constants.ts (93%) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts similarity index 98% rename from src/browser/renderer/BaseRenderLayer.ts rename to addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 0a9b8057..54038cc0 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderLayer } from './Types'; import { ICellData, IColor } from 'common/Types'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; -import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; -import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; -import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; +import { IGlyphIdentifier } from './atlas/Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; +import { BaseCharAtlas } from './atlas/BaseCharAtlas'; +import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; @@ -17,7 +18,7 @@ import { IBufferService, IDecorationService, IOptionsService } from 'common/serv import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; -import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +import { tryDrawCustomChar } from './CustomGlyphs'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; diff --git a/src/browser/renderer/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts similarity index 99% rename from src/browser/renderer/CursorRenderLayer.ts rename to addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 3fa576a9..60c1301d 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -4,7 +4,7 @@ */ import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; diff --git a/src/browser/renderer/CustomGlyphs.ts b/addons/xterm-addon-canvas/src/CustomGlyphs.ts similarity index 100% rename from src/browser/renderer/CustomGlyphs.ts rename to addons/xterm-addon-canvas/src/CustomGlyphs.ts diff --git a/src/browser/renderer/GridCache.test.ts b/addons/xterm-addon-canvas/src/GridCache.test.ts similarity index 96% rename from src/browser/renderer/GridCache.test.ts rename to addons/xterm-addon-canvas/src/GridCache.test.ts index 30d22e81..c4b1c220 100644 --- a/src/browser/renderer/GridCache.test.ts +++ b/addons/xterm-addon-canvas/src/GridCache.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { GridCache } from 'browser/renderer/GridCache'; +import { GridCache } from './GridCache'; describe('GridCache', () => { let grid: GridCache; diff --git a/src/browser/renderer/GridCache.ts b/addons/xterm-addon-canvas/src/GridCache.ts similarity index 100% rename from src/browser/renderer/GridCache.ts rename to addons/xterm-addon-canvas/src/GridCache.ts diff --git a/src/browser/renderer/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts similarity index 94% rename from src/browser/renderer/LinkRenderLayer.ts rename to addons/xterm-addon-canvas/src/LinkRenderLayer.ts index f1a9b13e..f92f6d54 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -5,8 +5,8 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { is256Color } from './atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; diff --git a/src/browser/renderer/Renderer.ts b/addons/xterm-addon-canvas/src/Renderer.ts similarity index 94% rename from src/browser/renderer/Renderer.ts rename to addons/xterm-addon-canvas/src/Renderer.ts index 66597931..629f69f4 100644 --- a/src/browser/renderer/Renderer.ts +++ b/addons/xterm-addon-canvas/src/Renderer.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { TextRenderLayer } from 'browser/renderer/TextRenderLayer'; -import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer'; -import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer'; -import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer'; +import { TextRenderLayer } from './TextRenderLayer'; +import { SelectionRenderLayer } from './SelectionRenderLayer'; +import { CursorRenderLayer } from './CursorRenderLayer'; +import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderLayer } from './Types'; +import { LinkRenderLayer } from './LinkRenderLayer'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; -import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; +import { removeTerminalFromCache } from './atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; let nextRendererId = 1; diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts similarity index 98% rename from src/browser/renderer/SelectionRenderLayer.ts rename to addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index ce4fe071..2fa82c0d 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -4,7 +4,7 @@ */ import { IRenderDimensions } from 'browser/renderer/Types'; -import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { BaseRenderLayer } from './BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; diff --git a/src/browser/renderer/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts similarity index 98% rename from src/browser/renderer/TextRenderLayer.ts rename to addons/xterm-addon-canvas/src/TextRenderLayer.ts index ef5a9b62..ea5fea0b 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -5,8 +5,8 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { CharData, ICellData } from 'common/Types'; -import { GridCache } from 'browser/renderer/GridCache'; -import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { GridCache } from './GridCache'; +import { BaseRenderLayer } from './BaseRenderLayer'; import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts new file mode 100644 index 00000000..6f5aff85 --- /dev/null +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { IColorSet } from 'browser/Types'; +import { IEvent } from 'common/EventEmitter'; + +// TODO: Use core interfaces +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +export interface IRequestRedrawEvent { + start: number; + end: number; +} + +/** + * Note that IRenderer implementations should emit the refresh event after + * rendering rows to the screen. + */ +export interface IRenderer extends IDisposable { + readonly dimensions: IRenderDimensions; + + /** + * Fires when the renderer is requesting to be redrawn on the next animation + * frame but is _not_ a result of content changing (eg. selection changes). + */ + readonly onRequestRedraw: IEvent; + + dispose(): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + renderRows(start: number, end: number): void; + clearTextureAtlas?(): void; +} + +export interface IRenderLayer extends IDisposable { + /** + * Called when the terminal loses focus. + */ + onBlur(): void; + + /** + * * Called when the terminal gets focus. + */ + onFocus(): void; + + /** + * Called when the cursor is moved. + */ + onCursorMove(): void; + + /** + * Called when options change. + */ + onOptionsChanged(): void; + + /** + * Called when the theme changes. + */ + setColors(colorSet: IColorSet): void; + + /** + * Called when the data in the grid has changed (or needs to be rendered + * again). + */ + onGridChanged(startRow: number, endRow: number): void; + + /** + * Calls when the selection changes. + */ + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + + /** + * Resize the render layer. + */ + resize(dim: IRenderDimensions): void; + + /** + * Clear the state of the render layer. + */ + reset(): void; + + /** + * Clears the texture atlas. + */ + clearTextureAtlas(): void; +} diff --git a/src/browser/renderer/atlas/BaseCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts similarity index 96% rename from src/browser/renderer/atlas/BaseCharAtlas.ts rename to addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts index 83c30d2f..03cf0285 100644 --- a/src/browser/renderer/atlas/BaseCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; +import { IGlyphIdentifier } from './Types'; import { IDisposable } from 'common/Types'; export abstract class BaseCharAtlas implements IDisposable { diff --git a/src/browser/renderer/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts similarity index 89% rename from src/browser/renderer/atlas/CharAtlasCache.ts rename to addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts index 257835ba..7d020dcc 100644 --- a/src/browser/renderer/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils'; -import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; -import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas'; -import { ICharAtlasConfig } from 'browser/renderer/atlas/Types'; +import { generateConfig, configEquals } from './CharAtlasUtils'; +import { BaseCharAtlas } from './BaseCharAtlas'; +import { DynamicCharAtlas } from './DynamicCharAtlas'; +import { ICharAtlasConfig } from './Types'; import { IColorSet } from 'browser/Types'; import { ITerminalOptions } from 'common/services/Services'; diff --git a/src/browser/renderer/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts similarity index 96% rename from src/browser/renderer/atlas/CharAtlasUtils.ts rename to addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts index 696c6c12..4c84c86a 100644 --- a/src/browser/renderer/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharAtlasConfig } from 'browser/renderer/atlas/Types'; +import { ICharAtlasConfig } from './Types'; import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { IColorSet, IPartialColorSet } from 'browser/Types'; import { ITerminalOptions } from 'common/services/Services'; diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts similarity index 98% rename from src/browser/renderer/atlas/DynamicCharAtlas.ts rename to addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts index 59069879..f5fc2c99 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; -import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types'; -import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; +import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; +import { BaseCharAtlas } from './BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; -import { LRUMap } from 'browser/renderer/atlas/LRUMap'; +import { LRUMap } from './LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'common/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; diff --git a/src/browser/renderer/atlas/LRUMap.test.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts similarity index 96% rename from src/browser/renderer/atlas/LRUMap.test.ts rename to addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts index 792f85a7..b24ad2af 100644 --- a/src/browser/renderer/atlas/LRUMap.test.ts +++ b/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { LRUMap } from 'browser/renderer/atlas/LRUMap'; +import { LRUMap } from './LRUMap'; describe('LRUMap', () => { it('can be used to store and retrieve values', () => { diff --git a/src/browser/renderer/atlas/LRUMap.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.ts similarity index 100% rename from src/browser/renderer/atlas/LRUMap.ts rename to addons/xterm-addon-canvas/src/atlas/LRUMap.ts diff --git a/src/browser/renderer/atlas/Types.d.ts b/addons/xterm-addon-canvas/src/atlas/Types.d.ts similarity index 100% rename from src/browser/renderer/atlas/Types.d.ts rename to addons/xterm-addon-canvas/src/atlas/Types.d.ts diff --git a/addons/xterm-addon-canvas/src/tsconfig.json b/addons/xterm-addon-canvas/src/tsconfig.json index b0c9f6be..1ce2a5e3 100644 --- a/addons/xterm-addon-canvas/src/tsconfig.json +++ b/addons/xterm-addon-canvas/src/tsconfig.json @@ -5,6 +5,7 @@ "lib": [ "dom", "es6", + "ES2017.Object" ], "rootDir": ".", "outDir": "../out", @@ -21,6 +22,7 @@ }, "strict": true, "downlevelIteration": true, + "experimentalDecorators": true, "types": [ "../../../node_modules/@types/mocha" ] diff --git a/addons/xterm-addon-webgl/src/CustomGlyphs.ts b/addons/xterm-addon-webgl/src/CustomGlyphs.ts new file mode 100644 index 00000000..e6adf5d1 --- /dev/null +++ b/addons/xterm-addon-webgl/src/CustomGlyphs.ts @@ -0,0 +1,646 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { throwIfFalsy } from 'browser/renderer/RendererUtils'; + +interface IBlockVector { + x: number; + y: number; + w: number; + h: number; +} + +export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefined } = { + // Block elements (0x2580-0x2590) + '▀': [{ x: 0, y: 0, w: 8, h: 4 }], // UPPER HALF BLOCK + '▁': [{ x: 0, y: 7, w: 8, h: 1 }], // LOWER ONE EIGHTH BLOCK + '▂': [{ x: 0, y: 6, w: 8, h: 2 }], // LOWER ONE QUARTER BLOCK + '▃': [{ x: 0, y: 5, w: 8, h: 3 }], // LOWER THREE EIGHTHS BLOCK + '▄': [{ x: 0, y: 4, w: 8, h: 4 }], // LOWER HALF BLOCK + '▅': [{ x: 0, y: 3, w: 8, h: 5 }], // LOWER FIVE EIGHTHS BLOCK + '▆': [{ x: 0, y: 2, w: 8, h: 6 }], // LOWER THREE QUARTERS BLOCK + '▇': [{ x: 0, y: 1, w: 8, h: 7 }], // LOWER SEVEN EIGHTHS BLOCK + '█': [{ x: 0, y: 0, w: 8, h: 8 }], // FULL BLOCK + '▉': [{ x: 0, y: 0, w: 7, h: 8 }], // LEFT SEVEN EIGHTHS BLOCK + '▊': [{ x: 0, y: 0, w: 6, h: 8 }], // LEFT THREE QUARTERS BLOCK + '▋': [{ x: 0, y: 0, w: 5, h: 8 }], // LEFT FIVE EIGHTHS BLOCK + '▌': [{ x: 0, y: 0, w: 4, h: 8 }], // LEFT HALF BLOCK + '▍': [{ x: 0, y: 0, w: 3, h: 8 }], // LEFT THREE EIGHTHS BLOCK + '▎': [{ x: 0, y: 0, w: 2, h: 8 }], // LEFT ONE QUARTER BLOCK + '▏': [{ x: 0, y: 0, w: 1, h: 8 }], // LEFT ONE EIGHTH BLOCK + '▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK + + // Block elements (0x2594-0x2595) + '▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK + '▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK + + // Terminal graphic characters (0x2596-0x259F) + '▖': [{ x: 0, y: 4, w: 4, h: 4 }], // QUADRANT LOWER LEFT + '▗': [{ x: 4, y: 4, w: 4, h: 4 }], // QUADRANT LOWER RIGHT + '▘': [{ x: 0, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT + '▙': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT + '▚': [{ x: 0, y: 0, w: 4, h: 4 }, { x: 4, y: 4, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND LOWER RIGHT + '▛': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT + '▜': [{ x: 0, y: 0, w: 8, h: 4 }, { x: 4, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT + '▝': [{ x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER RIGHT + '▞': [{ x: 4, y: 0, w: 4, h: 4 }, { x: 0, y: 4, w: 4, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT + '▟': [{ x: 4, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT + + // VERTICAL ONE EIGHTH BLOCK-2 through VERTICAL ONE EIGHTH BLOCK-7 + '\u{1FB70}': [{ x: 1, y: 0, w: 1, h: 8 }], + '\u{1FB71}': [{ x: 2, y: 0, w: 1, h: 8 }], + '\u{1FB72}': [{ x: 3, y: 0, w: 1, h: 8 }], + '\u{1FB73}': [{ x: 4, y: 0, w: 1, h: 8 }], + '\u{1FB74}': [{ x: 5, y: 0, w: 1, h: 8 }], + '\u{1FB75}': [{ x: 6, y: 0, w: 1, h: 8 }], + + // HORIZONTAL ONE EIGHTH BLOCK-2 through HORIZONTAL ONE EIGHTH BLOCK-7 + '\u{1FB76}': [{ x: 0, y: 1, w: 8, h: 1 }], + '\u{1FB77}': [{ x: 0, y: 2, w: 8, h: 1 }], + '\u{1FB78}': [{ x: 0, y: 3, w: 8, h: 1 }], + '\u{1FB79}': [{ x: 0, y: 4, w: 8, h: 1 }], + '\u{1FB7A}': [{ x: 0, y: 5, w: 8, h: 1 }], + '\u{1FB7B}': [{ x: 0, y: 6, w: 8, h: 1 }], + + // LEFT AND LOWER ONE EIGHTH BLOCK + '\u{1FB7C}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }], + // LEFT AND UPPER ONE EIGHTH BLOCK + '\u{1FB7D}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }], + // RIGHT AND UPPER ONE EIGHTH BLOCK + '\u{1FB7E}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }], + // RIGHT AND LOWER ONE EIGHTH BLOCK + '\u{1FB7F}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }], + // UPPER AND LOWER ONE EIGHTH BLOCK + '\u{1FB80}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }], + // HORIZONTAL ONE EIGHTH BLOCK-1358 + '\u{1FB81}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 2, w: 8, h: 1 }, { x: 0, y: 4, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }], + + // UPPER ONE QUARTER BLOCK + '\u{1FB82}': [{ x: 0, y: 0, w: 8, h: 2 }], + // UPPER THREE EIGHTHS BLOCK + '\u{1FB83}': [{ x: 0, y: 0, w: 8, h: 3 }], + // UPPER FIVE EIGHTHS BLOCK + '\u{1FB84}': [{ x: 0, y: 0, w: 8, h: 5 }], + // UPPER THREE QUARTERS BLOCK + '\u{1FB85}': [{ x: 0, y: 0, w: 8, h: 6 }], + // UPPER SEVEN EIGHTHS BLOCK + '\u{1FB86}': [{ x: 0, y: 0, w: 8, h: 7 }], + + // RIGHT ONE QUARTER BLOCK + '\u{1FB87}': [{ x: 6, y: 0, w: 2, h: 8 }], + // RIGHT THREE EIGHTHS B0OCK + '\u{1FB88}': [{ x: 5, y: 0, w: 3, h: 8 }], + // RIGHT FIVE EIGHTHS BL0CK + '\u{1FB89}': [{ x: 3, y: 0, w: 5, h: 8 }], + // RIGHT THREE QUARTERS 0LOCK + '\u{1FB8A}': [{ x: 2, y: 0, w: 6, h: 8 }], + // RIGHT SEVEN EIGHTHS B0OCK + '\u{1FB8B}': [{ x: 1, y: 0, w: 7, h: 8 }], + + // CHECKER BOARD FILL + '\u{1FB95}': [ + { x: 0, y: 0, w: 2, h: 2 }, { x: 4, y: 0, w: 2, h: 2 }, + { x: 2, y: 2, w: 2, h: 2 }, { x: 6, y: 2, w: 2, h: 2 }, + { x: 0, y: 4, w: 2, h: 2 }, { x: 4, y: 4, w: 2, h: 2 }, + { x: 2, y: 6, w: 2, h: 2 }, { x: 6, y: 6, w: 2, h: 2 } + ], + // INVERSE CHECKER BOARD FILL + '\u{1FB96}': [ + { x: 2, y: 0, w: 2, h: 2 }, { x: 6, y: 0, w: 2, h: 2 }, + { x: 0, y: 2, w: 2, h: 2 }, { x: 4, y: 2, w: 2, h: 2 }, + { x: 2, y: 4, w: 2, h: 2 }, { x: 6, y: 4, w: 2, h: 2 }, + { x: 0, y: 6, w: 2, h: 2 }, { x: 4, y: 6, w: 2, h: 2 } + ], + // HEAVY HORIZONTAL FILL (upper middle and lower one quarter block) + '\u{1FB97}': [{ x: 0, y: 2, w: 8, h: 2 }, { x: 0, y: 6, w: 8, h: 2 }] +}; + +type PatternDefinition = number[][]; + +/** + * Defines the repeating pattern used by special characters, the pattern is made up of a 2d array of + * pixel values to be filled (1) or not filled (0). + */ +const patternCharacterDefinitions: { [key: string]: PatternDefinition | undefined } = { + // Shade characters (0x2591-0x2593) + '░': [ // LIGHT SHADE (25%) + [1, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 0] + ], + '▒': [ // MEDIUM SHADE (50%) + [1, 0], + [0, 0], + [0, 1], + [0, 0] + ], + '▓': [ // DARK SHADE (75%) + [0, 1], + [1, 1], + [1, 0], + [1, 1] + ] +}; + +const enum Shapes { + /** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1', + /** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5', + + /** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5', + /** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5', + /** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1', + /** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5', + + /** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0', + /** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5', + /** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5', + /** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1', + + /** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0', + /** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5', + /** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5', + /** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1', + + /** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1', + + /** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled + /** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled + /** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled + /** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9', + /** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333', + /** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95', +} + +const enum Style { + NORMAL = 1, + BOLD = 3 +} + +/** + * @param xp The percentage of 15% of the x axis. + * @param yp The percentage of 15% of the x axis on the y axis. + */ +type DrawFunctionDefinition = (xp: number, yp: number) => string; + +/** + * This contains the definitions of all box drawing characters in the format of SVG paths (ie. the + * svg d attribute). + */ +export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number]: string | DrawFunctionDefinition } | undefined } = { + // Uniform normal and bold + '─': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT }, + '━': { [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '│': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM }, + '┃': { [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '┌': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM }, + '┏': { [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '┐': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM }, + '┓': { [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '└': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT }, + '┗': { [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '┘': { [Style.NORMAL]: Shapes.TOP_TO_LEFT }, + '┛': { [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '├': { [Style.NORMAL]: Shapes.T_RIGHT }, + '┣': { [Style.BOLD]: Shapes.T_RIGHT }, + '┤': { [Style.NORMAL]: Shapes.T_LEFT }, + '┫': { [Style.BOLD]: Shapes.T_LEFT }, + '┬': { [Style.NORMAL]: Shapes.T_BOTTOM }, + '┳': { [Style.BOLD]: Shapes.T_BOTTOM }, + '┴': { [Style.NORMAL]: Shapes.T_TOP }, + '┻': { [Style.BOLD]: Shapes.T_TOP }, + '┼': { [Style.NORMAL]: Shapes.CROSS }, + '╋': { [Style.BOLD]: Shapes.CROSS }, + '╴': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT }, + '╸': { [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '╵': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP }, + '╹': { [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '╶': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT }, + '╺': { [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '╷': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM }, + '╻': { [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + + // Double border + '═': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, + '║': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, + '╒': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` }, + '╓': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},1 L${.5 - xp},.5 L1,.5 M${.5 + xp},.5 L${.5 + xp},1` }, + '╔': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` }, + '╕': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L.5,${.5 - yp} L.5,1 M0,${.5 + yp} L.5,${.5 + yp}` }, + '╖': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},1 L${.5 + xp},.5 L0,.5 M${.5 - xp},.5 L${.5 - xp},1` }, + '╗': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},1` }, + '╘': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 + yp} L1,${.5 + yp} M.5,${.5 - yp} L1,${.5 - yp}` }, + '╙': { [Style.NORMAL]: (xp, yp) => `M1,.5 L${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` }, + '╚': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0 M1,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},0` }, + '╛': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L.5,${.5 + yp} L.5,0 M0,${.5 - yp} L.5,${.5 - yp}` }, + '╜': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 + xp},.5 L${.5 + xp},0 M${.5 - xp},.5 L${.5 - xp},0` }, + '╝': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M0,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},0` }, + '╞': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` }, + '╟': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1 M${.5 + xp},.5 L1,.5` }, + '╠': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, + '╡': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L.5,${.5 - yp} M0,${.5 + yp} L.5,${.5 + yp}` }, + '╢': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 - xp},.5 M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, + '╣': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},0 L${.5 + xp},1 M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0` }, + '╤': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp} M.5,${.5 + yp} L.5,1` }, + '╥': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},1 M${.5 + xp},.5 L${.5 + xp},1` }, + '╦': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` }, + '╧': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - yp} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, + '╨': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` }, + '╩': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L1,${.5 + yp} M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, + '╪': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, + '╫': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, + '╬': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, + + // Diagonal + '╱': { [Style.NORMAL]: 'M1,0 L0,1' }, + '╲': { [Style.NORMAL]: 'M0,0 L1,1' }, + '╳': { [Style.NORMAL]: 'M1,0 L0,1 M0,0 L1,1' }, + + // Mixed weight + '╼': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '╽': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '╾': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '╿': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┍': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┎': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┑': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┒': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┕': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┖': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┙': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┚': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┝': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┞': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┟': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┠': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '┡': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '┢': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '┥': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┦': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┧': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┨': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '┩': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '┪': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '┭': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┮': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┯': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '┰': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┱': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '┲': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '┵': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┶': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┷': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '┸': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┹': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '┺': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '┽': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┾': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}`, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┿': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '╀': { [Style.NORMAL]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}`, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '╁': { [Style.NORMAL]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '╂': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '╃': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '╄': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '╅': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '╆': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '╇': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}` }, + '╈': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}` }, + '╉': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}` }, + '╊': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}` }, + + // Dashed + '╌': { [Style.NORMAL]: Shapes.TWO_DASHES_HORIZONTAL }, + '╍': { [Style.BOLD]: Shapes.TWO_DASHES_HORIZONTAL }, + '┄': { [Style.NORMAL]: Shapes.THREE_DASHES_HORIZONTAL }, + '┅': { [Style.BOLD]: Shapes.THREE_DASHES_HORIZONTAL }, + '┈': { [Style.NORMAL]: Shapes.FOUR_DASHES_HORIZONTAL }, + '┉': { [Style.BOLD]: Shapes.FOUR_DASHES_HORIZONTAL }, + '╎': { [Style.NORMAL]: Shapes.TWO_DASHES_VERTICAL }, + '╏': { [Style.BOLD]: Shapes.TWO_DASHES_VERTICAL }, + '┆': { [Style.NORMAL]: Shapes.THREE_DASHES_VERTICAL }, + '┇': { [Style.BOLD]: Shapes.THREE_DASHES_VERTICAL }, + '┊': { [Style.NORMAL]: Shapes.FOUR_DASHES_VERTICAL }, + '┋': { [Style.BOLD]: Shapes.FOUR_DASHES_VERTICAL }, + + // Curved + '╭': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 + (yp / .15 * .5)} C.5,${.5 + (yp / .15 * .5)},.5,.5,1,.5` }, + '╮': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 + (yp / .15 * .5)} C.5,${.5 + (yp / .15 * .5)},.5,.5,0,.5` }, + '╯': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - (yp / .15 * .5)} C.5,${.5 - (yp / .15 * .5)},.5,.5,0,.5` }, + '╰': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - (yp / .15 * .5)} C.5,${.5 - (yp / .15 * .5)},.5,.5,1,.5` } +}; + +interface IVectorShape { + d: string; + type: VectorType; + /** Padding to apply to the vector's x axis in CSS pixels. */ + horizontalPadding?: number; +} + +const enum VectorType { + FILL, + STROKE +} + +/** + * This contains the definitions of the primarily used box drawing characters as vector shapes. The + * reason these characters are defined specially is to avoid common problems if a user's font has + * not been patched with powerline characters and also to get pixel perfect rendering as rendering + * issues can occur around AA/SPAA. + * + * Original symbols defined in https://github.com/powerline/fontpatcher + */ +export const powerlineDefinitions: { [index: string]: IVectorShape } = { + // Right triangle solid + '\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL }, + // Right triangle line + '\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, horizontalPadding: 0.5 }, + // Left triangle solid + '\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL }, + // Left triangle line + '\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, horizontalPadding: 0.5 }, + // Right semi-circle solid, + '\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL }, + // Right semi-circle line, + '\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE }, + // Left semi-circle solid, + '\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL }, + // Left semi-circle line, + '\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE } +}; + +/** + * Try drawing a custom block element or box drawing character, returning whether it was + * successfully drawn. + */ +export function tryDrawCustomChar( + ctx: CanvasRenderingContext2D, + c: string, + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): boolean { + const blockElementDefinition = blockElementDefinitions[c]; + if (blockElementDefinition) { + drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + const patternDefinition = patternCharacterDefinitions[c]; + if (patternDefinition) { + drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + const boxDrawingDefinition = boxDrawingDefinitions[c]; + if (boxDrawingDefinition) { + drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + const powerlineDefinition = powerlineDefinitions[c]; + if (powerlineDefinition) { + drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + return false; +} + +function drawBlockElementChar( + ctx: CanvasRenderingContext2D, + charDefinition: IBlockVector[], + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + for (let i = 0; i < charDefinition.length; i++) { + const box = charDefinition[i]; + const xEighth = scaledCellWidth / 8; + const yEighth = scaledCellHeight / 8; + ctx.fillRect( + xOffset + box.x * xEighth, + yOffset + box.y * yEighth, + box.w * xEighth, + box.h * yEighth + ); + } +} + +const cachedPatterns: Map> = new Map(); + +function drawPatternChar( + ctx: CanvasRenderingContext2D, + charDefinition: number[][], + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + let patternSet = cachedPatterns.get(charDefinition); + if (!patternSet) { + patternSet = new Map(); + cachedPatterns.set(charDefinition, patternSet); + } + const fillStyle = ctx.fillStyle; + if (typeof fillStyle !== 'string') { + throw new Error(`Unexpected fillStyle type "${fillStyle}"`); + } + let pattern = patternSet.get(fillStyle); + if (!pattern) { + const width = charDefinition[0].length; + const height = charDefinition.length; + const tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = width; + tmpCanvas.height = height; + const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d')); + const imageData = new ImageData(width, height); + + // Extract rgba from fillStyle + let r: number; + let g: number; + let b: number; + let a: number; + if (fillStyle.startsWith('#')) { + r = parseInt(fillStyle.slice(1, 3), 16); + g = parseInt(fillStyle.slice(3, 5), 16); + b = parseInt(fillStyle.slice(5, 7), 16); + a = fillStyle.length > 7 && parseInt(fillStyle.slice(7, 9), 16) || 1; + } else if (fillStyle.startsWith('rgba')) { + ([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e))); + } else { + throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`); + } + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + imageData.data[(y * width + x) * 4 ] = r; + imageData.data[(y * width + x) * 4 + 1] = g; + imageData.data[(y * width + x) * 4 + 2] = b; + imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255); + } + } + tmpCtx.putImageData(imageData, 0, 0); + pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null)); + patternSet.set(fillStyle, pattern); + } + ctx.fillStyle = pattern; + ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight); +} + +/** + * Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to + * canvas draw calls. + * + * Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐ + * ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤ + * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘ + * ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐ + * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤ + * └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘ + * + * Other: + * ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈ + * │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉ + * ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋ + * + * All box drawing characters: + * ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ + * ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ + * ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ + * ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ + * ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ + * ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ + * ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ + * ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ + * + * --- + * + * Box drawing alignment tests: █ + * ▉ + * ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ + * ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ + * ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ + * ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ + * ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ + * ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ + * ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ + * + * Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html + */ +function drawBoxDrawingChar( + ctx: CanvasRenderingContext2D, + charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) }, + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + ctx.strokeStyle = ctx.fillStyle; + for (const [fontWeight, instructions] of Object.entries(charDefinition)) { + ctx.beginPath(); + ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight); + let actualInstructions: string; + if (typeof instructions === 'function') { + const xp = .15; + const yp = .15 / scaledCellHeight * scaledCellWidth; + actualInstructions = instructions(xp, yp); + } else { + actualInstructions = instructions; + } + for (const instruction of actualInstructions.split(' ')) { + const type = instruction[0]; + const f = svgToCanvasInstructionMap[type]; + if (!f) { + console.error(`Could not find drawing instructions for "${type}"`); + continue; + } + const args: string[] = instruction.substring(1).split(','); + if (!args[0] || !args[1]) { + continue; + } + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset)); + } + ctx.stroke(); + ctx.closePath(); + } +} + +function drawPowerlineChar( + ctx: CanvasRenderingContext2D, + charDefinition: IVectorShape, + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + ctx.beginPath(); + ctx.lineWidth = window.devicePixelRatio; + for (const instruction of charDefinition.d.split(' ')) { + const type = instruction[0]; + const f = svgToCanvasInstructionMap[type]; + if (!f) { + console.error(`Could not find drawing instructions for "${type}"`); + continue; + } + const args: string[] = instruction.substring(1).split(','); + if (!args[0] || !args[1]) { + continue; + } + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.horizontalPadding)); + } + if (charDefinition.type === VectorType.STROKE) { + ctx.strokeStyle = ctx.fillStyle; + ctx.stroke(); + } else { + ctx.fill(); + } + ctx.closePath(); +} + +function clamp(value: number, max: number, min: number = 0): number { + return Math.max(Math.min(value, max), min); +} + +const svgToCanvasInstructionMap: { [index: string]: any } = { + 'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]), + 'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]), + 'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]) +}; + +function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, horizontalPadding: number = 0): number[] { + const result = args.map(e => parseFloat(e) || parseInt(e)); + + if (result.length < 2) { + throw new Error('Too few arguments for instruction'); + } + + for (let x = 0; x < result.length; x += 2) { + // Translate from 0-1 to 0-cellWidth + result[x] *= cellWidth - (horizontalPadding * 2 * window.devicePixelRatio); + // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp + // line at 100% devicePixelRatio + if (result[x] !== 0) { + result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0); + } + // Apply the cell's offset (ie. x*cellWidth) + result[x] += xOffset + (horizontalPadding * window.devicePixelRatio); + } + + for (let y = 1; y < result.length; y += 2) { + // Translate from 0-1 to 0-cellHeight + result[y] *= cellHeight; + // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp + // line at 100% devicePixelRatio + if (result[y] !== 0) { + result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0); + } + // Apply the cell's offset (ie. x*cellHeight) + result[y] += yOffset; + } + + return result; +} diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 05751c42..b3b9b973 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -4,15 +4,15 @@ */ import { ICharAtlasConfig } from './Types'; -import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; +import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { channels, color, rgba } from 'common/Color'; -import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +import { color, rgba } from 'common/Color'; +import { tryDrawCustomChar } from '../CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils'; // For debugging purposes, it can be useful to set this to a really tiny value, diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 0a5a0065..0aa2049c 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -7,7 +7,7 @@ import { IRenderLayer } from './Types'; import { acquireCharAtlas } from '../atlas/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; +import { TEXT_BASELINE } from 'browser/renderer/Constants'; import { IRenderDimensions } from 'browser/renderer/Types'; import { CellData } from 'common/buffer/CellData'; import { WebglCharAtlas } from 'atlas/WebglCharAtlas'; diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index b2d004f9..440dbe7b 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -5,7 +5,7 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { is256Color } from '../atlas/CharAtlasUtils'; import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index b0c9f6be..b111ae55 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -5,6 +5,8 @@ "lib": [ "dom", "es6", + "es2016.Array.Include", + "ES2017.Object" ], "rootDir": ".", "outDir": "../out", diff --git a/demo/client.ts b/demo/client.ts index d3b5af9c..e00279d7 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -10,6 +10,7 @@ // Use tsc version (yarn watch) import { Terminal } from '../out/browser/public/Terminal'; import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; +import { CanvasAddon } from '../addons/xterm-addon-canvas/out/CanvasAddon'; import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; @@ -60,6 +61,7 @@ interface IDemoAddon { canChange: boolean; ctor: T extends 'attach' ? typeof AttachAddon : + T extends 'canvas' ? typeof CanvasAddon : T extends 'fit' ? typeof FitAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : @@ -69,6 +71,7 @@ interface IDemoAddon { typeof WebglAddon; instance?: T extends 'attach' ? AttachAddon : + T extends 'canvas' ? CanvasAddon : T extends 'fit' ? FitAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 91c4a920..c59a065a 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -28,7 +28,6 @@ import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { C0, C1_ESCAPED } from 'common/data/EscapeSequences'; import { WindowsOptionsReportType } from '../common/InputHandler'; -import { Renderer } from 'browser/renderer/Renderer'; import { SelectionService } from 'browser/services/SelectionService'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; @@ -609,7 +608,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } private _createRenderer(): IRenderer { - return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier2); + return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); } /** diff --git a/src/browser/renderer/atlas/Constants.ts b/src/browser/renderer/Constants.ts similarity index 93% rename from src/browser/renderer/atlas/Constants.ts rename to src/browser/renderer/Constants.ts index c1701e97..ac698b85 100644 --- a/src/browser/renderer/atlas/Constants.ts +++ b/src/browser/renderer/Constants.ts @@ -6,10 +6,9 @@ import { isFirefox, isLegacyEdge } from 'common/Platform'; export const INVERTED_DEFAULT_COLOR = 257; + export const DIM_OPACITY = 0.5; // The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would // result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly // unaligned Powerline fonts (PR 3356#issuecomment-850928179). export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic'; - -export const CHAR_ATLAS_CELL_SPACING = 1; diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index 6818a926..cb1a85b4 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -54,56 +54,3 @@ export interface IRenderer extends IDisposable { renderRows(start: number, end: number): void; clearTextureAtlas?(): void; } - -export interface IRenderLayer extends IDisposable { - /** - * Called when the terminal loses focus. - */ - onBlur(): void; - - /** - * * Called when the terminal gets focus. - */ - onFocus(): void; - - /** - * Called when the cursor is moved. - */ - onCursorMove(): void; - - /** - * Called when options change. - */ - onOptionsChanged(): void; - - /** - * Called when the theme changes. - */ - setColors(colorSet: IColorSet): void; - - /** - * Called when the data in the grid has changed (or needs to be rendered - * again). - */ - onGridChanged(startRow: number, endRow: number): void; - - /** - * Calls when the selection changes. - */ - onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; - - /** - * Resize the render layer. - */ - resize(dim: IRenderDimensions): void; - - /** - * Clear the state of the render layer. - */ - reset(): void; - - /** - * Clears the texture atlas. - */ - clearTextureAtlas(): void; -} diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index a4500e39..991938e4 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -5,7 +5,7 @@ import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index fadf5032..35b8b0e7 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -4,13 +4,13 @@ */ import { IBufferLine, ICellData, IColor } from 'common/Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'common/Color'; import { IColorSet } from 'browser/Types'; -import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; +import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils'; diff --git a/tsconfig.all.json b/tsconfig.all.json index 6fa02446..4d2df306 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -7,6 +7,7 @@ { "path": "./test/api" }, { "path": "./test/benchmark" }, { "path": "./addons/xterm-addon-attach" }, + { "path": "./addons/xterm-addon-canvas" }, { "path": "./addons/xterm-addon-fit" }, { "path": "./addons/xterm-addon-ligatures" }, { "path": "./addons/xterm-addon-search" }, From 48cfd232195d171a980051f5f8ced0a94e91a06b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:33:10 -0700 Subject: [PATCH 04/11] Fix tests that depended on canvas renderer --- addons/xterm-addon-fit/test/FitAddon.api.ts | 8 ++++---- test/api/Terminal.api.ts | 12 +++--------- test/api/TestUtils.ts | 2 +- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index 987092ad..c35673cc 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -48,7 +48,7 @@ describe('FitAddon', () => { it('default', async function(): Promise { await loadFit(); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.equal(dimensions.cols, 87); + assert.equal(dimensions.cols, 86); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -56,7 +56,7 @@ describe('FitAddon', () => { it('width', async function(): Promise { await loadFit(1008); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.equal(dimensions.cols, 110); + assert.equal(dimensions.cols, 109); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -89,7 +89,7 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.equal(cols, 87); + assert.equal(cols, 86); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); @@ -99,7 +99,7 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.equal(cols, 110); + assert.equal(cols, 109); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index fa19b5b4..72b1ba34 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -726,9 +726,7 @@ describe('API Integration Tests', function(): void { describe('registerDecoration', () => { describe('bufferDecorations', () => { it('should register decorations and render them when terminal open is called', async () => { - await page.evaluate(`window.term = new Terminal({ allowProposedApi: true })`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.waitForSelector('.xterm-text-layer'); + await openTerminal(page); await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); @@ -757,9 +755,7 @@ describe('API Integration Tests', function(): void { }); describe('overviewRulerDecorations', () => { it('should not add an overview ruler when width is not set', async () => { - await page.evaluate(`window.term = new Terminal({ allowProposedApi: true })`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.waitForSelector('.xterm-text-layer'); + await openTerminal(page); await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); @@ -768,9 +764,7 @@ describe('API Integration Tests', function(): void { await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); }); it('should add an overview ruler when width is set', async () => { - await page.evaluate(`window.term = new Terminal({ allowProposedApi: true, overviewRulerWidth: 15 })`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.waitForSelector('.xterm-text-layer'); + await openTerminal(page, { overviewRulerWidth: 15 }); await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts index 2b4e96a4..5731009b 100644 --- a/test/api/TestUtils.ts +++ b/test/api/TestUtils.ts @@ -64,7 +64,7 @@ export function getBrowserType(): playwright.BrowserType { const browserType = getBrowserType(); const options: Record = { headless: process.argv.includes('--headless') From cbddd2029ebdb8ed7db699d07b43191f056e0adf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 27 Jul 2022 16:04:24 -0700 Subject: [PATCH 05/11] Get canvas renderer addon working --- addons/xterm-addon-canvas/src/CanvasAddon.ts | 46 ++++++++++--------- .../src/{Renderer.ts => CanvasRenderer.ts} | 2 +- demo/client.ts | 8 ++-- 3 files changed, 31 insertions(+), 25 deletions(-) rename addons/xterm-addon-canvas/src/{Renderer.ts => CanvasRenderer.ts} (99%) diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index 34fffb84..81594ef0 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -3,40 +3,44 @@ * @license MIT */ +import { IRenderService } from 'browser/services/Services'; +import { IColorSet } from 'browser/Types'; +import { CanvasRenderer } from './CanvasRenderer'; +import { IBufferService, IInstantiationService } from 'common/services/Services'; import { ITerminalAddon, Terminal } from 'xterm'; export class CanvasAddon implements ITerminalAddon { private _terminal?: Terminal; - // private _renderer?: WebglRenderer; + private _renderer?: CanvasRenderer; // private _onContextLoss = new EventEmitter(); // public get onContextLoss(): IEvent { return this._onContextLoss.event; } public activate(terminal: Terminal): void { - // if (!terminal.element) { - // throw new Error('Cannot activate WebglAddon before Terminal.open'); - // } - // if (isSafari) { - // throw new Error('Webgl is not currently supported on Safari'); - // } + if (!terminal.element) { + throw new Error('Cannot activate CanvasAddon before Terminal.open'); + } this._terminal = terminal; - // const renderService: IRenderService = (terminal as any)._core._renderService; - // const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; - // const decorationService: IDecorationService = (terminal as any)._core._decorationService; - // const colors: IColorSet = (terminal as any)._core._colorManager.colors; - // this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer); + const instantiationService: IInstantiationService = (terminal as any)._core._instantiationService; + const bufferService: IBufferService = (terminal as any)._core._renderService; + const renderService: IRenderService = (terminal as any)._core._renderService; + const colors: IColorSet = (terminal as any)._core._colorManager.colors; + const screenElement: HTMLElement = (terminal as any)._core.screenElement; + const linkifier = (terminal as any)._core.linkifier2; + this._renderer = instantiationService.createInstance(CanvasRenderer, colors, screenElement, linkifier); // this._renderer.onContextLoss(() => this._onContextLoss.fire()); - // renderService.setRenderer(this._renderer); + renderService.setRenderer(this._renderer); + renderService.onResize(bufferService.cols, bufferService.rows); } public dispose(): void { - // if (!this._terminal) { - // throw new Error('Cannot dispose WebglAddon because it is activated'); - // } - // const renderService: IRenderService = (this._terminal as any)._core._renderService; - // renderService.setRenderer((this._terminal as any)._core._createRenderer()); - // renderService.onResize(this._terminal.cols, this._terminal.rows); - // this._renderer?.dispose(); - // this._renderer = undefined; + if (!this._terminal) { + throw new Error('Cannot dispose CanvasAddon because it is activated'); + } + const renderService: IRenderService = (this._terminal as any)._core._renderService; + renderService.setRenderer((this._terminal as any)._core._createRenderer()); + renderService.onResize(this._terminal.cols, this._terminal.rows); + this._renderer?.dispose(); + this._renderer = undefined; } // public get textureAtlas(): HTMLCanvasElement | undefined { diff --git a/addons/xterm-addon-canvas/src/Renderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts similarity index 99% rename from addons/xterm-addon-canvas/src/Renderer.ts rename to addons/xterm-addon-canvas/src/CanvasRenderer.ts index 629f69f4..88c81ea7 100644 --- a/addons/xterm-addon-canvas/src/Renderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -18,7 +18,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; let nextRendererId = 1; -export class Renderer extends Disposable implements IRenderer { +export class CanvasRenderer extends Disposable implements IRenderer { private _id = nextRendererId++; private _renderLayers: IRenderLayer[]; diff --git a/demo/client.ts b/demo/client.ts index e00279d7..3ee533d0 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -54,7 +54,7 @@ let socketURL; let socket; let pid; -type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures'; +type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; @@ -84,6 +84,7 @@ interface IDemoAddon { const addons: { [T in AddonType]: IDemoAddon} = { attach: { name: 'attach', ctor: AttachAddon, canChange: false }, + canvas: { name: 'canvas', ctor: CanvasAddon, canChange: true }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, @@ -153,6 +154,7 @@ const disposeRecreateButtonHandler = () => { window.term = null; socket = null; addons.attach.instance = undefined; + addons.canvas.instance = undefined; addons.fit.instance = undefined; addons.search.instance = undefined; addons.serialize.instance = undefined; @@ -630,7 +632,7 @@ function writeCustomGlyphHandler() { } function loadTest() { - const isWebglEnabled = !!addons.webgl.instance; + const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom'; const testData = []; let byteCount = 0; for (let i = 0; i < 50; i++) { @@ -656,7 +658,7 @@ function loadTest() { term.write('', () => { const time = Math.round(performance.now() - start); const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); - term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${isWebglEnabled ? 'webgl' : 'canvas'} renderer)`); + term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); // Send ^C to get a new prompt term._core._onData.fire('\x03'); }); From 73b3e31b388c5eb014fb7e0d288b8def3ca74815 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 04:37:42 -0700 Subject: [PATCH 06/11] Canvas readme --- addons/xterm-addon-canvas/README.md | 22 +++++++++++++++++++- addons/xterm-addon-canvas/src/CanvasAddon.ts | 11 ---------- addons/xterm-addon-webgl/README.md | 1 + 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/addons/xterm-addon-canvas/README.md b/addons/xterm-addon-canvas/README.md index deec04c6..ed65967d 100644 --- a/addons/xterm-addon-canvas/README.md +++ b/addons/xterm-addon-canvas/README.md @@ -1,3 +1,23 @@ ## xterm-addon-canvas -TODO: Doc +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a canvas-based renderer using a 2d context to draw. This addon requires xterm.js v5+. + + +### Install + +```bash +npm install --save xterm-addon-canvas +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { CanvasAddon } from 'xterm-addon-canvas'; + +const terminal = new Terminal(); +terminal.open(element); +terminal.loadAddon(new CanvasAddon()); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts) for more advanced usage. diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index 81594ef0..8b8dcee6 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -12,8 +12,6 @@ import { ITerminalAddon, Terminal } from 'xterm'; export class CanvasAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: CanvasRenderer; - // private _onContextLoss = new EventEmitter(); - // public get onContextLoss(): IEvent { return this._onContextLoss.event; } public activate(terminal: Terminal): void { if (!terminal.element) { @@ -27,7 +25,6 @@ export class CanvasAddon implements ITerminalAddon { const screenElement: HTMLElement = (terminal as any)._core.screenElement; const linkifier = (terminal as any)._core.linkifier2; this._renderer = instantiationService.createInstance(CanvasRenderer, colors, screenElement, linkifier); - // this._renderer.onContextLoss(() => this._onContextLoss.fire()); renderService.setRenderer(this._renderer); renderService.onResize(bufferService.cols, bufferService.rows); } @@ -42,12 +39,4 @@ export class CanvasAddon implements ITerminalAddon { this._renderer?.dispose(); this._renderer = undefined; } - - // public get textureAtlas(): HTMLCanvasElement | undefined { - // return this._renderer?.textureAtlas; - // } - - // public clearTextureAtlas(): void { - // this._renderer?.clearCharAtlas(); - // } } diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md index da63e743..2519fb7b 100644 --- a/addons/xterm-addon-webgl/README.md +++ b/addons/xterm-addon-webgl/README.md @@ -16,6 +16,7 @@ import { Terminal } from 'xterm'; import { WebglAddon } from 'xterm-addon-webgl'; const terminal = new Terminal(); +terminal.open(element); terminal.loadAddon(new WebglAddon()); ``` From b3cffe2d309b13d760becd74cf10052392017a83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 04:43:03 -0700 Subject: [PATCH 07/11] Fix fit addon api tests on linux --- addons/xterm-addon-fit/test/FitAddon.api.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index c35673cc..caf8cc7d 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -48,7 +48,8 @@ describe('FitAddon', () => { it('default', async function(): Promise { await loadFit(); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.equal(dimensions.cols, 86); + assert.isAbove(dimensions.cols, 86); + assert.isBelow(dimensions.cols, 87); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -56,7 +57,8 @@ describe('FitAddon', () => { it('width', async function(): Promise { await loadFit(1008); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.equal(dimensions.cols, 109); + assert.isAbove(dimensions.cols, 109); + assert.isBelow(dimensions.cols, 110); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -89,7 +91,8 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.equal(cols, 86); + assert.isAbove(cols, 86); + assert.isBelow(cols, 87); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); @@ -99,7 +102,8 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.equal(cols, 109); + assert.isAbove(cols, 109); + assert.isBelow(cols, 110); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); From ced5c304f1fd8ffe90171309130a9d138819adc0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 04:46:18 -0700 Subject: [PATCH 08/11] Move CustomGlyphs.ts into core --- addons/xterm-addon-canvas/LICENSE | 2 +- .../xterm-addon-canvas/src/BaseRenderLayer.ts | 2 +- addons/xterm-addon-webgl/src/CustomGlyphs.ts | 646 ------------------ .../src/atlas/WebglCharAtlas.ts | 2 +- .../browser/renderer}/CustomGlyphs.ts | 0 5 files changed, 3 insertions(+), 649 deletions(-) delete mode 100644 addons/xterm-addon-webgl/src/CustomGlyphs.ts rename {addons/xterm-addon-canvas/src => src/browser/renderer}/CustomGlyphs.ts (100%) diff --git a/addons/xterm-addon-canvas/LICENSE b/addons/xterm-addon-canvas/LICENSE index b9dc26fe..e597698c 100644 --- a/addons/xterm-addon-canvas/LICENSE +++ b/addons/xterm-addon-canvas/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js) +Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 54038cc0..9a6464c1 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -18,7 +18,7 @@ import { IBufferService, IDecorationService, IOptionsService } from 'common/serv import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; -import { tryDrawCustomChar } from './CustomGlyphs'; +import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; diff --git a/addons/xterm-addon-webgl/src/CustomGlyphs.ts b/addons/xterm-addon-webgl/src/CustomGlyphs.ts deleted file mode 100644 index e6adf5d1..00000000 --- a/addons/xterm-addon-webgl/src/CustomGlyphs.ts +++ /dev/null @@ -1,646 +0,0 @@ -/** - * Copyright (c) 2021 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { throwIfFalsy } from 'browser/renderer/RendererUtils'; - -interface IBlockVector { - x: number; - y: number; - w: number; - h: number; -} - -export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefined } = { - // Block elements (0x2580-0x2590) - '▀': [{ x: 0, y: 0, w: 8, h: 4 }], // UPPER HALF BLOCK - '▁': [{ x: 0, y: 7, w: 8, h: 1 }], // LOWER ONE EIGHTH BLOCK - '▂': [{ x: 0, y: 6, w: 8, h: 2 }], // LOWER ONE QUARTER BLOCK - '▃': [{ x: 0, y: 5, w: 8, h: 3 }], // LOWER THREE EIGHTHS BLOCK - '▄': [{ x: 0, y: 4, w: 8, h: 4 }], // LOWER HALF BLOCK - '▅': [{ x: 0, y: 3, w: 8, h: 5 }], // LOWER FIVE EIGHTHS BLOCK - '▆': [{ x: 0, y: 2, w: 8, h: 6 }], // LOWER THREE QUARTERS BLOCK - '▇': [{ x: 0, y: 1, w: 8, h: 7 }], // LOWER SEVEN EIGHTHS BLOCK - '█': [{ x: 0, y: 0, w: 8, h: 8 }], // FULL BLOCK - '▉': [{ x: 0, y: 0, w: 7, h: 8 }], // LEFT SEVEN EIGHTHS BLOCK - '▊': [{ x: 0, y: 0, w: 6, h: 8 }], // LEFT THREE QUARTERS BLOCK - '▋': [{ x: 0, y: 0, w: 5, h: 8 }], // LEFT FIVE EIGHTHS BLOCK - '▌': [{ x: 0, y: 0, w: 4, h: 8 }], // LEFT HALF BLOCK - '▍': [{ x: 0, y: 0, w: 3, h: 8 }], // LEFT THREE EIGHTHS BLOCK - '▎': [{ x: 0, y: 0, w: 2, h: 8 }], // LEFT ONE QUARTER BLOCK - '▏': [{ x: 0, y: 0, w: 1, h: 8 }], // LEFT ONE EIGHTH BLOCK - '▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK - - // Block elements (0x2594-0x2595) - '▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK - '▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK - - // Terminal graphic characters (0x2596-0x259F) - '▖': [{ x: 0, y: 4, w: 4, h: 4 }], // QUADRANT LOWER LEFT - '▗': [{ x: 4, y: 4, w: 4, h: 4 }], // QUADRANT LOWER RIGHT - '▘': [{ x: 0, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT - '▙': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT - '▚': [{ x: 0, y: 0, w: 4, h: 4 }, { x: 4, y: 4, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND LOWER RIGHT - '▛': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT - '▜': [{ x: 0, y: 0, w: 8, h: 4 }, { x: 4, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT - '▝': [{ x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER RIGHT - '▞': [{ x: 4, y: 0, w: 4, h: 4 }, { x: 0, y: 4, w: 4, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT - '▟': [{ x: 4, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT - - // VERTICAL ONE EIGHTH BLOCK-2 through VERTICAL ONE EIGHTH BLOCK-7 - '\u{1FB70}': [{ x: 1, y: 0, w: 1, h: 8 }], - '\u{1FB71}': [{ x: 2, y: 0, w: 1, h: 8 }], - '\u{1FB72}': [{ x: 3, y: 0, w: 1, h: 8 }], - '\u{1FB73}': [{ x: 4, y: 0, w: 1, h: 8 }], - '\u{1FB74}': [{ x: 5, y: 0, w: 1, h: 8 }], - '\u{1FB75}': [{ x: 6, y: 0, w: 1, h: 8 }], - - // HORIZONTAL ONE EIGHTH BLOCK-2 through HORIZONTAL ONE EIGHTH BLOCK-7 - '\u{1FB76}': [{ x: 0, y: 1, w: 8, h: 1 }], - '\u{1FB77}': [{ x: 0, y: 2, w: 8, h: 1 }], - '\u{1FB78}': [{ x: 0, y: 3, w: 8, h: 1 }], - '\u{1FB79}': [{ x: 0, y: 4, w: 8, h: 1 }], - '\u{1FB7A}': [{ x: 0, y: 5, w: 8, h: 1 }], - '\u{1FB7B}': [{ x: 0, y: 6, w: 8, h: 1 }], - - // LEFT AND LOWER ONE EIGHTH BLOCK - '\u{1FB7C}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }], - // LEFT AND UPPER ONE EIGHTH BLOCK - '\u{1FB7D}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }], - // RIGHT AND UPPER ONE EIGHTH BLOCK - '\u{1FB7E}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }], - // RIGHT AND LOWER ONE EIGHTH BLOCK - '\u{1FB7F}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }], - // UPPER AND LOWER ONE EIGHTH BLOCK - '\u{1FB80}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }], - // HORIZONTAL ONE EIGHTH BLOCK-1358 - '\u{1FB81}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 2, w: 8, h: 1 }, { x: 0, y: 4, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }], - - // UPPER ONE QUARTER BLOCK - '\u{1FB82}': [{ x: 0, y: 0, w: 8, h: 2 }], - // UPPER THREE EIGHTHS BLOCK - '\u{1FB83}': [{ x: 0, y: 0, w: 8, h: 3 }], - // UPPER FIVE EIGHTHS BLOCK - '\u{1FB84}': [{ x: 0, y: 0, w: 8, h: 5 }], - // UPPER THREE QUARTERS BLOCK - '\u{1FB85}': [{ x: 0, y: 0, w: 8, h: 6 }], - // UPPER SEVEN EIGHTHS BLOCK - '\u{1FB86}': [{ x: 0, y: 0, w: 8, h: 7 }], - - // RIGHT ONE QUARTER BLOCK - '\u{1FB87}': [{ x: 6, y: 0, w: 2, h: 8 }], - // RIGHT THREE EIGHTHS B0OCK - '\u{1FB88}': [{ x: 5, y: 0, w: 3, h: 8 }], - // RIGHT FIVE EIGHTHS BL0CK - '\u{1FB89}': [{ x: 3, y: 0, w: 5, h: 8 }], - // RIGHT THREE QUARTERS 0LOCK - '\u{1FB8A}': [{ x: 2, y: 0, w: 6, h: 8 }], - // RIGHT SEVEN EIGHTHS B0OCK - '\u{1FB8B}': [{ x: 1, y: 0, w: 7, h: 8 }], - - // CHECKER BOARD FILL - '\u{1FB95}': [ - { x: 0, y: 0, w: 2, h: 2 }, { x: 4, y: 0, w: 2, h: 2 }, - { x: 2, y: 2, w: 2, h: 2 }, { x: 6, y: 2, w: 2, h: 2 }, - { x: 0, y: 4, w: 2, h: 2 }, { x: 4, y: 4, w: 2, h: 2 }, - { x: 2, y: 6, w: 2, h: 2 }, { x: 6, y: 6, w: 2, h: 2 } - ], - // INVERSE CHECKER BOARD FILL - '\u{1FB96}': [ - { x: 2, y: 0, w: 2, h: 2 }, { x: 6, y: 0, w: 2, h: 2 }, - { x: 0, y: 2, w: 2, h: 2 }, { x: 4, y: 2, w: 2, h: 2 }, - { x: 2, y: 4, w: 2, h: 2 }, { x: 6, y: 4, w: 2, h: 2 }, - { x: 0, y: 6, w: 2, h: 2 }, { x: 4, y: 6, w: 2, h: 2 } - ], - // HEAVY HORIZONTAL FILL (upper middle and lower one quarter block) - '\u{1FB97}': [{ x: 0, y: 2, w: 8, h: 2 }, { x: 0, y: 6, w: 8, h: 2 }] -}; - -type PatternDefinition = number[][]; - -/** - * Defines the repeating pattern used by special characters, the pattern is made up of a 2d array of - * pixel values to be filled (1) or not filled (0). - */ -const patternCharacterDefinitions: { [key: string]: PatternDefinition | undefined } = { - // Shade characters (0x2591-0x2593) - '░': [ // LIGHT SHADE (25%) - [1, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 0] - ], - '▒': [ // MEDIUM SHADE (50%) - [1, 0], - [0, 0], - [0, 1], - [0, 0] - ], - '▓': [ // DARK SHADE (75%) - [0, 1], - [1, 1], - [1, 0], - [1, 1] - ] -}; - -const enum Shapes { - /** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1', - /** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5', - - /** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5', - /** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5', - /** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1', - /** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5', - - /** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0', - /** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5', - /** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5', - /** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1', - - /** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0', - /** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5', - /** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5', - /** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1', - - /** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1', - - /** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled - /** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled - /** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled - /** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9', - /** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333', - /** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95', -} - -const enum Style { - NORMAL = 1, - BOLD = 3 -} - -/** - * @param xp The percentage of 15% of the x axis. - * @param yp The percentage of 15% of the x axis on the y axis. - */ -type DrawFunctionDefinition = (xp: number, yp: number) => string; - -/** - * This contains the definitions of all box drawing characters in the format of SVG paths (ie. the - * svg d attribute). - */ -export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number]: string | DrawFunctionDefinition } | undefined } = { - // Uniform normal and bold - '─': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT }, - '━': { [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, - '│': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM }, - '┃': { [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, - '┌': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM }, - '┏': { [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, - '┐': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM }, - '┓': { [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, - '└': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT }, - '┗': { [Style.BOLD]: Shapes.TOP_TO_RIGHT }, - '┘': { [Style.NORMAL]: Shapes.TOP_TO_LEFT }, - '┛': { [Style.BOLD]: Shapes.TOP_TO_LEFT }, - '├': { [Style.NORMAL]: Shapes.T_RIGHT }, - '┣': { [Style.BOLD]: Shapes.T_RIGHT }, - '┤': { [Style.NORMAL]: Shapes.T_LEFT }, - '┫': { [Style.BOLD]: Shapes.T_LEFT }, - '┬': { [Style.NORMAL]: Shapes.T_BOTTOM }, - '┳': { [Style.BOLD]: Shapes.T_BOTTOM }, - '┴': { [Style.NORMAL]: Shapes.T_TOP }, - '┻': { [Style.BOLD]: Shapes.T_TOP }, - '┼': { [Style.NORMAL]: Shapes.CROSS }, - '╋': { [Style.BOLD]: Shapes.CROSS }, - '╴': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT }, - '╸': { [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '╵': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP }, - '╹': { [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '╶': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT }, - '╺': { [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '╷': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM }, - '╻': { [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - - // Double border - '═': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, - '║': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, - '╒': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` }, - '╓': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},1 L${.5 - xp},.5 L1,.5 M${.5 + xp},.5 L${.5 + xp},1` }, - '╔': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` }, - '╕': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L.5,${.5 - yp} L.5,1 M0,${.5 + yp} L.5,${.5 + yp}` }, - '╖': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},1 L${.5 + xp},.5 L0,.5 M${.5 - xp},.5 L${.5 - xp},1` }, - '╗': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},1` }, - '╘': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 + yp} L1,${.5 + yp} M.5,${.5 - yp} L1,${.5 - yp}` }, - '╙': { [Style.NORMAL]: (xp, yp) => `M1,.5 L${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` }, - '╚': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0 M1,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},0` }, - '╛': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L.5,${.5 + yp} L.5,0 M0,${.5 - yp} L.5,${.5 - yp}` }, - '╜': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 + xp},.5 L${.5 + xp},0 M${.5 - xp},.5 L${.5 - xp},0` }, - '╝': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M0,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},0` }, - '╞': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` }, - '╟': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1 M${.5 + xp},.5 L1,.5` }, - '╠': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, - '╡': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L.5,${.5 - yp} M0,${.5 + yp} L.5,${.5 + yp}` }, - '╢': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 - xp},.5 M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, - '╣': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},0 L${.5 + xp},1 M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0` }, - '╤': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp} M.5,${.5 + yp} L.5,1` }, - '╥': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},1 M${.5 + xp},.5 L${.5 + xp},1` }, - '╦': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` }, - '╧': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - yp} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, - '╨': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` }, - '╩': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L1,${.5 + yp} M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, - '╪': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, - '╫': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, - '╬': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, - - // Diagonal - '╱': { [Style.NORMAL]: 'M1,0 L0,1' }, - '╲': { [Style.NORMAL]: 'M0,0 L1,1' }, - '╳': { [Style.NORMAL]: 'M1,0 L0,1 M0,0 L1,1' }, - - // Mixed weight - '╼': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '╽': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '╾': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '╿': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '┍': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '┎': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '┑': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '┒': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '┕': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '┖': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '┙': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '┚': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '┝': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '┞': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '┟': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '┠': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, - '┡': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, - '┢': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, - '┥': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '┦': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '┧': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '┨': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, - '┩': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT }, - '┪': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, - '┭': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '┮': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '┯': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, - '┰': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '┱': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, - '┲': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, - '┵': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '┶': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '┷': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, - '┸': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '┹': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_LEFT }, - '┺': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, - '┽': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, - '┾': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}`, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, - '┿': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, - '╀': { [Style.NORMAL]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}`, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, - '╁': { [Style.NORMAL]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, - '╂': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, - '╃': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT }, - '╄': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, - '╅': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, - '╆': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, - '╇': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}` }, - '╈': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}` }, - '╉': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}` }, - '╊': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}` }, - - // Dashed - '╌': { [Style.NORMAL]: Shapes.TWO_DASHES_HORIZONTAL }, - '╍': { [Style.BOLD]: Shapes.TWO_DASHES_HORIZONTAL }, - '┄': { [Style.NORMAL]: Shapes.THREE_DASHES_HORIZONTAL }, - '┅': { [Style.BOLD]: Shapes.THREE_DASHES_HORIZONTAL }, - '┈': { [Style.NORMAL]: Shapes.FOUR_DASHES_HORIZONTAL }, - '┉': { [Style.BOLD]: Shapes.FOUR_DASHES_HORIZONTAL }, - '╎': { [Style.NORMAL]: Shapes.TWO_DASHES_VERTICAL }, - '╏': { [Style.BOLD]: Shapes.TWO_DASHES_VERTICAL }, - '┆': { [Style.NORMAL]: Shapes.THREE_DASHES_VERTICAL }, - '┇': { [Style.BOLD]: Shapes.THREE_DASHES_VERTICAL }, - '┊': { [Style.NORMAL]: Shapes.FOUR_DASHES_VERTICAL }, - '┋': { [Style.BOLD]: Shapes.FOUR_DASHES_VERTICAL }, - - // Curved - '╭': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 + (yp / .15 * .5)} C.5,${.5 + (yp / .15 * .5)},.5,.5,1,.5` }, - '╮': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 + (yp / .15 * .5)} C.5,${.5 + (yp / .15 * .5)},.5,.5,0,.5` }, - '╯': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - (yp / .15 * .5)} C.5,${.5 - (yp / .15 * .5)},.5,.5,0,.5` }, - '╰': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - (yp / .15 * .5)} C.5,${.5 - (yp / .15 * .5)},.5,.5,1,.5` } -}; - -interface IVectorShape { - d: string; - type: VectorType; - /** Padding to apply to the vector's x axis in CSS pixels. */ - horizontalPadding?: number; -} - -const enum VectorType { - FILL, - STROKE -} - -/** - * This contains the definitions of the primarily used box drawing characters as vector shapes. The - * reason these characters are defined specially is to avoid common problems if a user's font has - * not been patched with powerline characters and also to get pixel perfect rendering as rendering - * issues can occur around AA/SPAA. - * - * Original symbols defined in https://github.com/powerline/fontpatcher - */ -export const powerlineDefinitions: { [index: string]: IVectorShape } = { - // Right triangle solid - '\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL }, - // Right triangle line - '\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, horizontalPadding: 0.5 }, - // Left triangle solid - '\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL }, - // Left triangle line - '\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, horizontalPadding: 0.5 }, - // Right semi-circle solid, - '\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL }, - // Right semi-circle line, - '\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE }, - // Left semi-circle solid, - '\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL }, - // Left semi-circle line, - '\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE } -}; - -/** - * Try drawing a custom block element or box drawing character, returning whether it was - * successfully drawn. - */ -export function tryDrawCustomChar( - ctx: CanvasRenderingContext2D, - c: string, - xOffset: number, - yOffset: number, - scaledCellWidth: number, - scaledCellHeight: number -): boolean { - const blockElementDefinition = blockElementDefinitions[c]; - if (blockElementDefinition) { - drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); - return true; - } - - const patternDefinition = patternCharacterDefinitions[c]; - if (patternDefinition) { - drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); - return true; - } - - const boxDrawingDefinition = boxDrawingDefinitions[c]; - if (boxDrawingDefinition) { - drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); - return true; - } - - const powerlineDefinition = powerlineDefinitions[c]; - if (powerlineDefinition) { - drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); - return true; - } - - return false; -} - -function drawBlockElementChar( - ctx: CanvasRenderingContext2D, - charDefinition: IBlockVector[], - xOffset: number, - yOffset: number, - scaledCellWidth: number, - scaledCellHeight: number -): void { - for (let i = 0; i < charDefinition.length; i++) { - const box = charDefinition[i]; - const xEighth = scaledCellWidth / 8; - const yEighth = scaledCellHeight / 8; - ctx.fillRect( - xOffset + box.x * xEighth, - yOffset + box.y * yEighth, - box.w * xEighth, - box.h * yEighth - ); - } -} - -const cachedPatterns: Map> = new Map(); - -function drawPatternChar( - ctx: CanvasRenderingContext2D, - charDefinition: number[][], - xOffset: number, - yOffset: number, - scaledCellWidth: number, - scaledCellHeight: number -): void { - let patternSet = cachedPatterns.get(charDefinition); - if (!patternSet) { - patternSet = new Map(); - cachedPatterns.set(charDefinition, patternSet); - } - const fillStyle = ctx.fillStyle; - if (typeof fillStyle !== 'string') { - throw new Error(`Unexpected fillStyle type "${fillStyle}"`); - } - let pattern = patternSet.get(fillStyle); - if (!pattern) { - const width = charDefinition[0].length; - const height = charDefinition.length; - const tmpCanvas = document.createElement('canvas'); - tmpCanvas.width = width; - tmpCanvas.height = height; - const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d')); - const imageData = new ImageData(width, height); - - // Extract rgba from fillStyle - let r: number; - let g: number; - let b: number; - let a: number; - if (fillStyle.startsWith('#')) { - r = parseInt(fillStyle.slice(1, 3), 16); - g = parseInt(fillStyle.slice(3, 5), 16); - b = parseInt(fillStyle.slice(5, 7), 16); - a = fillStyle.length > 7 && parseInt(fillStyle.slice(7, 9), 16) || 1; - } else if (fillStyle.startsWith('rgba')) { - ([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e))); - } else { - throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`); - } - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - imageData.data[(y * width + x) * 4 ] = r; - imageData.data[(y * width + x) * 4 + 1] = g; - imageData.data[(y * width + x) * 4 + 2] = b; - imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255); - } - } - tmpCtx.putImageData(imageData, 0, 0); - pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null)); - patternSet.set(fillStyle, pattern); - } - ctx.fillStyle = pattern; - ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight); -} - -/** - * Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to - * canvas draw calls. - * - * Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐ - * ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤ - * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘ - * ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐ - * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤ - * └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘ - * - * Other: - * ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈ - * │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉ - * ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋ - * - * All box drawing characters: - * ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ - * ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ - * ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ - * ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ - * ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ - * ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ - * ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ - * ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ - * - * --- - * - * Box drawing alignment tests: █ - * ▉ - * ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ - * ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ - * ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ - * ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ - * ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ - * ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ - * ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ - * - * Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html - */ -function drawBoxDrawingChar( - ctx: CanvasRenderingContext2D, - charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) }, - xOffset: number, - yOffset: number, - scaledCellWidth: number, - scaledCellHeight: number -): void { - ctx.strokeStyle = ctx.fillStyle; - for (const [fontWeight, instructions] of Object.entries(charDefinition)) { - ctx.beginPath(); - ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight); - let actualInstructions: string; - if (typeof instructions === 'function') { - const xp = .15; - const yp = .15 / scaledCellHeight * scaledCellWidth; - actualInstructions = instructions(xp, yp); - } else { - actualInstructions = instructions; - } - for (const instruction of actualInstructions.split(' ')) { - const type = instruction[0]; - const f = svgToCanvasInstructionMap[type]; - if (!f) { - console.error(`Could not find drawing instructions for "${type}"`); - continue; - } - const args: string[] = instruction.substring(1).split(','); - if (!args[0] || !args[1]) { - continue; - } - f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset)); - } - ctx.stroke(); - ctx.closePath(); - } -} - -function drawPowerlineChar( - ctx: CanvasRenderingContext2D, - charDefinition: IVectorShape, - xOffset: number, - yOffset: number, - scaledCellWidth: number, - scaledCellHeight: number -): void { - ctx.beginPath(); - ctx.lineWidth = window.devicePixelRatio; - for (const instruction of charDefinition.d.split(' ')) { - const type = instruction[0]; - const f = svgToCanvasInstructionMap[type]; - if (!f) { - console.error(`Could not find drawing instructions for "${type}"`); - continue; - } - const args: string[] = instruction.substring(1).split(','); - if (!args[0] || !args[1]) { - continue; - } - f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.horizontalPadding)); - } - if (charDefinition.type === VectorType.STROKE) { - ctx.strokeStyle = ctx.fillStyle; - ctx.stroke(); - } else { - ctx.fill(); - } - ctx.closePath(); -} - -function clamp(value: number, max: number, min: number = 0): number { - return Math.max(Math.min(value, max), min); -} - -const svgToCanvasInstructionMap: { [index: string]: any } = { - 'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]), - 'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]), - 'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]) -}; - -function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, horizontalPadding: number = 0): number[] { - const result = args.map(e => parseFloat(e) || parseInt(e)); - - if (result.length < 2) { - throw new Error('Too few arguments for instruction'); - } - - for (let x = 0; x < result.length; x += 2) { - // Translate from 0-1 to 0-cellWidth - result[x] *= cellWidth - (horizontalPadding * 2 * window.devicePixelRatio); - // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp - // line at 100% devicePixelRatio - if (result[x] !== 0) { - result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0); - } - // Apply the cell's offset (ie. x*cellWidth) - result[x] += xOffset + (horizontalPadding * window.devicePixelRatio); - } - - for (let y = 1; y < result.length; y += 2) { - // Translate from 0-1 to 0-cellHeight - result[y] *= cellHeight; - // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp - // line at 100% devicePixelRatio - if (result[y] !== 0) { - result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0); - } - // Apply the cell's offset (ie. x*cellHeight) - result[y] += yOffset; - } - - return result; -} diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b3b9b973..8294c651 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -12,7 +12,7 @@ import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; import { color, rgba } from 'common/Color'; -import { tryDrawCustomChar } from '../CustomGlyphs'; +import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils'; // For debugging purposes, it can be useful to set this to a really tiny value, diff --git a/addons/xterm-addon-canvas/src/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts similarity index 100% rename from addons/xterm-addon-canvas/src/CustomGlyphs.ts rename to src/browser/renderer/CustomGlyphs.ts From c52eb8898f1b860cbe697bf4850744ba0de90a5c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 04:50:40 -0700 Subject: [PATCH 09/11] Simplify addon tsconfigs --- addons/xterm-addon-canvas/src/tsconfig.json | 3 +-- addons/xterm-addon-webgl/src/tsconfig.json | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-canvas/src/tsconfig.json b/addons/xterm-addon-canvas/src/tsconfig.json index 1ce2a5e3..d954ec49 100644 --- a/addons/xterm-addon-canvas/src/tsconfig.json +++ b/addons/xterm-addon-canvas/src/tsconfig.json @@ -4,8 +4,7 @@ "target": "es5", "lib": [ "dom", - "es6", - "ES2017.Object" + "es6" ], "rootDir": ".", "outDir": "../out", diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index b111ae55..206d52ae 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -4,9 +4,7 @@ "target": "es5", "lib": [ "dom", - "es6", - "es2016.Array.Include", - "ES2017.Object" + "es6" ], "rootDir": ".", "outDir": "../out", From 119b64181b8af5b5bff677e1d68a3d55a3cb1772 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 04:51:49 -0700 Subject: [PATCH 10/11] Fix fit addon api tests --- addons/xterm-addon-fit/test/FitAddon.api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index caf8cc7d..8a1c647a 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -49,7 +49,7 @@ describe('FitAddon', () => { await loadFit(); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); assert.isAbove(dimensions.cols, 86); - assert.isBelow(dimensions.cols, 87); + assert.isBelow(dimensions.cols, 88); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -58,7 +58,7 @@ describe('FitAddon', () => { await loadFit(1008); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); assert.isAbove(dimensions.cols, 109); - assert.isBelow(dimensions.cols, 110); + assert.isBelow(dimensions.cols, 111); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -92,7 +92,7 @@ describe('FitAddon', () => { const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); assert.isAbove(cols, 86); - assert.isBelow(cols, 87); + assert.isBelow(cols, 88); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); @@ -103,7 +103,7 @@ describe('FitAddon', () => { const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); assert.isAbove(cols, 109); - assert.isBelow(cols, 110); + assert.isBelow(cols, 111); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); From e5037a6d1e2806f1e8f0e6a4abd5e8e39902af1e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Jul 2022 05:00:10 -0700 Subject: [PATCH 11/11] Fix fit addon api tests on linux+firefox --- addons/xterm-addon-fit/test/FitAddon.api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index 8a1c647a..ef4618ea 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -48,7 +48,7 @@ describe('FitAddon', () => { it('default', async function(): Promise { await loadFit(); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.isAbove(dimensions.cols, 86); + assert.isAbove(dimensions.cols, 85); assert.isBelow(dimensions.cols, 88); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); @@ -57,7 +57,7 @@ describe('FitAddon', () => { it('width', async function(): Promise { await loadFit(1008); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.isAbove(dimensions.cols, 109); + assert.isAbove(dimensions.cols, 108); assert.isBelow(dimensions.cols, 111); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); @@ -91,7 +91,7 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.isAbove(cols, 86); + assert.isAbove(cols, 85); assert.isBelow(cols, 88); assert.isAbove(rows, 24); assert.isBelow(rows, 29); @@ -102,7 +102,7 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.isAbove(cols, 109); + assert.isAbove(cols, 108); assert.isBelow(cols, 111); assert.isAbove(rows, 24); assert.isBelow(rows, 29);