Skip to content

Commit 0151805

Browse files
authored
Merge pull request #5285 from Tyriar/5284_detailed_liga
Support detailed ligatures and variants
2 parents 9e20641 + 408d099 commit 0151805

File tree

7 files changed

+77
-32
lines changed

7 files changed

+77
-32
lines changed

addons/addon-ligatures/src/LigaturesAddon.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ export interface ITerminalAddon {
1515

1616
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
1717
private readonly _fallbackLigatures: string[];
18+
private readonly _fontFeatureSettings?: string;
1819

1920
private _terminal: Terminal | undefined;
2021
private _characterJoinerId: number | undefined;
2122

2223
constructor(options?: Partial<ILigatureOptions>) {
24+
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
2325
this._fallbackLigatures = (options?.fallbackLigatures || [
2426
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
2527
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
26-
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
27-
'<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
28-
':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
29-
'++', '+++', '<!--', '<!---', '<***>'
28+
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
29+
'<~~', '</', '</>', '/>', '~~>', '==', '!=', '/=', '~=', '<>', '===', '!==', '!===',
30+
'<:', ':=', '*=', '*+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+*', '=*', '=:', ':>',
31+
'/*', '*/', '+++', '<!--', '<!---'
3032
]).sort((a, b) => b.length - a.length);
33+
this._fontFeatureSettings = options?.fontFeatureSettings;
3134
}
3235

3336
public activate(terminal: Terminal): void {
@@ -36,7 +39,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
3639
}
3740
this._terminal = terminal;
3841
this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
39-
terminal.element.style.fontFeatureSettings = '"liga" on, "calt" on';
42+
terminal.element.style.fontFeatureSettings = this._fontFeatureSettings ?? '"calt" on';
4043
}
4144

4245
public dispose(): void {

addons/addon-ligatures/src/Types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55

66
export interface ILigatureOptions {
77
fallbackLigatures: string[];
8+
fontFeatureSettings: string;
89
}

addons/addon-ligatures/typings/addon-ligatures.d.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ declare module '@xterm/addon-ligatures' {
2323
constructor(options?: Partial<ILigatureOptions>);
2424

2525
/**
26-
* Activates the addon
26+
* Activates the addon. Note that if webgl is also being used, that addon
27+
* should be reactivated after ligatures is activated in order to apply
28+
* {@link ILigatureOptions.fontFeatureSettings} to the texture atlas.
29+
*
2730
*
2831
* @param terminal The terminal the addon is being loaded in.
2932
*/
@@ -40,19 +43,28 @@ declare module '@xterm/addon-ligatures' {
4043
*/
4144
export interface ILigatureOptions {
4245
/**
43-
* Fallback ligatures to use when the font access API is either not supported by the browser or
44-
* access is denied. The default set of ligatures is taken from Iosevka's default "calt"
45-
* ligation set: https://typeof.net/Iosevka/
46+
* Fallback ligatures to use when the font access API is either not
47+
* supported by the browser or access is denied. The default set of
48+
* ligatures is taken from Iosevka's default "calt" ligation set:
49+
* https://typeof.net/Iosevka/
4650
*
4751
* ```
4852
* <-- <--- <<- <- -> ->> --> --->
4953
* <== <=== <<= <= => =>> ==> ===> >= >>=
50-
* <-> <--> <---> <----> <=> <==> <===> <====> -------->
51-
* <~~ <~ ~> ~~> :: ::: == != === !==
52-
* := :- :+ <* <*> *> <| <|> |> +: -: =: :>
53-
* ++ +++ <!-- <!--- <***>
54-
* ```
54+
* <-> <--> <---> <----> <=> <==> <===> <====> :: :::
55+
* <~~ </ </> /> ~~> == != /= ~= <> === !== !===
56+
* <: := *= *+ <* <*> *> <| <|> |> +* =* =: :>
57+
* /* <close block comment> +++ <!-- <!---
5558
*/
5659
fallbackLigatures: string[]
60+
61+
/**
62+
* The CSS `font-feature-settings` value to use for enabling ligatures. This
63+
* also supports font variants for example with a value like
64+
* `"calt" on, "ss03"`.
65+
*
66+
* The default value is `"calt" on`.
67+
*/
68+
fontFeatureSettings: string;
5769
}
5870
}

addons/addon-webgl/src/GlyphRenderer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,9 @@ export class GlyphRenderer extends Disposable {
238238

239239
// Get the glyph
240240
if (chars && chars.length > 1) {
241-
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
241+
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false, this._terminal.element);
242242
} else {
243-
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
243+
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false, this._terminal.element);
244244
}
245245

246246
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);

demo/client.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -637,17 +637,25 @@ function initAddons(term: Terminal): void {
637637
}
638638
return;
639639
}
640+
function postInitWebgl(): void {
641+
setTimeout(() => {
642+
setTextureAtlas(addons.webgl.instance.textureAtlas);
643+
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
644+
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
645+
}, 500);
646+
}
647+
function preDisposeWebgl(): void {
648+
if (addons.webgl.instance.textureAtlas) {
649+
addons.webgl.instance.textureAtlas.remove();
650+
}
651+
}
640652
if (checkbox.checked) {
641653
// HACK: Manually remove addons that cannot be changes
642654
addon.instance = new (addon as IDemoAddon<Exclude<AddonType, 'attach'>>).ctor();
643655
try {
644656
term.loadAddon(addon.instance);
645657
if (name === 'webgl') {
646-
setTimeout(() => {
647-
setTextureAtlas(addons.webgl.instance.textureAtlas);
648-
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
649-
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
650-
}, 0);
658+
postInitWebgl();
651659
} else if (name === 'unicode11') {
652660
term.unicode.activeVersion = '11';
653661
} else if (name === 'unicodeGraphemes') {
@@ -663,13 +671,24 @@ function initAddons(term: Terminal): void {
663671
}
664672
} else {
665673
if (name === 'webgl') {
666-
addons.webgl.instance.textureAtlas.remove();
674+
preDisposeWebgl();
667675
} else if (name === 'unicode11' || name === 'unicodeGraphemes') {
668676
term.unicode.activeVersion = '6';
669677
}
670678
addon.instance!.dispose();
671679
addon.instance = undefined;
672680
}
681+
if (name === 'ligatures') {
682+
// Recreate webgl when ligatures are toggled so texture atlas picks up any font feature
683+
// settings changes
684+
if (addons.webgl.instance) {
685+
preDisposeWebgl();
686+
addons.webgl.instance.dispose();
687+
addons.webgl.instance = new addons.webgl.ctor();
688+
term.loadAddon(addons.webgl.instance);
689+
postInitWebgl();
690+
}
691+
}
673692
});
674693
const label = document.createElement('label');
675694
label.classList.add('addon');

src/browser/renderer/shared/TextureAtlas.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export class TextureAtlas implements ITextureAtlas {
103103
}
104104

105105
public dispose(): void {
106+
this._tmpCanvas.remove();
106107
for (const page of this.pages) {
107108
page.canvas.remove();
108109
}
@@ -122,7 +123,7 @@ export class TextureAtlas implements ITextureAtlas {
122123
for (let i = 33; i < 126; i++) {
123124
queue.enqueue(() => {
124125
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
125-
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
126+
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, false, undefined);
126127
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
127128
}
128129
});
@@ -242,12 +243,12 @@ export class TextureAtlas implements ITextureAtlas {
242243
}
243244
}
244245

245-
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph {
246-
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight);
246+
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
247+
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
247248
}
248249

249-
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph {
250-
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight);
250+
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
251+
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight, domContainer);
251252
}
252253

253254
/**
@@ -259,11 +260,12 @@ export class TextureAtlas implements ITextureAtlas {
259260
bg: number,
260261
fg: number,
261262
ext: number,
262-
restrictToCellHeight: boolean = false
263+
restrictToCellHeight: boolean,
264+
domContainer: HTMLElement | undefined
263265
): IRasterizedGlyph {
264266
$glyph = cacheMap.get(key, bg, fg, ext);
265267
if (!$glyph) {
266-
$glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight);
268+
$glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight, domContainer);
267269
cacheMap.set(key, bg, fg, ext, $glyph);
268270
}
269271
return $glyph;
@@ -423,12 +425,20 @@ export class TextureAtlas implements ITextureAtlas {
423425
return this._config.colors.contrastCache;
424426
}
425427

426-
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
428+
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
427429
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
428430

429431
// Uncomment for debugging
430432
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
431433

434+
// Attach the canvas to the DOM in order to inherit font-feature-settings
435+
// from the parent elements. This is necessary for ligatures and variants to
436+
// work.
437+
if (domContainer && this._tmpCanvas.parentElement !== domContainer) {
438+
this._tmpCanvas.style.display = 'none';
439+
domContainer.append(this._tmpCanvas);
440+
}
441+
432442
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
433443
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
434444
// giant ligatures (eg. =====>) don't impact overall performance.

src/browser/renderer/shared/Types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ export interface ITextureAtlas extends IDisposable {
108108
* Clear all glyphs from the texture atlas.
109109
*/
110110
clearTexture(): void;
111-
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph;
112-
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph;
111+
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph;
112+
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph;
113113
}
114114

115115
/**

0 commit comments

Comments
 (0)