react-x11 2.18.3 → 2.18.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.18.3",
3
+ "version": "2.18.5",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
package/src/Reconciler.js CHANGED
@@ -67,6 +67,7 @@ import { watchAppearance } from './appearance.js';
67
67
  import { setDesktopIntegration } from './desktopintegration.js';
68
68
  import { ForeignNode } from './foreignnodes.js';
69
69
  import { GlAreaNode } from './glnodes.js';
70
+ import { beginGlOverlay } from './gloverlay.js';
70
71
  import {
71
72
  createRegisteredNode,
72
73
  elementDefinition,
@@ -954,11 +955,17 @@ export async function createRoot(options = {}) {
954
955
  // extension; the scale ladder; the monitor layout and the work-area
955
956
  // property), so they run concurrently: each is internally a chain of one
956
957
  // to three round trips, and awaiting them in sequence made every app pay
957
- // the sum where the slowest chain is the true floor.
958
+ // the sum where the slowest chain is the true floor. The fourth is a
959
+ // single extension query, too short to set that floor: whether this is
960
+ // XQuartz, the X server nothing can be drawn over a GL surface on, which
961
+ // decides whether a `<glarea>`'s children get panes and what
962
+ // `useSupports('glOverlay')` answers on the first render
963
+ // (src/gloverlay.js).
958
964
  await Promise.all([
959
965
  beginCompositing(app),
960
966
  beginScale(app, rest.scale),
961
967
  beginScreens(app),
968
+ beginGlOverlay(app),
962
969
  ]);
963
970
 
964
971
  // How fast a caret blinks, how long a double click has, how far a press
package/src/appcontext.js CHANGED
@@ -60,9 +60,10 @@ export function useApp() {
60
60
  }
61
61
 
62
62
  // 'nativeControls', 'embedding' and 'glOverlay' are properties of the
63
- // backend, decided before the first render and never changing after — so
64
- // their subscription has nothing to deliver and their snapshot is a property
65
- // test.
63
+ // backend — and 'glOverlay' of the X server too, which `createRoot` asks
64
+ // before the first render (src/gloverlay.js) — decided before anything
65
+ // renders and never changing after, so their subscription has nothing to
66
+ // deliver and their snapshot is a property test.
66
67
  const NEVER_CHANGES = () => () => {};
67
68
 
68
69
  // What `useSupports` watches and reads, per feature. `read` answers a
@@ -169,7 +170,10 @@ const FEATURES = {
169
170
  * GL surface on this connection — laid out in its box, painted on panes over
170
171
  * the surface, hit before it. Both backends draw them; what differs is
171
172
  * translucency, composited by Core Animation on the Cocoa backend and opaque
172
- * on X11 (docs/elements.md says exactly how). It is the question to ask
173
+ * on X11 (docs/elements.md says exactly how). The exception is XQuartz,
174
+ * where it is false: the macOS window server composites every GL surface
175
+ * above everything the X server draws, so nothing can be drawn over one, and
176
+ * the children are laid out but never drawn. It is the question to ask
173
177
  * before handing a surface its HUD rather than drawing that some other way:
174
178
  *
175
179
  * ```jsx
@@ -177,7 +181,8 @@ const FEATURES = {
177
181
  * <glarea onDraw={drawMap}>{overlay && <Legend />}</glarea>
178
182
  * ```
179
183
  *
180
- * A property of the backend too, and it never changes.
184
+ * A property of the backend and the display, settled before the first
185
+ * render, and it never changes.
181
186
  */
182
187
  export function useSupports(feature) {
183
188
  const app = useApp();
@@ -324,6 +324,17 @@ export class BackendContext2D {
324
324
  // chunked stroke leaves only its last chunk behind)
325
325
  this._cmds = [];
326
326
  this._pathStale = false;
327
+ // A bridge that takes a whole path in one call (`ctxPath`, the Windows
328
+ // bridge's) is handed it that way when something paints it: the verbs
329
+ // below only record. A graph's edges are tens of thousands of `lineTo`s
330
+ // a frame, and a call across the boundary for each was milliseconds of
331
+ // every frame spent on the crossing. A bridge without it is handed the
332
+ // path a point at a time, as it is built, exactly as before.
333
+ // Asked with `in` as well: a test's bridge that answers every name with
334
+ // a no-op must not look like one that takes paths whole.
335
+ this._bulkPaths =
336
+ 'ctxPath' in native && typeof native.ctxPath === 'function';
337
+ this._pathBuf = null;
327
338
  this._strokeChunking = !NO_STROKE_CHUNKING;
328
339
  }
329
340
 
@@ -670,6 +681,21 @@ export class BackendContext2D {
670
681
  _emit(surface, from, to) {
671
682
  const c = this._cmds;
672
683
  const n = this._native;
684
+ if (this._bulkPaths) {
685
+ // one call, through a buffer kept between paths — the record is the
686
+ // same stream the bridge parses, arguments and all
687
+ const count = to - from;
688
+ if (count <= 0) return;
689
+ let buf = this._pathBuf;
690
+ if (!buf || buf.length < count) {
691
+ buf = this._pathBuf = new Float64Array(
692
+ Math.max(count, (buf?.length ?? 512) * 2),
693
+ );
694
+ }
695
+ for (let i = 0; i < count; i++) buf[i] = c[from + i];
696
+ n.ctxPath(surface, buf.subarray(0, count));
697
+ return;
698
+ }
673
699
  for (let i = from; i < to;) {
674
700
  const op = c[i];
675
701
  const a = i + 1;
@@ -720,26 +746,44 @@ export class BackendContext2D {
720
746
 
721
747
  beginPath() {
722
748
  this._cmds.length = 0;
749
+ if (this._bulkPaths) {
750
+ this._s();
751
+ // uploaded, empty or not, when something paints it
752
+ this._pathStale = true;
753
+ return;
754
+ }
723
755
  this._pathStale = false;
724
756
  this._native.ctxBeginPath(this._s());
725
757
  }
726
758
 
759
+ /**
760
+ * The surface to add a path command to now, or null where the path is
761
+ * only recorded and handed over whole when painted (`_bulkPaths`). Either
762
+ * way a fresh surface drops the path first, as `_s` has always done.
763
+ */
764
+ _pathFor() {
765
+ if (!this._bulkPaths) return this._path();
766
+ this._s();
767
+ this._pathStale = true;
768
+ return null;
769
+ }
770
+
727
771
  moveTo(x, y) {
728
- const surface = this._path();
772
+ const surface = this._pathFor();
729
773
  this._cmds.push(P_MOVE, x, y);
730
- this._native.ctxMoveTo(surface, x, y);
774
+ if (surface !== null) this._native.ctxMoveTo(surface, x, y);
731
775
  }
732
776
 
733
777
  lineTo(x, y) {
734
- const surface = this._path();
778
+ const surface = this._pathFor();
735
779
  this._cmds.push(P_LINE, x, y);
736
- this._native.ctxLineTo(surface, x, y);
780
+ if (surface !== null) this._native.ctxLineTo(surface, x, y);
737
781
  }
738
782
 
739
783
  rect(x, y, w, h) {
740
- const surface = this._path();
784
+ const surface = this._pathFor();
741
785
  this._cmds.push(P_RECT, x, y, w, h);
742
- this._native.ctxRect(surface, x, y, w, h);
786
+ if (surface !== null) this._native.ctxRect(surface, x, y, w, h);
743
787
  }
744
788
 
745
789
  roundRect(x, y, w, h, radii) {
@@ -750,7 +794,7 @@ export class BackendContext2D {
750
794
  else if (r.length === 3) r = [r[0], r[1], r[2], r[1]];
751
795
  const cap = Math.min(Math.abs(w) / 2, Math.abs(h) / 2);
752
796
  const clamp = (v) => Math.max(0, Math.min(Number(v) || 0, cap));
753
- const surface = this._path();
797
+ const surface = this._pathFor();
754
798
  const [r0, r1, r2, r3] = [
755
799
  clamp(r[0]),
756
800
  clamp(r[1]),
@@ -758,37 +802,40 @@ export class BackendContext2D {
758
802
  clamp(r[3]),
759
803
  ];
760
804
  this._cmds.push(P_ROUND, x, y, w, h, r0, r1, r2, r3);
761
- this._native.ctxRoundRect(surface, x, y, w, h, r0, r1, r2, r3);
805
+ if (surface !== null)
806
+ this._native.ctxRoundRect(surface, x, y, w, h, r0, r1, r2, r3);
762
807
  }
763
808
 
764
809
  arc(x, y, radius, start, end, anticlockwise = false) {
765
- const surface = this._path();
810
+ const surface = this._pathFor();
766
811
  this._cmds.push(P_ARC, x, y, radius, start, end, anticlockwise ? 1 : 0);
767
- this._native.ctxArc(surface, x, y, radius, start, end, anticlockwise);
812
+ if (surface !== null)
813
+ this._native.ctxArc(surface, x, y, radius, start, end, anticlockwise);
768
814
  }
769
815
 
770
816
  ellipse(x, y, rx, ry) {
771
- const surface = this._path();
817
+ const surface = this._pathFor();
772
818
  this._cmds.push(P_ELLIPSE, x, y, rx, ry);
773
- this._native.ctxEllipse(surface, x, y, rx, ry);
819
+ if (surface !== null) this._native.ctxEllipse(surface, x, y, rx, ry);
774
820
  }
775
821
 
776
822
  bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
777
- const surface = this._path();
823
+ const surface = this._pathFor();
778
824
  this._cmds.push(P_CURVE, c1x, c1y, c2x, c2y, x, y);
779
- this._native.ctxCurveTo(surface, c1x, c1y, c2x, c2y, x, y);
825
+ if (surface !== null)
826
+ this._native.ctxCurveTo(surface, c1x, c1y, c2x, c2y, x, y);
780
827
  }
781
828
 
782
829
  quadraticCurveTo(cx, cy, x, y) {
783
- const surface = this._path();
830
+ const surface = this._pathFor();
784
831
  this._cmds.push(P_QUAD, cx, cy, x, y);
785
- this._native.ctxQuadTo(surface, cx, cy, x, y);
832
+ if (surface !== null) this._native.ctxQuadTo(surface, cx, cy, x, y);
786
833
  }
787
834
 
788
835
  closePath() {
789
- const surface = this._path();
836
+ const surface = this._pathFor();
790
837
  this._cmds.push(P_CLOSE);
791
- this._native.ctxClosePath(surface);
838
+ if (surface !== null) this._native.ctxClosePath(surface);
792
839
  }
793
840
 
794
841
  // --- painting ------------------------------------------------------------
package/src/glnodes.js CHANGED
@@ -10,9 +10,10 @@ import { cssColorStraight } from 'ntk';
10
10
  // re-exported so the GL element layer stays one import for consumers
11
11
  export { directGLFailure, hasDirectGL } from './glbackend.js';
12
12
 
13
- import { GlOverlay, canOverlay } from './gloverlay.js';
13
+ import { GlOverlay, canOverlay, overlayRefusal } from './gloverlay.js';
14
14
  import { layoutDiff } from './nodes/damage.js';
15
15
  import { Node } from './nodes/node.js';
16
+ import { DEV } from './nodes/util.js';
16
17
  import { FramePacer, resolveFrameRate } from './pacing.js';
17
18
 
18
19
  // One visual query per (app, spec): GetFBConfigs is a round trip and every
@@ -37,6 +38,27 @@ function recordGlxFailure(app, err) {
37
38
  if (app && err && !glxFailures.has(app)) glxFailures.set(app, err);
38
39
  }
39
40
 
41
+ // Apps already told that a <glarea>'s children are not drawn on their
42
+ // display. Once per app, as for a <foreign> that cannot embed: the answer
43
+ // belongs to the server, so the next surface to find out carries no news.
44
+ const warnedNoOverlay = new WeakSet();
45
+
46
+ /**
47
+ * Say, once per app and in development only, that the children of a
48
+ * `<glarea>` are not drawn here. They lay out and render like anyone's and
49
+ * nothing fails, so without this a HUD that never shows looks like a bug in
50
+ * the HUD.
51
+ */
52
+ function warnNoOverlay(app) {
53
+ if (!DEV || !app || warnedNoOverlay.has(app)) return;
54
+ warnedNoOverlay.add(app);
55
+ const why = overlayRefusal(app);
56
+ console.warn(
57
+ 'react-x11: the children of a <glarea> are not drawn on this connection, ' +
58
+ `and useSupports('glOverlay') is false${why ? ` — ${why}` : ''}.`,
59
+ );
60
+ }
61
+
40
62
  /**
41
63
  * The visual and depth to create the GL child window with.
42
64
  *
@@ -121,7 +143,9 @@ const px = (v) => Math.max(1, Math.round(v || 0));
121
143
  * The X child window is stacked above everything drawn in the parent, so the
122
144
  * parent's 2D content cannot overlap it — but this node's own children do:
123
145
  * they are laid out in its box like a `<box>`'s and drawn above the surface,
124
- * on panes of their own (src/gloverlay.js).
146
+ * on panes of their own (src/gloverlay.js). Everywhere but XQuartz, where
147
+ * nothing can be drawn over a GL surface: there they are laid out and not
148
+ * drawn (`canOverlay`).
125
149
  *
126
150
  * Pointer input over the surface is the tree's, on both backends: a press,
127
151
  * a drag or a wheel over it is a synthetic event at the child under the
@@ -636,7 +660,14 @@ export class GlAreaNode extends Node {
636
660
  */
637
661
  _syncOverlay() {
638
662
  if (!this._overlay) {
639
- if (this.children.length === 0 || !canOverlay(this.app)) {
663
+ // Where nothing can be drawn over the surface the children get no
664
+ // panes at all, rather than panes nobody sees: on XQuartz a pane over
665
+ // the whole surface is what hides the GL frame (src/gloverlay.js). So
666
+ // they are laid out and never drawn, and the pointer over them is the
667
+ // surface's.
668
+ const refused = this.children.length !== 0 && !canOverlay(this.app);
669
+ if (refused) warnNoOverlay(this.app);
670
+ if (this.children.length === 0 || refused) {
640
671
  this.root?._overlaid?.delete(this);
641
672
  return false;
642
673
  }
package/src/globalmenu.js CHANGED
@@ -108,6 +108,13 @@ const NO_AUTO_START = 2;
108
108
  */
109
109
  const menuPathFor = (xid) => `/com/react_x11/menus/${xid}`;
110
110
 
111
+ /**
112
+ * Per window, the moment it is free: settles once every exporter that has
113
+ * published on it so far has stopped. The next one to publish there waits for
114
+ * it — see `GlobalMenuExport.takeTurn()`.
115
+ */
116
+ const vacancies = new Map();
117
+
111
118
  /**
112
119
  * The feature's off switch.
113
120
  *
@@ -167,6 +174,20 @@ export class GlobalMenuExport extends DbusMenuExport {
167
174
  this.path = null;
168
175
  this.xid = null;
169
176
  this.window = null;
177
+ /** The windows this exporter has queued for, by xid. Each is held until
178
+ * `stop()`, so publishing on one again does not queue behind itself —
179
+ * see `takeTurn()`. */
180
+ this.claimed = new Set();
181
+ /** Settles when `stop()` is called, `stopped` as a promise, so a publish
182
+ * still waiting for its window stops waiting. */
183
+ this.halted = new Promise((resolve) => {
184
+ this.halt = resolve;
185
+ });
186
+ /** Settles once `stop()` has finished, which is what the next exporter on
187
+ * a window this one claimed waits for — see `takeTurn()`. */
188
+ this.gone = new Promise((resolve) => {
189
+ this.markGone = resolve;
190
+ });
170
191
  }
171
192
 
172
193
  // ------------------------------------------------------------------ setup
@@ -281,6 +302,21 @@ export class GlobalMenuExport extends DbusMenuExport {
281
302
  // call" case rather than the "too early" one.
282
303
  if (!xid) return;
283
304
 
305
+ // One menu per window at a time — see `takeTurn()`.
306
+ const before = this.takeTurn(xid);
307
+ if (before) {
308
+ // Given up if this one is stopped meanwhile. That frees the window no
309
+ // sooner: whoever is next in line waits for `before` as well.
310
+ await Promise.race([before, this.halted]);
311
+ // How long that took is up to the menu ahead, and for a second bar in
312
+ // the window it is that bar's whole mount. So what `_sync()` decided
313
+ // before it is stale, and it decides again. A panel that went meanwhile
314
+ // would otherwise get a RegisterWindow sent to nobody, and the error
315
+ // ends `start()` in a teardown: a menu that never reaches the next
316
+ // panel either.
317
+ return this._sync();
318
+ }
319
+
284
320
  const dbus = await loadTransport();
285
321
  const path = menuPathFor(xid);
286
322
  if (!dbus.isValidObjectPath(path)) return;
@@ -320,6 +356,49 @@ export class GlobalMenuExport extends DbusMenuExport {
320
356
  this.onChange(true);
321
357
  }
322
358
 
359
+ /**
360
+ * Queue for a window, the first time this exporter publishes on it, and
361
+ * hand back what to wait for: the moment every exporter that published
362
+ * there before this one has stopped, or `null` when there is nothing to
363
+ * wait for.
364
+ *
365
+ * **One menu per window at a time.** Everything a menu holds is per window:
366
+ * the object at `menuPathFor(xid)`, the registrar's entry for the xid, and
367
+ * the two X properties. A `<MenuBar>` remounted in its window, or one with
368
+ * `globalMenu` switched off and on, puts a new exporter on all three while
369
+ * the old one is still leaving — its teardown asks `ListNames` and waits
370
+ * for `UnregisterWindow` before it unexports. That unexport is
371
+ * dbus-native's `registration.remove()`, which removes whatever is at the
372
+ * path when it runs, whoever exported it. So the new menu lost its object
373
+ * while the registrar and the properties still named it, and its bar,
374
+ * told `exported`, drew nothing: no menu anywhere. The old one's
375
+ * unregistration and property deletes race the new one's registration and
376
+ * property writes the same way. They happened to land first, which is
377
+ * harmless, but nothing made them, so the whole publish waits and not only
378
+ * the export. A second bar in a window that already has one waits too: it
379
+ * stays drawn in the window until the first one goes, rather than
380
+ * displacing it.
381
+ *
382
+ * Every one before it, not only the last: a menu stopped while it was
383
+ * still waiting is gone at once, and the one it was waiting for may not
384
+ * be.
385
+ *
386
+ * A window is held until this exporter stops, not only while it is
387
+ * published, so a panel that restarts gets the menu back from the same
388
+ * exporter without it queueing behind itself.
389
+ */
390
+ takeTurn(xid) {
391
+ if (this.claimed.has(xid)) return null;
392
+ this.claimed.add(xid);
393
+ const before = vacancies.get(xid);
394
+ const vacated = Promise.all([before, this.gone]);
395
+ vacancies.set(xid, vacated);
396
+ void vacated.then(() => {
397
+ if (vacancies.get(xid) === vacated) vacancies.delete(xid);
398
+ });
399
+ return before ?? null;
400
+ }
401
+
323
402
  /**
324
403
  * Plasma reads these instead of asking the registrar.
325
404
  *
@@ -390,15 +469,24 @@ export class GlobalMenuExport extends DbusMenuExport {
390
469
  async stop() {
391
470
  if (this.stopped) return;
392
471
  // Set first, so a publish already in flight sees it and undoes itself at
393
- // its next checkpoint rather than finishing.
472
+ // its next checkpoint rather than finishing, and one still waiting for
473
+ // its window stops waiting.
394
474
  this.stopped = true;
395
- // Then wait for it. `teardown()` reads `registration`, `xid` and `window`
396
- // across several awaits of its own, so running it *beside* a publish lets
397
- // each undo the other's work: the classic end state is `exported === true`
398
- // with the object unexported and the window unregistered — no bar in the
399
- // window, nothing in the panel, and no event left that could recover it.
400
- await this.syncing?.catch(() => {});
401
- await this.teardown();
475
+ this.halt();
476
+ try {
477
+ // Then wait for it. `teardown()` reads `registration`, `xid` and
478
+ // `window` across several awaits of its own, so running it *beside* a
479
+ // publish lets each undo the other's work: the classic end state is
480
+ // `exported === true` with the object unexported and the window
481
+ // unregistered — no bar in the window, nothing in the panel, and no
482
+ // event left that could recover it.
483
+ await this.syncing?.catch(() => {});
484
+ await this.teardown();
485
+ } finally {
486
+ // Whatever happened, the window is free: the next menu on it would
487
+ // otherwise wait for good, and never reach the panel.
488
+ this.markGone();
489
+ }
402
490
  }
403
491
 
404
492
  async teardown() {
@@ -427,6 +515,14 @@ export class GlobalMenuExport extends DbusMenuExport {
427
515
  }
428
516
  }
429
517
 
518
+ /** Test seam, not public: forget which windows are held. A previous test's
519
+ * menus are on a bus it has closed, so there is nothing to wait for, and one
520
+ * a failed test never stopped would otherwise hold its window for the rest
521
+ * of the run — every mock app numbers its first window alike. */
522
+ export function _resetGlobalMenuState() {
523
+ vacancies.clear();
524
+ }
525
+
430
526
  /**
431
527
  * Is this window's menu bar being drawn by the desktop?
432
528
  *
package/src/gloverlay.js CHANGED
@@ -35,6 +35,15 @@
35
35
  // node-x11's in-process server, where this is tested, has none — and a
36
36
  // window as big as the surface would keep a backing pixmap the size of a
37
37
  // map to show a legend in its corner.
38
+ // - **XQuartz: no, and not opaquely either.** Every GL surface there, the
39
+ // direct ones Apple-DRI exports and indirect GLX's alike, is a surface of
40
+ // the macOS window server, composited above everything the X server draws
41
+ // in the window (hw/xquartz/xpr/dri.c makes it `XP_MAPPED_ABOVE`). X's
42
+ // stacking never reaches it: a pane stacked over the surface's window is
43
+ // drawn under the frame, cutting that window down with SHAPE leaves the
44
+ // frame covering all of it, and only a pane over the whole surface shows —
45
+ // by hiding the frame (issue #653). So no pane is made there, and
46
+ // `canOverlay` says so from the first render on (`beginGlOverlay`).
38
47
  //
39
48
  // A pane selects no input, so the pointer over one reaches the owning window
40
49
  // by the same propagation that brings it the pointer over the surface
@@ -75,18 +84,85 @@ const MOVE_BLIT_MAX_REPAINT = 0.75;
75
84
  // one variable away from the plain repaint (docs/debugging.md).
76
85
  const NO_BLIT = process.env.REACT_X11_NO_SCROLL_BLIT === '1';
77
86
 
87
+ // Connections on which nothing can be drawn over a GL surface, each with the
88
+ // reason (`beginGlOverlay`), and the probe that finds out, once per app.
89
+ const refusals = new WeakMap();
90
+ const probes = new WeakMap();
91
+
92
+ const XQUARTZ =
93
+ 'this display is XQuartz, where the macOS window server composites every ' +
94
+ 'GL surface above everything the X server draws in its window. Draw the ' +
95
+ 'overlay beside the surface or in GL instead, or run on the Cocoa backend ' +
96
+ '(the default on macOS), which composites it';
97
+
78
98
  /**
79
99
  * Whether the children of a `<glarea>` can be drawn over its surface on this
80
100
  * connection: a backend that composites a pane itself, or one that can make
81
- * the plain child window a pane is on X11. One function for the element and
82
- * for `useSupports('glOverlay')`, which have to agree — the rule `canEmbed`
83
- * follows for `<foreign>`.
101
+ * the plain child window a pane is on X11 — on any server but XQuartz, where
102
+ * nothing can be drawn over a GL surface (`beginGlOverlay`). One function
103
+ * for the element and for `useSupports('glOverlay')`, which have to agree —
104
+ * the rule `canEmbed` follows for `<foreign>`.
84
105
  */
85
106
  export function canOverlay(app) {
86
- return (
87
- typeof app?.createOverlayPane === 'function' ||
88
- typeof app?.createWindow === 'function'
89
- );
107
+ if (typeof app?.createOverlayPane === 'function') return true;
108
+ return typeof app?.createWindow === 'function' && !refusals.has(app);
109
+ }
110
+
111
+ /** Why nothing is drawn over a GL surface on this connection, for the
112
+ * warning a `<glarea>` with children gives there — or null. */
113
+ export function overlayRefusal(app) {
114
+ return (app && refusals.get(app)) ?? null;
115
+ }
116
+
117
+ /**
118
+ * A backend saying nothing can be drawn over a GL surface on this app, and
119
+ * why: one whose child windows are not X windows, and that has no
120
+ * composited pane either — on Windows a window with a parent is a GL
121
+ * surface of its own (src/win32/glarea.js), and a bridge without layers has
122
+ * nothing else to offer. Before the first render, like `beginGlOverlay`, so
123
+ * `useSupports('glOverlay')` never changes its answer.
124
+ */
125
+ export function refuseOverlay(app, reason) {
126
+ if (app) refusals.set(app, reason);
127
+ }
128
+
129
+ /**
130
+ * Is this an X server where nothing can be drawn over a GL surface? Asked
131
+ * in `createRoot`, with the other startup probes and before the first
132
+ * render, so that `canOverlay` answers the same from the first frame on —
133
+ * a component choosing how to draw by it would otherwise build one way and
134
+ * tear that down a frame later.
135
+ *
136
+ * The server is XQuartz, and the question is its Apple-DRI extension, which
137
+ * no other server has: every GL surface XQuartz makes comes from the
138
+ * machinery that extension exports, direct and indirect alike. Asked of the
139
+ * server rather than of `app.glCapabilities()`, because the default policy
140
+ * never probes direct rendering and XQuartz's indirect GLX hides the panes
141
+ * all the same. One QueryExtension; where ntk's direct-rendering probe has
142
+ * asked already, node-x11 answers it from the reply it kept.
143
+ */
144
+ export function beginGlOverlay(app) {
145
+ if (!app) return Promise.resolve();
146
+ let pending = probes.get(app);
147
+ if (pending) return pending;
148
+ const X = app.X;
149
+ pending =
150
+ typeof app.createOverlayPane === 'function' ||
151
+ typeof X?.QueryExtension !== 'function'
152
+ ? Promise.resolve()
153
+ : new Promise((resolve) => {
154
+ try {
155
+ X.QueryExtension('Apple-DRI', (err, reply) => {
156
+ if (!err && reply?.present) refusals.set(app, XQUARTZ);
157
+ resolve();
158
+ });
159
+ } catch {
160
+ // a connection that cannot ask is one this cannot find out about
161
+ resolve();
162
+ }
163
+ });
164
+ probes.set(app, pending);
165
+ return pending;
90
166
  }
91
167
 
92
168
  /** A rect grown out to whole device pixels. */
@@ -590,35 +666,46 @@ export class GlOverlay {
590
666
  _paintPane(pane, passes) {
591
667
  const area = this.area;
592
668
  const root = area.root;
593
- const ctx = pane.context();
594
- if (!ctx || !root) return;
669
+ if (!root) return;
595
670
  // an opaque pane is filled with what the surface starts its frames from;
596
671
  // a composited one is cleared, and shows the frame itself
597
672
  const ground = pane.transparent ? null : groundOf(area.props);
598
- ctx.save();
599
- try {
600
- ctx.translate(-pane.rect.x, -pane.rect.y);
601
- for (const pass of passes) {
602
- ctx.save();
603
- try {
604
- ctx.beginPath();
605
- ctx.rect(pass.x, pass.y, pass.width, pass.height);
606
- ctx.clip();
607
- if (ground) {
608
- ctx.fillStyle = ground;
609
- ctx.fillRect(pass.x, pass.y, pass.width, pass.height);
610
- } else {
611
- ctx.clearRect(pass.x, pass.y, pass.width, pass.height);
612
- }
613
- root._paintDamage = pass;
614
- area._paintChildren(ctx);
615
- } finally {
616
- root._paintDamage = null;
617
- ctx.restore();
673
+ // one pass, drawn by a context in window coordinates
674
+ const paintPass = (ctx, pass) => {
675
+ ctx.save();
676
+ try {
677
+ ctx.beginPath();
678
+ ctx.rect(pass.x, pass.y, pass.width, pass.height);
679
+ ctx.clip();
680
+ if (ground) {
681
+ ctx.fillStyle = ground;
682
+ ctx.fillRect(pass.x, pass.y, pass.width, pass.height);
683
+ } else {
684
+ ctx.clearRect(pass.x, pass.y, pass.width, pass.height);
618
685
  }
686
+ root._paintDamage = pass;
687
+ area._paintChildren(ctx);
688
+ } finally {
689
+ root._paintDamage = null;
690
+ ctx.restore();
691
+ }
692
+ };
693
+ if (typeof pane.wnd.paintPasses === 'function') {
694
+ // A pane drawn a rect at a time — a DirectComposition surface hands
695
+ // out a context per `BeginDraw`, valid until `EndDraw` — opens each
696
+ // pass itself (src/win32/overlay.js), the way such a window takes its
697
+ // frame through `presentFrame`.
698
+ pane.wnd.paintPasses(passes, paintPass);
699
+ } else {
700
+ const ctx = pane.context();
701
+ if (!ctx) return;
702
+ ctx.save();
703
+ try {
704
+ ctx.translate(-pane.rect.x, -pane.rect.y);
705
+ for (const pass of passes) paintPass(ctx, pass);
706
+ } finally {
707
+ ctx.restore();
619
708
  }
620
- } finally {
621
- ctx.restore();
622
709
  }
623
710
  // A layer's contents are a copy the bitmap is pushed to; an X window's
624
711
  // backing store is blitted by ntk on its own, from the paint above
package/src/index.d.ts CHANGED
@@ -216,7 +216,10 @@ export function useClipboard(): Clipboard;
216
216
  * before it. Both backends draw them; translucency is the difference,
217
217
  * composited by Core Animation on Cocoa and opaque on X11, where what a
218
218
  * child leaves unpainted shows the surface's `clearColor` (docs/elements.md).
219
- * Ask it before handing a surface its HUD. A property of the backend.
219
+ * False on XQuartz, whose GL surfaces the macOS window server composites
220
+ * above everything the X server draws: nothing can be drawn over one there.
221
+ * Ask it before handing a surface its HUD. A property of the backend and the
222
+ * display, settled before the first render.
220
223
  */
221
224
  export type SupportsFeature =
222
225
  'transparency' | 'shaders' | 'nativeControls' | 'embedding' | 'glOverlay';
@@ -314,8 +314,16 @@ export class WindowInvalidate {
314
314
  // the whole reason this is not just `needsLayout`: a scroll moves an
315
315
  // offset applied during `absolutize` and leaves every yoga node exactly
316
316
  // as it was, at input rate, on the biggest trees in any app.
317
- if (reason !== 'scroll') this._floorsDirty = true;
318
- if (reason !== 'scroll' && reason !== 'resize') {
317
+ //
318
+ // Nor does a box that only moved (`'position'`, from `applyProps`):
319
+ // absolutely positioned and sized by its own width and height, it is
320
+ // out of every ancestor's flow and sizes nothing inside it by its
321
+ // insets. A card dragged across a graph — or a thousand of them
322
+ // panned — was a full measurement of the content floors a step.
323
+ if (reason !== 'scroll' && reason !== 'position') {
324
+ this._floorsDirty = true;
325
+ }
326
+ if (reason !== 'scroll' && reason !== 'resize' && reason !== 'position') {
319
327
  this._floorsContentDirty = true;
320
328
  }
321
329
  }