diff --git a/README.md b/README.md index 85c2ac18c6..730fc9af9e 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,10 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. +### Code structure + +`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker. + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 5237fa5b9e..34ab143bd3 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -4,9 +4,10 @@ */ import { ICharset, ILinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData, FontWeight } from './Types'; +import { LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types'; import { IColorSet, IRenderer } from './renderer/Interfaces'; import { IMouseZoneManager } from './input/Interfaces'; +import { FontWeight } from './shared/Types'; export interface IBrowser { isNode: boolean; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 608ca4124b..934f6c6bd8 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -4,7 +4,7 @@ */ import { MouseHelper } from './utils/MouseHelper'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; diff --git a/src/Terminal.ts b/src/Terminal.ts index 70b7deaa02..3746cb5859 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -35,7 +35,7 @@ import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import { MouseHelper } from './utils/MouseHelper'; import { CHARSETS } from './Charsets'; import { CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; @@ -44,7 +44,6 @@ import { BELL_SOUND } from './utils/Sounds'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { IMouseZoneManager } from './input/Interfaces'; import { MouseZoneManager } from './input/MouseZoneManager'; -import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; import { IRenderer } from './renderer/Interfaces'; // Let it work inside Node.js for automated testing purposes. @@ -591,8 +590,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.document = this.parent.ownerDocument; this.body = this.document.body; - initializeCharAtlas(this.document); - // Create main element container this.element = this.document.createElement('div'); this.element.classList.add('terminal'); diff --git a/src/Types.ts b/src/Types.ts index 3c502b025d..3263282e06 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -16,5 +16,3 @@ export enum LinkHoverEventTypes { TOOLTIP = 'linktooltip', LEAVE = 'linkleave' } - -export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 1458ba762d..2738843c05 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -5,7 +5,8 @@ import { ITerminal, ITheme } from '../Interfaces'; import { IColorSet } from '../renderer/Interfaces'; -import { isFirefox } from '../utils/Browser'; +import { isFirefox } from '../shared/utils/Browser'; +import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; export const CHAR_ATLAS_CELL_SPACING = 1; @@ -65,8 +66,28 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC } } + const canvasFactory = (width: number, height: number) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + }; + + const charAtlasConfig: ICharAtlasRequest = { + scaledCharWidth, + scaledCharHeight, + fontSize: terminal.options.fontSize, + fontFamily: terminal.options.fontFamily, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + background: colors.background, + foreground: colors.foreground, + ansiColors: colors.ansi, + devicePixelRatio: window.devicePixelRatio + }; + const newEntry: ICharAtlasCacheEntry = { - bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, terminal.options.fontWeight, terminal.options.fontWeightBold, colors.background, colors.foreground, colors.ansi), + bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig), config: newConfig, ownedBy: [terminal] }; @@ -109,124 +130,3 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } - -let generator: CharAtlasGenerator; - -/** - * Initializes the char atlas generator. - * @param document The document. - */ -export function initialize(document: Document): void { - if (!generator) { - generator = new CharAtlasGenerator(document); - } -} - -class CharAtlasGenerator { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; - - constructor(private _document: Document) { - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d', {alpha: false}); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, fontWeight: string, fontWeightBold: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise { - const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; - this._canvas.width = 255 * cellWidth; - this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight; - - this._ctx.fillStyle = background; - this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); - - this._ctx.save(); - this._ctx.fillStyle = foreground; - this._ctx.font = this._getFont(fontWeight, fontSize, fontFamily); - this._ctx.textBaseline = 'top'; - - // Default color - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); - this._ctx.restore(); - } - // Default color bold - this._ctx.save(); - this._ctx.font = this._getFont(fontWeightBold, fontSize, fontFamily); - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); - this._ctx.restore(); - } - this._ctx.restore(); - - // Colors 0-15 - this._ctx.font = this._getFont(fontWeight, fontSize, fontFamily); - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._ctx.font = this._getFont(fontWeightBold, fontSize, fontFamily); - } - const y = (colorIndex + 2) * cellHeight; - // Draw ascii characters - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillStyle = ansiColors[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y); - this._ctx.restore(); - } - } - this._ctx.restore(); - - // Support is patchy for createImageBitmap at the moment, pass a canvas back - // if support is lacking as drawImage works there too. Firefox is also - // included here as ImageBitmap appears both buggy and has horrible - // performance (tested on v55). - if (!('createImageBitmap' in window) || isFirefox) { - // Regenerate canvas and context as they are now owned by the char atlas - const result = this._canvas; - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - return result; - } - - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - - // Remove the background color from the image so characters may overlap - const r = parseInt(background.substr(1, 2), 16); - const g = parseInt(background.substr(3, 2), 16); - const b = parseInt(background.substr(5, 2), 16); - this._clearColor(charAtlasImageData, r, g, b); - - const promise = window.createImageBitmap(charAtlasImageData); - // Clear the rect while the promise is in progress - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - return promise; - } - - private _clearColor(imageData: ImageData, r: number, g: number, b: number): void { - for (let offset = 0; offset < imageData.data.length; offset += 4) { - if (imageData.data[offset] === r && - imageData.data[offset + 1] === g && - imageData.data[offset + 2] === b) { - imageData.data[offset + 3] = 0; - } - } - } - - private _getFont(fontWeight: string, fontSize: number, fontFamily: string): string { - return `${fontWeight} ${fontSize * window.devicePixelRatio}px ${fontFamily}`; - } -} diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/CharAtlasGenerator.ts new file mode 100644 index 0000000000..9b895c9c39 --- /dev/null +++ b/src/shared/CharAtlasGenerator.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { isFirefox } from './utils/Browser'; +import { FontWeight } from './Types'; + +declare const Promise: any; + +export interface IOffscreenCanvas { + width: number; + height: number; + getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D; + transferToImageBitmap(): ImageBitmap; +} + +export interface ICharAtlasRequest { + scaledCharWidth: number; + scaledCharHeight: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + background: string; + foreground: string; + ansiColors: string[]; + devicePixelRatio: number; +} + +export const CHAR_ATLAS_CELL_SPACING = 1; + +/** + * Generates a char atlas. + * @param context The window or worker context. + * @param canvasFactory A function to generate a canvas with a width or height. + * @param request The config for the new char atlas. + */ +export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise { + const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + const canvas = canvasFactory( + /*255 ascii chars*/255 * cellWidth, + (/*default+default bold*/2 + /*0-15*/16) * cellHeight + ); + const ctx = canvas.getContext('2d', {alpha: false}); + + ctx.fillStyle = request.background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.fillStyle = request.foreground; + ctx.font = getFont(request.fontWeight, request); + ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.restore(); + } + // Default color bold + ctx.save(); + ctx.font = getFont(request.fontWeightBold, request); + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.restore(); + } + ctx.restore(); + + // Colors 0-15 + ctx.font = getFont(request.fontWeight, request); + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + ctx.font = getFont(request.fontWeightBold, request); + } + const y = (colorIndex + 2) * cellHeight; + // Draw ascii characters + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + ctx.clip(); + ctx.fillStyle = request.ansiColors[colorIndex]; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.restore(); + } + } + ctx.restore(); + + // Support is patchy for createImageBitmap at the moment, pass a canvas back + // if support is lacking as drawImage works there too. Firefox is also + // included here as ImageBitmap appears both buggy and has horrible + // performance (tested on v55). + if (!('createImageBitmap' in context) || isFirefox) { + // Don't attempt to clear background colors if createImageBitmap is not supported + if (canvas instanceof HTMLCanvasElement) { + // Just return the HTMLCanvas if it's a HTMLCanvasElement + return canvas; + } else { + // Transfer to an ImageBitmap is this is an OffscreenCanvas + return new Promise(r => r(canvas.transferToImageBitmap())); + } + } + + const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // Remove the background color from the image so characters may overlap + const r = parseInt(request.background.substr(1, 2), 16); + const g = parseInt(request.background.substr(3, 2), 16); + const b = parseInt(request.background.substr(5, 2), 16); + clearColor(charAtlasImageData, r, g, b); + + return context.createImageBitmap(charAtlasImageData); +} + +/** + * Makes a partiicular rgb color in an ImageData completely transparent. + */ +function clearColor(imageData: ImageData, r: number, g: number, b: number): void { + for (let offset = 0; offset < imageData.data.length; offset += 4) { + if (imageData.data[offset] === r && + imageData.data[offset + 1] === g && + imageData.data[offset + 2] === b) { + imageData.data[offset + 3] = 0; + } + } +} + +function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string { + return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`; +} diff --git a/src/shared/Types.ts b/src/shared/Types.ts new file mode 100644 index 0000000000..b61525bd42 --- /dev/null +++ b/src/shared/Types.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + + export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; diff --git a/src/utils/Browser.ts b/src/shared/utils/Browser.ts similarity index 75% rename from src/utils/Browser.ts rename to src/shared/utils/Browser.ts index 48c0c3748f..1d4774d21d 100644 --- a/src/utils/Browser.ts +++ b/src/shared/utils/Browser.ts @@ -3,8 +3,6 @@ * @license MIT */ -import { contains } from './Generic'; - const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; @@ -20,3 +18,12 @@ export const isIpad = platform === 'iPad'; export const isIphone = platform === 'iPhone'; export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); export const isLinux = platform.indexOf('Linux') >= 0; + +/** + * Return if the given array contains the given element + * @param {Array} array The array to search for the given element. + * @param {Object} el The element to look for into the array + */ +export function contains(arr: any[], el: any): boolean { + return arr.indexOf(el) >= 0; +}; diff --git a/src/utils/Generic.ts b/src/utils/Generic.ts deleted file mode 100644 index 4bc6c487fe..0000000000 --- a/src/utils/Generic.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -/** - * Return if the given array contains the given element - * @param {Array} array The array to search for the given element. - * @param {Object} el The element to look for into the array - */ -export function contains(arr: any[], el: any): boolean { - return arr.indexOf(el) >= 0; -} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 80e2527746..8e8900a171 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -6,7 +6,7 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces'; import { LineData } from '../Types'; import { Buffer } from '../Buffer'; -import * as Browser from './Browser'; +import * as Browser from '../shared/utils/Browser'; import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal {