diff --git a/css/xterm.css b/css/xterm.css index 819654e4..5a6e088a 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -112,15 +112,6 @@ top: 0; } -.xterm-char-measure-element { - display: inline-block; - visibility: hidden; - position: absolute; - top: 0; - left: -9999em; - line-height: normal; -} - .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index b1c136f2..4349b222 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -108,7 +108,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._dimensionsStyleElement.remove(); })); - this._widthCache = new WidthCache(this._document, this._helperContainer); + this._widthCache = new WidthCache(); this._widthCache.setFont( this._optionsService.rawOptions.fontFamily, this._optionsService.rawOptions.fontSize, diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 14c4ade1..ad95037c 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -15,13 +15,13 @@ import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } import { TestWidthCache } from 'browser/renderer/dom/WidthCache.test'; const dom = new jsdom.JSDOM(''); -const EMPTY_WIDTH = new TestWidthCache(dom.window.document, dom.window.document.createElement('div')); describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; + let widthCache: TestWidthCache; beforeEach(() => { dom = new jsdom.JSDOM(''); @@ -35,22 +35,23 @@ describe('DomRendererRowFactory', () => { new MockThemeService() ); lineData = createEmptyLineData(2); + widthCache = new TestWidthCache(); }); describe('createRow', () => { it('should not create anything for an empty row', () => { - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), '' ); }); it('should set correct attributes for double width characters', () => { - EMPTY_WIDTH.widths['語'] = [10, 10, 10, 10]; + widthCache.setWidths({ '語': 10 }); lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)])); // There should be no element for the following "empty" cell lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), '' ); @@ -58,7 +59,7 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const spans = rowFactory.createRow(lineData, 0, true, style, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, style, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ` ` ); @@ -66,7 +67,7 @@ describe('DomRendererRowFactory', () => { }); it('should add class for cursor blink', () => { - const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, true, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, true, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ` ` ); @@ -85,7 +86,7 @@ describe('DomRendererRowFactory', () => { new MockThemeService() ); for (const inactiveStyle of ['outline', 'block', 'bar', 'underline', 'none']){ - const spans = rowFactory.createRow(lineData, 0, true, 'block', inactiveStyle, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, 'block', inactiveStyle, 0, false, 5, widthCache, -1, -1); if (inactiveStyle === 'none') { assert.equal(extractHtml(spans), ` `); @@ -108,7 +109,7 @@ describe('DomRendererRowFactory', () => { new MockDecorationService(), new MockThemeService() ); - const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ` ` ); @@ -119,7 +120,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -129,7 +130,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -139,7 +140,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -152,7 +153,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.SINGLE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -163,7 +164,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.DOUBLE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -174,7 +175,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.CURLY; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -185,7 +186,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.DOTTED; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -196,7 +197,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.DASHED; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -207,7 +208,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -217,7 +218,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -230,7 +231,7 @@ describe('DomRendererRowFactory', () => { cell.fg &= ~Attributes.PCOLOR_MASK; cell.fg |= i; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), `a` ); @@ -244,7 +245,7 @@ describe('DomRendererRowFactory', () => { cell.bg &= ~Attributes.PCOLOR_MASK; cell.bg |= i; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), `a` ); @@ -256,7 +257,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE; cell.bg |= Attributes.CM_P16 | 1; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -267,7 +268,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= FgFlags.INVERSE; cell.bg |= Attributes.CM_P16 | 1; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -277,7 +278,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -290,7 +291,7 @@ describe('DomRendererRowFactory', () => { cell.fg &= ~Attributes.PCOLOR_MASK; cell.fg |= i; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), `a` ); @@ -302,7 +303,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3; cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -313,7 +314,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE; cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -325,7 +326,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); rowFactory.handleSelectionChanged([1, 0], [2, 0], false); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'ab' ); @@ -333,7 +334,7 @@ describe('DomRendererRowFactory', () => { it('should force whitespace cells to be rendered above the background', () => { lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); rowFactory.handleSelectionChanged([0, 0], [2, 0], false); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ' a' ); @@ -350,7 +351,7 @@ describe('DomRendererRowFactory', () => { }); it('should not create anything for an empty row', () => { - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), '' ); @@ -360,18 +361,18 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'abc' ); }); it('should not merge codepoints with different spacing', () => { - EMPTY_WIDTH.widths['€'] = [2, 2, 2, 2]; + widthCache.setWidths({ '€': 2 }); lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'ac' ); @@ -386,7 +387,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(1, aColor1); lineData.setCell(2, bColor2); lineData.setCell(3, bColor2); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'aabb' ); @@ -398,7 +399,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'X', 1, 'X'.charCodeAt(0)])); lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'aaXbb' ); @@ -411,7 +412,7 @@ describe('DomRendererRowFactory', () => { nullCell.bg = Attributes.CM_P16 | 2; lineData.setCell(3, nullCell); lineData.setCell(4, nullCell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ' ' ); @@ -421,38 +422,36 @@ describe('DomRendererRowFactory', () => { const nullCell = lineData.loadCell(0, new CellData()); nullCell.bg = Attributes.CM_P16 | 1; lineData.setCell(0, nullCell); - let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ' ' ); lineData.setCell(1, nullCell); - spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ' ' ); lineData.setCell(2, nullCell); lineData.setCell(3, nullCell); - spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ' ' ); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); - spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), ' a' ); }); it('should apply correct positive or negative spacing', () => { - EMPTY_WIDTH.widths['€'] = [2, 2, 2, 2]; // too small, should add 3px - EMPTY_WIDTH.widths['語'] = [10, 10, 10, 10]; // exact match for its width, should merge - EMPTY_WIDTH.widths['𝄞'] = [7, 7, 7, 7]; // too wide, should subtract -2px + widthCache.setWidths({ '€': 2, '語': 10, '𝄞': 7 }); // €: too small (+3px), 語: exact, 𝄞: too wide (-2px) lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)])); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -1, -1); assert.equal(extractHtml(spans), 'ac語𝄞' ); @@ -466,7 +465,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)])); lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); lineData.setCell(6, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, 2, 4); assert.equal(extractHtml(spans), 'aaxxxbb' ); @@ -477,7 +476,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)])); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, 2, 4); assert.equal(extractHtml(spans), 'aax x' ); @@ -487,7 +486,7 @@ describe('DomRendererRowFactory', () => { for (let i = 0; i < 10; ++i) { lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); } - const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -100, 100); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, widthCache, -100, 100); assert.equal(extractHtml(spans), 'aaaaaaaaaa' ); diff --git a/src/browser/renderer/dom/WidthCache.test.ts b/src/browser/renderer/dom/WidthCache.test.ts index fc524437..c1e511c9 100644 --- a/src/browser/renderer/dom/WidthCache.test.ts +++ b/src/browser/renderer/dom/WidthCache.test.ts @@ -4,10 +4,20 @@ */ import * as assert from 'assert'; -import { WidthCache, WidthCacheSettings } from 'browser/renderer/dom/WidthCache'; -import jsdom = require('jsdom'); +import { IWidthCacheFontVariantCanvas, WidthCache, WidthCacheSettings } from 'browser/renderer/dom/WidthCache'; +class MockWidthCacheFontVariantCanvas implements IWidthCacheFontVariantCanvas { + public widths: { [key: string]: number } = {}; + + public setFont(_fontFamily: string, _fontSize: number, _fontWeight: unknown, _italic: boolean): void { + } + + public measure(c: string): number { + return this.widths[c] ?? 5; + } +} + export class TestWidthCache extends WidthCache { public get flat(): Float32Array { return (this as any)._flat; @@ -15,13 +25,18 @@ export class TestWidthCache extends WidthCache { public get holey(): Map | undefined { return (this as any)._holey; } + public get canvasElements(): MockWidthCacheFontVariantCanvas[] { + return (this as any)._canvasElements; + } - public widths: {[key: string]: [number, number, number, number]} = {}; - protected _measure(c: string, variant: number): number { - if (this.widths[c] !== undefined) { - return this.widths[c][variant]; + constructor() { + super(() => new MockWidthCacheFontVariantCanvas()); + } + + public setWidths(widths: { [key: string]: number }): void { + for (const canvas of this.canvasElements) { + canvas.widths = widths; } - return 5; // 5 is default width in tests in DomRendererRowFactory.test.ts } } @@ -36,8 +51,7 @@ function castf32(v: number): number { describe('WidthCache', () => { let wc: TestWidthCache; beforeEach(() => { - const dom = new jsdom.JSDOM(''); - wc = new TestWidthCache(dom.window.document, dom.window.document.createElement('div')); + wc = new TestWidthCache(); wc.setFont('monospace', 15, 'normal', 'bold'); }); describe('cache invalidation', () => { diff --git a/src/browser/renderer/dom/WidthCache.ts b/src/browser/renderer/dom/WidthCache.ts index 03d6cb70..3a8b1506 100644 --- a/src/browser/renderer/dom/WidthCache.ts +++ b/src/browser/renderer/dom/WidthCache.ts @@ -3,6 +3,7 @@ * @license MIT */ +import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IDisposable } from 'common/Types'; import { FontWeight } from 'common/services/Services'; @@ -24,6 +25,10 @@ const enum FontVariant { BOLD_ITALIC = 3 } +export interface IWidthCacheFontVariantCanvas { + setFont(fontFamily: string, fontSize: number, fontWeight: FontWeight, italic: boolean): void; + measure(c: string): number; +} export class WidthCache implements IDisposable { // flat cache for regular variant up to CacheSettings.FLAT_SIZE @@ -42,50 +47,24 @@ export class WidthCache implements IDisposable { private _fontSize = 0; private _weight: FontWeight = 'normal'; private _weightBold: FontWeight = 'bold'; - private _container: HTMLDivElement; - private _measureElements: HTMLSpanElement[] = []; + private _canvasElements: IWidthCacheFontVariantCanvas[] = []; - constructor(_document: Document, _helperContainer: HTMLElement) { - this._container = _document.createElement('div'); - this._container.classList.add('xterm-width-cache-measure-container'); - this._container.setAttribute('aria-hidden', 'true'); - // SP should stack in spans - this._container.style.whiteSpace = 'pre'; - // avoid undercuts in non-monospace fonts from kerning - this._container.style.fontKerning = 'none'; - - const regular = _document.createElement('span'); - regular.classList.add('xterm-char-measure-element'); - - const bold = _document.createElement('span'); - bold.classList.add('xterm-char-measure-element'); - bold.style.fontWeight = 'bold'; - - const italic = _document.createElement('span'); - italic.classList.add('xterm-char-measure-element'); - italic.style.fontStyle = 'italic'; - - const boldItalic = _document.createElement('span'); - boldItalic.classList.add('xterm-char-measure-element'); - boldItalic.style.fontWeight = 'bold'; - boldItalic.style.fontStyle = 'italic'; - - // NOTE: must be in order of FontVariant - this._measureElements = [regular, bold, italic, boldItalic]; - this._container.appendChild(regular); - this._container.appendChild(bold); - this._container.appendChild(italic); - this._container.appendChild(boldItalic); - - _helperContainer.appendChild(this._container); + constructor( + canvasFactory: () => IWidthCacheFontVariantCanvas = () => new WidthCacheFontVariantCanvas() + ) { + this._canvasElements = [ + canvasFactory(), + canvasFactory(), + canvasFactory(), + canvasFactory() + ]; this.clear(); } public dispose(): void { - this._container.remove(); // remove elements from DOM - this._measureElements.length = 0; // release element refs - this._holey = undefined; // free cache memory via GC + this._canvasElements.length = 0; + this._holey = undefined; // free cache memory via GC } /** @@ -104,10 +83,11 @@ export class WidthCache implements IDisposable { */ public setFont(font: string, fontSize: number, weight: FontWeight, weightBold: FontWeight): void { // skip if nothing changed - if (font === this._font - && fontSize === this._fontSize - && weight === this._weight - && weightBold === this._weightBold + if ( + font === this._font && + fontSize === this._fontSize && + weight === this._weight && + weightBold === this._weightBold ) { return; } @@ -117,12 +97,10 @@ export class WidthCache implements IDisposable { this._weight = weight; this._weightBold = weightBold; - this._container.style.fontFamily = this._font; - this._container.style.fontSize = `${this._fontSize}px`; - this._measureElements[FontVariant.REGULAR].style.fontWeight = `${weight}`; - this._measureElements[FontVariant.BOLD].style.fontWeight = `${weightBold}`; - this._measureElements[FontVariant.ITALIC].style.fontWeight = `${weight}`; - this._measureElements[FontVariant.BOLD_ITALIC].style.fontWeight = `${weightBold}`; + this._canvasElements[FontVariant.REGULAR].setFont(font, fontSize, weight, false); + this._canvasElements[FontVariant.BOLD].setFont(font, fontSize, weightBold, false); + this._canvasElements[FontVariant.ITALIC].setFont(font, fontSize, weight, true); + this._canvasElements[FontVariant.BOLD_ITALIC].setFont(font, fontSize, weightBold, true); this.clear(); } @@ -160,8 +138,25 @@ export class WidthCache implements IDisposable { } protected _measure(c: string, variant: FontVariant): number { - const el = this._measureElements[variant]; - el.textContent = c.repeat(WidthCacheSettings.REPEAT); - return el.offsetWidth / WidthCacheSettings.REPEAT; + return this._canvasElements[variant].measure(c); + } +} + +class WidthCacheFontVariantCanvas implements IWidthCacheFontVariantCanvas { + private _canvas: OffscreenCanvas; + private _ctx: OffscreenCanvasRenderingContext2D; + + constructor() { + this._canvas = new OffscreenCanvas(1, 1); + this._ctx = throwIfFalsy(this._canvas.getContext('2d')); + } + + public setFont(fontFamily: string, fontSize: number, fontWeight: FontWeight, italic: boolean): void { + const fontStyle = italic ? 'italic' : ''; + this._ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`.trim(); + } + + public measure(c: string): number { + return this._ctx.measureText(c).width; } }