Skip to content

Commit

Permalink
fix promise return, reject on missing family name, doc polish
Browse files Browse the repository at this point in the history
  • Loading branch information
jerch committed Oct 6, 2024
1 parent 104c63d commit 085a275
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 48 deletions.
159 changes: 121 additions & 38 deletions addons/addon-web-fonts/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## @xterm/addon-web-fonts

Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). This addon requires xterm.js v5+.
Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js).
This addon requires xterm.js v5+.

### Install

Expand All @@ -10,33 +11,43 @@ npm install --save @xterm/addon-web-fonts

### Issue with Webfonts

Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). Since font files tend to be quite big assets, browser engines often postpone their loading to an actual styling request of a codepoint matching a font file's `unicode-range`. In short - font files will not be loaded until really needed.
Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts).
Since font files tend to be quite big assets, browser engines often postpone their loading
to an actual styling request of a codepoint matching a font file's `unicode-range`.
In short - font files will not be loaded until really needed.

xterm.js on the other hand heavily relies on exact measurements of character glyphs to layout its output. This is done by determining the glyph width (DOM renderer) or by creating a glyph texture (WebGl renderer) for every output character.
For performance reasons both is done in synchronous code and cached. This logic only works properly,
if a font glyph is available on its first usage, or a wrong glyph from a fallback font chosen by the browser will be used instead.
xterm.js on the other hand heavily relies on exact measurement of character glyphs
to layout its output. This is done by determining the glyph width (DOM renderer) or
by creating a glyph texture (WebGl renderer) for every output character.
For performance reasons both is done in synchronous code and cached.
This logic only works properly, if a font glyph is available on its first usage,
otherwise the browser will pick a glyph from a fallback font messing up the metrics.

For webfonts and xterm.js this means, that we cannot rely on the default loading strategy of the browser, but have to preload the font files before using that font in xterm.js.
For webfonts and xterm.js this means that we cannot rely on the default loading strategy
of the browser, but have to preload the font files before using that font in xterm.js.


### Static Preloading for the Rescue?

If you dont mind higher initial loading times of the embedding document, you can tell the browser to preload the needed font files by placing the following link elements in the document's head above any other CSS/Javascript:
If you dont mind higher initial loading times with a white page shown,
you can tell the browser to preload the needed font files by placing the following
link elements in the document's head above any other CSS/Javascript:
```html
<link rel="preload" as="font" href="/path/to/your/fontfile1.woff" type="font/woff2" crossorigin="anonymous">
<link rel="preload" as="font" href="/path/to/your/fontfile2.woff" type="font/woff2" crossorigin="anonymous">
...
<!-- CSS with font-face rules matching the URLs above -->
```
Downside of this approach is the much higher initial loading time showing as a white page. Browsers also will resort to system fonts, if the preloading takes too long, so with a slow connection or a very big font this solves literally nothing.
Browsers also will resort to system fonts, if the preloading takes too long.
So this solution has only a very limited scope.


### Preloading with WebFontsAddon
### Loading with WebFontsAddon

The webfonts addon offers several ways to deal with the loading of font assets without leaving the terminal in an unusable state.
The webfonts addon offers several ways to deal with font assets loading
without leaving the terminal in an unusable state.


Recap - normally boostrapping of a new terminal involves these basic steps:
Recap - normally boostrapping of a new terminal involves these basic steps (Typescript):

```typescript
import { Terminal } from '@xterm/xterm';
Expand All @@ -46,67 +57,76 @@ import { XYAddon } from '@xterm/addon-xy';
const terminal = new Terminal({fontFamily: 'monospace'});

// create and load all addons you want to use, e.g. fit addon
const xyAddon = new XYAddon();
terminal.loadAddon(xyAddon);
const xyInstance = new XYAddon();
terminal.loadAddon(xyInstance);

// finally: call `open` of the terminal instance
terminal.open(your_terminal_div_element); // <-- critical path for webfonts
// more boostrapping goes here ...
```
This code is guaranteed to work in all browsers synchronously, as the identifier `monospace`
will always be available. It will also work synchronously with any installed system font,
but breaks horribly with webfonts. The actual culprit here is the call to `terminal.open`,
which attaches the terminal to the DOM and starts the renderer with all the glyph caching
mentioned above, while the webfont is not yet fully available.

This synchronous code is guaranteed to work in all browsers, as the font `monospace` will always be available.
It will also work that way with any installed system font, but breaks horribly for webfonts. The actual culprit here is the call to `terminal.open`, which attaches the terminal to the DOM and starts the renderer with all the glyph caching mentioned above, while the webfont is not fully available yet.

To fix that, the webfonts addon provides a waiting condition:
To fix that, the webfonts addon provides a waiting condition (Typescript):
```typescript
import { Terminal } from '@xterm/xterm';
import { XYAddon } from '@xterm/addon-xy';
import { WebFontsAddon } from '@xterm/addon-web-fonts';

// create a `Terminal` instance, now with webfonts
const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'});
const xyAddon = new XYAddon();
terminal.loadAddon(xyAddon);
const xyInstance = new XYAddon();
terminal.loadAddon(xyInstance);

const webFontsAddon = new WebFontsAddon();
terminal.loadAddon(webFontsAddon);
const webFontsInstance = new WebFontsAddon();
terminal.loadAddon(webFontsInstance);

// wait for webfonts to be fully loaded
webFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => {
webFontsInstance.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => {
terminal.open(your_terminal_div_element);
// more boostrapping goes here ...
});
```
Here `loadFonts` will look up the font face objects in `document.fonts` and load them before continuing.
For this to work, you have to make sure, that the CSS `font-face` rules for these webfonts are loaded
on the initial document load (more precise - by the time this code runs).

Please note, that this code cannot run synchronous anymore, so you will have to split your
bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole
bootstrapping under that waiting condition (import `loadFonts` for a static variant):
Here `loadFonts` will look up the font face objects in `document.fonts`
and load them before continuing. For this to work, you have to make sure,
that the CSS `font-face` rules for these webfonts are loaded beforehand,
otherwise `loadFonts` will not find the font family names (promise will be
rejected for missing font family names).

Please note, that this cannot run synchronous anymore, so you will have to split your
bootstrapping code into several stages. If that is too much of a hassle,
you can also move the whole bootstrapping under the waiting condition by using
the static loader instead (Typescript):
```typescript
import { Terminal } from '@xterm/xterm';
import { XYAddon } from '@xterm/addon-xy';
import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts';
// import static loader
import { loadFonts } from '@xterm/addon-web-fonts';

loadFonts(['Web Mono 1', 'Super Powerline']).then(() => {
// create a `Terminal` instance, now with webfonts
const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'});
const xyAddon = new XYAddon();
terminal.loadAddon(xyAddon);
const xyInstance = new XYAddon();
terminal.loadAddon(xyInstance);

const webFontsAddon = new WebFontsAddon();
terminal.loadAddon(webFontsAddon);
// optional when using static loader
const webfontsInstance = new WebFontsAddon();
terminal.loadAddon(webfontsInstance);

terminal.open(your_terminal_div_element);
// more boostrapping goes here ...
});
```
With the static loader creating and loading of the actual addon can be omitted,
as fonts are already loaded before any terminal setup happens.

### Webfont Loading at Runtime

Given you have a terminal already running and want to change the font family to a different not yet loaded webfont.
That can be achieved like this:
Given you have a terminal already running and want to change the font family
to a different not yet loaded webfont:
```typescript
// either create font face objects in javascript
const ff1 = new FontFace('New Web Mono', url1, ...);
Expand All @@ -133,5 +153,68 @@ loadFonts(['New Web Mono']).then(() => {
});
```

### Forced Layout Update

If you have the addon loaded into your terminal, you can force the terminal to update
the layout with the method `WebFontsAddon.relayout`. This might come handy,
if the terminal shows webfont related output issue for unknown reasons:
```typescript
...
// given - terminal shows weird font issues, run:
webFontsInstance.relayout().then(() => {
// also run resize logic here, e.g. fit addon
fitAddon.fit();
});
```
Note that this method is only meant as a quickfix on a running terminal to keep it
in a working condition. A production-ready integration should never rely on it,
better fix the real root cause (most likely not properly awaiting the font loader
higher up in the code).


### Webfonts from Fontsource

The addon has been tested to work with webfonts from fontsource.
Javascript example for `vite` with ESM import:
```javascript
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { loadFonts } from '@xterm/addon-web-fonts';
import '@xterm/xterm/css/xterm.css';
import '@fontsource/roboto-mono';
import '@fontsource/roboto-mono/400.css';
import '@fontsource/roboto-mono/400-italic.css';
import '@fontsource/roboto-mono/700.css';
import '@fontsource/roboto-mono/700-italic.css';

async function main() {
let fontFamily = '"Roboto Mono", monospace';
try {
await loadFonts(['Roboto Mono']);
} catch (e) {
fontFamily = 'monospace';
}

const terminal = new Terminal({ fontFamily });
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(document.getElementById('your-xterm-container-div'));
fitAddon.fit();

// sync writing shows up in Roboto Mono w'o FOUT
// and a fallback to monospace
terminal.write('put any unicode char here');
}

main();
```
The fontsource packages download the font files to your project folder to be delivered
from there later on. For security sensitive projects this should be the preferred way,
as it brings the font files under your control.

The example furthermore contains proper exception handling with a fallback
(skipped in all other examples for better readability).

---

See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts) for more advanced usage.
Also see the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts).
13 changes: 6 additions & 7 deletions addons/addon-web-fonts/src/WebFontsAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,37 +94,36 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
const familyFiltered = ffs.filter(ff => font === unquote(ff.family));
toLoad = toLoad.concat(familyFiltered);
if (!familyFiltered.length) {
console.warn(`font family "${font}" not registered in document.fonts`);
return Promise.reject(`font family "${font}" not registered in document.fonts`);
}
}
}
return Promise.all(toLoad.map(ff => ff.load()));
}


export async function loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
await document.fonts.ready;
return _loadFonts(fonts);
export function loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
return document.fonts.ready.then(() => _loadFonts(fonts));
}


export class WebFontsAddon implements ITerminalAddon, IWebFontsApi {
private _term: Terminal | undefined;

constructor(public forceInitialRelayout: boolean = true) { }
constructor(public initialRelayout: boolean = true) { }

public dispose(): void {
this._term = undefined;
}

public activate(term: Terminal): void {
this._term = term;
if (this.forceInitialRelayout) {
if (this.initialRelayout) {
document.fonts.ready.then(() => this.relayout());
}
}

public async loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
public loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
return loadFonts(fonts);
}

Expand Down
6 changes: 3 additions & 3 deletions addons/addon-web-fonts/typings/addon-web-fonts.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ declare module '@xterm/addon-web-fonts' {
*/
export class WebFontsAddon implements ITerminalAddon {
/**
* @param forceInitialRelayout Force initial relayout, if a webfont was found (default true).
* @param initialRelayout Force initial relayout, if a webfont was found (default true).
*/
constructor(forceInitialRelayout?: boolean);
constructor(initialRelayout?: boolean);
public activate(terminal: Terminal): void;
public dispose(): void;

Expand Down Expand Up @@ -44,7 +44,7 @@ declare module '@xterm/addon-web-fonts' {
* Call this method, if a terminal with webfonts is stuck with broken
* glyph metrics.
*
* Returns a promise on completion.
* The returned promise will resolve, when font loading and layouting are done.
*/
public relayout(): Promise<void>;
}
Expand Down

0 comments on commit 085a275

Please sign in to comment.