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,393 @@
1
+ // What the window manager has done with a window, as something a component
2
+ // re-renders on.
3
+ //
4
+ // A `<window>` asks for `fullscreen` or `maximized` and the window manager
5
+ // decides — and it decides on its own account too, when the user hits a
6
+ // hotkey or the titlebar button. `nodes.js` sends the requests; this is the
7
+ // half that reads back what actually happened, plus the two things an app
8
+ // needs that are not states at all: whether it has the keyboard, and whether
9
+ // anyone can see it.
10
+ //
11
+ // ## Where each field comes from
12
+ //
13
+ // | | |
14
+ // | --- | --- |
15
+ // | `focused` | FocusIn/FocusOut, via the event manager that already tracks it |
16
+ // | `minimized` `maximized` `fullscreen` `states` | `_NET_WM_STATE`, as ntk's `statechange` event |
17
+ // | `obscured` | VisibilityNotify |
18
+ // | `desktop` | `_NET_WM_DESKTOP` |
19
+ //
20
+ // Nothing here is asked for until something subscribes. `_NET_WM_STATE`
21
+ // costs a PropertyChange selection and a round trip per change,
22
+ // VisibilityNotify costs a mask bit, and an app that never asks pays for
23
+ // neither.
24
+ //
25
+ // ## The compositor caveat, because it will otherwise be found as a bug
26
+ //
27
+ // **`obscured` is always false when a compositing manager is running**, and
28
+ // one is running on every stock GNOME and KDE session. A composited window
29
+ // is redirected to an offscreen pixmap, so the server considers it entirely
30
+ // visible whatever is stacked on top of it, and VisibilityNotify says so.
31
+ // That is X working as designed and there is no protocol answer to it.
32
+ //
33
+ // So `obscured` is the bare-WM optimisation it looks like, and `visible` —
34
+ // which folds in `minimized`, the signal that *does* survive compositing —
35
+ // is the field to branch on. An animation paused on `visible` stops when the
36
+ // window is minimized everywhere, and additionally when it is buried on a
37
+ // desktop with no compositor.
38
+
39
+ import { useCallback, useSyncExternalStore } from 'react';
40
+
41
+ import { useAppOrNull } from './appcontext.js';
42
+ import { useTopLevelWindow, windowIdOf } from './windowid.js';
43
+
44
+ const VISIBILITY_NOTIFY = 15;
45
+ const VISIBILITY_CHANGE_MASK = 65536; // x11.eventMask.VisibilityChange
46
+ const FULLY_OBSCURED = 2;
47
+ const DESKTOP_PROPERTY = '_NET_WM_DESKTOP';
48
+
49
+ /**
50
+ * What is true before the window manager has said anything.
51
+ *
52
+ * `focused` and `visible` start **true**, and that is the whole reason this
53
+ * constant is written out rather than being a pile of falses: a window opens
54
+ * focused and on screen far more often than not, and a title bar that
55
+ * renders dimmed on the first frame and un-dims on the second is a visible
56
+ * flash on every launch. Starting from the common case makes the first paint
57
+ * right and the correction — for the rarer window that opened in the
58
+ * background — the thing that costs a re-render.
59
+ */
60
+ const UNKNOWN = Object.freeze({
61
+ focused: true,
62
+ visible: true,
63
+ obscured: false,
64
+ minimized: false,
65
+ maximized: false,
66
+ fullscreen: false,
67
+ states: Object.freeze([]),
68
+ desktop: null,
69
+ });
70
+
71
+ const sessions = new WeakMap();
72
+
73
+ const SAME = (a, b) =>
74
+ a.focused === b.focused &&
75
+ a.visible === b.visible &&
76
+ a.obscured === b.obscured &&
77
+ a.minimized === b.minimized &&
78
+ a.maximized === b.maximized &&
79
+ a.fullscreen === b.fullscreen &&
80
+ a.desktop === b.desktop &&
81
+ a.states.length === b.states.length &&
82
+ a.states.every((s, i) => s === b.states[i]);
83
+
84
+ class WindowStateSession {
85
+ constructor(node) {
86
+ this.node = node;
87
+ this.snapshot = UNKNOWN;
88
+ this.listeners = new Set();
89
+ this.armed = false;
90
+ this.stopped = false;
91
+ this._desktopAtom = null;
92
+ this._offs = [];
93
+ }
94
+
95
+ /**
96
+ * Fold new fields into the snapshot and notify. A change that changes
97
+ * nothing notifies nobody: `statechange` fires on every `_NET_WM_STATE`
98
+ * rewrite, and window managers rewrite it with the same contents more
99
+ * often than they change it.
100
+ */
101
+ publish(values) {
102
+ const states = values.states ?? this.snapshot.states;
103
+ const next = Object.freeze({
104
+ ...this.snapshot,
105
+ ...values,
106
+ states: Object.freeze([...states]),
107
+ // Derived rather than reported, in one place, so `visible` cannot
108
+ // disagree with the fields it is made of.
109
+ minimized: values.states
110
+ ? states.includes('hidden')
111
+ : this.snapshot.minimized,
112
+ maximized: values.states
113
+ ? states.includes('maximized_vert') && states.includes('maximized_horz')
114
+ : this.snapshot.maximized,
115
+ fullscreen: values.states
116
+ ? states.includes('fullscreen')
117
+ : this.snapshot.fullscreen,
118
+ });
119
+ const settled = Object.freeze({
120
+ ...next,
121
+ visible: !next.minimized && !next.obscured,
122
+ });
123
+ if (SAME(settled, this.snapshot)) return;
124
+ this.snapshot = settled;
125
+ for (const fn of [...this.listeners]) {
126
+ try {
127
+ fn();
128
+ } catch {
129
+ // one subscriber throwing must not take the others with it, nor the
130
+ // X event loop this runs on
131
+ }
132
+ }
133
+ }
134
+
135
+ stop() {
136
+ this.stopped = true;
137
+ for (const off of this._offs) {
138
+ try {
139
+ off();
140
+ } catch {
141
+ // a window already destroyed takes its listeners with it
142
+ }
143
+ }
144
+ this._offs.length = 0;
145
+ this.listeners.clear();
146
+ }
147
+ }
148
+
149
+ /**
150
+ * The `WindowNode` behind a ref, a node, an ntk window or an XID.
151
+ *
152
+ * Popups are **not** excluded, unlike `topLevelWindows()`: a `<popup>` is
153
+ * override-redirect so the window manager never gives it a `_NET_WM_STATE`,
154
+ * and reporting the defaults for one is a truer answer than refusing to
155
+ * resolve it.
156
+ */
157
+ function nodeOf(app, target) {
158
+ const value =
159
+ target &&
160
+ typeof target === 'object' &&
161
+ 'current' in target &&
162
+ !target.isWindow
163
+ ? target.current
164
+ : target;
165
+ if (!value) return null;
166
+ if (value.isWindow && value.window) return value;
167
+ const id = windowIdOf(value);
168
+ if (id == null) return null;
169
+ return (
170
+ (app?._rootChildren ?? []).find(
171
+ (node) => node?.isWindow && node.window?.id === id,
172
+ ) ?? null
173
+ );
174
+ }
175
+
176
+ function sessionFor(node) {
177
+ let session = sessions.get(node);
178
+ if (!session) {
179
+ session = new WindowStateSession(node);
180
+ sessions.set(node, session);
181
+ }
182
+ return session;
183
+ }
184
+
185
+ /**
186
+ * Start listening on the X side. Once per window: the mask bits and the
187
+ * PropertyChange selection cannot be given back when the last subscriber
188
+ * leaves, and re-arming on the next one would cost another round trip for a
189
+ * selection the server still has.
190
+ */
191
+ function arm(session) {
192
+ if (session.armed) return;
193
+ const node = session.node;
194
+ const wnd = node.window;
195
+ // Not armed yet, rather than armed against nothing: a window realizes
196
+ // during the commit that created it, so a subscription that got here first
197
+ // gets another chance on the next one.
198
+ if (!wnd) return;
199
+ session.armed = true;
200
+
201
+ // Focus, from the event manager rather than from ntk directly: it already
202
+ // dedups FocusIn/FocusOut and already knows the answer for a window that
203
+ // has not seen either yet.
204
+ if (typeof node.onWindowFocusChange === 'function') {
205
+ session._offs.push(
206
+ node.onWindowFocusChange((focused) => {
207
+ if (!session.stopped) session.publish({ focused });
208
+ }),
209
+ );
210
+ const now = node.events?.windowFocused;
211
+ if (typeof now === 'boolean') session.publish({ focused: now });
212
+ }
213
+
214
+ // _NET_WM_STATE. Adding the listener is what makes ntk select
215
+ // PropertyChange and intern the atom, so the read below is only for the
216
+ // state the window already had when this ran.
217
+ if (typeof wnd.on === 'function') {
218
+ const onState = (states) => {
219
+ if (!session.stopped) session.publish({ states: states ?? [] });
220
+ };
221
+ wnd.on('statechange', onState);
222
+ session._offs.push(() => wnd.off?.('statechange', onState));
223
+ Promise.resolve(wnd.getWmStates?.()).then(
224
+ (states) => states && !session.stopped && session.publish({ states }),
225
+ () => {},
226
+ );
227
+
228
+ // _NET_WM_DESKTOP rides on the PropertyChange selection the line above
229
+ // already asked for, so watching it costs one comparison per property
230
+ // change rather than a second selection.
231
+ const onProperty = (ev) => {
232
+ if (session.stopped || ev?.atom == null) return;
233
+ if (ev.atom !== session._desktopAtom) return;
234
+ readDesktop(session);
235
+ };
236
+ wnd.on('property', onProperty);
237
+ session._offs.push(() => wnd.off?.('property', onProperty));
238
+ Promise.resolve(wnd.atom?.(DESKTOP_PROPERTY)).then(
239
+ (atom) => {
240
+ session._desktopAtom = atom;
241
+ if (!session.stopped) readDesktop(session);
242
+ },
243
+ () => {},
244
+ );
245
+ }
246
+
247
+ watchVisibility(session);
248
+ }
249
+
250
+ function readDesktop(session) {
251
+ Promise.resolve(
252
+ session.node.window?.getProperty?.(DESKTOP_PROPERTY, { as: 'numbers' }),
253
+ ).then(
254
+ (values) => {
255
+ if (session.stopped) return;
256
+ const desktop = values?.[0];
257
+ // 0xffffffff is EWMH for "on every desktop", which `states` already
258
+ // reports as `sticky` — a workspace number is the wrong shape for it.
259
+ session.publish({
260
+ desktop:
261
+ typeof desktop === 'number' && desktop !== 0xffffffff
262
+ ? desktop
263
+ : null,
264
+ });
265
+ },
266
+ () => {},
267
+ );
268
+ }
269
+
270
+ /**
271
+ * VisibilityNotify, which ntk has no event name for — its mask table stops
272
+ * at the events a widget toolkit needs — so the mask goes on through
273
+ * `selectInput` (which ORs into ntk's own tracked mask, so nothing it adds
274
+ * later drops this) and the event is read off the raw connection.
275
+ */
276
+ function watchVisibility(session) {
277
+ const wnd = session.node.window;
278
+ const X = session.node.app?.X ?? wnd?.X;
279
+ if (!X?.on || typeof wnd?.selectInput !== 'function') return;
280
+
281
+ const onEvent = (ev) => {
282
+ if (session.stopped) return;
283
+ if (ev.type !== VISIBILITY_NOTIFY || ev.wid !== wnd.id) return;
284
+ session.publish({ obscured: ev.state === FULLY_OBSCURED });
285
+ };
286
+ X.on('event', onEvent);
287
+ session._offs.push(() => X.off?.('event', onEvent));
288
+
289
+ Promise.resolve(wnd.selectInput(VISIBILITY_CHANGE_MASK)).catch(() => {
290
+ // a window destroyed between the ref resolving and this call; the
291
+ // defaults stand and nothing further will arrive
292
+ });
293
+ }
294
+
295
+ /** What is known about a window right now. Stable until it changes. */
296
+ export function windowStateSnapshot(app, target) {
297
+ const node = nodeOf(app, target);
298
+ if (!node) return UNKNOWN;
299
+ return sessions.get(node)?.snapshot ?? UNKNOWN;
300
+ }
301
+
302
+ /** Subscribe to a window's state. Not public — `useWindowState()` is. */
303
+ export function watchWindowState(app, target, onChange) {
304
+ const node = nodeOf(app, target);
305
+ if (!node) return () => {};
306
+ const session = sessionFor(node);
307
+ arm(session);
308
+ session.listeners.add(onChange);
309
+ return () => session.listeners.delete(onChange);
310
+ }
311
+
312
+ /**
313
+ * Test seam: state what the window manager did, without a window manager.
314
+ * Marks the session armed, so nothing reaches for `_NET_WM_STATE` behind the
315
+ * value; `states` re-derives `minimized`/`maximized`/`fullscreen` the way a
316
+ * real `statechange` does.
317
+ */
318
+ export function setWindowStateForTests(app, target, values) {
319
+ const node = nodeOf(app, target);
320
+ if (!node) return null;
321
+ const session = sessionFor(node);
322
+ session.armed = true;
323
+ session.publish(values);
324
+ return session.snapshot;
325
+ }
326
+
327
+ /** Tear down with the window. Called from `WindowNode.destroy`. */
328
+ export function endWindowState(node) {
329
+ const session = sessions.get(node);
330
+ if (!session) return;
331
+ session.stop();
332
+ sessions.delete(node);
333
+ }
334
+
335
+ /**
336
+ * What the window manager has done with this window, live.
337
+ *
338
+ * ```jsx
339
+ * const { focused, visible, fullscreen } = useWindowState();
340
+ *
341
+ * // a title bar that dims when the window is not the active one
342
+ * <text style={{ color: focused ? theme.text : theme.textMuted }}>{title}</text>
343
+ *
344
+ * // and work that is pointless while nobody can see it
345
+ * useEffect(() => {
346
+ * if (!visible) return;
347
+ * const id = setInterval(poll, 1000);
348
+ * return () => clearInterval(id);
349
+ * }, [visible]);
350
+ * ```
351
+ *
352
+ * | | |
353
+ * | --- | --- |
354
+ * | `focused` | this window has the keyboard |
355
+ * | `visible` | **the one to branch on** — not minimized, not fully covered |
356
+ * | `minimized` | `_NET_WM_STATE_HIDDEN`: iconified, or shaded away |
357
+ * | `maximized` | both axes; one axis alone shows up in `states` |
358
+ * | `fullscreen` | what the WM actually did, not what `<window fullscreen>` asked for |
359
+ * | `obscured` | fully covered by other windows — **always false under a compositor** |
360
+ * | `states` | the raw `_NET_WM_STATE` names, e.g. `['maximized_vert', 'focused']` |
361
+ * | `desktop` | the workspace index, or null (a sticky window shows up in `states`) |
362
+ *
363
+ * With no argument it reads the window the component is in, inferred the way
364
+ * `useTopLevelWindow()` infers it — exact for the one-window app, and a
365
+ * documented guess for a tree with several. Pass a ref to be certain:
366
+ *
367
+ * ```jsx
368
+ * const win = useRef(null);
369
+ * const { fullscreen } = useWindowState(win);
370
+ * return <window ref={win}>…</window>;
371
+ * ```
372
+ *
373
+ * Nothing is asked of the server until something calls this, and the first
374
+ * render answers from the defaults — focused and visible — rather than
375
+ * waiting for a round trip. See the compositor note in this file's header
376
+ * before reaching for `obscured`.
377
+ */
378
+ export function useWindowState(ref = null) {
379
+ const app = useAppOrNull();
380
+ const owner = useTopLevelWindow();
381
+ const target = ref ?? owner;
382
+ const subscribe = useCallback(
383
+ (onChange) => watchWindowState(app, target, onChange),
384
+ [app, target],
385
+ );
386
+ const snapshot = useCallback(
387
+ () => windowStateSnapshot(app, target),
388
+ [app, target],
389
+ );
390
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
391
+ }
392
+
393
+ /** @typedef {typeof UNKNOWN} WindowState */