react-x11 2.18.2 → 2.18.4
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/cocoa/overlay.js +40 -3
- package/src/glnodes.js +133 -7
- package/src/globalmenu.js +104 -8
- package/src/gloverlay.js +340 -22
- package/src/index.d.ts +4 -1
- package/src/nodes/animation.js +2 -0
- package/src/nodes/damage.js +8 -0
- package/src/nodes/invalidate.js +126 -10
- package/src/nodes/node.js +24 -4
- package/src/nodes/position.js +11 -2
- package/src/nodes/scrollable.js +5 -2
- package/src/nodes/scrollblit.js +90 -24
- package/src/nodes/window/flush.js +40 -18
- package/src/nodes/window/window.js +4 -0
- package/src/statusnotifier.js +49 -7
- package/src/styles.js +22 -0
- package/src/svgnodes.js +76 -15
- package/src/types/elements.d.ts +6 -5
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/cocoa/overlay.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// so where two surfaces overlap, both overlays are above both frames.
|
|
12
12
|
//
|
|
13
13
|
// It speaks the verbs of a window the overlay drives on X11 — `setState`,
|
|
14
|
-
// `map`, `unmap`, `getContext`, `destroy` — plus `present`,
|
|
15
|
-
// was painted on the layer: ntk blits an X window's backing
|
|
16
|
-
// own, where a layer's contents are a copy the bitmap has to be
|
|
14
|
+
// `map`, `unmap`, `getContext`, `scrollRegion`, `destroy` — plus `present`,
|
|
15
|
+
// which puts what was painted on the layer: ntk blits an X window's backing
|
|
16
|
+
// store on its own, where a layer's contents are a copy the bitmap has to be
|
|
17
|
+
// pushed to.
|
|
17
18
|
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
19
|
import { withoutActions } from './quiet.js';
|
|
19
20
|
|
|
@@ -135,6 +136,42 @@ export class CocoaOverlayPane {
|
|
|
135
136
|
return this._ctx;
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Move the pixels of `rect` (pane coordinates, device px) by (dx, dy)
|
|
141
|
+
* inside the bitmap — ntk `Window.scrollRegion`'s contract, which the
|
|
142
|
+
* overlay speaks to either backend: the band that survives the shift
|
|
143
|
+
* inside `rect` moves, the rest of it is left as it was, and false means
|
|
144
|
+
* nothing moved, so the caller repaints instead. A child that only moved
|
|
145
|
+
* is one of these rather than a repaint (src/gloverlay.js, issue #644).
|
|
146
|
+
*
|
|
147
|
+
* The bitmap is the pane's own and the layer holds a copy of it, so the
|
|
148
|
+
* move lands on screen with the frame's `present`, after the strips it
|
|
149
|
+
* uncovered are painted — never halfway.
|
|
150
|
+
*/
|
|
151
|
+
scrollRegion(rect, dx, dy) {
|
|
152
|
+
if (this.destroyed || !this._surface) return false;
|
|
153
|
+
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
154
|
+
// a bitmap of another size is one the next paint replaces, whole
|
|
155
|
+
const size = this._surfaceSize;
|
|
156
|
+
if (
|
|
157
|
+
size.width !== Math.max(1, this.rect.width) ||
|
|
158
|
+
size.height !== Math.max(1, this.rect.height)
|
|
159
|
+
) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const moved = this._native.scrollSurface(
|
|
163
|
+
this._surface,
|
|
164
|
+
Math.round(rect.x),
|
|
165
|
+
Math.round(rect.y),
|
|
166
|
+
Math.round(rect.width),
|
|
167
|
+
Math.round(rect.height),
|
|
168
|
+
dx,
|
|
169
|
+
dy,
|
|
170
|
+
);
|
|
171
|
+
if (moved) this._dirty = true;
|
|
172
|
+
return Boolean(moved);
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
/** What was painted, onto the layer — a copy, so the bitmap is free to be
|
|
139
176
|
* painted again at once. */
|
|
140
177
|
present() {
|
package/src/glnodes.js
CHANGED
|
@@ -10,8 +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
|
+
import { layoutDiff } from './nodes/damage.js';
|
|
14
15
|
import { Node } from './nodes/node.js';
|
|
16
|
+
import { DEV } from './nodes/util.js';
|
|
15
17
|
import { FramePacer, resolveFrameRate } from './pacing.js';
|
|
16
18
|
|
|
17
19
|
// One visual query per (app, spec): GetFBConfigs is a round trip and every
|
|
@@ -36,6 +38,27 @@ function recordGlxFailure(app, err) {
|
|
|
36
38
|
if (app && err && !glxFailures.has(app)) glxFailures.set(app, err);
|
|
37
39
|
}
|
|
38
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
|
+
|
|
39
62
|
/**
|
|
40
63
|
* The visual and depth to create the GL child window with.
|
|
41
64
|
*
|
|
@@ -120,7 +143,9 @@ const px = (v) => Math.max(1, Math.round(v || 0));
|
|
|
120
143
|
* The X child window is stacked above everything drawn in the parent, so the
|
|
121
144
|
* parent's 2D content cannot overlap it — but this node's own children do:
|
|
122
145
|
* they are laid out in its box like a `<box>`'s and drawn above the surface,
|
|
123
|
-
* 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`).
|
|
124
149
|
*
|
|
125
150
|
* Pointer input over the surface is the tree's, on both backends: a press,
|
|
126
151
|
* a drag or a wheel over it is a synthetic event at the child under the
|
|
@@ -320,11 +345,102 @@ export class GlAreaNode extends Node {
|
|
|
320
345
|
};
|
|
321
346
|
}
|
|
322
347
|
|
|
348
|
+
/**
|
|
349
|
+
* `Node.absolutize`, with the children walked one at a time where a pane
|
|
350
|
+
* holds them — the frame's layout diff running (a bounded frame), no
|
|
351
|
+
* scroll shift riding over this node, no layout algorithm of its own
|
|
352
|
+
* placing them, and this node's own rect where it was, so the panes are
|
|
353
|
+
* too. A child that moved and changed nothing else is then reported to
|
|
354
|
+
* the overlay instead of claimed (`_absolutizeChild`); anywhere else the
|
|
355
|
+
* walk is the plain one.
|
|
356
|
+
*/
|
|
323
357
|
absolutize(originX, originY) {
|
|
324
|
-
|
|
358
|
+
const overlay = this._overlay;
|
|
359
|
+
if (
|
|
360
|
+
!overlay?.panes.length ||
|
|
361
|
+
!layoutDiff.sink ||
|
|
362
|
+
layoutDiff.shift ||
|
|
363
|
+
this._host !== null ||
|
|
364
|
+
!this.yoga
|
|
365
|
+
) {
|
|
366
|
+
super.absolutize(originX, originY);
|
|
367
|
+
this._syncGeometry();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this._placed = true;
|
|
371
|
+
const was = this.abs;
|
|
372
|
+
this._assignAbs(
|
|
373
|
+
originX + this.yoga.getComputedLeft(),
|
|
374
|
+
originY + this.yoga.getComputedTop(),
|
|
375
|
+
this.yoga.getComputedWidth(),
|
|
376
|
+
this.yoga.getComputedHeight(),
|
|
377
|
+
);
|
|
378
|
+
if (this.props.onLayout) this._reportLayout();
|
|
379
|
+
// `_assignAbs` keeps the object when nothing changed
|
|
380
|
+
const still = this.abs === was;
|
|
381
|
+
const { x, y } = this.abs;
|
|
382
|
+
for (const child of this.children) {
|
|
383
|
+
if (child.isWindow) continue;
|
|
384
|
+
if (still) this._absolutizeChild(child, x, y, overlay);
|
|
385
|
+
else child.absolutize(x, y);
|
|
386
|
+
}
|
|
325
387
|
this._syncGeometry();
|
|
326
388
|
}
|
|
327
389
|
|
|
390
|
+
/**
|
|
391
|
+
* One child, laid out from (originX, originY). When all that changed is
|
|
392
|
+
* where it sits — the same size, somewhere else, a plain box drawn where
|
|
393
|
+
* layout put it — it is walked with the layout diff's `shift` set to the
|
|
394
|
+
* move (issue #644): every descendant that lands where it was plus the
|
|
395
|
+
* move claims nothing, exactly as under a scroll blit (issue #398), and
|
|
396
|
+
* one that lands anywhere else claims both ends. The child itself is one
|
|
397
|
+
* of the first kind, so the move claims nothing at all here; the overlay
|
|
398
|
+
* is told of it instead, with the reach its pixels had, and settles it
|
|
399
|
+
* after the panes are placed (`GlOverlay.settleMoves`) — moved on the
|
|
400
|
+
* pane, or claimed where it was and where it is.
|
|
401
|
+
*
|
|
402
|
+
* `layoutDiff.ride` marks the walk as this kind, for the one node that
|
|
403
|
+
* drops the diff under a shift: a scroll pane laid out again claims its
|
|
404
|
+
* own box there, since the move claimed nothing for it
|
|
405
|
+
* (nodes/scrollable.js).
|
|
406
|
+
*/
|
|
407
|
+
_absolutizeChild(child, originX, originY, overlay) {
|
|
408
|
+
const y = child.yoga;
|
|
409
|
+
const old = child.abs;
|
|
410
|
+
if (
|
|
411
|
+
!y ||
|
|
412
|
+
child.hidden ||
|
|
413
|
+
child.style?.display === 'none' ||
|
|
414
|
+
child.isScroller?.() ||
|
|
415
|
+
!(old.width > 0 && old.height > 0) ||
|
|
416
|
+
this.root?._placedNodes?.has(child)
|
|
417
|
+
) {
|
|
418
|
+
child.absolutize(originX, originY);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const dx = originX + y.getComputedLeft() - old.x;
|
|
422
|
+
const dy = originY + y.getComputedTop() - old.y;
|
|
423
|
+
if (
|
|
424
|
+
(dx === 0 && dy === 0) ||
|
|
425
|
+
y.getComputedWidth() !== old.width ||
|
|
426
|
+
y.getComputedHeight() !== old.height
|
|
427
|
+
) {
|
|
428
|
+
child.absolutize(originX, originY);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// before it moves: every pixel of it a pane holds
|
|
432
|
+
const reach = child.paintBounds();
|
|
433
|
+
layoutDiff.shift = { x: dx, y: dy };
|
|
434
|
+
layoutDiff.ride = true;
|
|
435
|
+
try {
|
|
436
|
+
child.absolutize(originX, originY);
|
|
437
|
+
} finally {
|
|
438
|
+
layoutDiff.shift = null;
|
|
439
|
+
layoutDiff.ride = false;
|
|
440
|
+
}
|
|
441
|
+
overlay.noteMove(child, reach, dx, dy);
|
|
442
|
+
}
|
|
443
|
+
|
|
328
444
|
// the scroll fast path moves `abs` without coming through absolutize
|
|
329
445
|
// (issue #405), and the real X window has to follow it all the same
|
|
330
446
|
_shiftAbs(dx, dy) {
|
|
@@ -537,18 +653,28 @@ export class GlAreaNode extends Node {
|
|
|
537
653
|
|
|
538
654
|
/**
|
|
539
655
|
* The owning window's frame, after layout (`WindowNode._syncOverlays`):
|
|
540
|
-
* panes for where the children are now
|
|
541
|
-
*
|
|
656
|
+
* panes for where the children are now, and the moves the layout pass
|
|
657
|
+
* reported settled against them — moved on a pane, or claimed. True when
|
|
658
|
+
* a pane was made, resized or dropped, or a move claimed something — a
|
|
659
|
+
* paint the frame then owes.
|
|
542
660
|
*/
|
|
543
661
|
_syncOverlay() {
|
|
544
662
|
if (!this._overlay) {
|
|
545
|
-
|
|
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) {
|
|
546
671
|
this.root?._overlaid?.delete(this);
|
|
547
672
|
return false;
|
|
548
673
|
}
|
|
549
674
|
this._overlay = new GlOverlay(this);
|
|
550
675
|
}
|
|
551
|
-
|
|
676
|
+
let changed = this._overlay.sync();
|
|
677
|
+
if (this._overlay.settleMoves(this.root)) changed = true;
|
|
552
678
|
if (this._overlay.panes.length) this._joinSurfaces();
|
|
553
679
|
if (this.children.length === 0 && this._overlay.panes.length === 0) {
|
|
554
680
|
this._overlay = null;
|
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
|
*
|