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 +1 -1
- package/src/Reconciler.js +8 -1
- package/src/appcontext.js +10 -5
- package/src/backend/context2d.js +65 -18
- package/src/glnodes.js +34 -3
- package/src/globalmenu.js +104 -8
- package/src/gloverlay.js +118 -31
- package/src/index.d.ts +4 -1
- package/src/nodes/invalidate.js +10 -2
- package/src/nodes/node.js +15 -3
- package/src/nodes/scrollblit.js +12 -2
- package/src/nodes/window/flush.js +20 -3
- package/src/statusnotifier.js +49 -7
- package/src/svgnodes.js +76 -15
- package/src/types/elements.d.ts +6 -5
- package/src/win32/a11y.js +66 -6
- package/src/win32/app.js +83 -9
- package/src/win32/fonts.js +26 -7
- package/src/win32/overlay.js +244 -0
- package/src/win32/panewindow.js +6 -0
- package/src/win32/window.js +71 -1
package/package.json
CHANGED
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
|
|
64
|
-
//
|
|
65
|
-
//
|
|
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).
|
|
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
|
|
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();
|
package/src/backend/context2d.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
810
|
+
const surface = this._pathFor();
|
|
766
811
|
this._cmds.push(P_ARC, x, y, radius, start, end, anticlockwise ? 1 : 0);
|
|
767
|
-
|
|
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.
|
|
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.
|
|
823
|
+
const surface = this._pathFor();
|
|
778
824
|
this._cmds.push(P_CURVE, c1x, c1y, c2x, c2y, x, y);
|
|
779
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
ctx.
|
|
601
|
-
|
|
602
|
-
ctx.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
ctx.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
*
|
|
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';
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -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
|
-
|
|
318
|
-
|
|
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
|
}
|