react-x11 2.16.0 → 2.17.0
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 +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/acceleratorhooks.js +40 -6
- package/src/anchor.js +20 -2
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +204 -6
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +2 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +12 -1
- package/src/idle.js +59 -1
- package/src/index.d.ts +51 -1
- package/src/index.js +30 -3
- package/src/keysymchars.js +47 -0
- package/src/keysyms.d.ts +19 -1
- package/src/keysyms.js +107 -8
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/screens.js +159 -24
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +21 -0
- package/src/types/filedialog.d.ts +3 -1
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/wayland/xkb.js +170 -59
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +128 -20
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// The Windows text engine: DirectWrite behind the same `app.fonts` contract
|
|
2
|
+
// ntk's FontManager answers on X11 and CoreText answers on macOS. The renderer
|
|
3
|
+
// touches exactly this surface (docs/windows.md §"Text"):
|
|
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 }) -> face
|
|
10
|
+
// fonts.fallbackFor(codepoint, family, { weight, style }) -> face | null
|
|
11
|
+
//
|
|
12
|
+
// Index spaces, because two meet here, exactly as they do in the Cocoa engine:
|
|
13
|
+
// `lines[].start/end` are UTF-16 code units — what `rangeBands` in
|
|
14
|
+
// nodes/text.js compares against — while `caretPosition()` takes and
|
|
15
|
+
// `indexAt()` returns code points, which is what the selection and the caret
|
|
16
|
+
// speak. DirectWrite is UTF-16 end to end, like CoreText, so the conversion
|
|
17
|
+
// happens at this boundary and nowhere else.
|
|
18
|
+
import { cssColorStraight } from 'ntk';
|
|
19
|
+
|
|
20
|
+
// REACT_X11_WIN32_DEBUG=1 reports the size each paragraph is shaped at, which
|
|
21
|
+
// is how to tell a scale that never reached the text engine from one that did.
|
|
22
|
+
const DEBUG = process.env.REACT_X11_WIN32_DEBUG === '1';
|
|
23
|
+
|
|
24
|
+
// A span's vocabulary is **ntk's TextLayout's, not CSS's**: `family`, `size`,
|
|
25
|
+
// `weight`, `style` — not `fontFamily`, `fontSize` and the rest. That is what
|
|
26
|
+
// `collectSpans` and `resolvedTextStyle` in nodes/text.js produce, and what
|
|
27
|
+
// the CoreText engine reads.
|
|
28
|
+
//
|
|
29
|
+
// Getting it wrong is silent and looks like something else entirely: every
|
|
30
|
+
// paragraph falls through to this default, so the whole app renders at one
|
|
31
|
+
// size whatever it asked for, and on a 150% display it reads as "the scale
|
|
32
|
+
// never reached the text engine" when the scale was never the problem. 14 is
|
|
33
|
+
// the same floor the Cocoa engine uses.
|
|
34
|
+
const DEFAULT_SIZE = 14;
|
|
35
|
+
|
|
36
|
+
// The size a coverage question is asked at. A cmap does not depend on one, so
|
|
37
|
+
// every `hasGlyph`/`glyphIdFor` shares a single handle per face rather than
|
|
38
|
+
// resolving one per size the caller happens to be drawing at.
|
|
39
|
+
const PROBE_SIZE = 16;
|
|
40
|
+
|
|
41
|
+
/** The CSS generics, which name no face on their own. */
|
|
42
|
+
const GENERIC_FAMILIES = new Set([
|
|
43
|
+
'sans-serif',
|
|
44
|
+
'serif',
|
|
45
|
+
'monospace',
|
|
46
|
+
'cursive',
|
|
47
|
+
'system-ui',
|
|
48
|
+
'ui-sans-serif',
|
|
49
|
+
'ui-monospace',
|
|
50
|
+
]);
|
|
51
|
+
function sizeOf(value) {
|
|
52
|
+
return typeof value === 'number' && value > 0 ? value : DEFAULT_SIZE;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// DirectWrite takes a numeric weight; CSS lets a style say the word.
|
|
56
|
+
function weightOf(value) {
|
|
57
|
+
if (typeof value === 'number') return value;
|
|
58
|
+
if (value === 'bold') return 700;
|
|
59
|
+
if (value === 'bolder') return 800;
|
|
60
|
+
if (value === 'lighter') return 300;
|
|
61
|
+
const parsed = Number(value);
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : 400;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A font stack resolves to the first family this machine actually has, which
|
|
66
|
+
// is the job fontconfig does on X11 and the system collection does here. The
|
|
67
|
+
// generic names are left for the bridge, which knows what Segoe UI is.
|
|
68
|
+
const GENERIC = new Set([
|
|
69
|
+
'sans-serif',
|
|
70
|
+
'serif',
|
|
71
|
+
'monospace',
|
|
72
|
+
'system-ui',
|
|
73
|
+
'cursive',
|
|
74
|
+
]);
|
|
75
|
+
function familyOf(native, stack) {
|
|
76
|
+
if (!stack) return 'sans-serif';
|
|
77
|
+
for (const raw of String(stack).split(',')) {
|
|
78
|
+
const name = raw.trim().replace(/^["']|["']$/g, '');
|
|
79
|
+
if (!name) continue;
|
|
80
|
+
if (GENERIC.has(name)) return name;
|
|
81
|
+
if (native.fontExists(name)) return name;
|
|
82
|
+
}
|
|
83
|
+
return 'sans-serif';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function colorOf(value) {
|
|
87
|
+
if (!value) return null;
|
|
88
|
+
try {
|
|
89
|
+
const [r, g, b, a] = cssColorStraight(value);
|
|
90
|
+
return { r, g, b, a };
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Code-unit offset of each code point, so the two index spaces can be
|
|
97
|
+
* converted without walking the string twice per query. */
|
|
98
|
+
function codeUnitOffsets(text) {
|
|
99
|
+
const offsets = [];
|
|
100
|
+
for (let i = 0; i < text.length;) {
|
|
101
|
+
offsets.push(i);
|
|
102
|
+
i += text.codePointAt(i) > 0xffff ? 2 : 1;
|
|
103
|
+
}
|
|
104
|
+
offsets.push(text.length);
|
|
105
|
+
return offsets;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class Win32TextLayout {
|
|
109
|
+
constructor(manager, handle, text, metrics) {
|
|
110
|
+
this._manager = manager;
|
|
111
|
+
this._native = manager._native;
|
|
112
|
+
// BackendContext2D reads this when it draws a layout, which is what makes
|
|
113
|
+
// the name part of the bridge contract rather than ours.
|
|
114
|
+
this._handle = handle;
|
|
115
|
+
this._text = text;
|
|
116
|
+
this._metrics = metrics;
|
|
117
|
+
this._offsets = null;
|
|
118
|
+
// Tells BackendContext2D that the ink comes from the context's own fill
|
|
119
|
+
// rather than from inside the layout — so a <text> whose colour is a
|
|
120
|
+
// gradient reaches drawLayoutGradient.
|
|
121
|
+
this._contextInk = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get width() {
|
|
125
|
+
return this._metrics.width;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get height() {
|
|
129
|
+
return this._metrics.height;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get lines() {
|
|
133
|
+
return this._metrics.lines;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The min-content width, which yoga's floors pass asks for with a width
|
|
137
|
+
* offer of zero. DirectWrite answers it natively (`DetermineMinWidth`),
|
|
138
|
+
* where CoreText cannot and the Cocoa engine breaks at `linebreak`'s
|
|
139
|
+
* opportunities itself. */
|
|
140
|
+
get minWidth() {
|
|
141
|
+
return this._metrics.minWidth;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
draw(ctx, x, y) {
|
|
145
|
+
ctx._drawLayout(this, x, y);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
indexAt(x, y) {
|
|
149
|
+
const unit = this._native.layoutIndexAt(this._handle, x, y);
|
|
150
|
+
return this._cpOf(unit);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
caretPosition(cp) {
|
|
154
|
+
return this._native.layoutCaret(this._handle, this._cuOf(cp));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_cuOf(cp) {
|
|
158
|
+
this._offsets ??= codeUnitOffsets(this._text);
|
|
159
|
+
const at = Math.max(0, Math.min(this._offsets.length - 1, cp));
|
|
160
|
+
return this._offsets[at];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_cpOf(unit) {
|
|
164
|
+
this._offsets ??= codeUnitOffsets(this._text);
|
|
165
|
+
// The last offset not past `unit` — a binary search would be the same
|
|
166
|
+
// answer, and paragraphs here are short enough that it would not pay.
|
|
167
|
+
let cp = 0;
|
|
168
|
+
while (cp + 1 < this._offsets.length && this._offsets[cp + 1] <= unit) cp++;
|
|
169
|
+
return cp;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
destroy() {
|
|
173
|
+
if (this._handle == null) return;
|
|
174
|
+
this._native.layoutRelease(this._handle);
|
|
175
|
+
this._handle = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A face, as far as a renderer that positions glyphs itself reads one.
|
|
180
|
+
* Size is deferred the way ntk's is. */
|
|
181
|
+
class Win32Face {
|
|
182
|
+
constructor(manager, family, { weight = 400, style = 'normal' } = {}) {
|
|
183
|
+
this._manager = manager;
|
|
184
|
+
this.family = family;
|
|
185
|
+
this.weight = weightOf(weight);
|
|
186
|
+
this.style = style;
|
|
187
|
+
this.italic = style === 'italic' || style === 'oblique';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
metrics(size) {
|
|
191
|
+
return this._manager._native.fontMetrics(
|
|
192
|
+
this.family,
|
|
193
|
+
size,
|
|
194
|
+
this.weight,
|
|
195
|
+
this.italic,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The bridge's handle for this face at a pixel size — a resolved
|
|
201
|
+
* IDWriteFontFace and the em size to draw it at, which is the pair
|
|
202
|
+
* DirectWrite's DrawGlyphRun takes and the pair CoreText folds into one
|
|
203
|
+
* CTFont. Cached on the manager, so the same face at the same size is one
|
|
204
|
+
* object across a frame and `drawGlyphs` can batch by it.
|
|
205
|
+
*/
|
|
206
|
+
_handle(size) {
|
|
207
|
+
return this._manager._handleFor(this, size);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Coverage. A code point (ntk's contract) or a string — the two forms
|
|
212
|
+
* `hasGlyph` has been asked in.
|
|
213
|
+
*/
|
|
214
|
+
hasGlyph(codepoint) {
|
|
215
|
+
if (typeof codepoint === 'number')
|
|
216
|
+
return this.glyphIdFor(codepoint) !== null;
|
|
217
|
+
const handle = this._handle(PROBE_SIZE);
|
|
218
|
+
if (!handle) return false;
|
|
219
|
+
return this._manager._native.fontHasGlyph(handle, String(codepoint));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Glyph id for a code point, or `null` when this face does not map it —
|
|
224
|
+
* the lookup twin of `hasGlyph` (ntk#254). A cmap lookup only: no shaping,
|
|
225
|
+
* so text that needs ligatures or marks goes through the layout path, and
|
|
226
|
+
* `hasGlyphRuns` on the renderer's side is the gate that decides.
|
|
227
|
+
*
|
|
228
|
+
* The size does not matter to a cmap, so this asks at one fixed size and
|
|
229
|
+
* the answers are shared rather than looked up per size.
|
|
230
|
+
*/
|
|
231
|
+
glyphIdFor(codepoint) {
|
|
232
|
+
if (typeof codepoint !== 'number') return null;
|
|
233
|
+
const handle = this._handle(PROBE_SIZE);
|
|
234
|
+
if (!handle) return null;
|
|
235
|
+
return this._manager._native.fontGlyphForCodepoint(handle, codepoint);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Nominal (unshaped) horizontal advance of a glyph id, in pixels at
|
|
239
|
+
* `size`. */
|
|
240
|
+
advanceOf(glyphId, size) {
|
|
241
|
+
const handle = this._handle(size);
|
|
242
|
+
if (!handle) return 0;
|
|
243
|
+
const advances = this._manager._native.fontGlyphAdvances(handle, [glyphId]);
|
|
244
|
+
return advances?.[0] ?? 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class Win32FontManager {
|
|
249
|
+
constructor(native) {
|
|
250
|
+
this._native = native;
|
|
251
|
+
this._faces = new Map();
|
|
252
|
+
// family|weight|italic|size -> the bridge's handle, or null when there is
|
|
253
|
+
// no such face
|
|
254
|
+
this._handles = new Map();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* A face at a size, as the bridge's handle. Cached on the four things that
|
|
259
|
+
* pick it, because a terminal asks for the same handful of faces on every
|
|
260
|
+
* frame — and because `BackendContext2D.drawGlyphs` groups a frame's glyphs
|
|
261
|
+
* by handle identity, so two asks for the same face must answer the same
|
|
262
|
+
* number or a line of text goes out as one call per glyph.
|
|
263
|
+
*/
|
|
264
|
+
_handleFor(face, size) {
|
|
265
|
+
const px = Math.max(1, Math.round((size ?? 0) * 64)) / 64;
|
|
266
|
+
const key = `${face.family}|${face.weight}|${face.italic ? 1 : 0}|${px}`;
|
|
267
|
+
let handle = this._handles.get(key);
|
|
268
|
+
if (handle === undefined) {
|
|
269
|
+
handle =
|
|
270
|
+
this._native.fontHandle(face.family, px, face.weight, face.italic) ||
|
|
271
|
+
null;
|
|
272
|
+
this._handles.set(key, handle);
|
|
273
|
+
}
|
|
274
|
+
return handle;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The handle a glyph run draws with (`BackendContext2D.drawGlyphs`): a face
|
|
279
|
+
* this engine made, or one of ntk's `Font` objects, which carries the family
|
|
280
|
+
* it was opened as — `loadFont()` registered the file with DirectWrite under
|
|
281
|
+
* that name, so asking for it by name reaches the same face.
|
|
282
|
+
*/
|
|
283
|
+
_runHandle(font, size) {
|
|
284
|
+
if (font instanceof Win32Face) return font._handle(size);
|
|
285
|
+
const family = font?.familyName ?? font?.family ?? font?.postscriptName;
|
|
286
|
+
if (!family) return null;
|
|
287
|
+
return this._handleFor(
|
|
288
|
+
{
|
|
289
|
+
family: String(family),
|
|
290
|
+
weight: weightOf(font.weight ?? 400),
|
|
291
|
+
italic: font.italic === true || font.style === 'italic',
|
|
292
|
+
},
|
|
293
|
+
size,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A font the app ships rather than one the system has — `loadFont()`'s
|
|
299
|
+
* backend half. DirectWrite keeps app-supplied faces in a collection of
|
|
300
|
+
* their own, and the bridge names that collection for exactly the families
|
|
301
|
+
* that came from it, so afterwards the font is asked for by name like any
|
|
302
|
+
* other.
|
|
303
|
+
*
|
|
304
|
+
* `null` on purpose: src/fonts.js keeps the fontkit face it already opened
|
|
305
|
+
* as the handle — that is the one whose metrics an app can read — and what
|
|
306
|
+
* draws is resolved by family name against the collection above.
|
|
307
|
+
*/
|
|
308
|
+
load(source, opts = {}) {
|
|
309
|
+
let data = source;
|
|
310
|
+
if (typeof source === 'string') {
|
|
311
|
+
// A path goes to DirectWrite as a path: it maps the file itself, and
|
|
312
|
+
// the face outlives anything this process would have to hold.
|
|
313
|
+
data = source;
|
|
314
|
+
} else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
315
|
+
data = source;
|
|
316
|
+
} else {
|
|
317
|
+
throw new Error(
|
|
318
|
+
'react-x11: loadFont — expected a file path or font bytes, got ' +
|
|
319
|
+
typeof source,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
// The name to reach it by, which `loadFont` has already decided — the
|
|
323
|
+
// file's own family, or one the caller asked for. A `postscriptName`
|
|
324
|
+
// alongside it narrows the registration to that one face, which is what
|
|
325
|
+
// naming a single face of a family (`Bahnschrift-Light`) means: without
|
|
326
|
+
// it the name would reach every face in the file and the weight would be
|
|
327
|
+
// whatever matching landed on.
|
|
328
|
+
const loaded = this._native.fontLoad(data, {
|
|
329
|
+
family: opts.family || undefined,
|
|
330
|
+
postscriptName: opts.postscriptName || undefined,
|
|
331
|
+
});
|
|
332
|
+
if (!loaded) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
'react-x11: loadFont — DirectWrite could not read the font' +
|
|
335
|
+
(typeof source === 'string' ? ` in ${source}` : ' data') +
|
|
336
|
+
'. It reads .ttf, .otf and .ttc; a .woff or .woff2 has to be ' +
|
|
337
|
+
'unwrapped first.',
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
// Faces matched before this one existed were matched against a smaller
|
|
341
|
+
// set of fonts, and one of them may have fallen back to what this
|
|
342
|
+
// replaces.
|
|
343
|
+
this._faces.clear();
|
|
344
|
+
this._handles.clear();
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The catalogue seam ntk exposes as `fonts.source` — what the fonts app
|
|
350
|
+
* browses. On X that is fontconfig; here it is DirectWrite's collections,
|
|
351
|
+
* the app's own faces before the system's.
|
|
352
|
+
*
|
|
353
|
+
* Pattern syntax: the family, with fontconfig's `:modifiers` tolerated and
|
|
354
|
+
* ignored (`Consolas:bold`, `:lang=ru` — the part after the colon is
|
|
355
|
+
* fontconfig vocabulary DirectWrite does not speak). A query that is only
|
|
356
|
+
* a modifier has no family in it and lists the catalogue instead, which is
|
|
357
|
+
* what the app does with `:lang=ja` on a backend that cannot answer it.
|
|
358
|
+
*/
|
|
359
|
+
get source() {
|
|
360
|
+
return (this._source ??= {
|
|
361
|
+
matchSortedAsync: async ({ family } = {}) => {
|
|
362
|
+
const pattern = String(family ?? '').trim();
|
|
363
|
+
let name = pattern.split(':')[0].trim();
|
|
364
|
+
if (name && GENERIC_FAMILIES.has(name.toLowerCase())) {
|
|
365
|
+
// Rendering resolves the generics itself (ResolveFamily in the
|
|
366
|
+
// bridge); the catalogue wants a family it can actually enumerate,
|
|
367
|
+
// and these are the faces that resolution lands on.
|
|
368
|
+
name =
|
|
369
|
+
{
|
|
370
|
+
serif: 'Times New Roman',
|
|
371
|
+
monospace: 'Consolas',
|
|
372
|
+
'ui-monospace': 'Consolas',
|
|
373
|
+
cursive: 'Segoe Script',
|
|
374
|
+
}[name.toLowerCase()] ?? 'Segoe UI';
|
|
375
|
+
}
|
|
376
|
+
const rows = this._native.listFonts(
|
|
377
|
+
name ? { family: name } : { limit: 400 },
|
|
378
|
+
);
|
|
379
|
+
return rows
|
|
380
|
+
.filter((row) => row.path)
|
|
381
|
+
.map((row) => ({
|
|
382
|
+
path: row.path,
|
|
383
|
+
postscriptName: row.postscriptName,
|
|
384
|
+
family: row.family,
|
|
385
|
+
style: row.style,
|
|
386
|
+
// fontconfig's charset, which DirectWrite does not expose as a
|
|
387
|
+
// string. Empty rather than wrong: the app shows it when it has
|
|
388
|
+
// one and says nothing when it does not.
|
|
389
|
+
charset: '',
|
|
390
|
+
}));
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* One IDWriteTextLayout over the joined string, with the spans as formatting
|
|
397
|
+
* ranges. That is what makes a paragraph of mixed <text> chunks a single
|
|
398
|
+
* layout rather than one per chunk — and therefore what makes line breaking
|
|
399
|
+
* work *across* them, which is the whole reason the contract takes spans
|
|
400
|
+
* rather than a string.
|
|
401
|
+
*/
|
|
402
|
+
layout(spans, base = {}, options = {}) {
|
|
403
|
+
const list =
|
|
404
|
+
typeof spans === 'string' ? [{ text: spans, ...base }] : (spans ?? []);
|
|
405
|
+
let text = '';
|
|
406
|
+
const ranges = [];
|
|
407
|
+
for (const span of list) {
|
|
408
|
+
const piece = String(span?.text ?? '');
|
|
409
|
+
if (!piece) continue;
|
|
410
|
+
const start = text.length;
|
|
411
|
+
text += piece;
|
|
412
|
+
const ink = colorOf(span.color ?? base.color);
|
|
413
|
+
ranges.push({
|
|
414
|
+
start,
|
|
415
|
+
length: piece.length,
|
|
416
|
+
family: familyOf(this._native, span.family ?? base.family),
|
|
417
|
+
size: sizeOf(span.size ?? base.size),
|
|
418
|
+
weight: weightOf(span.weight ?? base.weight ?? 400),
|
|
419
|
+
italic: (span.style ?? base.style) === 'italic',
|
|
420
|
+
// A variable face's axes. Passed per span as well as on the base,
|
|
421
|
+
// because a span carries its own and a paragraph is one layout.
|
|
422
|
+
variations: span.variations ?? base.variations ?? undefined,
|
|
423
|
+
...(ink ? ink : {}),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const handle = this._native.layoutCreate(
|
|
428
|
+
text,
|
|
429
|
+
{
|
|
430
|
+
family: familyOf(this._native, base.family),
|
|
431
|
+
size: sizeOf(base.size),
|
|
432
|
+
weight: weightOf(base.weight ?? 400),
|
|
433
|
+
italic: base.style === 'italic',
|
|
434
|
+
variations: base.variations ?? undefined,
|
|
435
|
+
maxWidth: options.maxWidth,
|
|
436
|
+
align: options.align,
|
|
437
|
+
lineHeight: options.lineHeight,
|
|
438
|
+
maxLines: options.maxLines,
|
|
439
|
+
rtl: options.direction === 'rtl',
|
|
440
|
+
},
|
|
441
|
+
ranges,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const raw = this._native.layoutMetrics(handle);
|
|
445
|
+
if (DEBUG) {
|
|
446
|
+
// The family is in here because the one way this goes quietly wrong is
|
|
447
|
+
// `familyOf` not recognising a name and answering `sans-serif`: the
|
|
448
|
+
// text still draws, in the wrong face, at the wrong metrics, and the
|
|
449
|
+
// only tell is a width that does not match the face the app thinks it
|
|
450
|
+
// asked for.
|
|
451
|
+
console.error(
|
|
452
|
+
`[win32] layout "${text.slice(0, 24)}" ${raw.width}x${raw.height} ` +
|
|
453
|
+
`family=${JSON.stringify(ranges[0]?.family)} size=${base.size} ` +
|
|
454
|
+
`vars=${JSON.stringify(base.variations ?? null)}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// `descent` is what halfLeading() in nodes/text.js subtracts to recreate
|
|
459
|
+
// CSS half-leading, and DirectWrite reports a baseline and a height per
|
|
460
|
+
// line rather than a descent.
|
|
461
|
+
const lines = (raw.lines ?? []).map((line) => ({
|
|
462
|
+
...line,
|
|
463
|
+
descent: Math.max(0, line.height - line.baseline),
|
|
464
|
+
// `rangeBands` walks a line's runs to build a selection highlight, and
|
|
465
|
+
// reads each one's direction off a nested `run` object — ntk's shape.
|
|
466
|
+
// A line with no runs is not an empty line here, it is a crash:
|
|
467
|
+
// `line.runs is not iterable`.
|
|
468
|
+
runs: (line.runs ?? []).map((run) => ({
|
|
469
|
+
...run,
|
|
470
|
+
run: { direction: run.rtl ? 'rtl' : 'ltr' },
|
|
471
|
+
})),
|
|
472
|
+
}));
|
|
473
|
+
return new Win32TextLayout(this, handle, text, { ...raw, lines });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
match(family, options = {}) {
|
|
477
|
+
const name = familyOf(this._native, family);
|
|
478
|
+
const key = `${name}|${weightOf(options.weight)}|${options.style ?? 'normal'}`;
|
|
479
|
+
let face = this._faces.get(key);
|
|
480
|
+
if (!face) {
|
|
481
|
+
face = new Win32Face(this, name, options);
|
|
482
|
+
this._faces.set(key, face);
|
|
483
|
+
}
|
|
484
|
+
return face;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The system font fallback. DirectWrite's own
|
|
489
|
+
* `IDWriteFontFallback::MapCharacters` is the right answer and is not bound
|
|
490
|
+
* yet, so this reports that it has none rather than guessing a family —
|
|
491
|
+
* a wrong face is worse than tofu, because nothing downstream can tell it
|
|
492
|
+
* went wrong.
|
|
493
|
+
*/
|
|
494
|
+
fallbackFor() {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|