react-x11 2.2.1 → 2.3.1
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/README.md +5 -3
- package/package.json +12 -5
- package/src/Reconciler.js +76 -11
- package/src/anchor.js +15 -5
- package/src/appcontext.js +27 -7
- package/src/cocoa/app.js +557 -0
- package/src/cocoa/bezels.js +161 -0
- package/src/cocoa/context2d.js +679 -0
- package/src/cocoa/fonts.js +541 -0
- package/src/cocoa/glarea.js +321 -0
- package/src/cocoa/globalmenu.js +149 -0
- package/src/cocoa/keymap.js +86 -0
- package/src/cocoa/native.js +51 -0
- package/src/cocoa/panehost.js +50 -0
- package/src/cocoa/panewindow.js +236 -0
- package/src/cocoa/presenter.js +837 -0
- package/src/cocoa/window.js +383 -0
- package/src/components/Button.js +68 -0
- package/src/components/Checkbox.js +43 -0
- package/src/components/Radio.js +37 -1
- package/src/components/Select.js +73 -28
- package/src/components/Slider.js +59 -0
- package/src/components/Switch.js +52 -0
- package/src/components/native.js +142 -0
- package/src/decorations.js +21 -24
- package/src/editmenu.js +23 -14
- package/src/events.js +11 -2
- package/src/frame/childmain.js +9 -0
- package/src/frame/index.js +162 -2
- package/src/glnodes.js +43 -0
- package/src/globalmenu.js +11 -3
- package/src/index.d.ts +10 -0
- package/src/node.d.ts +19 -6
- package/src/nodes.js +107 -33
- package/src/paintcache.js +15 -4
- package/src/palette.js +14 -0
- package/src/styles.js +5 -0
- package/src/testing/index.d.ts +8 -0
- package/src/types/elements.d.ts +10 -0
- package/src/types/events.d.ts +5 -2
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// The Cocoa text engine: CoreText behind the same `app.fonts` contract ntk's
|
|
2
|
+
// FontManager answers on the X11 backend. The renderer touches exactly this
|
|
3
|
+
// surface (docs/macos.md §"Text: the engine contract"):
|
|
4
|
+
//
|
|
5
|
+
// fonts.layout(spans, base, { maxWidth, align, lineHeight, maxLines,
|
|
6
|
+
// overflow, direction })
|
|
7
|
+
// -> { width, height, lines, draw(ctx, x, y),
|
|
8
|
+
// indexAt(x, y), caretPosition(cp) }
|
|
9
|
+
// fonts.match(family, { weight, style }) -> { metrics(size) }
|
|
10
|
+
//
|
|
11
|
+
// Index spaces, because two meet here: `lines[].start/end` and
|
|
12
|
+
// `runs[].start/end` are UTF-16 code units (what `rangeBands` in nodes.js
|
|
13
|
+
// compares against), while `caretPosition()` takes and `indexAt()` returns
|
|
14
|
+
// code points (what the selection and caret code speak). CoreText itself is
|
|
15
|
+
// UTF-16 end to end; the code-point conversion happens at this boundary and
|
|
16
|
+
// nowhere else.
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { inflateSync } from 'node:zlib';
|
|
19
|
+
|
|
20
|
+
import { cssColorStraight, Font } from 'ntk';
|
|
21
|
+
|
|
22
|
+
import { loadNative } from './native.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* WOFF v1 -> sfnt: the same table directory, zlib per table. ~40 lines of
|
|
26
|
+
* unwrapping is what lets every `.woff` an app already ships keep working
|
|
27
|
+
* on this backend without a converter in the build.
|
|
28
|
+
*/
|
|
29
|
+
function woffToSfnt(woff) {
|
|
30
|
+
const numTables = woff.readUInt16BE(12);
|
|
31
|
+
const flavor = woff.readUInt32BE(4);
|
|
32
|
+
const entries = [];
|
|
33
|
+
for (let i = 0; i < numTables; i++) {
|
|
34
|
+
const at = 44 + i * 20;
|
|
35
|
+
const compLength = woff.readUInt32BE(at + 8);
|
|
36
|
+
const origLength = woff.readUInt32BE(at + 12);
|
|
37
|
+
const offset = woff.readUInt32BE(at + 4);
|
|
38
|
+
const compressed = woff.subarray(offset, offset + compLength);
|
|
39
|
+
entries.push({
|
|
40
|
+
tag: woff.readUInt32BE(at),
|
|
41
|
+
checksum: woff.readUInt32BE(at + 16),
|
|
42
|
+
data: compLength === origLength ? compressed : inflateSync(compressed),
|
|
43
|
+
origLength,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const headerSize = 12 + numTables * 16;
|
|
47
|
+
let total = headerSize;
|
|
48
|
+
for (const entry of entries) total += (entry.origLength + 3) & ~3;
|
|
49
|
+
const out = Buffer.alloc(total);
|
|
50
|
+
out.writeUInt32BE(flavor, 0);
|
|
51
|
+
out.writeUInt16BE(numTables, 4);
|
|
52
|
+
const pow2 = 1 << Math.floor(Math.log2(numTables));
|
|
53
|
+
out.writeUInt16BE(pow2 * 16, 6); // searchRange
|
|
54
|
+
out.writeUInt16BE(Math.floor(Math.log2(numTables)), 8); // entrySelector
|
|
55
|
+
out.writeUInt16BE(numTables * 16 - pow2 * 16, 10); // rangeShift
|
|
56
|
+
let dataAt = headerSize;
|
|
57
|
+
entries.forEach((entry, i) => {
|
|
58
|
+
const dir = 12 + i * 16;
|
|
59
|
+
out.writeUInt32BE(entry.tag, dir);
|
|
60
|
+
out.writeUInt32BE(entry.checksum, dir + 4);
|
|
61
|
+
out.writeUInt32BE(dataAt, dir + 8);
|
|
62
|
+
out.writeUInt32BE(entry.origLength, dir + 12);
|
|
63
|
+
entry.data.copy(out, dataAt);
|
|
64
|
+
dataAt += (entry.origLength + 3) & ~3;
|
|
65
|
+
});
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** family list string -> array: 'Inter, "SF Pro", sans-serif' */
|
|
70
|
+
function familyList(family) {
|
|
71
|
+
if (Array.isArray(family)) return family;
|
|
72
|
+
return String(family ?? 'sans-serif')
|
|
73
|
+
.split(',')
|
|
74
|
+
.map((f) => f.trim().replace(/^["']|["']$/g, ''))
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function numericWeight(weight) {
|
|
79
|
+
if (typeof weight === 'number') return weight;
|
|
80
|
+
if (weight === 'bold') return 700;
|
|
81
|
+
if (weight === 'medium') return 500;
|
|
82
|
+
if (weight === 'semibold') return 600;
|
|
83
|
+
if (weight === 'light') return 300;
|
|
84
|
+
return 400;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isItalic = (style) => style === 'italic' || style === 'oblique';
|
|
88
|
+
|
|
89
|
+
function parseColor(color) {
|
|
90
|
+
const parsed = cssColorStraight(color ?? '#000');
|
|
91
|
+
return parsed ?? [0, 0, 0, 1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** UTF-16 offset of each code point boundary, plus the end. */
|
|
95
|
+
function codeUnitOffsets(text) {
|
|
96
|
+
const offsets = [0];
|
|
97
|
+
for (const ch of text) offsets.push(offsets[offsets.length - 1] + ch.length);
|
|
98
|
+
return offsets;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class CocoaTextLayout {
|
|
102
|
+
constructor(native, raw, text) {
|
|
103
|
+
this._native = native;
|
|
104
|
+
this._handle = raw.handle;
|
|
105
|
+
this._text = text;
|
|
106
|
+
this._cpToCu = codeUnitOffsets(text);
|
|
107
|
+
this.width = raw.width;
|
|
108
|
+
this.height = raw.height;
|
|
109
|
+
this.lines = raw.lines;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_cuOf(cp) {
|
|
113
|
+
const t = this._cpToCu;
|
|
114
|
+
return t[Math.max(0, Math.min(cp, t.length - 1))];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_cpOf(cu) {
|
|
118
|
+
const t = this._cpToCu;
|
|
119
|
+
// t is sorted; layouts are short, a linear walk is fine
|
|
120
|
+
for (let i = 0; i < t.length; i++) if (t[i] >= cu) return i;
|
|
121
|
+
return t.length - 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
draw(ctx, x, y) {
|
|
125
|
+
ctx._drawLayout(this, x, y);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Code point boundary nearest the point, in layout coordinates. */
|
|
129
|
+
indexAt(x, y) {
|
|
130
|
+
return this._cpOf(this._native.layoutIndexAt(this._handle, x, y));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Caret rect for a code-point index: { x, y, height }. */
|
|
134
|
+
caretPosition(cp) {
|
|
135
|
+
return this._native.layoutCaret(this._handle, this._cuOf(cp));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ALIGN_FLUSH = { left: 0, center: 0.5, right: 1 };
|
|
140
|
+
|
|
141
|
+
function flushFor(align, direction) {
|
|
142
|
+
if (align === 'start' || align === undefined) {
|
|
143
|
+
return direction === 'rtl' ? 1 : 0;
|
|
144
|
+
}
|
|
145
|
+
if (align === 'end') return direction === 'rtl' ? 0 : 1;
|
|
146
|
+
return ALIGN_FLUSH[align] ?? 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const GENERIC_FAMILIES = new Set([
|
|
150
|
+
'sans-serif',
|
|
151
|
+
'serif',
|
|
152
|
+
'monospace',
|
|
153
|
+
'cursive',
|
|
154
|
+
'system-ui',
|
|
155
|
+
'ui-sans-serif',
|
|
156
|
+
'ui-monospace',
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
export class CocoaFontManager {
|
|
160
|
+
constructor() {
|
|
161
|
+
this._native = loadNative();
|
|
162
|
+
this._fonts = new Map(); // family|weight|italic|size -> handle
|
|
163
|
+
this._faces = new Map(); // family|weight|italic -> face wrapper
|
|
164
|
+
this._registered = new Map(); // lowercase family -> [{cg, weight, italic}]
|
|
165
|
+
this._byKey = new Map(); // ntk Font key -> { cg } | { ps }
|
|
166
|
+
this._sized = new Map(); // face key|size|variations -> CTFont handle
|
|
167
|
+
this._layouts = new Map(); // layout signature -> CocoaTextLayout (LRU)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `openFont()`'s engine seam (src/fonts.js `openThrough`): parse the face
|
|
172
|
+
* with fontkit — the object applications read metrics and axes off — and
|
|
173
|
+
* feed the same bytes to CoreText so the face RENDERS here too, by
|
|
174
|
+
* family name or as a `span.font`. An opened file is a face the user
|
|
175
|
+
* chose to look at; a backend where it measures but draws as the system
|
|
176
|
+
* font answers a different question than the one asked.
|
|
177
|
+
*/
|
|
178
|
+
_open(candidate) {
|
|
179
|
+
const { key, path, data, postscriptName } = candidate;
|
|
180
|
+
const font =
|
|
181
|
+
path !== undefined
|
|
182
|
+
? Font.loadSync(path, postscriptName)
|
|
183
|
+
: Font.fromData(data, candidate);
|
|
184
|
+
try {
|
|
185
|
+
let bytes =
|
|
186
|
+
path !== undefined
|
|
187
|
+
? readFileSync(path)
|
|
188
|
+
: Buffer.isBuffer(data)
|
|
189
|
+
? data
|
|
190
|
+
: Buffer.from(data.buffer ?? data);
|
|
191
|
+
const magic = bytes.length >= 4 ? bytes.readUInt32BE(0) : 0;
|
|
192
|
+
if (magic === 0x774f4646) bytes = woffToSfnt(bytes);
|
|
193
|
+
if (magic !== 0x774f4632 /* woff2 stays fontkit-only */) {
|
|
194
|
+
const info = this._native.fontFromData(bytes);
|
|
195
|
+
// a .ttc's data handle is its FIRST face; when a specific face was
|
|
196
|
+
// asked for and this is not it, leave rendering to the PostScript
|
|
197
|
+
// route (installed collections resolve there anyway)
|
|
198
|
+
const face = postscriptName ?? font.postscriptName;
|
|
199
|
+
if (info && (!face || info.postScriptName === face)) {
|
|
200
|
+
this._byKey.set(font.key ?? key, { cg: info.cg });
|
|
201
|
+
const family = (info.familyName ?? '').toLowerCase();
|
|
202
|
+
if (family) {
|
|
203
|
+
const faces = this._registered.get(family) ?? [];
|
|
204
|
+
faces.push({
|
|
205
|
+
cg: info.cg,
|
|
206
|
+
weight: numericWeight(info.weight),
|
|
207
|
+
italic: Boolean(info.italic),
|
|
208
|
+
});
|
|
209
|
+
this._registered.set(family, faces);
|
|
210
|
+
}
|
|
211
|
+
this._fonts.clear();
|
|
212
|
+
this._faces.clear();
|
|
213
|
+
this._sized.clear();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// the fontkit face still measures; rendering falls back by family
|
|
218
|
+
}
|
|
219
|
+
return font;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A CTFont for an ntk `Font` face handed over as `span.font` — the way
|
|
224
|
+
* the fonts app renders the face it opened, and the way `loadFont`'s
|
|
225
|
+
* faces reach glyphs. Resolution: an installed face by its exact
|
|
226
|
+
* PostScript name; otherwise the file's own bytes (unwrapping `.woff`),
|
|
227
|
+
* cached per face key.
|
|
228
|
+
*/
|
|
229
|
+
_faceFont(face, size, variations) {
|
|
230
|
+
const key = face.key ?? `${face.path ?? ''}#${face.postscriptName ?? ''}`;
|
|
231
|
+
let entry = this._byKey.get(key);
|
|
232
|
+
if (!entry) {
|
|
233
|
+
entry = {};
|
|
234
|
+
const ps = face.postscriptName;
|
|
235
|
+
if (ps && this._native.fontByPostScriptName(ps, 12)) {
|
|
236
|
+
entry.ps = ps;
|
|
237
|
+
} else {
|
|
238
|
+
let data = null;
|
|
239
|
+
if (face.path) {
|
|
240
|
+
try {
|
|
241
|
+
data = readFileSync(face.path);
|
|
242
|
+
} catch {
|
|
243
|
+
data = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!data && face.fk?.stream?.buffer) {
|
|
247
|
+
data = Buffer.from(face.fk.stream.buffer);
|
|
248
|
+
}
|
|
249
|
+
if (data) {
|
|
250
|
+
if (data.length >= 4 && data.readUInt32BE(0) === 0x774f4646) {
|
|
251
|
+
data = woffToSfnt(data);
|
|
252
|
+
}
|
|
253
|
+
const info = this._native.fontFromData(data);
|
|
254
|
+
if (info) entry.cg = info.cg;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this._byKey.set(key, entry);
|
|
258
|
+
}
|
|
259
|
+
const sizedKey = `${key}|${size}|${variations ? JSON.stringify(variations) : ''}`;
|
|
260
|
+
let handle = this._sized.get(sizedKey);
|
|
261
|
+
if (handle) return handle;
|
|
262
|
+
if (entry.ps) handle = this._native.fontByPostScriptName(entry.ps, size);
|
|
263
|
+
else if (entry.cg) handle = this._native.cgFontWithSize(entry.cg, size);
|
|
264
|
+
if (!handle) return null;
|
|
265
|
+
handle = this._withVariations(handle, variations);
|
|
266
|
+
this._sized.set(sizedKey, handle);
|
|
267
|
+
return handle;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_withVariations(handle, variations) {
|
|
271
|
+
if (
|
|
272
|
+
variations &&
|
|
273
|
+
typeof variations === 'object' &&
|
|
274
|
+
Object.keys(variations).length > 0
|
|
275
|
+
) {
|
|
276
|
+
return this._native.fontApplyVariations(handle, variations);
|
|
277
|
+
}
|
|
278
|
+
return handle;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The catalogue seam ntk exposes as `fonts.source` — what the fonts app
|
|
283
|
+
* browses. On X that is fontconfig; here it is CoreText's collection.
|
|
284
|
+
* Pattern syntax: the family, with fontconfig's `:modifiers` tolerated
|
|
285
|
+
* and ignored (`Menlo:bold`, `:lang=ru` — the part after the colon is
|
|
286
|
+
* fontconfig vocabulary CoreText does not speak).
|
|
287
|
+
*/
|
|
288
|
+
get source() {
|
|
289
|
+
return (this._source ??= {
|
|
290
|
+
matchSortedAsync: async ({ family } = {}) => {
|
|
291
|
+
const pattern = String(family ?? '').trim();
|
|
292
|
+
let name = pattern.split(':')[0].trim();
|
|
293
|
+
if (name && GENERIC_FAMILIES.has(name.toLowerCase())) {
|
|
294
|
+
// Rendering resolves generics to the system face, but its family
|
|
295
|
+
// is a hidden name (`.AppleSystemUIFont`) the catalogue cannot
|
|
296
|
+
// enumerate — a browser wants the visible families instead.
|
|
297
|
+
name =
|
|
298
|
+
{
|
|
299
|
+
serif: 'Times New Roman',
|
|
300
|
+
monospace: 'Menlo',
|
|
301
|
+
'ui-monospace': 'Menlo',
|
|
302
|
+
cursive: 'Snell Roundhand',
|
|
303
|
+
}[name.toLowerCase()] ?? 'Helvetica Neue';
|
|
304
|
+
}
|
|
305
|
+
const rows = this._native.listFonts(
|
|
306
|
+
name ? { family: name } : { limit: 400 },
|
|
307
|
+
);
|
|
308
|
+
return rows
|
|
309
|
+
.filter((row) => row.path)
|
|
310
|
+
.map((row) => ({
|
|
311
|
+
path: row.path,
|
|
312
|
+
postscriptName: row.postScriptName,
|
|
313
|
+
family: row.familyName,
|
|
314
|
+
style: row.styleName,
|
|
315
|
+
charset: '',
|
|
316
|
+
}));
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_font(family, weight, italic, size) {
|
|
322
|
+
const key = `${family}|${weight}|${italic}|${size}`;
|
|
323
|
+
let handle = this._fonts.get(key);
|
|
324
|
+
if (!handle) {
|
|
325
|
+
// Faces the app loaded win over system matching for their family —
|
|
326
|
+
// an app that ships a font means the one it ships (src/fonts.js).
|
|
327
|
+
for (const name of familyList(family)) {
|
|
328
|
+
const faces = this._registered.get(name.toLowerCase());
|
|
329
|
+
if (!faces?.length) continue;
|
|
330
|
+
let best = null;
|
|
331
|
+
let bestCost = Infinity;
|
|
332
|
+
for (const face of faces) {
|
|
333
|
+
const cost =
|
|
334
|
+
Math.abs(face.weight - weight) +
|
|
335
|
+
(face.italic === italic ? 0 : 1000);
|
|
336
|
+
if (cost < bestCost) {
|
|
337
|
+
bestCost = cost;
|
|
338
|
+
best = face;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
handle = this._native.cgFontWithSize(best.cg, size);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
handle ??= this._native.matchFont({
|
|
345
|
+
families: familyList(family),
|
|
346
|
+
size,
|
|
347
|
+
weight,
|
|
348
|
+
italic,
|
|
349
|
+
});
|
|
350
|
+
this._fonts.set(key, handle);
|
|
351
|
+
}
|
|
352
|
+
return handle;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* A face by family/weight/style — what `textBoxTrim` reads `capHeight`
|
|
357
|
+
* from. The face defers size, like ntk's: `metrics(size)` answers for a
|
|
358
|
+
* concrete pixel size.
|
|
359
|
+
*/
|
|
360
|
+
match(family, { weight, style } = {}) {
|
|
361
|
+
const w = numericWeight(weight);
|
|
362
|
+
const italic = isItalic(style);
|
|
363
|
+
const key = `${family}|${w}|${italic}`;
|
|
364
|
+
let face = this._faces.get(key);
|
|
365
|
+
if (!face) {
|
|
366
|
+
const manager = this;
|
|
367
|
+
face = {
|
|
368
|
+
metrics(size) {
|
|
369
|
+
return manager._native.fontMetrics(
|
|
370
|
+
manager._font(family, w, italic, size ?? 14),
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
hasGlyph(char) {
|
|
374
|
+
return manager._native.fontHasGlyph(
|
|
375
|
+
manager._font(family, w, italic, 14),
|
|
376
|
+
String(char),
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
this._faces.set(key, face);
|
|
381
|
+
}
|
|
382
|
+
return face;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* `loadFont()`'s engine half: register the face with CoreText so the
|
|
387
|
+
* process can match it by family name (with real weight/italic traits —
|
|
388
|
+
* they are read off the file, so four weights of one family resolve the
|
|
389
|
+
* way `fontWeight` expects). `source` is a path or the font bytes, the
|
|
390
|
+
* two shapes `src/fonts.js` hands over.
|
|
391
|
+
*
|
|
392
|
+
* Container rule: CoreText reads sfnt (ttf/otf/ttc). A `.woff` is those
|
|
393
|
+
* same tables zlib-wrapped and is unwrapped here; a `.woff2` is a
|
|
394
|
+
* different compression CoreText cannot take and this engine does not
|
|
395
|
+
* rebuild — the error says what to load instead.
|
|
396
|
+
*/
|
|
397
|
+
load(source, opts = {}) {
|
|
398
|
+
let data;
|
|
399
|
+
if (typeof source === 'string') {
|
|
400
|
+
data = readFileSync(source);
|
|
401
|
+
} else if (Buffer.isBuffer(source)) {
|
|
402
|
+
data = source;
|
|
403
|
+
} else if (source instanceof Uint8Array) {
|
|
404
|
+
data = Buffer.from(source.buffer, source.byteOffset, source.byteLength);
|
|
405
|
+
} else {
|
|
406
|
+
throw new Error(
|
|
407
|
+
'react-x11: loadFont — expected a file path or font bytes, got ' +
|
|
408
|
+
typeof source,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const magic = data.length >= 4 ? data.readUInt32BE(0) : 0;
|
|
412
|
+
if (magic === 0x774f4632 /* wOF2 */) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
'react-x11: loadFont — this is a .woff2 file, and the macOS font ' +
|
|
415
|
+
'loader (CoreText) does not read that container. Load the .ttf, ' +
|
|
416
|
+
'.otf or .woff of the same face instead — @fontsource packages ' +
|
|
417
|
+
'ship a .woff beside every .woff2.',
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (magic === 0x774f4646 /* wOFF */) data = woffToSfnt(data);
|
|
421
|
+
const info = this._native.fontFromData(data);
|
|
422
|
+
if (!info) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
'react-x11: loadFont — CoreText could not read the font data' +
|
|
425
|
+
(typeof source === 'string' ? ` in ${source}` : '') +
|
|
426
|
+
' (expected .ttf, .otf, .ttc or .woff).',
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
// The process's own handle to the face, kept in a registry family
|
|
430
|
+
// matching consults FIRST — CoreText registration of in-memory data is
|
|
431
|
+
// best-effort at most, and rendering must not depend on it.
|
|
432
|
+
const family = (opts.family ?? info.familyName ?? '').toLowerCase();
|
|
433
|
+
if (family) {
|
|
434
|
+
const faces = this._registered.get(family) ?? [];
|
|
435
|
+
faces.push({
|
|
436
|
+
cg: info.cg,
|
|
437
|
+
weight: numericWeight(opts.weight ?? info.weight),
|
|
438
|
+
italic: opts.style ? isItalic(opts.style) : Boolean(info.italic),
|
|
439
|
+
});
|
|
440
|
+
this._registered.set(family, faces);
|
|
441
|
+
}
|
|
442
|
+
// matches resolved before this face existed are stale now
|
|
443
|
+
this._fonts.clear();
|
|
444
|
+
this._faces.clear();
|
|
445
|
+
this._sized.clear();
|
|
446
|
+
this._layouts.clear();
|
|
447
|
+
// `null` on purpose: src/fonts.js keeps the fontkit face it already
|
|
448
|
+
// opened as the handle, which is the one whose metrics apps can read;
|
|
449
|
+
// the registry above is what rendering resolves against.
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
layout(spans, base, options = {}) {
|
|
454
|
+
// ntk's contract: a bare string or a single span are one-span
|
|
455
|
+
// paragraphs. TextInputNode's value layout passes the string form, and
|
|
456
|
+
// iterating a string as spans was a caret pinned to x = 0.
|
|
457
|
+
if (typeof spans === 'string') spans = [{ text: spans }];
|
|
458
|
+
else if (!Array.isArray(spans)) spans = [spans];
|
|
459
|
+
// Memoized: an immediate-mode caller (the fonts app's specimen canvas,
|
|
460
|
+
// a hover pass) lays the same paragraph out every repaint, and a
|
|
461
|
+
// CTFramesetter per pointer move is what sluggish feels like. The key
|
|
462
|
+
// is everything shaping reads; ~64 entries covers a screenful.
|
|
463
|
+
const signature = JSON.stringify([
|
|
464
|
+
spans.map((sp) => [
|
|
465
|
+
sp.text,
|
|
466
|
+
sp.family ?? base.family,
|
|
467
|
+
sp.size ?? base.size,
|
|
468
|
+
sp.weight ?? base.weight,
|
|
469
|
+
sp.style ?? base.style,
|
|
470
|
+
sp.color ?? base.color ?? null,
|
|
471
|
+
sp.variations ?? base.variations ?? null,
|
|
472
|
+
(sp.font ?? base.font)?.key ?? null,
|
|
473
|
+
]),
|
|
474
|
+
options.maxWidth,
|
|
475
|
+
options.align,
|
|
476
|
+
options.lineHeight,
|
|
477
|
+
options.maxLines,
|
|
478
|
+
options.overflow,
|
|
479
|
+
options.direction,
|
|
480
|
+
]);
|
|
481
|
+
const hit = this._layouts.get(signature);
|
|
482
|
+
if (hit) {
|
|
483
|
+
// refresh LRU position
|
|
484
|
+
this._layouts.delete(signature);
|
|
485
|
+
this._layouts.set(signature, hit);
|
|
486
|
+
return hit;
|
|
487
|
+
}
|
|
488
|
+
const { maxWidth, align, lineHeight, maxLines, overflow, direction } =
|
|
489
|
+
options;
|
|
490
|
+
const nativeSpans = [];
|
|
491
|
+
let text = '';
|
|
492
|
+
let contextInk = false;
|
|
493
|
+
for (const span of spans) {
|
|
494
|
+
const t = String(span.text ?? '');
|
|
495
|
+
if (!t) continue;
|
|
496
|
+
text += t;
|
|
497
|
+
const size = span.size ?? base.size ?? 14;
|
|
498
|
+
const variations = span.variations ?? base.variations;
|
|
499
|
+
// A span may carry the face itself — an ntk Font from openFont(),
|
|
500
|
+
// which is how the fonts app renders exactly the file it opened.
|
|
501
|
+
const face = span.font ?? base.font;
|
|
502
|
+
let handle =
|
|
503
|
+
face && (face.postscriptName || face.path || face.fk)
|
|
504
|
+
? this._faceFont(face, size, variations)
|
|
505
|
+
: null;
|
|
506
|
+
handle ??= this._withVariations(
|
|
507
|
+
this._font(
|
|
508
|
+
span.family ?? base.family,
|
|
509
|
+
numericWeight(span.weight ?? base.weight),
|
|
510
|
+
isItalic(span.style ?? base.style),
|
|
511
|
+
size,
|
|
512
|
+
),
|
|
513
|
+
variations,
|
|
514
|
+
);
|
|
515
|
+
const color = span.color ?? base.color;
|
|
516
|
+
if (color == null) contextInk = true;
|
|
517
|
+
nativeSpans.push({
|
|
518
|
+
text: t,
|
|
519
|
+
font: handle,
|
|
520
|
+
...(color == null ? {} : { color: parseColor(color) }),
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const raw = this._native.createLayout({
|
|
524
|
+
spans: nativeSpans,
|
|
525
|
+
maxWidth:
|
|
526
|
+
Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : undefined,
|
|
527
|
+
align: flushFor(align, direction),
|
|
528
|
+
lineHeight: typeof lineHeight === 'number' ? lineHeight : undefined,
|
|
529
|
+
maxLines: Number.isFinite(maxLines) ? maxLines : undefined,
|
|
530
|
+
ellipsis: overflow === 'ellipsis',
|
|
531
|
+
rtl: direction === 'rtl',
|
|
532
|
+
});
|
|
533
|
+
const layout = new CocoaTextLayout(this._native, raw, text);
|
|
534
|
+
layout._contextInk = contextInk;
|
|
535
|
+
this._layouts.set(signature, layout);
|
|
536
|
+
if (this._layouts.size > 64) {
|
|
537
|
+
this._layouts.delete(this._layouts.keys().next().value);
|
|
538
|
+
}
|
|
539
|
+
return layout;
|
|
540
|
+
}
|
|
541
|
+
}
|