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/frames.js ADDED
@@ -0,0 +1,73 @@
1
+ // Which windows owe a frame.
2
+ //
3
+ // Shared between the paint root (nodes.js — it records the debt in
4
+ // `invalidate()` and pays it in `flush()`) and the event dispatcher
5
+ // (events.js — it pays early, for discrete input). Its own module because
6
+ // nodes.js already imports events.js, and the dispatcher cannot import the
7
+ // node back without a cycle.
8
+ //
9
+ // The set answers "which windows have unpainted damage" in O(1) at event
10
+ // time. A tree walk would answer it too, but it is walked once per discrete
11
+ // event on a tree that can be thousands of nodes, to find the one or two
12
+ // roots that a `Set.add` in `invalidate()` already knew about.
13
+
14
+ const pending = new Set();
15
+
16
+ /** `node` has damage and no paint yet. Idempotent. */
17
+ export function addPendingFrame(node) {
18
+ pending.add(node);
19
+ }
20
+
21
+ /** `node` is painting (or has nothing left to paint). */
22
+ export function clearPendingFrame(node) {
23
+ pending.delete(node);
24
+ }
25
+
26
+ /**
27
+ * Paint every window that owes a frame and whose server is caught up — the
28
+ * synchronous half of the frame clock (issue #141).
29
+ *
30
+ * A discrete input has exactly one visual response: a button darkens, a
31
+ * caret lands, focus moves. Scheduling that response through
32
+ * `requestAnimationFrame` charges it up to a frame interval plus a fence
33
+ * round trip before it reaches the wire, on top of a paint that measures in
34
+ * the hundreds of microseconds. So the dispatcher calls this when the
35
+ * handler unwinds, and the pixels go out with the handler's own requests
36
+ * instead.
37
+ *
38
+ * Two things make that safe to do unconditionally at the call site:
39
+ *
40
+ * - **The frame gate.** A window whose last frame has not landed keeps the
41
+ * frame it already scheduled. Painting it now would be work nobody sees:
42
+ * ntk defers the present until the frame ends, where it coalesces with
43
+ * the one already pending. This is what keeps a burst sane — spin a wheel
44
+ * and the first notch paints immediately, the rest fold into one catch-up
45
+ * frame. "Update instantly, then catch up", with the frame clock as the
46
+ * backpressure.
47
+ *
48
+ * On ntk >= 7 what ends a frame is the display: the server reports the
49
+ * present it executed at a vertical blank, so the gate now means "the
50
+ * display has not shown the last frame" rather than "the socket has not
51
+ * acknowledged it". Same call, later and truer signal — and it is also
52
+ * what keeps us off a backing pixmap the server is still copying from.
53
+ * - **Every window answers for itself.** The set is app-wide, so a click
54
+ * that dirtied both a menu and the window under it paints both, each
55
+ * gated on its own window's frame clock.
56
+ *
57
+ * A window whose scheduled frame this pays off keeps that scheduled
58
+ * callback; it runs and finds `needsPaint` false, which is already a cheap
59
+ * no-op.
60
+ */
61
+ export function flushPendingFrames() {
62
+ if (pending.size === 0) return;
63
+ for (const node of [...pending]) {
64
+ const wnd = node.window;
65
+ // ntk < 5.2.0 (sidorares/ntk#148) cannot say whether the frame has
66
+ // landed, and a missing answer is not a "no" — without the gate a burst
67
+ // would paint every event, so the paced frame stays the safe choice.
68
+ // Same for a headless mock window that models no frame clock at all.
69
+ if (typeof wnd?.frameInFlight !== 'function') continue;
70
+ if (wnd.frameInFlight()) continue;
71
+ node.flush();
72
+ }
73
+ }
@@ -0,0 +1,114 @@
1
+ // Which GL backend a connection got, asked synchronously.
2
+ //
3
+ // The question is about the **connection**, not the machine. ntk's
4
+ // `glCapabilities()` answers the machine one — "could direct rendering work
5
+ // here" — and consults `glPolicy` only far enough to honour `'off'`, so on a
6
+ // DRI3-capable box it resolves `{ direct: true }` under the default
7
+ // `'indirect'` policy too. What actually draws is what the policy asked for,
8
+ // so that is what this module reports: the same rule ntk's own `backendFor()`
9
+ // applies when `getContext` picks a backend, kept in step here because it is
10
+ // not part of ntk's public API (issue #357).
11
+ //
12
+ // ntk settles the probe during its connection handshake — it runs it whenever
13
+ // `glPolicy` could select direct — so by the time any element is created the
14
+ // answer is a property read rather than a round trip. That is what lets
15
+ // `<shaderMaterial>` fail at creation, with a reason, instead of rendering an
16
+ // empty surface.
17
+ //
18
+ // A leaf module on purpose: it imports nothing of ours, so both
19
+ // `appcontext.js` (for `useSupports('shaders')`) and the node layer can use
20
+ // it without the two importing each other.
21
+
22
+ import { GLError } from 'ntk';
23
+
24
+ /** Does this connection's policy ever choose the direct backend? */
25
+ const wantsDirect = (app) => {
26
+ const mode = app?.glPolicy?.mode;
27
+ return mode === 'auto' || mode === 'direct';
28
+ };
29
+
30
+ /** ntk's coded-error shape — `code` to branch on, `hint` for what to do. */
31
+ function glError(code, message, hint) {
32
+ const err = new Error(message);
33
+ err.code = code;
34
+ if (hint) err.hint = hint;
35
+ return err;
36
+ }
37
+
38
+ /**
39
+ * Is the direct backend — the one with shaders — what this connection draws
40
+ * through?
41
+ *
42
+ * False under a policy that never asks for it, whatever the machine could do,
43
+ * because the policy is what decides which backend is built.
44
+ */
45
+ export function hasDirectGL(app) {
46
+ return wantsDirect(app) && !!app._glCapsResolved?.direct;
47
+ }
48
+
49
+ /**
50
+ * Why it is not, as a coded error — or null when it is available, or when the
51
+ * probe that would say has not answered yet.
52
+ *
53
+ * Worth surfacing rather than describing: the reasons are genuinely different
54
+ * (a policy that never asked, no GPU addon, a connection that cannot pass
55
+ * descriptors, a server without DRI3) and only one of them is "turn the
56
+ * policy on". A message that guesses sends people to check the wrong thing.
57
+ *
58
+ * The policy reasons are minted here because ntk has none to give: its probe
59
+ * consults the mode only for `'off'`, so a connection left on the default
60
+ * `'indirect'` carries capabilities with no `reason` in them at all — and on
61
+ * a capable machine, with `direct: true` in them.
62
+ */
63
+ export function directGLFailure(app) {
64
+ const mode = app?.glPolicy?.mode;
65
+ if (mode === 'off') {
66
+ return glError(
67
+ GLError.DISABLED,
68
+ "glPolicy is 'off' on this connection, so no GL context is created at all",
69
+ "No <glarea> draws under this policy. Pass glPolicy: 'auto'\n" +
70
+ 'to createRoot(), or drop the option, to get a context back.',
71
+ );
72
+ }
73
+ if (!wantsDirect(app)) {
74
+ return glError(
75
+ 'GL_POLICY_INDIRECT',
76
+ "glPolicy is 'indirect' — ntk's default — so this connection draws " +
77
+ 'through indirect GLX, which has no shaders whatever this machine could do',
78
+ "createRoot({ glPolicy: 'auto' }) takes the direct backend where it is\n" +
79
+ 'available and this one everywhere else, and NTK_GL_POLICY=auto switches\n' +
80
+ 'a single run. app.glCapabilities() still answers what the machine can do.',
81
+ );
82
+ }
83
+ const caps = app._glCapsResolved;
84
+ return caps && !caps.direct ? (caps.reason ?? null) : null;
85
+ }
86
+
87
+ /**
88
+ * Call `onChange` when that answer settles, and hand back the unsubscribe —
89
+ * the subscribe half of `useSupports('shaders')`.
90
+ *
91
+ * It settles at most once and then holds still. Under a policy that could
92
+ * pick direct, `createRoot()` waits for ntk's probe before it hands the app
93
+ * back, so the first render already reads the final answer and there is
94
+ * nothing to watch; under one that could not, the answer is false and stays
95
+ * false. What is left is a policy raised after connecting, which has missed
96
+ * the handshake probe — `glCapabilities()` is idempotent and cached, and is
97
+ * the same call ntk makes lazily when a context is created, so asking it here
98
+ * settles the answer rather than leaving one component reading false and the
99
+ * next reading true.
100
+ */
101
+ export function watchDirectGL(app, onChange) {
102
+ if (!wantsDirect(app) || app._glCapsResolved) return () => {};
103
+ if (typeof app.glCapabilities !== 'function') return () => {};
104
+ let live = true;
105
+ // a probe that throws is a probe that found nothing: the answer stays
106
+ // false, and re-reading a boolean that did not move costs a comparison
107
+ const settled = () => {
108
+ if (live) onChange();
109
+ };
110
+ app.glCapabilities().then(settled, settled);
111
+ return () => {
112
+ live = false;
113
+ };
114
+ }
package/src/glnodes.js CHANGED
@@ -5,40 +5,79 @@
5
5
  // (NEXT_STEPS §4), sized and positioned by the parent's yoga layout like any
6
6
  // other drawn node. Everything about the surface is here; the scene graph
7
7
  // that draws into it comes later (docs/glx-plan.md).
8
- import { cssColor } from 'ntk';
8
+ import { cssColorStraight } from 'ntk';
9
+
10
+ // re-exported so the GL element layer stays one import for consumers
11
+ export { directGLFailure, hasDirectGL } from './glbackend.js';
9
12
 
10
13
  import { Node } from './nodes.js';
11
- import { ScenePointer, sceneWantsPointer } from './pointer3d.js';
12
- import { SceneRenderer } from './scene3d.js';
13
14
 
14
15
  // One visual query per (app, spec): GetFBConfigs is a round trip and every
15
16
  // <glarea> in an app wants the same answer.
16
17
  const configCache = new WeakMap();
17
18
 
19
+ // Whether GL setup has already been found impossible on this connection.
20
+ // Every reason it fails — no GLX extension, indirect GLX disabled, no
21
+ // matching visual — is a property of the X server, not of one surface, so
22
+ // the first <glarea> to find out saves the rest from asking, so a second
23
+ // surface can report `onError` on its first frame instead of showing an
24
+ // empty box for the round trip it would otherwise take.
25
+ const glxFailures = new WeakMap();
26
+
27
+ /** The error that made GL unavailable on `app`, or null if all is well (or
28
+ * not yet known). Carries ntk's `code` — see `GLXError`. */
29
+ export function glxFailure(app) {
30
+ return (app && glxFailures.get(app)) || null;
31
+ }
32
+
33
+ function recordGlxFailure(app, err) {
34
+ if (app && err && !glxFailures.has(app)) glxFailures.set(app, err);
35
+ }
36
+
37
+ /**
38
+ * The visual and depth to create the GL child window with.
39
+ *
40
+ * `chooseGLConfig` answers for whichever backend ntk's `glPolicy` selected
41
+ * and tags the result with `backend`; it is the newer name and the one to
42
+ * prefer, because asking the older `chooseGLXConfig` would pin the surface to
43
+ * indirect GLX no matter what the policy says. The fallback keeps `<glarea>`
44
+ * working on an ntk that predates the direct backend.
45
+ */
18
46
  export function glxConfig(app, spec) {
19
47
  const key = JSON.stringify(spec ?? null);
20
48
  let perApp = configCache.get(app);
21
49
  if (!perApp) configCache.set(app, (perApp = new Map()));
22
50
  let promise = perApp.get(key);
23
51
  if (!promise) {
24
- promise =
25
- typeof app.chooseGLXConfig === 'function'
26
- ? app.chooseGLXConfig(spec)
27
- : Promise.reject(
28
- new Error(
29
- 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLXConfig)',
30
- ),
31
- );
52
+ if (typeof app.chooseGLConfig === 'function') {
53
+ promise = app.chooseGLConfig(spec);
54
+ } else if (typeof app.chooseGLXConfig === 'function') {
55
+ promise = app
56
+ .chooseGLXConfig(spec)
57
+ .then((config) => ({ backend: 'indirect', ...config }));
58
+ } else {
59
+ promise = Promise.reject(
60
+ new Error(
61
+ 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
62
+ ),
63
+ );
64
+ }
32
65
  perApp.set(key, promise);
33
66
  }
34
67
  return promise;
35
68
  }
36
69
 
37
- /** clearColor as a CSS string or an [r, g, b, a] float tuple. */
70
+ /**
71
+ * clearColor as a CSS string or an [r, g, b, a] float tuple.
72
+ *
73
+ * Straight alpha, not premultiplied: this goes to `glClearColor`, and GL
74
+ * takes unassociated components. ntk's `cssColor` premultiplies for XRender,
75
+ * which would darken any translucent clear colour.
76
+ */
38
77
  function clearColorOf(props) {
39
78
  const value = props.clearColor ?? 'black';
40
79
  if (Array.isArray(value)) return value.length === 4 ? value : [...value, 1];
41
- const parsed = cssColor(value);
80
+ const parsed = cssColorStraight(value);
42
81
  return parsed ?? [0, 0, 0, 1];
43
82
  }
44
83
 
@@ -74,8 +113,6 @@ export class GlAreaNode extends Node {
74
113
  this._realizing = false;
75
114
  this._frameScheduled = false;
76
115
  this._created = false;
77
- this.scene = new SceneRenderer(this);
78
- this.pointer = new ScenePointer(this);
79
116
  this._pointerDirty = true;
80
117
  }
81
118
 
@@ -95,24 +132,55 @@ export class GlAreaNode extends Node {
95
132
  if (this.window || this.destroyed || this._realizing) return;
96
133
  const parent = this.root?.window;
97
134
  if (!parent || typeof this.app?.createWindow !== 'function') return;
135
+ // asked and answered — and whoever asked first already reported it
136
+ const known = glxFailure(this.app);
137
+ if (known) return this._failed(known, { reported: true });
98
138
  this._realizing = true;
99
139
  glxConfig(this.app, this.props.glx).then(
100
140
  (config) => {
101
141
  this._realizing = false;
102
142
  if (this.destroyed || this.window) return;
103
- this._create(config, parent);
143
+ // getContext() throws rather than rejects when the display has no
144
+ // GLX at all, and a throw in here would escape as an unhandled
145
+ // rejection instead of reaching onError
146
+ try {
147
+ this._create(config, parent);
148
+ } catch (err) {
149
+ this._failed(err);
150
+ }
104
151
  },
105
152
  (err) => {
106
153
  this._realizing = false;
107
- this.error = err;
108
- this.props.onError?.(err);
109
- if (!this.props.onError) {
110
- console.warn(`react-x11: <glarea> has no GL surface: ${err.message}`);
111
- }
154
+ this._failed(err);
112
155
  },
113
156
  );
114
157
  }
115
158
 
159
+ /**
160
+ * GL is not available: report it once, and give up the X child window if
161
+ * one was created. It would otherwise sit over this rect as an unpainted
162
+ * hole, hiding whatever the fallback draws in its place.
163
+ *
164
+ * `reported` means ntk has already written the diagnosis to the console —
165
+ * it does that for a failed context, and its message is the better one.
166
+ */
167
+ _failed(err, { reported = false } = {}) {
168
+ this._realizing = false;
169
+ if (this.error) return;
170
+ this.error = err;
171
+ recordGlxFailure(this.app, err);
172
+ this.gl = null;
173
+ if (this.window) {
174
+ this.window.destroy?.();
175
+ this.window = null;
176
+ this.rect = null;
177
+ }
178
+ if (this.props.onError) this.props.onError(err);
179
+ else if (!reported) {
180
+ console.warn(`react-x11: <glarea> has no GL surface: ${err.message}`);
181
+ }
182
+ }
183
+
116
184
  _create(config, parent) {
117
185
  const rect = this._geometry();
118
186
  const wnd = this.app.createWindow({
@@ -132,6 +200,17 @@ export class GlAreaNode extends Node {
132
200
  this.config = config;
133
201
  wnd._reactX11Node = this;
134
202
  this.gl = wnd.getContext('opengl', config);
203
+ // a buffer freed by the display is a frame that can be drawn again
204
+ if (typeof this.gl?.onFrameAvailable !== 'undefined') {
205
+ this.gl.onFrameAvailable = () => this.requestFrame();
206
+ }
207
+ // The context is only usable once MakeCurrent has answered, and that is
208
+ // where a server refusing indirect GLX says so (ntk gives the rejection
209
+ // an err.code — see GLXError). Nothing here awaits it: GL calls queue
210
+ // until the tag arrives, so this only has to catch the failure.
211
+ this.gl?.ready?.catch((err) => {
212
+ if (!this.destroyed) this._failed(err, { reported: true });
213
+ });
135
214
  wnd.on?.('expose', () => this.requestFrame());
136
215
  wnd.map?.();
137
216
  this.requestFrame();
@@ -191,18 +270,37 @@ export class GlAreaNode extends Node {
191
270
  _drawFrame() {
192
271
  const gl = this.gl;
193
272
  if (!gl || this.destroyed) return;
273
+ const direct = gl.backend === 'direct';
274
+ // On the direct backend every buffer may still be held by the display,
275
+ // and drawing into one before it comes back would paint what is on
276
+ // screen. `onFrameAvailable` asks for this frame again when one frees.
277
+ if (direct && gl.canRender && !gl.canRender()) return;
278
+ // binds this surface — the GPU context is shared between every <glarea>
279
+ // on the connection — and picks up a resize
280
+ gl.makeCurrent?.();
281
+
194
282
  const { width, height } = this.rect;
195
- const info = { width, height, node: this };
283
+ // x/y are where the node's origin sits in the drawable being drawn
284
+ // into (DrawInfo's contract) — a <glarea> draws into its own X window,
285
+ // so that is the origin.
286
+ const info = { width, height, x: 0, y: 0, node: this };
196
287
  if (!this._created) {
197
288
  this._created = true;
198
289
  this.props.onCreated?.(gl, info);
199
290
  }
200
- this._syncPointerListeners();
201
- gl.Viewport(0, 0, width, height);
202
291
  const [r, g, b, a] = clearColorOf(this.props);
203
- gl.ClearColor(r, g, b, a);
204
- gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
205
- this.scene.render(gl, info);
292
+ // The two backends spell GL differently — PascalCase OpenGL 1.x against
293
+ // camelCase ES 2 — and neither pretends to be the other, so the handful
294
+ // of calls this element makes itself are written both ways.
295
+ if (direct) {
296
+ gl.viewport(0, 0, width, height);
297
+ gl.clearColor(r, g, b, a);
298
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
299
+ } else {
300
+ gl.Viewport(0, 0, width, height);
301
+ gl.ClearColor(r, g, b, a);
302
+ gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
303
+ }
206
304
  this.props.onDraw?.(gl, info);
207
305
  gl.SwapBuffers();
208
306
  if (this.props.frameLoop === 'always') this.requestFrame();
@@ -220,45 +318,6 @@ export class GlAreaNode extends Node {
220
318
  else this.window?.map?.();
221
319
  }
222
320
 
223
- /**
224
- * X pointer events are only worth selecting when something in the scene
225
- * listens for them — the r3f rule that only handler-bearing objects take
226
- * part in picking, applied one level up, to the wire.
227
- */
228
- _syncPointerListeners() {
229
- if (!this._pointerDirty || !this.window || this.pointer.attached) return;
230
- this._pointerDirty = false;
231
- if (!sceneWantsPointer(this.children)) return;
232
- this.pointer.attach(this.window);
233
- }
234
-
235
- /** A scene node was added, removed, or gained/lost pointer handlers. */
236
- _sceneChanged() {
237
- this._pointerDirty = true;
238
- this.requestFrame();
239
- }
240
-
241
- /** scene children (<mesh>, <group>, …) attach to this surface. */
242
- insertBefore(child, beforeChild) {
243
- super.insertBefore(child, beforeChild);
244
- child._setSurface?.(this);
245
- this._sceneChanged();
246
- }
247
-
248
- removeChild(child) {
249
- super.removeChild(child);
250
- this.invalidateGeometry(child);
251
- this.pointer.forget(child);
252
- this._sceneChanged();
253
- }
254
-
255
- /** A removed geometry's display list is no longer needed server-side. */
256
- invalidateGeometry(node) {
257
- if (!node) return;
258
- if (node.isGeometry) this.scene.forget(this.gl, node);
259
- for (const child of node.children ?? []) this.invalidateGeometry(child);
260
- }
261
-
262
321
  // the child window covers this rect: nothing to paint into the parent's
263
322
  // 2d context, and no drawn children are allowed under it
264
323
  paint() {}
@@ -266,7 +325,6 @@ export class GlAreaNode extends Node {
266
325
  destroySubtree() {
267
326
  if (this.destroyed) return;
268
327
  super.destroySubtree();
269
- this.scene.dispose(this.gl);
270
328
  this.gl?.destroy?.();
271
329
  this.gl = null;
272
330
  this.window?.destroy?.();