Skip to content

Commit 5697086

Browse files
authored
Merge pull request #3416 from meganrogge/merogge/pixelPerfect
pixel perfect canvas rendering of box and block characters
2 parents 9490c5c + 1a75416 commit 5697086

File tree

16 files changed

+708
-18
lines changed

16 files changed

+708
-18
lines changed

addons/xterm-addon-webgl/src/WebglRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
230230
return;
231231
}
232232

233-
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
233+
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
234234
if (!('getRasterizedGlyph' in atlas)) {
235235
throw new Error('The webgl renderer only works with the webgl char atlas');
236236
}

addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
2828
export function acquireCharAtlas(
2929
terminal: Terminal,
3030
colors: IColorSet,
31+
scaledCellWidth: number,
32+
scaledCellHeight: number,
3133
scaledCharWidth: number,
3234
scaledCharHeight: number
3335
): WebglCharAtlas {
34-
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
36+
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors);
3537

3638
// Check to see if the terminal already owns this config
3739
for (let i = 0; i < charAtlasCache.length; i++) {

addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const NULL_COLOR: IColor = {
1313
rgba: 0
1414
};
1515

16-
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
16+
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
1717
// null out some fields that don't matter
1818
const clonedColors: IColorSet = {
1919
foreground: colors.foreground,
@@ -28,7 +28,12 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
2828
contrastCache: colors.contrastCache
2929
};
3030
return {
31+
customGlyphs: terminal.getOption('customGlyphs'),
3132
devicePixelRatio: window.devicePixelRatio,
33+
letterSpacing: terminal.getOption('letterSpacing'),
34+
lineHeight: terminal.getOption('lineHeight'),
35+
scaledCellWidth,
36+
scaledCellHeight,
3237
scaledCharWidth,
3338
scaledCharHeight,
3439
fontFamily: terminal.getOption('fontFamily'),
@@ -49,6 +54,9 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
4954
}
5055
}
5156
return a.devicePixelRatio === b.devicePixelRatio &&
57+
a.customGlyphs === b.customGlyphs &&
58+
a.lineHeight === b.lineHeight &&
59+
a.letterSpacing === b.letterSpacing &&
5260
a.fontFamily === b.fontFamily &&
5361
a.fontSize === b.fontSize &&
5462
a.fontWeight === b.fontWeight &&

addons/xterm-addon-webgl/src/atlas/Types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ export interface IGlyphIdentifier {
1717
}
1818

1919
export interface ICharAtlasConfig {
20+
customGlyphs: boolean;
2021
devicePixelRatio: number;
22+
letterSpacing: number;
23+
lineHeight: number;
2124
fontSize: number;
2225
fontFamily: string;
2326
fontWeight: FontWeight;
2427
fontWeightBold: FontWeight;
28+
scaledCellWidth: number;
29+
scaledCellHeight: number;
2530
scaledCharWidth: number;
2631
scaledCharHeight: number;
2732
allowTransparency: boolean;

addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IColor } from 'browser/Types';
1212
import { IDisposable } from 'xterm';
1313
import { AttributeData } from 'common/buffer/AttributeData';
1414
import { channels, rgba } from 'browser/Color';
15+
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
1516

1617
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
1718
// 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 {
8384
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
8485

8586
this._tmpCanvas = document.createElement('canvas');
86-
this._tmpCanvas.width = this._config.scaledCharWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
87-
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
87+
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
88+
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
8889
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
8990
}
9091

@@ -390,8 +391,16 @@ export class WebglCharAtlas implements IDisposable {
390391
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
391392
const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
392393

394+
// Draw custom characters if applicable
395+
let drawSuccess = false;
396+
if (this._config.customGlyphs !== false) {
397+
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
398+
}
399+
393400
// Draw the character
394-
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
401+
if (!drawSuccess) {
402+
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
403+
}
395404

396405
// Draw underline and strikethrough
397406
if (underline || strikethrough) {

addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
9292
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
9393
return;
9494
}
95-
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
95+
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight);
9696
this._charAtlas.warmUp();
9797
}
9898

addons/xterm-addon-webgl/test/WebglRenderer.api.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* @license MIT
44
*/
55

6-
import { ITerminalOptions } from '../../../src/common/Types';
7-
import { ITheme } from 'xterm';
86
import { assert } from 'chai';
9-
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
107
import { Browser, Page } from 'playwright';
8+
import { ITheme } from 'xterm';
9+
import { getBrowserType, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils';
10+
import { ITerminalOptions } from '../../../src/common/Types';
1111

1212
const APP = 'http://127.0.0.1:3001/test';
1313

@@ -890,6 +890,20 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
890890
return await page.evaluate(`Array.from(window.result)`);
891891
}
892892

893+
async function getCellPixels(col: number, row: number): Promise<number[]> {
894+
await page.evaluate(`
895+
window.gl = window.term._core._renderService._renderer._gl;
896+
window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4);
897+
window.d = window.term._core._renderService.dimensions;
898+
window.gl.readPixels(
899+
Math.floor(${col - 1} * window.d.scaledCellWidth),
900+
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight),
901+
window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
902+
);
903+
`);
904+
return await page.evaluate(`Array.from(window.result)`);
905+
}
906+
893907
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
894908
const browserType = getBrowserType();
895909
browser = await browserType.launch({

demo/client.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ if (document.location.pathname === '/test') {
147147
createTerminal();
148148
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
149149
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
150+
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
150151
}
151152

152153
function createTerminal(): void {
@@ -431,3 +432,49 @@ function serializeButtonHandler(): void {
431432
term.write(output);
432433
}
433434
}
435+
436+
437+
function writeCustomGlyphHandler() {
438+
term.write('\n\r');
439+
term.write('\n\r');
440+
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
441+
term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r');
442+
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r');
443+
term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r');
444+
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r');
445+
term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r');
446+
term.write('\n\r');
447+
term.write('Other:\n\r');
448+
term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r');
449+
term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r');
450+
term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r');
451+
term.write('\n\r');
452+
term.write('All box drawing characters:\n\r');
453+
term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r');
454+
term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r');
455+
term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r');
456+
term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r');
457+
term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r');
458+
term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r');
459+
term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r');
460+
term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r');
461+
term.write('Box drawing alignment tests:\x1b[31m █\n\r');
462+
term.write(' ▉\n\r');
463+
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
464+
term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r');
465+
term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r');
466+
term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r');
467+
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
468+
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
469+
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
470+
term.write('Box drawing alignment tests:\x1b[32m █\n\r');
471+
term.write(' ▉\n\r');
472+
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
473+
term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r');
474+
term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r');
475+
term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r');
476+
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
477+
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
478+
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
479+
window.scrollTo(0, 0);
480+
}

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ <h3>Style</h3>
5050
</div>
5151
<hr/>
5252
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
53+
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
5354
<script src="dist/client-bundle.js" defer ></script>
5455
</body>
5556
</html>

src/browser/Terminal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
224224
// The DOM renderer needs a row refresh to update the cursor styles
225225
this.refresh(this.buffer.y, this.buffer.y);
226226
break;
227+
case 'customGlyphs':
227228
case 'drawBoldTextInBrightColors':
228229
case 'letterSpacing':
229230
case 'lineHeight':

0 commit comments

Comments
 (0)