Merge pull request #3416 from meganrogge/merogge/pixelPerfect

pixel perfect canvas rendering of box and block characters
This commit is contained in:
Daniel Imms
2021-08-19 06:00:58 -07:00
committed by GitHub
16 changed files with 708 additions and 18 deletions
@@ -230,7 +230,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
return;
}
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
if (!('getRasterizedGlyph' in atlas)) {
throw new Error('The webgl renderer only works with the webgl char atlas');
}
@@ -28,10 +28,12 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
export function acquireCharAtlas(
terminal: Terminal,
colors: IColorSet,
scaledCellWidth: number,
scaledCellHeight: number,
scaledCharWidth: number,
scaledCharHeight: number
): WebglCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
@@ -13,7 +13,7 @@ const NULL_COLOR: IColor = {
rgba: 0
};
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IColorSet = {
foreground: colors.foreground,
@@ -28,7 +28,12 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
contrastCache: colors.contrastCache
};
return {
customGlyphs: terminal.getOption('customGlyphs'),
devicePixelRatio: window.devicePixelRatio,
letterSpacing: terminal.getOption('letterSpacing'),
lineHeight: terminal.getOption('lineHeight'),
scaledCellWidth,
scaledCellHeight,
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.getOption('fontFamily'),
@@ -49,6 +54,9 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
}
}
return a.devicePixelRatio === b.devicePixelRatio &&
a.customGlyphs === b.customGlyphs &&
a.lineHeight === b.lineHeight &&
a.letterSpacing === b.letterSpacing &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
+5
View File
@@ -17,11 +17,16 @@ export interface IGlyphIdentifier {
}
export interface ICharAtlasConfig {
customGlyphs: boolean;
devicePixelRatio: number;
letterSpacing: number;
lineHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
@@ -12,6 +12,7 @@ import { IColor } from 'browser/Types';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { channels, rgba } from 'browser/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -83,8 +84,8 @@ export class WebglCharAtlas implements IDisposable {
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
this._tmpCanvas = document.createElement('canvas');
this._tmpCanvas.width = this._config.scaledCharWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
}
@@ -390,8 +391,16 @@ export class WebglCharAtlas implements IDisposable {
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
// Draw custom characters if applicable
let drawSuccess = false;
if (this._config.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
}
// Draw the character
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
if (!drawSuccess) {
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
}
// Draw underline and strikethrough
if (underline || strikethrough) {
@@ -92,7 +92,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas.warmUp();
}
@@ -3,11 +3,11 @@
* @license MIT
*/
import { ITerminalOptions } from '../../../src/common/Types';
import { ITheme } from 'xterm';
import { assert } from 'chai';
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, Page } from 'playwright';
import { ITheme } from 'xterm';
import { getBrowserType, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils';
import { ITerminalOptions } from '../../../src/common/Types';
const APP = 'http://127.0.0.1:3001/test';
@@ -890,6 +890,20 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
return await page.evaluate(`Array.from(window.result)`);
}
async function getCellPixels(col: number, row: number): Promise<number[]> {
await page.evaluate(`
window.gl = window.term._core._renderService._renderer._gl;
window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4);
window.d = window.term._core._renderService.dimensions;
window.gl.readPixels(
Math.floor(${col - 1} * window.d.scaledCellWidth),
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight),
window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
);
`);
return await page.evaluate(`Array.from(window.result)`);
}
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
const browserType = getBrowserType();
browser = await browserType.launch({
+47
View File
@@ -147,6 +147,7 @@ if (document.location.pathname === '/test') {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
}
function createTerminal(): void {
@@ -431,3 +432,49 @@ function serializeButtonHandler(): void {
term.write(output);
}
}
function writeCustomGlyphHandler() {
term.write('\n\r');
term.write('\n\r');
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r');
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r');
term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r');
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r');
term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r');
term.write('\n\r');
term.write('Other:\n\r');
term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r');
term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r');
term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r');
term.write('\n\r');
term.write('All box drawing characters:\n\r');
term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r');
term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r');
term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r');
term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r');
term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r');
term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r');
term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r');
term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r');
term.write('Box drawing alignment tests:\x1b[31m █\n\r');
term.write(' ▉\n\r');
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r');
term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r');
term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r');
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
term.write('Box drawing alignment tests:\x1b[32m █\n\r');
term.write(' ▉\n\r');
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r');
term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r');
term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r');
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
window.scrollTo(0, 0);
}
+1
View File
@@ -50,6 +50,7 @@
</div>
<hr/>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
<script src="dist/client-bundle.js" defer ></script>
</body>
</html>
+1
View File
@@ -224,6 +224,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// The DOM renderer needs a row refresh to update the cursor styles
this.refresh(this.buffer.y, this.buffer.y);
break;
case 'customGlyphs':
case 'drawBoldTextInBrightColors':
case 'letterSpacing':
case 'lineHeight':
+30 -8
View File
@@ -17,6 +17,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { channels, color, rgba } from 'browser/Color';
import { removeElementFromParent } from 'browser/Dom';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -259,10 +260,20 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.font = this._getFont(false, false);
this._ctx.textBaseline = 'ideographic';
this._clipRow(y);
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.options.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
}
// Draw the character
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
}
/**
@@ -373,14 +384,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (cell.isDim()) {
this._ctx.globalAlpha = DIM_OPACITY;
}
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.options.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
}
// Draw the character
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
this._ctx.restore();
}
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param y The row to clip.
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,6 +21,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
cursorBlink: false,
cursorStyle: 'block',
cursorWidth: 1,
customGlyphs: true,
bellSound: DEFAULT_BELL_SOUND,
bellStyle: 'none',
drawBoldTextInBrightColors: true,
+1
View File
@@ -244,6 +244,7 @@ export interface ITerminalOptions {
cursorBlink: boolean;
cursorStyle: 'block' | 'underline' | 'bar';
cursorWidth: number;
customGlyphs: boolean;
disableStdin: boolean;
drawBoldTextInBrightColors: boolean;
fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined;
+8
View File
@@ -78,6 +78,14 @@ declare module 'xterm-headless' {
*/
cursorWidth?: number;
/**
* Whether to draw custom glyphs for block element and box drawing characters instead of using
* the font. This should typically result in better rendering with continuous lines, even when
* line height and letter spacing is used. Note that this doesn't work with the DOM renderer
* which renders all characters using the font. The default is true.
*/
customGlyphs?: boolean;
/**
* Whether input should be disabled.
*/
+8
View File
@@ -90,6 +90,14 @@ declare module 'xterm' {
*/
cursorWidth?: number;
/**
* Whether to draw custom glyphs for block element and box drawing characters instead of using
* the font. This should typically result in better rendering with continuous lines, even when
* line height and letter spacing is used. Note that this doesn't work with the DOM renderer
* which renders all characters using the font. The default is true.
*/
customGlyphs?: boolean;
/**
* Whether input should be disabled.
*/