ntk 8.12.3 → 8.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/fontconfig.js +204 -15
- package/lib/text/fontmanager.js +93 -43
- package/lib/text/fontsource.js +7 -7
- package/package.json +1 -1
package/lib/fontconfig.js
CHANGED
|
@@ -255,6 +255,9 @@ function cacheMatches(fc, out) {
|
|
|
255
255
|
return list;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
// fc -> { promise, base }: one child per pattern however many callers ask.
|
|
259
|
+
// `base` is where a prewarm writes its answer (`spawnToFiles`), null for a
|
|
260
|
+
// child whose answer only reaches the event loop.
|
|
258
261
|
const inflight = new Map();
|
|
259
262
|
|
|
260
263
|
/**
|
|
@@ -270,13 +273,14 @@ const inflight = new Map();
|
|
|
270
273
|
*/
|
|
271
274
|
function runFcMatch(fc) {
|
|
272
275
|
const pending = inflight.get(fc);
|
|
273
|
-
if (pending) return pending;
|
|
276
|
+
if (pending) return pending.promise;
|
|
274
277
|
const cp = childProcess();
|
|
275
278
|
if (!cp) return Promise.reject(noFontsError(NOT_NODE));
|
|
276
279
|
|
|
277
|
-
const
|
|
280
|
+
const entry = { promise: null, base: null };
|
|
281
|
+
entry.promise = new Promise((resolve, reject) => {
|
|
278
282
|
cp.execFile('fc-match', [...fcMatchArgs, fc], fcMatchOpts, (err, out, stderr) => {
|
|
279
|
-
inflight.delete(fc);
|
|
283
|
+
if (inflight.get(fc) === entry) inflight.delete(fc);
|
|
280
284
|
if (!err) return resolve(out);
|
|
281
285
|
// execFile hands stderr to the callback; execFileSync hangs it on the
|
|
282
286
|
// error, which is where the shared diagnosis reads it from
|
|
@@ -284,8 +288,152 @@ function runFcMatch(fc) {
|
|
|
284
288
|
reject(err);
|
|
285
289
|
});
|
|
286
290
|
});
|
|
287
|
-
inflight.set(fc,
|
|
288
|
-
return promise;
|
|
291
|
+
inflight.set(fc, entry);
|
|
292
|
+
return entry.promise;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// What a prewarm's child runs: fc-match with its answer in a file, then —
|
|
296
|
+
// once that file is complete — the exit status in another, which is what a
|
|
297
|
+
// synchronous caller waits for (`answerSync`). Nothing but shell builtins
|
|
298
|
+
// runs after fc-match, so a PATH that holds fc-match alone is enough.
|
|
299
|
+
const PREWARM_SCRIPT = 'fc-match "$@" > "$0.out" 2> "$0.err"; echo $? > "$0.done"';
|
|
300
|
+
|
|
301
|
+
// The directory prewarms write into, made on first use: undefined until
|
|
302
|
+
// then, null where it cannot be (no shell, no writable tmpdir), which leaves
|
|
303
|
+
// prewarms on the event loop the way they always were.
|
|
304
|
+
let prewarmDir;
|
|
305
|
+
let prewarmCount = 0;
|
|
306
|
+
|
|
307
|
+
function prewarmBase() {
|
|
308
|
+
const fs = builtin('node:fs');
|
|
309
|
+
const path = builtin('node:path');
|
|
310
|
+
if (prewarmDir === undefined) {
|
|
311
|
+
prewarmDir = null;
|
|
312
|
+
const p = globalThis.process;
|
|
313
|
+
const os = builtin('node:os');
|
|
314
|
+
if (p && p.platform !== 'win32' && fs && os && path && fs.existsSync('/bin/sh')) {
|
|
315
|
+
try {
|
|
316
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ntk-fc-'));
|
|
317
|
+
p.once('exit', () => {
|
|
318
|
+
try {
|
|
319
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
320
|
+
} catch {
|
|
321
|
+
// a tmpdir cleaned up under us is already clean
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
prewarmDir = dir;
|
|
325
|
+
} catch {
|
|
326
|
+
// a read-only or missing tmpdir: prewarm the old way
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return prewarmDir === null ? null : path.join(prewarmDir, String(prewarmCount++));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The status and answer a prewarm left, once it has left them; null while
|
|
334
|
+
* it is still running. */
|
|
335
|
+
function readPrewarm(base) {
|
|
336
|
+
const fs = builtin('node:fs');
|
|
337
|
+
let done;
|
|
338
|
+
try {
|
|
339
|
+
done = fs.readFileSync(`${base}.done`, 'utf8');
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
// `echo` writes the status and the newline in one go, but a read can land
|
|
344
|
+
// between the file appearing and its bytes arriving
|
|
345
|
+
if (!done.endsWith('\n')) return null;
|
|
346
|
+
const status = Number(done);
|
|
347
|
+
let out = '';
|
|
348
|
+
if (status === 0) {
|
|
349
|
+
try {
|
|
350
|
+
out = fs.readFileSync(`${base}.out`, 'utf8');
|
|
351
|
+
} catch {
|
|
352
|
+
return { status: -1, out: '' };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { status, out };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function removePrewarm(base) {
|
|
359
|
+
const fs = builtin('node:fs');
|
|
360
|
+
for (const ext of ['.out', '.err', '.done']) {
|
|
361
|
+
try {
|
|
362
|
+
fs.unlinkSync(base + ext);
|
|
363
|
+
} catch {
|
|
364
|
+
// never written, or already gone
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A prewarm's child, answering through files so that the synchronous path
|
|
371
|
+
* can take its answer without the event loop (`answerSync`) — which the
|
|
372
|
+
* first text layout is holding, from inside a render, for as long as the
|
|
373
|
+
* first frame takes. A prewarm answered through `execFile` alone lands after
|
|
374
|
+
* that, so the layout it was started for spawned fc-match a second time and
|
|
375
|
+
* waited for that one instead.
|
|
376
|
+
*
|
|
377
|
+
* Null where files cannot be used; the caller prewarms the old way.
|
|
378
|
+
*/
|
|
379
|
+
function spawnToFiles(fc) {
|
|
380
|
+
const cp = childProcess();
|
|
381
|
+
const base = cp ? prewarmBase() : null;
|
|
382
|
+
if (base === null) return null;
|
|
383
|
+
let child;
|
|
384
|
+
try {
|
|
385
|
+
child = cp.spawn('/bin/sh', ['-c', PREWARM_SCRIPT, base, ...fcMatchArgs, fc], {
|
|
386
|
+
stdio: 'ignore'
|
|
387
|
+
});
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const entry = { promise: null, base, answer: undefined };
|
|
392
|
+
entry.promise = new Promise((resolve, reject) => {
|
|
393
|
+
const settle = () => {
|
|
394
|
+
if (inflight.get(fc) === entry) inflight.delete(fc);
|
|
395
|
+
// answered already, by a synchronous caller that could not wait
|
|
396
|
+
if (entry.answer === undefined) {
|
|
397
|
+
const left = readPrewarm(base);
|
|
398
|
+
removePrewarm(base);
|
|
399
|
+
entry.answer = left && left.status === 0 ? left.out : null;
|
|
400
|
+
}
|
|
401
|
+
if (entry.answer !== null) resolve(entry.answer);
|
|
402
|
+
else reject(new Error(`fc-match for "${fc}" did not answer`));
|
|
403
|
+
};
|
|
404
|
+
child.once('error', settle);
|
|
405
|
+
child.once('exit', settle);
|
|
406
|
+
});
|
|
407
|
+
inflight.set(fc, entry);
|
|
408
|
+
return entry;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// the synchronous path's nap while a prewarm finishes
|
|
412
|
+
const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
413
|
+
|
|
414
|
+
// How long the synchronous path waits on a prewarm before asking fc-match
|
|
415
|
+
// itself. fc-match answers in tens of milliseconds, and a prewarm that is
|
|
416
|
+
// running has a head start on any spawn that could replace it.
|
|
417
|
+
const PREWARM_WAIT_MS = 3000;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* An in-flight prewarm's answer, waited for synchronously: its stdout, or
|
|
421
|
+
* null if it failed or took too long — the caller then spawns fc-match
|
|
422
|
+
* itself, which is also what reports why.
|
|
423
|
+
*/
|
|
424
|
+
function answerSync(fc, entry) {
|
|
425
|
+
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
426
|
+
let left = readPrewarm(entry.base);
|
|
427
|
+
while (left === null && performance.now() < deadline) {
|
|
428
|
+
Atomics.wait(nap, 0, 0, 1);
|
|
429
|
+
left = readPrewarm(entry.base);
|
|
430
|
+
}
|
|
431
|
+
if (inflight.get(fc) === entry) inflight.delete(fc);
|
|
432
|
+
if (entry.answer === undefined) {
|
|
433
|
+
entry.answer = left && left.status === 0 ? left.out : null;
|
|
434
|
+
if (left !== null) removePrewarm(entry.base);
|
|
435
|
+
}
|
|
436
|
+
return entry.answer;
|
|
289
437
|
}
|
|
290
438
|
|
|
291
439
|
/**
|
|
@@ -294,8 +442,10 @@ function runFcMatch(fc) {
|
|
|
294
442
|
* matchSortedSync is deliberately synchronous — it answers from inside text
|
|
295
443
|
* layout — so the first layout for a pattern pays the fc-match spawn (~50ms)
|
|
296
444
|
* as a first-paint stall. Starting the same command here with a non-blocking
|
|
297
|
-
*
|
|
298
|
-
* the critical path (issue #182).
|
|
445
|
+
* spawn, while the X connection is still being set up, moves that cost off
|
|
446
|
+
* the critical path (issue #182). The child answers through files as well as
|
|
447
|
+
* through the event loop, so a layout that asks for the pattern before the
|
|
448
|
+
* loop has run takes the prewarm's answer rather than spawning its own.
|
|
299
449
|
*
|
|
300
450
|
* Never rejects and never reports: a prewarm is an optimization, and an app
|
|
301
451
|
* that never renders text must not crash — or even warn — over a missing
|
|
@@ -304,9 +454,6 @@ function runFcMatch(fc) {
|
|
|
304
454
|
* `unavailable`: the first sync throw keeps its original spawn error as
|
|
305
455
|
* `cause`.
|
|
306
456
|
*
|
|
307
|
-
* A sync call racing this one wins — the child's result is discarded
|
|
308
|
-
* whenever the pattern is already cached by the time it exits.
|
|
309
|
-
*
|
|
310
457
|
* `matchSorted` is the reporting variant: same spawn, same cache, but it
|
|
311
458
|
* awaits an answer and so has somewhere to put a failure.
|
|
312
459
|
*
|
|
@@ -316,7 +463,8 @@ function runFcMatch(fc) {
|
|
|
316
463
|
export function prewarm(pattern = {}) {
|
|
317
464
|
const fc = patternFor(pattern);
|
|
318
465
|
if (sortedCache.has(fc) || unavailable) return Promise.resolve();
|
|
319
|
-
|
|
466
|
+
const pending = inflight.get(fc)?.promise ?? spawnToFiles(fc)?.promise ?? runFcMatch(fc);
|
|
467
|
+
return pending.then(
|
|
320
468
|
(out) => {
|
|
321
469
|
if (!sortedCache.has(fc)) {
|
|
322
470
|
const list = parseMatches(out);
|
|
@@ -327,6 +475,31 @@ export function prewarm(pattern = {}) {
|
|
|
327
475
|
);
|
|
328
476
|
}
|
|
329
477
|
|
|
478
|
+
// the faces a family is asked for in: regular, bold, and both in italic
|
|
479
|
+
const FACES = [
|
|
480
|
+
[400, 'normal'],
|
|
481
|
+
[700, 'normal'],
|
|
482
|
+
[400, 'italic'],
|
|
483
|
+
[700, 'italic']
|
|
484
|
+
];
|
|
485
|
+
const facesWarmed = new Set();
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Prewarm a family in the four faces text is set in — once per family.
|
|
489
|
+
*
|
|
490
|
+
* A document asks for its family's faces one at a time, as layout reaches
|
|
491
|
+
* the first bold word, the first emphasis, and each ask that misses is a
|
|
492
|
+
* synchronous fc-match on the way to the first frame: a Markdown document
|
|
493
|
+
* waited on five of them, 600 ms on XQuartz. Started together they run side
|
|
494
|
+
* by side, and each later ask finds its answer there.
|
|
495
|
+
*/
|
|
496
|
+
export function prewarmFaces(pattern = {}) {
|
|
497
|
+
const family = pattern.family || 'sans-serif';
|
|
498
|
+
if (facesWarmed.has(family) || unavailable) return;
|
|
499
|
+
facesWarmed.add(family);
|
|
500
|
+
for (const [weight, style] of FACES) prewarm({ family, weight, style });
|
|
501
|
+
}
|
|
502
|
+
|
|
330
503
|
/**
|
|
331
504
|
* The same match list as `matchSortedSync`, without blocking for it.
|
|
332
505
|
*
|
|
@@ -352,10 +525,18 @@ export async function matchSorted(pattern = {}) {
|
|
|
352
525
|
if (unavailable) throw noFontsError(unavailable);
|
|
353
526
|
|
|
354
527
|
let out;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
528
|
+
const pending = inflight.get(fc);
|
|
529
|
+
if (pending?.base) {
|
|
530
|
+
// a prewarm running for the pattern: its answer, or — if it has none —
|
|
531
|
+
// a spawn of our own, which is what reports why
|
|
532
|
+
out = await pending.promise.catch(() => undefined);
|
|
533
|
+
}
|
|
534
|
+
if (out === undefined) {
|
|
535
|
+
try {
|
|
536
|
+
out = await runFcMatch(fc);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
throw fcMatchError(err);
|
|
539
|
+
}
|
|
359
540
|
}
|
|
360
541
|
// A sync call may have answered this pattern while the child ran. Its list
|
|
361
542
|
// is the cached one, and candidates memoize their parsed charset, so hand
|
|
@@ -385,6 +566,14 @@ export function matchSortedSync(pattern) {
|
|
|
385
566
|
if (cached) return cached;
|
|
386
567
|
if (unavailable) throw noFontsError(unavailable);
|
|
387
568
|
|
|
569
|
+
// The family's other faces are the next asks (`prewarmFaces`): started
|
|
570
|
+
// now, they run beside this one instead of after it.
|
|
571
|
+
prewarmFaces(pattern);
|
|
572
|
+
const pending = inflight.get(fc);
|
|
573
|
+
if (pending?.base) {
|
|
574
|
+
const out = answerSync(fc, pending);
|
|
575
|
+
if (out !== null) return sortedCache.get(fc) ?? cacheMatches(fc, out);
|
|
576
|
+
}
|
|
388
577
|
let out;
|
|
389
578
|
try {
|
|
390
579
|
out = execFileSync('fc-match', [...fcMatchArgs, fc], fcMatchOpts);
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -63,6 +63,60 @@ function shapingKeyOf(style) {
|
|
|
63
63
|
return `${feats}|${language ?? ''}|${letterSpacing || 0}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* How many shaped words a generation of the memo holds. Two are kept, so a
|
|
68
|
+
* working set up to twice this survives a relayout; what was asked for in
|
|
69
|
+
* neither generation is dropped when the current one fills.
|
|
70
|
+
*/
|
|
71
|
+
const SHAPE_GENERATION = 4000;
|
|
72
|
+
|
|
73
|
+
// The part of a shaping key a style decides, per style object. Built once a
|
|
74
|
+
// span rather than once a word: TextLayout asks for every word of a span with
|
|
75
|
+
// the same object, and the 2D context builds one object per `font` it is
|
|
76
|
+
// given. Neither changes an object after handing it over, so the only field
|
|
77
|
+
// checked again is the resolved face, the one a caller sets after building
|
|
78
|
+
// the rest.
|
|
79
|
+
const shapePrefixes = new WeakMap();
|
|
80
|
+
|
|
81
|
+
/** The key a style's words are memoized under, with the embedding levels
|
|
82
|
+
* they were shaped at: one string per style and level, kept, so a lookup
|
|
83
|
+
* hashes nothing it has not hashed before. */
|
|
84
|
+
function shapeGroupOf(style, levelsKey) {
|
|
85
|
+
const font = style.font;
|
|
86
|
+
let known = shapePrefixes.get(style);
|
|
87
|
+
if (known === undefined || known.font !== font) {
|
|
88
|
+
known = { font, prefix: shapePrefixOf(style), levels: new Map() };
|
|
89
|
+
shapePrefixes.set(style, known);
|
|
90
|
+
}
|
|
91
|
+
// a fragment's levels are one number in the common case, a list only
|
|
92
|
+
// for mixed-direction text; a list is not worth keeping
|
|
93
|
+
if (levelsKey.length > 2) return `${known.prefix}|${levelsKey}`;
|
|
94
|
+
let group = known.levels.get(levelsKey);
|
|
95
|
+
if (group === undefined) {
|
|
96
|
+
group = `${known.prefix}|${levelsKey}`;
|
|
97
|
+
known.levels.set(levelsKey, group);
|
|
98
|
+
}
|
|
99
|
+
return group;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shapePrefixOf(style) {
|
|
103
|
+
const font = style.font;
|
|
104
|
+
// A resolved `font` already carries its coordinates in its key, so the
|
|
105
|
+
// variations and optical-size fragments only earn their keep on the
|
|
106
|
+
// family path — where two points of one axis would otherwise share a
|
|
107
|
+
// shaped run, and the second would be drawn with the first's advances.
|
|
108
|
+
// Features, language and spacing change the glyphs a word shapes to, so
|
|
109
|
+
// they are part of the key on both paths: without them a word shaped once
|
|
110
|
+
// plain answered every later request for it with the plain glyphs, and a
|
|
111
|
+
// `tnum` asked for after that point was silently ignored.
|
|
112
|
+
const shaping = shapingKeyOf(style);
|
|
113
|
+
return font
|
|
114
|
+
? `${font.key}|${style.size}|${style.weight}|${style.style}|${shaping}`
|
|
115
|
+
: `${style.family}|${style.size}|${style.weight}|${style.style}|${variationsKeyOf(
|
|
116
|
+
style.variations
|
|
117
|
+
)}|${opticalKeyOf(style)}|${shaping}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
66
120
|
/** Cache-key fragment for the optical size, `''` when the axis is left alone. */
|
|
67
121
|
function opticalKeyOf(style) {
|
|
68
122
|
return opticalSizeFor(style) ?? '';
|
|
@@ -136,7 +190,11 @@ export default class FontManager {
|
|
|
136
190
|
this._matches = new Map(); // family|weight|style -> Font
|
|
137
191
|
this._fallbacks = new Map(); // family|weight|style -> Map(codepoint -> Font|null)
|
|
138
192
|
this._registered = []; // { font, family (lowercase), weight, italic }
|
|
139
|
-
|
|
193
|
+
// word-level shaping memo, in two generations (`_shapeCached`): each a
|
|
194
|
+
// map from a style's key to the words shaped in it
|
|
195
|
+
this._shapeCache = new Map();
|
|
196
|
+
this._shapeCacheBefore = new Map();
|
|
197
|
+
this._shapeCount = 0;
|
|
140
198
|
}
|
|
141
199
|
|
|
142
200
|
/** the FontSource in effect (explicit, else the process-wide default) */
|
|
@@ -355,53 +413,45 @@ export default class FontManager {
|
|
|
355
413
|
}
|
|
356
414
|
|
|
357
415
|
/**
|
|
358
|
-
* Memoized shaping used by TextLayout and the canvas text path
|
|
359
|
-
*
|
|
360
|
-
* number when uniform for the whole
|
|
361
|
-
* comma-separated per-char levels.
|
|
416
|
+
* Memoized shaping used by TextLayout and the canvas text path, bounded at
|
|
417
|
+
* two generations of `SHAPE_GENERATION` words. `levelsKey` is a compact
|
|
418
|
+
* embedding-levels encoding: a single number when uniform for the whole
|
|
419
|
+
* fragment (the common case), else comma-separated per-char levels.
|
|
362
420
|
*/
|
|
363
421
|
_shapeCached(text, style, levelsKey = '0') {
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this
|
|
384
|
-
this._shapeCache.set(key, shaped);
|
|
385
|
-
return shaped;
|
|
422
|
+
// A hit in this generation is the whole cost of a word a paragraph has
|
|
423
|
+
// shaped before — a relayout at a new width asks for every word again,
|
|
424
|
+
// a hundred thousand of them in a long document — so it is two lookups
|
|
425
|
+
// and nothing built: the style's key is the same string every time
|
|
426
|
+
// (`shapeGroupOf`), and the word is the only thing hashed. What the
|
|
427
|
+
// generation before holds is carried over when asked for; what neither
|
|
428
|
+
// was asked for goes when the generation turns.
|
|
429
|
+
const group = shapeGroupOf(style, levelsKey);
|
|
430
|
+
let words = this._shapeCache.get(group);
|
|
431
|
+
let shaped = words?.get(text);
|
|
432
|
+
if (shaped) return shaped;
|
|
433
|
+
shaped = this._shapeCacheBefore.get(group)?.get(text);
|
|
434
|
+
if (!shaped) {
|
|
435
|
+
let levels;
|
|
436
|
+
if (levelsKey.includes(',')) {
|
|
437
|
+
levels = levelsKey.split(',').map(Number);
|
|
438
|
+
} else {
|
|
439
|
+
levels = new Uint8Array(text.length).fill(Number(levelsKey));
|
|
440
|
+
}
|
|
441
|
+
shaped = shapeText(this, text, style, levels);
|
|
386
442
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
if (this._shapeCount >= SHAPE_GENERATION) {
|
|
444
|
+
this._shapeCacheBefore = this._shapeCache;
|
|
445
|
+
this._shapeCache = new Map();
|
|
446
|
+
this._shapeCount = 0;
|
|
447
|
+
words = undefined;
|
|
392
448
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// a live UI's working set sits at the recent end and survives, where
|
|
397
|
-
// the old wholesale clear() re-shaped everything on screen
|
|
398
|
-
let drop = this._shapeCache.size >> 1;
|
|
399
|
-
for (const k of this._shapeCache.keys()) {
|
|
400
|
-
if (drop-- <= 0) break;
|
|
401
|
-
this._shapeCache.delete(k);
|
|
402
|
-
}
|
|
449
|
+
if (!words) {
|
|
450
|
+
words = new Map();
|
|
451
|
+
this._shapeCache.set(group, words);
|
|
403
452
|
}
|
|
404
|
-
|
|
453
|
+
words.set(text, shaped);
|
|
454
|
+
this._shapeCount++;
|
|
405
455
|
return shaped;
|
|
406
456
|
}
|
|
407
457
|
|
package/lib/text/fontsource.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
matchSorted,
|
|
52
52
|
matchSortedSync,
|
|
53
53
|
noFontsError,
|
|
54
|
-
|
|
54
|
+
prewarmFaces,
|
|
55
55
|
supported
|
|
56
56
|
} from '../fontconfig.js';
|
|
57
57
|
import Font from './font.js';
|
|
@@ -116,13 +116,13 @@ export function parseFamilies(family) {
|
|
|
116
116
|
export class FontconfigFontSource {
|
|
117
117
|
constructor() {
|
|
118
118
|
// Construction is the moment fontconfig is chosen as the lookup path, so
|
|
119
|
-
// the fc-match
|
|
119
|
+
// the fc-match spawns for the pattern every widget default resolves to —
|
|
120
120
|
// sans-serif at regular weight, exactly as FontManager.match normalizes
|
|
121
|
-
// it —
|
|
122
|
-
// layout for ~50ms. Fire-and-forget:
|
|
123
|
-
// constructed where fc-match is
|
|
124
|
-
// is actually rendered.
|
|
125
|
-
|
|
121
|
+
// it — and for its bold and italic start here, asynchronously, instead of
|
|
122
|
+
// stalling the first text layout for ~50ms each. Fire-and-forget:
|
|
123
|
+
// prewarm never rejects, and a source constructed where fc-match is
|
|
124
|
+
// missing stays silent until (unless) text is actually rendered.
|
|
125
|
+
prewarmFaces({ family: 'sans-serif' });
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
matchSorted(pattern) {
|