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
@@ -0,0 +1,272 @@
1
+ /**
2
+ * The standard edit menu — its rows, their geometry and their painting.
3
+ *
4
+ * A browser gives `<input>` a context menu without being asked, and that is
5
+ * the bar here: a bare `<textinput>` gets Undo/Cut/Copy/Paste with no
6
+ * wiring at all. That rules out building it from the `Menu` components,
7
+ * which live a layer above the nodes and cannot be mounted from one — so
8
+ * the rows are measured and drawn here, and a node hangs them in a
9
+ * `<popup>` (`openEditMenu`, nodes.js, which is the public seam).
10
+ *
11
+ * Deliberately free of node imports: this takes plain data, a measuring
12
+ * function and a 2d context. It can be tested without a tree, and falls
13
+ * back to fixed metrics when there are no fonts, which is how the headless
14
+ * tests see it.
15
+ *
16
+ * The item vocabulary is the one `MenuBar` uses — `type: 'separator'`, a
17
+ * `shortcut` as dbusmenu's `aas` — even though these rows never reach a bus:
18
+ * two spellings for "separator" in one codebase is a trap for whoever writes
19
+ * the third menu. `editMenuGeometry` normalises both into the row it returns,
20
+ * so everything below it works on a boolean and a string.
21
+ */
22
+ import { formatShortcut, isSeparator } from './menuitem.js';
23
+
24
+ const ROW_HEIGHT = 22;
25
+ const SEPARATOR_HEIGHT = 7;
26
+ const PAD_X = 10;
27
+ const PAD_Y = 4;
28
+ const SHORTCUT_GAP = 28;
29
+ const MIN_WIDTH = 150;
30
+ // headless (no fonts): enough to keep the rows a plausible size
31
+ const FALLBACK_CHAR_WIDTH = 7;
32
+
33
+ /** Matches the widget palette's defaults, so the built-in menu and a
34
+ * `ContextMenu` next to it do not look like different toolkits. */
35
+ export const EDIT_MENU_COLORS = {
36
+ background: 'white',
37
+ border: '#b2bec3',
38
+ text: '#2d3436',
39
+ dim: '#7f8c8d',
40
+ highlight: '#2980b9',
41
+ highlightText: 'white',
42
+ separator: '#dfe6e9',
43
+ };
44
+
45
+ /** Palette for a node: its theme where it has one, defaults otherwise. */
46
+ export function editMenuColors(theme) {
47
+ if (!theme) return EDIT_MENU_COLORS;
48
+ return {
49
+ background: theme.surface ?? EDIT_MENU_COLORS.background,
50
+ border: theme.border ?? EDIT_MENU_COLORS.border,
51
+ text: theme.text ?? EDIT_MENU_COLORS.text,
52
+ dim: theme.textMuted ?? EDIT_MENU_COLORS.dim,
53
+ highlight: theme.hoverBackground ?? EDIT_MENU_COLORS.highlight,
54
+ highlightText: theme.hoverText ?? EDIT_MENU_COLORS.highlightText,
55
+ separator: theme.track ?? EDIT_MENU_COLORS.separator,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * The rows, in the standard order, each enabled exactly when it would do
61
+ * something — the whole of the menu's policy, as a pure function of the
62
+ * verbs a target offers (`openEditMenu`, nodes.js).
63
+ *
64
+ * **A verb that was not handed over is a row that is not there**, rather
65
+ * than a greyed one. It was motivated by `<textinput sensitive>`, where a
66
+ * disabled Copy over a password reads as a bug in the application rather
67
+ * than as a decision — but it is also exactly what a surface with nothing
68
+ * to edit needs: a read-only view that offers `copy` and `selectAll` gets a
69
+ * two-row menu, not six rows with four of them dead. So this is the rule,
70
+ * not an exception in the field: never render a verb the caller withheld.
71
+ *
72
+ * Enablement is the other half and is *not* the caller's to decide row by
73
+ * row: Cut and Copy follow the selection, Paste follows what the server
74
+ * says about the clipboard, and Undo/Redo/Select All follow the three
75
+ * `can*` answers only their owner can give. One implementation of that is
76
+ * the point of the seam.
77
+ *
78
+ * @param {object} actions the verb interface — see `openEditMenu`
79
+ * @param {{canPaste?: boolean}} [state] what only the connection knows
80
+ */
81
+ export function editMenuItems(actions = {}, { canPaste = true } = {}) {
82
+ const has = (verb) => typeof actions[verb] === 'function';
83
+ const selected = Boolean(actions.hasSelection);
84
+ const groups = [
85
+ [
86
+ has('undo') && {
87
+ id: 'undo',
88
+ label: 'Undo',
89
+ shortcut: [['Control', 'Z']],
90
+ enabled: Boolean(actions.canUndo),
91
+ },
92
+ has('redo') && {
93
+ id: 'redo',
94
+ label: 'Redo',
95
+ shortcut: [['Control', 'Shift', 'Z']],
96
+ enabled: Boolean(actions.canRedo),
97
+ },
98
+ ],
99
+ [
100
+ has('cut') && {
101
+ id: 'cut',
102
+ label: 'Cut',
103
+ shortcut: [['Control', 'X']],
104
+ enabled: selected,
105
+ },
106
+ has('copy') && {
107
+ id: 'copy',
108
+ label: 'Copy',
109
+ shortcut: [['Control', 'C']],
110
+ enabled: selected,
111
+ },
112
+ has('paste') && {
113
+ id: 'paste',
114
+ label: 'Paste',
115
+ shortcut: [['Control', 'V']],
116
+ enabled: canPaste,
117
+ },
118
+ ],
119
+ [
120
+ has('selectAll') && {
121
+ id: 'selectAll',
122
+ label: 'Select All',
123
+ shortcut: [['Control', 'A']],
124
+ // the default is "there is something to select": only the target
125
+ // knows when everything already is
126
+ enabled: actions.canSelectAll !== false,
127
+ },
128
+ ],
129
+ ]
130
+ .map((group) => group.filter(Boolean))
131
+ .filter((group) => group.length > 0);
132
+ // a separator between the groups that survived, and never one at either
133
+ // end — a menu missing a whole group must not show the gap where it was
134
+ return groups.flatMap((group, i) =>
135
+ i === 0 ? group : [{ type: 'separator' }, ...group],
136
+ );
137
+ }
138
+
139
+ /**
140
+ * Lay the rows out and size the popup that holds them.
141
+ *
142
+ * @param {Array<{id?, label?, shortcut?, type?, enabled?}>} items
143
+ * @param {(text: string) => number|null} [measure] text width, or null when
144
+ * nothing can be measured yet
145
+ */
146
+ export function editMenuGeometry(items, measure) {
147
+ const widthOf = (text) => {
148
+ if (!text) return 0;
149
+ const w = measure?.(text);
150
+ return typeof w === 'number' && w > 0
151
+ ? w
152
+ : text.length * FALLBACK_CHAR_WIDTH;
153
+ };
154
+ const rows = [];
155
+ let y = PAD_Y;
156
+ let widest = 0;
157
+ for (const item of items) {
158
+ const separator = isSeparator(item);
159
+ const shortcut = formatShortcut(item.shortcut);
160
+ const height = separator ? SEPARATOR_HEIGHT : ROW_HEIGHT;
161
+ rows.push({ ...item, separator, shortcut, y, height });
162
+ y += height;
163
+ if (separator) continue;
164
+ widest = Math.max(
165
+ widest,
166
+ widthOf(item.label) + (shortcut ? SHORTCUT_GAP + widthOf(shortcut) : 0),
167
+ );
168
+ }
169
+ return {
170
+ rows,
171
+ width: Math.max(MIN_WIDTH, Math.ceil(widest + PAD_X * 2)),
172
+ height: Math.ceil(y + PAD_Y),
173
+ };
174
+ }
175
+
176
+ /** Row under a y coordinate, or -1. Separators and disabled rows are not
177
+ * hoverable — moving onto one clears the highlight rather than leaving a
178
+ * stale row lit. */
179
+ export function editMenuIndexAt(geometry, y) {
180
+ for (let i = 0; i < geometry.rows.length; i++) {
181
+ const row = geometry.rows[i];
182
+ if (row.separator || row.enabled === false) continue;
183
+ if (y >= row.y && y < row.y + row.height) return i;
184
+ }
185
+ return -1;
186
+ }
187
+
188
+ /** The next selectable row in `dir`, wrapping, or -1 if there are none. */
189
+ export function editMenuStep(geometry, from, dir) {
190
+ const rows = geometry.rows;
191
+ if (rows.length === 0) return -1;
192
+ let i = from < 0 ? (dir > 0 ? -1 : rows.length) : from;
193
+ for (let k = 0; k < rows.length; k++) {
194
+ i += dir;
195
+ if (i < 0) i = rows.length - 1;
196
+ if (i >= rows.length) i = 0;
197
+ const row = rows[i];
198
+ if (!row.separator && row.enabled !== false) return i;
199
+ }
200
+ return -1;
201
+ }
202
+
203
+ function panelPath(ctx, width, height, radius) {
204
+ ctx.beginPath();
205
+ if (radius > 0 && typeof ctx.roundRect === 'function') {
206
+ ctx.roundRect(0.5, 0.5, width - 1, height - 1, radius);
207
+ } else {
208
+ ctx.rect(0.5, 0.5, width - 1, height - 1);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * @param {object} ctx ntk 2d context, translated to the popup's origin
214
+ * @param {object} opts
215
+ * @param {(text: string, color: string) => object|null} opts.layoutOf shaped
216
+ * text, or null when there are no fonts — the rows still paint, unlabelled
217
+ */
218
+ export function paintEditMenu(
219
+ ctx,
220
+ { geometry, active = -1, colors = EDIT_MENU_COLORS, layoutOf, radius = 4 },
221
+ ) {
222
+ const { width, height, rows } = geometry;
223
+
224
+ panelPath(ctx, width, height, radius);
225
+ ctx.fillStyle = colors.background;
226
+ ctx.fill();
227
+ ctx.strokeStyle = colors.border;
228
+ ctx.lineWidth = 1;
229
+ ctx.stroke();
230
+
231
+ const label = (text, color, x, row) => {
232
+ const layout = layoutOf?.(text, color);
233
+ if (!layout) return;
234
+ const line = layout.lines?.[0];
235
+ const ink = line ? line.ascent + line.descent : layout.height;
236
+ layout.draw(ctx, x, row.y + Math.max(0, (row.height - ink) / 2));
237
+ };
238
+
239
+ for (let i = 0; i < rows.length; i++) {
240
+ const row = rows[i];
241
+ if (row.separator) {
242
+ ctx.fillStyle = colors.separator;
243
+ ctx.fillRect(
244
+ PAD_X,
245
+ Math.round(row.y + row.height / 2) + 0.5,
246
+ width - PAD_X * 2,
247
+ 1,
248
+ );
249
+ continue;
250
+ }
251
+ const disabled = row.enabled === false;
252
+ const on = i === active && !disabled;
253
+ if (on) {
254
+ ctx.fillStyle = colors.highlight;
255
+ ctx.fillRect(1, row.y, width - 2, row.height);
256
+ }
257
+ const color = disabled
258
+ ? colors.dim
259
+ : on
260
+ ? colors.highlightText
261
+ : colors.text;
262
+ label(row.label, color, PAD_X, row);
263
+ if (row.shortcut) {
264
+ // the shortcut column stays quieter than the label, except in the
265
+ // highlighted row where it has to stay legible on the accent
266
+ const shortcutColor = on ? colors.highlightText : colors.dim;
267
+ const layout = layoutOf?.(row.shortcut, shortcutColor);
268
+ const w = layout?.width ?? 0;
269
+ label(row.shortcut, shortcutColor, width - PAD_X - w, row);
270
+ }
271
+ }
272
+ }
package/src/errors.js ADDED
@@ -0,0 +1,98 @@
1
+ // Where a throw from user code goes when React is not on the stack.
2
+ //
3
+ // An event handler runs from an X event, not from a render, so no error
4
+ // boundary can catch it — a boundary only sees throws React itself invoked.
5
+ // Left bare, the throw unwinds out of the dispatcher into ntk's socket
6
+ // `data` handler and takes the process with it, or wedges the frame loop,
7
+ // which is worse: it looks like a hang rather than a crash.
8
+
9
+ // container -> the root's onUncaughtError, when it set one
10
+ const perContainer = new WeakMap();
11
+
12
+ export function setErrorHandler(container, fn) {
13
+ if (fn) perContainer.set(container, fn);
14
+ else perContainer.delete(container);
15
+ }
16
+
17
+ /** The component that rendered this node, which is the name worth printing
18
+ * — `<box>` alone never tells anyone whose box it was. */
19
+ export function ownerName(node) {
20
+ const type = node?._reactFiber?._debugOwner?.type;
21
+ if (!type) return null;
22
+ return type.displayName || type.name || '(anonymous)';
23
+ }
24
+
25
+ /** A GUI process whose tree threw must not exit 0 — that lies to CI and to
26
+ * a supervisor. Guarded because the playground bundle runs in a browser. */
27
+ function markFailed() {
28
+ if (typeof process !== 'undefined' && process) process.exitCode = 1;
29
+ }
30
+
31
+ /**
32
+ * Report a throw from a user callback, then carry on.
33
+ *
34
+ * Carrying on is the point: one bad tooltip handler must not stop the rest
35
+ * of the dispatch or the frame loop. The default still refuses to swallow —
36
+ * it logs what threw and where, and sets `process.exitCode`. A root that
37
+ * passed `onUncaughtError` decides for itself, because for a kiosk "crash
38
+ * loudly" is the right answer and for a text editor it is not.
39
+ */
40
+ export function reportHandlerError(node, handler, error) {
41
+ const element = node?.kind ? `<${node.kind}>` : '(unknown)';
42
+ const owner = ownerName(node);
43
+ const custom = node?.app && perContainer.get(node.app);
44
+ if (custom) {
45
+ custom(error, {
46
+ componentStack: owner ? `\n in ${owner}` : undefined,
47
+ element,
48
+ handler,
49
+ node,
50
+ });
51
+ return;
52
+ }
53
+ console.error(
54
+ `react-x11: ${handler} on ${element}${owner ? ` in ${owner}` : ''} threw. ` +
55
+ 'It ran from an X event rather than a render, so no error boundary ' +
56
+ 'could catch it; dispatch continues.',
57
+ error,
58
+ );
59
+ markFailed();
60
+ }
61
+
62
+ /** Wrap a call to user code so a throw is reported instead of escaping. */
63
+ export function callHandler(node, handler, fn, ev) {
64
+ try {
65
+ fn(ev);
66
+ } catch (error) {
67
+ reportHandlerError(node, handler, error);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * The root error callbacks React itself invokes, as `(error, errorInfo)`.
73
+ * `errorInfo.componentStack` is the whole reason a boundary is debuggable,
74
+ * and printing it beside the error is most of what makes these useful.
75
+ */
76
+ export const defaultRootHandlers = {
77
+ onUncaughtError(error, errorInfo) {
78
+ console.error(
79
+ 'react-x11: uncaught error' + (errorInfo?.componentStack ?? ''),
80
+ error,
81
+ );
82
+ markFailed();
83
+ },
84
+ onCaughtError(error, errorInfo) {
85
+ // an error boundary handled this one, so the process is still healthy
86
+ console.error(
87
+ 'react-x11: error caught by a boundary' +
88
+ (errorInfo?.componentStack ?? ''),
89
+ error,
90
+ );
91
+ },
92
+ onRecoverableError(error, errorInfo) {
93
+ console.error(
94
+ 'react-x11: recoverable error' + (errorInfo?.componentStack ?? ''),
95
+ error,
96
+ );
97
+ },
98
+ };