Skip to content

Commit

Permalink
Merge pull request #5285 from Tyriar/5284_detailed_liga
Browse files Browse the repository at this point in the history
Support detailed ligatures and variants
  • Loading branch information
Tyriar authored Jan 8, 2025
2 parents 9e20641 + 408d099 commit 0151805
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 32 deletions.
13 changes: 8 additions & 5 deletions addons/addon-ligatures/src/LigaturesAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ export interface ITerminalAddon {

export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
private readonly _fallbackLigatures: string[];
private readonly _fontFeatureSettings?: string;

private _terminal: Terminal | undefined;
private _characterJoinerId: number | undefined;

constructor(options?: Partial<ILigatureOptions>) {
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
this._fallbackLigatures = (options?.fallbackLigatures || [
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
'<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
'++', '+++', '<!--', '<!---', '<***>'
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
'<~~', '</', '</>', '/>', '~~>', '==', '!=', '/=', '~=', '<>', '===', '!==', '!===',
'<:', ':=', '*=', '*+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+*', '=*', '=:', ':>',
'/*', '*/', '+++', '<!--', '<!---'
]).sort((a, b) => b.length - a.length);
this._fontFeatureSettings = options?.fontFeatureSettings;
}

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

public dispose(): void {
Expand Down
1 change: 1 addition & 0 deletions addons/addon-ligatures/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

export interface ILigatureOptions {
fallbackLigatures: string[];
fontFeatureSettings: string;
}
30 changes: 21 additions & 9 deletions addons/addon-ligatures/typings/addon-ligatures.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ declare module '@xterm/addon-ligatures' {
constructor(options?: Partial<ILigatureOptions>);

/**
* Activates the addon
* Activates the addon. Note that if webgl is also being used, that addon
* should be reactivated after ligatures is activated in order to apply
* {@link ILigatureOptions.fontFeatureSettings} to the texture atlas.
*
*
* @param terminal The terminal the addon is being loaded in.
*/
Expand All @@ -40,19 +43,28 @@ declare module '@xterm/addon-ligatures' {
*/
export interface ILigatureOptions {
/**
* Fallback ligatures to use when the font access API is either not supported by the browser or
* access is denied. The default set of ligatures is taken from Iosevka's default "calt"
* ligation set: https://typeof.net/Iosevka/
* Fallback ligatures to use when the font access API is either not
* supported by the browser or access is denied. The default set of
* ligatures is taken from Iosevka's default "calt" ligation set:
* https://typeof.net/Iosevka/
*
* ```
* <-- <--- <<- <- -> ->> --> --->
* <== <=== <<= <= => =>> ==> ===> >= >>=
* <-> <--> <---> <----> <=> <==> <===> <====> -------->
* <~~ <~ ~> ~~> :: ::: == != === !==
* := :- :+ <* <*> *> <| <|> |> +: -: =: :>
* ++ +++ <!-- <!--- <***>
* ```
* <-> <--> <---> <----> <=> <==> <===> <====> :: :::
* <~~ </ </> /> ~~> == != /= ~= <> === !== !===
* <: := *= *+ <* <*> *> <| <|> |> +* =* =: :>
* /* <close block comment> +++ <!-- <!---
*/
fallbackLigatures: string[]

/**
* The CSS `font-feature-settings` value to use for enabling ligatures. This
* also supports font variants for example with a value like
* `"calt" on, "ss03"`.
*
* The default value is `"calt" on`.
*/
fontFeatureSettings: string;
}
}
4 changes: 2 additions & 2 deletions addons/addon-webgl/src/GlyphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ export class GlyphRenderer extends Disposable {

// Get the glyph
if (chars && chars.length > 1) {
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false);
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext, false, this._terminal.element);
} else {
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false);
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext, false, this._terminal.element);
}

$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
Expand Down
31 changes: 25 additions & 6 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,17 +637,25 @@ function initAddons(term: Terminal): void {
}
return;
}
function postInitWebgl(): void {
setTimeout(() => {
setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
}, 500);
}
function preDisposeWebgl(): void {
if (addons.webgl.instance.textureAtlas) {
addons.webgl.instance.textureAtlas.remove();
}
}
if (checkbox.checked) {
// HACK: Manually remove addons that cannot be changes
addon.instance = new (addon as IDemoAddon<Exclude<AddonType, 'attach'>>).ctor();
try {
term.loadAddon(addon.instance);
if (name === 'webgl') {
setTimeout(() => {
setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
}, 0);
postInitWebgl();
} else if (name === 'unicode11') {
term.unicode.activeVersion = '11';
} else if (name === 'unicodeGraphemes') {
Expand All @@ -663,13 +671,24 @@ function initAddons(term: Terminal): void {
}
} else {
if (name === 'webgl') {
addons.webgl.instance.textureAtlas.remove();
preDisposeWebgl();
} else if (name === 'unicode11' || name === 'unicodeGraphemes') {
term.unicode.activeVersion = '6';
}
addon.instance!.dispose();
addon.instance = undefined;
}
if (name === 'ligatures') {
// Recreate webgl when ligatures are toggled so texture atlas picks up any font feature
// settings changes
if (addons.webgl.instance) {
preDisposeWebgl();
addons.webgl.instance.dispose();
addons.webgl.instance = new addons.webgl.ctor();
term.loadAddon(addons.webgl.instance);
postInitWebgl();
}
}
});
const label = document.createElement('label');
label.classList.add('addon');
Expand Down
26 changes: 18 additions & 8 deletions src/browser/renderer/shared/TextureAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export class TextureAtlas implements ITextureAtlas {
}

public dispose(): void {
this._tmpCanvas.remove();
for (const page of this.pages) {
page.canvas.remove();
}
Expand All @@ -122,7 +123,7 @@ export class TextureAtlas implements ITextureAtlas {
for (let i = 33; i < 126; i++) {
queue.enqueue(() => {
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, false, undefined);
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
}
});
Expand Down Expand Up @@ -242,12 +243,12 @@ export class TextureAtlas implements ITextureAtlas {
}
}

public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight);
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
}

public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight);
public getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
return this._getFromCacheMap(this._cacheMap, code, bg, fg, ext, restrictToCellHeight, domContainer);
}

/**
Expand All @@ -259,11 +260,12 @@ export class TextureAtlas implements ITextureAtlas {
bg: number,
fg: number,
ext: number,
restrictToCellHeight: boolean = false
restrictToCellHeight: boolean,
domContainer: HTMLElement | undefined
): IRasterizedGlyph {
$glyph = cacheMap.get(key, bg, fg, ext);
if (!$glyph) {
$glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight);
$glyph = this._drawToCache(key, bg, fg, ext, restrictToCellHeight, domContainer);
cacheMap.set(key, bg, fg, ext, $glyph);
}
return $glyph;
Expand Down Expand Up @@ -423,12 +425,20 @@ export class TextureAtlas implements ITextureAtlas {
return this._config.colors.contrastCache;
}

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

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

// Attach the canvas to the DOM in order to inherit font-feature-settings
// from the parent elements. This is necessary for ligatures and variants to
// work.
if (domContainer && this._tmpCanvas.parentElement !== domContainer) {
this._tmpCanvas.style.display = 'none';
domContainer.append(this._tmpCanvas);
}

// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
Expand Down
4 changes: 2 additions & 2 deletions src/browser/renderer/shared/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ export interface ITextureAtlas extends IDisposable {
* Clear all glyphs from the texture atlas.
*/
clearTexture(): void;
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph;
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean): IRasterizedGlyph;
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph;
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph;
}

/**
Expand Down

0 comments on commit 0151805

Please sign in to comment.