From b7320794bbf04914ededf769d2724502f181a019 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Fri, 2 Apr 2021 10:34:14 -0700
Subject: [PATCH] Ligature support for dom renderer
Fixes #3283
---
css/xterm.css | 1 -
src/browser/Terminal.ts | 2 +-
src/browser/TestUtils.test.ts | 15 +++-
src/browser/renderer/Renderer.ts | 2 +-
src/browser/renderer/dom/DomRenderer.ts | 7 +-
.../dom/DomRendererRowFactory.test.ts | 39 ++++-----
.../renderer/dom/DomRendererRowFactory.ts | 80 +++++++++++++++----
src/common/services/InstantiationService.ts | 2 +
src/common/services/Services.ts | 2 +
9 files changed, 108 insertions(+), 42 deletions(-)
diff --git a/css/xterm.css b/css/xterm.css
index 7ddcc2d0..831a89c6 100644
--- a/css/xterm.css
+++ b/css/xterm.css
@@ -36,7 +36,6 @@
*/
.xterm {
- font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts
index 2525fd57..76c7716f 100644
--- a/src/browser/Terminal.ts
+++ b/src/browser/Terminal.ts
@@ -555,7 +555,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.linkifier, this.linkifier2, this._instantiationService);
+ case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier, this.linkifier2);
case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier, this.linkifier2);
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts
index 7ab7d9b1..c577b566 100644
--- a/src/browser/TestUtils.test.ts
+++ b/src/browser/TestUtils.test.ts
@@ -5,7 +5,7 @@
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
-import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
+import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
@@ -411,3 +411,16 @@ export class MockRenderService implements IRenderService {
throw new Error('Method not implemented.');
}
}
+
+export class MockCharacterJoinerService implements ICharacterJoinerService {
+ public serviceBrand: undefined;
+ public register(handler: (text: string) => [number, number][]): number {
+ return 0;
+ }
+ public deregister(joinerId: number): boolean {
+ return true;
+ }
+ public getJoinedCharacters(row: number): [number, number][] {
+ return [];
+ }
+}
diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts
index c88ce256..d5de40db 100644
--- a/src/browser/renderer/Renderer.ts
+++ b/src/browser/renderer/Renderer.ts
@@ -33,7 +33,7 @@ export class Renderer extends Disposable implements IRenderer {
private readonly _screenElement: HTMLElement,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
- instantiationService: IInstantiationService,
+ @IInstantiationService instantiationService: IInstantiationService,
@IBufferService private readonly _bufferService: IBufferService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index 8dd1ac0e..dccdb877 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -9,7 +9,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
-import { IOptionsService, IBufferService } from 'common/services/Services';
+import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'browser/Color';
import { removeElementFromParent } from 'browser/Dom';
@@ -49,6 +49,7 @@ export class DomRenderer extends Disposable implements IRenderer {
private readonly _viewportElement: HTMLElement,
private readonly _linkifier: ILinkifier,
private readonly _linkifier2: ILinkifier2,
+ @IInstantiationService instantiationService: IInstantiationService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService
@@ -80,7 +81,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this._injectCss();
- this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors);
+ this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
@@ -364,7 +365,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const row = y + this._bufferService.buffer.ydisp;
const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = this._optionsService.options.cursorStyle;
- rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
+ rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
}
}
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
index b6604b14..9eacb97a 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
@@ -12,6 +12,7 @@ import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test';
import { css } from 'browser/Color';
+import { MockCharacterJoinerService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -20,7 +21,7 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
- rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {
+ rowFactory = new DomRendererRowFactory(dom.window.document, {
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
@@ -43,13 +44,13 @@ describe('DomRendererRowFactory', () => {
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
- } as any);
+ } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }));
lineData = createEmptyLineData(2);
});
describe('createRow', () => {
it('should not create anything for an empty row', () => {
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
''
);
@@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => {
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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'語'
);
@@ -67,7 +68,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
- const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
` `
);
@@ -75,7 +76,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for cursor blink', () => {
- const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20);
assert.equal(getFragmentHtml(fragment),
` `
);
@@ -84,7 +85,7 @@ describe('DomRendererRowFactory', () => {
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -95,7 +96,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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -105,7 +106,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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -115,7 +116,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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -125,7 +126,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -138,7 +139,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`a`
);
@@ -152,7 +153,7 @@ describe('DomRendererRowFactory', () => {
cell.bg &= ~Attributes.PCOLOR_MASK;
cell.bg |= i;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`a`
);
@@ -164,7 +165,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -175,7 +176,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -185,7 +186,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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -198,7 +199,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
- const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`a`
);
@@ -210,7 +211,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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
@@ -221,7 +222,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 fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
+ const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'a'
);
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts
index c7c87451..a052db54 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -10,6 +10,8 @@ import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { color, rgba } from 'browser/Color';
import { IColorSet, IColor } from 'browser/Types';
+import { ICharacterJoinerService } from 'browser/services/Services';
+import { JoinedCellData } from 'browser/services/CharacterJoinerService';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
@@ -26,8 +28,9 @@ export class DomRendererRowFactory {
constructor(
private readonly _document: Document,
- private readonly _optionsService: IOptionsService,
- private _colors: IColorSet
+ private _colors: IColorSet,
+ @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
+ @IOptionsService private readonly _optionsService: IOptionsService
) {
}
@@ -35,9 +38,11 @@ export class DomRendererRowFactory {
this._colors = colors;
}
- public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
+ public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
+ const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
+ console.log('joinedRanges', joinedRanges.map(e => e[0] + '->' + e[1]).join(','));
// Find the line length first, this prevents the need to output a bunch of
// empty cells at the end. This cannot easily be integrated into the main
// loop below because of the colCount feature (which can be removed after we
@@ -53,18 +58,59 @@ export class DomRendererRowFactory {
for (let x = 0; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
- const width = this._workCell.getWidth();
+ let width = this._workCell.getWidth();
// The character to the left is a wide character, drawing is owned by the char at x-1
if (width === 0) {
continue;
}
+ // If true, indicates that the current character(s) to draw were joined.
+ let isJoined = false;
+ let lastCharX = x;
+
+ // Process any joined character ranges as needed. Because of how the
+ // ranges are produced, we know that they are valid for the characters
+ // and attributes of our input.
+ let cell = this._workCell;
+ if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
+ isJoined = true;
+ const range = joinedRanges.shift()!;
+
+ // We already know the exact start and end column of the joined range,
+ // so we get the string and width representing it directly
+
+ cell = new JoinedCellData(
+ this._workCell,
+ lineData.translateToString(true, range[0], range[1]),
+ range[1] - range[0]
+ );
+
+ // Skip over the cells occupied by this range in the loop
+ lastCharX = range[1] - 1;
+
+ // Recalculate width
+ width = cell.getWidth();
+ }
+
const charElement = this._document.createElement('span');
if (width > 1) {
charElement.style.width = `${cellWidth * width}px`;
}
+ if (isJoined) {
+ // Ligatures in the DOM renderer must use display inline, as they may not show with
+ // inline-block if they are outside the bounds of the element
+ charElement.style.display = 'inline';
+
+ // The DOM renderer colors the background of the cursor but for ligatures all cells are
+ // joined. The workaround here is to show a cursor around the whole ligature so it shows up,
+ // the cursor looks the same when on any character of the ligature though
+ if (cursorX >= x && cursorX <= lastCharX) {
+ cursorX = x;
+ }
+ }
+
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
@@ -85,33 +131,33 @@ export class DomRendererRowFactory {
}
}
- if (this._workCell.isBold()) {
+ if (cell.isBold()) {
charElement.classList.add(BOLD_CLASS);
}
- if (this._workCell.isItalic()) {
+ if (cell.isItalic()) {
charElement.classList.add(ITALIC_CLASS);
}
- if (this._workCell.isDim()) {
+ if (cell.isDim()) {
charElement.classList.add(DIM_CLASS);
}
- if (this._workCell.isUnderline()) {
+ if (cell.isUnderline()) {
charElement.classList.add(UNDERLINE_CLASS);
}
- if (this._workCell.isInvisible()) {
+ if (cell.isInvisible()) {
charElement.textContent = WHITESPACE_CELL_CHAR;
} else {
- charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
+ charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR;
}
- let fg = this._workCell.getFgColor();
- let fgColorMode = this._workCell.getFgColorMode();
- let bg = this._workCell.getBgColor();
- let bgColorMode = this._workCell.getBgColorMode();
- const isInverse = !!this._workCell.isInverse();
+ let fg = cell.getFgColor();
+ let fgColorMode = cell.getFgColorMode();
+ let bg = cell.getBgColor();
+ let bgColorMode = cell.getBgColorMode();
+ const isInverse = !!cell.isInverse();
if (isInverse) {
const temp = fg;
fg = bg;
@@ -125,7 +171,7 @@ export class DomRendererRowFactory {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
- if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
+ if (cell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
fg += 8;
}
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
@@ -168,6 +214,8 @@ export class DomRendererRowFactory {
}
fragment.appendChild(charElement);
+
+ x = lastCharX;
}
return fragment;
}
diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts
index e5727fa6..8280948a 100644
--- a/src/common/services/InstantiationService.ts
+++ b/src/common/services/InstantiationService.ts
@@ -42,6 +42,8 @@ export class ServiceCollection {
}
export class InstantiationService implements IInstantiationService {
+ public serviceBrand: undefined;
+
private readonly _services: ServiceCollection = new ServiceCollection();
constructor() {
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 8b21f100..ce297322 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -157,6 +157,8 @@ type GetLeadingNonServiceArgs =
export const IInstantiationService = createDecorator('InstantiationService');
export interface IInstantiationService {
+ serviceBrand: undefined;
+
setService(id: IServiceIdentifier, instance: T): void;
getService(id: IServiceIdentifier): T | undefined;
createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R;