react-x11 1.2.0 → 2.0.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/fonts.js ADDED
@@ -0,0 +1,301 @@
1
+ // Opening a font file, from the application's side of the seam.
2
+ //
3
+ // An app that wants to *look at* a font — a picker, a specimen, a
4
+ // preferences page that shows a family name — or to *ship* one had to leave
5
+ // react-x11 to do either (issue #346). The reading half was ntk's own class,
6
+ // `Font.loadSync(path)`, reached through an import an application author has
7
+ // no reason to know exists; the drawing half was a second, unrelated call,
8
+ // `app.fonts.load(path, { family: 'a-name-i-made-up' })`, and the made-up
9
+ // name then had to be kept consistent between the two by hand. Two calls,
10
+ // two mental models, and a string with no author.
11
+ //
12
+ // The two verbs here are the two things that were being confused:
13
+ //
14
+ // openFont(app, source) read the file. Nothing about the app changes.
15
+ // loadFont(app, source) read it *and* make it drawable, under a family
16
+ // name taken off the file and handed back.
17
+ //
18
+ // **Why opening does not register.** A registered face is not just an entry
19
+ // in a lookup table: ntk consults every registered font, in registration
20
+ // order, for *any codepoint the current face is missing* (`fallbackFor`), so
21
+ // an app that registered each file it merely previewed would quietly change
22
+ // which face draws the bullets and the curly quotes in its own UI. Reading a
23
+ // file and installing a font are different acts, so they are different
24
+ // calls, and the one whose name says "open" is the one that changes nothing.
25
+ //
26
+ // **Why both go through `app.fonts`.** The manager is the connection's one
27
+ // font cache, and a `Font` carries a glyph cache and a server-side glyphset
28
+ // with it: a second copy of a face is a second atlas uploaded over the wire,
29
+ // and a node built against one copy cannot be painted by the other. So
30
+ // neither verb parses a file the manager has already read — `loadFont` after
31
+ // `openFont` on the same path re-uses the open face rather than opening a
32
+ // second one, and a face fontconfig already matched is not re-read either.
33
+ //
34
+ // The same rule is why `react-x11/ntk` exists, for the code that does need
35
+ // `Font` itself: import ntk through react-x11, never as a dependency of its
36
+ // own (docs/styling.md#a-font-file-of-your-own).
37
+
38
+ import { basename, extname } from 'node:path';
39
+
40
+ import { Font } from 'ntk';
41
+
42
+ /**
43
+ * Per-app bookkeeping. Keyed on the app rather than stored on it, like
44
+ * `inputtime.js`: one process can drive several roots on several
45
+ * connections, and a family registered on one is not registered on another.
46
+ *
47
+ * `opened` maps a source key to the handle for it, which is what makes both
48
+ * verbs idempotent — a component that calls `useFont` on every render
49
+ * re-reads nothing and registers nothing twice. `claims` is the family
50
+ * namespace: `family|weight|italic` -> the key of the file that holds it.
51
+ */
52
+ const state = new WeakMap();
53
+
54
+ function stateFor(app) {
55
+ let s = state.get(app);
56
+ if (!s) {
57
+ s = { opened: new Map(), dataKeys: new WeakMap(), claims: new Map(), n: 0 };
58
+ state.set(app, s);
59
+ }
60
+ return s;
61
+ }
62
+
63
+ function managerOf(app, fn) {
64
+ const fonts = app?.fonts;
65
+ if (!fonts) {
66
+ throw new Error(
67
+ `react-x11: ${fn}(app, …) needs the ntk connection — the app from ` +
68
+ '`createRoot({ app })` or `useApp()`, which is what carries `app.fonts`',
69
+ );
70
+ }
71
+ return fonts;
72
+ }
73
+
74
+ /**
75
+ * The cache key for a source.
76
+ *
77
+ * A path's key is the one ntk computes for itself
78
+ * (`` `${path}#${postscriptName}` ``, its font.js) rather than one of ours,
79
+ * which is what makes `loadFont`'s `app.fonts.load()` land on the face
80
+ * `openFont` already opened instead of parsing the file a second time.
81
+ *
82
+ * Bytes have no name to key on, so they are keyed by the identity of the
83
+ * buffer they arrived in — the same array is the same font, a copy of it is
84
+ * not, which is the only answer available without hashing megabytes.
85
+ */
86
+ function keyFor(st, source, postscriptName) {
87
+ if (typeof source === 'string') return `${source}#${postscriptName || ''}`;
88
+ let key = st.dataKeys.get(source);
89
+ if (!key) {
90
+ key = `data:${st.n++}#${postscriptName || ''}`;
91
+ st.dataKeys.set(source, key);
92
+ }
93
+ return key;
94
+ }
95
+
96
+ /**
97
+ * ntk's cache, reached through the method that owns it.
98
+ *
99
+ * `_open` is underscored because ntk has no public "open without
100
+ * registering" — this module *is* that route from an application, which is
101
+ * the whole of issue #346. If a future ntk renames it, parsing directly is
102
+ * the honest fallback: a duplicated face costs memory and an extra glyph
103
+ * atlas, where throwing would cost the feature entirely.
104
+ */
105
+ function openThrough(fonts, candidate) {
106
+ if (typeof fonts._open === 'function') return fonts._open(candidate);
107
+ return candidate.path !== undefined
108
+ ? Font.loadSync(candidate.path, candidate.postscriptName)
109
+ : Font.fromData(candidate.data, candidate);
110
+ }
111
+
112
+ /** `/usr/share/fonts/Inter-Variable.ttf` -> `Inter-Variable`. */
113
+ function stemOf(source) {
114
+ if (typeof source !== 'string') return '';
115
+ return basename(source, extname(source));
116
+ }
117
+
118
+ /**
119
+ * Where in a family this face sits — `weight|italic`, the two things ntk
120
+ * resolves a family's faces by.
121
+ *
122
+ * This mirrors ntk's own `detectStyle`, which is not exported. Mirroring is
123
+ * safe here in a way it would not be on the paint path: the answer only
124
+ * decides what a file is *named*, so drifting from ntk costs a scoped alias
125
+ * where none was needed, never a wrong glyph.
126
+ */
127
+ function faceOf(font, opts) {
128
+ const os2 = font.fk?.['OS/2'];
129
+ const weight = opts.weight ?? (os2 ? os2.usWeightClass : 400);
130
+ const italic =
131
+ opts.style !== undefined
132
+ ? /italic|oblique/i.test(opts.style)
133
+ : !!(
134
+ os2?.fsSelection?.italic ||
135
+ font.fk?.italicAngle ||
136
+ /italic|oblique/i.test(font.fk?.subfamilyName || '')
137
+ );
138
+ return `${weight}|${italic}`;
139
+ }
140
+
141
+ /**
142
+ * The family name a loaded file is registered under, when the caller did not
143
+ * name one.
144
+ *
145
+ * **The font's own name**, which is the decision worth stating: an app that
146
+ * ships `Inter.ttf` wants `fontFamily: 'Inter'` to mean *its* Inter, in
147
+ * every style it has already written, and registered faces beat fontconfig
148
+ * in `match()` — this is `@font-face` behaving the way the web taught
149
+ * everyone it behaves.
150
+ *
151
+ * **Scoped only when the face would be unreachable**, which is the other
152
+ * half. A family is a set of faces, so `Inter-Regular.ttf` and
153
+ * `Inter-Bold.ttf` both want to be "Inter" and `fontWeight` picks between
154
+ * them — that is a family being assembled, not a collision. Two *different*
155
+ * files claiming the same family at the same weight and slant are a
156
+ * collision, because ntk resolves that tie to whichever was registered first
157
+ * and the second file would never draw: it gets `Ubuntu 2`. Nobody has to
158
+ * know that happened — the family to draw with is the one `loadFont` hands
159
+ * back, and a caller that always uses it is always right.
160
+ *
161
+ * A name the `family` option asked for is used verbatim: two faces under one
162
+ * alias is a thing a caller may well mean, and second-guessing a name
163
+ * somebody chose is worse than the tie. It still claims its slot, so a later
164
+ * auto-derived name never lands on top of it.
165
+ */
166
+ function claim(st, key, base, face, verbatim) {
167
+ const slot = (name) => `${name.toLowerCase()}|${face}`;
168
+ const held = (name) => st.claims.get(slot(name));
169
+ if (verbatim || !held(base) || held(base) === key) {
170
+ st.claims.set(slot(base), key);
171
+ return base;
172
+ }
173
+ for (let n = 2; ; n++) {
174
+ const alias = `${base} ${n}`;
175
+ if (!held(alias) || held(alias) === key) {
176
+ st.claims.set(slot(alias), key);
177
+ return alias;
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Read a font file: metrics, coverage, variation axes.
184
+ *
185
+ * ```js
186
+ * const font = openFont(app, '/usr/share/fonts/truetype/inter/Inter.ttf');
187
+ * font.familyName; // 'Inter'
188
+ * font.metrics(30); // { ascent, descent, lineGap, capHeight, … }
189
+ * font.variationAxes; // { wght: { name, min, default, max } }
190
+ * font.hasGlyph(0x20b8); // is the tenge sign in this face?
191
+ * ```
192
+ *
193
+ * `source` is a path to a `.ttf`/`.otf`/`.ttc`/`.woff`/`.woff2`, or the
194
+ * file's bytes. `postscriptName` picks one face out of a `.ttc` collection
195
+ * (the first otherwise).
196
+ *
197
+ * **Nothing about the app changes.** The face is not registered, so it is
198
+ * not a candidate for any `fontFamily` — and, the part that is easy to miss,
199
+ * it does not join the fallback chain either, which is what stops a font
200
+ * browser from changing the glyphs its own UI falls back to. {@link loadFont}
201
+ * is the call for when the point is to draw with it.
202
+ *
203
+ * The file is read once per app: the face is cached in the connection's font
204
+ * manager, so opening it again — every render, every repaint — hands back
205
+ * the same `Font`, and so does {@link loadFont} afterwards.
206
+ *
207
+ * Throws what ntk throws for a file that is missing or unparseable. When the
208
+ * path came from a user (a file dialog), that is a `try`/`catch` around this
209
+ * call rather than an error boundary.
210
+ *
211
+ * @param {object} app the ntk connection (`useApp()`)
212
+ * @param {string|Uint8Array|Buffer} source path, or the font file's bytes
213
+ * @param {{ postscriptName?: string }} [opts]
214
+ * @returns {object} ntk's `Font`
215
+ */
216
+ export function openFont(app, source, opts = {}) {
217
+ const fonts = managerOf(app, 'openFont');
218
+ const st = stateFor(app);
219
+ const key = keyFor(st, source, opts.postscriptName);
220
+ const known = st.opened.get(key);
221
+ if (known) return known.font;
222
+ const font = openThrough(fonts, {
223
+ key,
224
+ postscriptName: opts.postscriptName,
225
+ ...(typeof source === 'string' ? { path: source } : { data: source }),
226
+ });
227
+ st.opened.set(key, { font, family: null });
228
+ return font;
229
+ }
230
+
231
+ /**
232
+ * Read a font file **and** register it, so `fontFamily` can name it.
233
+ *
234
+ * ```jsx
235
+ * const { font, family } = loadFont(app, '/path/to/Inter.ttf');
236
+ * <text style={{ fontFamily: family, fontSize: 20 }}>Handgloves</text>;
237
+ * ```
238
+ *
239
+ * `family` is read off the file rather than invented by the caller: the
240
+ * font's own name, so an app that ships `Inter.ttf` can go on writing
241
+ * `fontFamily: 'Inter'` in the styles it already has. Several faces of one
242
+ * family — regular, bold, italic — all keep that name and `fontWeight` picks
243
+ * between them. Only a second file that would be *unreachable* under it,
244
+ * same family at the same weight and slant, is scoped: it comes back as
245
+ * `Inter 2`. Draw with the name that comes back and the question never
246
+ * arises.
247
+ *
248
+ * A registered face wins over fontconfig for its name, which is the point:
249
+ * an app that ships a font means the one it ships. Pass `family` to name it
250
+ * something else — `loadFont(app, picked, { family: 'preview' })` keeps a
251
+ * file the user chose out of the way of the app's own type. `weight` and
252
+ * `style` override what the file says about itself; `postscriptName` picks a
253
+ * face out of a `.ttc`.
254
+ *
255
+ * Registering also puts the face in the fallback chain, ahead of the system
256
+ * fonts: text in some other family that has no glyph for a character borrows
257
+ * it from here before fontconfig is asked. That is what an app shipping a
258
+ * symbol or icon face wants, and {@link openFont} is the call for when it is
259
+ * not.
260
+ *
261
+ * Idempotent per app and file: calling it again returns the same handle and
262
+ * registers nothing twice.
263
+ *
264
+ * @param {object} app the ntk connection (`useApp()`)
265
+ * @param {string|Uint8Array|Buffer} source path, or the font file's bytes
266
+ * @param {{ family?: string, weight?: number|string, style?: string,
267
+ * postscriptName?: string }} [opts]
268
+ * @returns {{ font: object, family: string }} ntk's `Font`, and the family
269
+ * name to draw it with
270
+ */
271
+ export function loadFont(app, source, opts = {}) {
272
+ const fonts = managerOf(app, 'loadFont');
273
+ const st = stateFor(app);
274
+ const key = keyFor(st, source, opts.postscriptName);
275
+ const known = st.opened.get(key);
276
+ if (known?.family && (!opts.family || opts.family === known.family)) {
277
+ return known;
278
+ }
279
+
280
+ // Opened first, because the name to register under is read off the face —
281
+ // and this is the read that does not happen twice: for a path, `load()`
282
+ // below computes the same cache key and gets this very font back.
283
+ const font = openFont(app, source, opts);
284
+ const family = claim(
285
+ st,
286
+ key,
287
+ opts.family || font.familyName || stemOf(source) || 'font',
288
+ faceOf(font, opts),
289
+ !!opts.family,
290
+ );
291
+
292
+ const registered = fonts.load(source, { ...opts, family });
293
+ // Bytes are the one source `load` cannot look up: the key it makes for
294
+ // them counts entries rather than naming the data, so it parses the buffer
295
+ // again and the face above is not the one that got registered. Keep the
296
+ // registered face — it is the one that will be drawn with, and so the one
297
+ // whose glyph cache is worth sharing — and let the other go.
298
+ const handle = { font: registered ?? font, family };
299
+ st.opened.set(key, handle);
300
+ return handle;
301
+ }