react-x11 2.12.0 → 2.13.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.
- package/package.json +3 -2
- package/src/Reconciler.js +4 -14
- package/src/appcontext.js +59 -30
- package/src/cocoa/app.js +11 -0
- package/src/cocoa/glarea.js +4 -2
- package/src/cocoa/overlay.js +159 -0
- package/src/cocoa/promotion.js +13 -0
- package/src/embedding.js +31 -0
- package/src/events.js +72 -12
- package/src/foreignnodes.js +56 -3
- package/src/glnodes.js +171 -40
- package/src/gloverlay.js +383 -0
- package/src/host.d.ts +0 -1
- package/src/index.d.ts +14 -1
- package/src/node.d.ts +12 -1
- package/src/nodes/window/flush.js +35 -0
- package/src/nodes/window/window.js +9 -0
- package/src/types/elements.d.ts +15 -13
package/src/glnodes.js
CHANGED
|
@@ -10,6 +10,7 @@ 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
14
|
import { Node } from './nodes/node.js';
|
|
14
15
|
import { FramePacer, resolveFrameRate } from './pacing.js';
|
|
15
16
|
|
|
@@ -105,13 +106,22 @@ const px = (v) => Math.max(1, Math.round(v || 0));
|
|
|
105
106
|
* vocabulary as `<window frameRate>`. Defaults to the owning window's.
|
|
106
107
|
* - `glx` — a `chooseGLXConfig` spec, e.g. `{ DEPTH_SIZE: 24 }`.
|
|
107
108
|
*
|
|
108
|
-
* The X child window is stacked above everything drawn in the parent, so
|
|
109
|
-
* content cannot overlap it —
|
|
109
|
+
* The X child window is stacked above everything drawn in the parent, so the
|
|
110
|
+
* parent's 2D content cannot overlap it — but this node's own children do:
|
|
111
|
+
* they are laid out in its box like a `<box>`'s and drawn above the surface,
|
|
112
|
+
* on panes of their own (src/gloverlay.js).
|
|
113
|
+
*
|
|
114
|
+
* Pointer input over the surface is the tree's, on both backends: a press,
|
|
115
|
+
* a drag or a wheel over it is a synthetic event at the child under the
|
|
116
|
+
* pointer, or at this node, bubbling to its ancestors like anyone else's
|
|
117
|
+
* (`hitSurface` says how).
|
|
110
118
|
*/
|
|
111
119
|
export class GlAreaNode extends Node {
|
|
112
120
|
constructor(props, app) {
|
|
113
121
|
super('glarea', props, app);
|
|
114
122
|
this.window = null;
|
|
123
|
+
// the panes the children are drawn on, while there are children
|
|
124
|
+
this._overlay = null;
|
|
115
125
|
this.gl = null;
|
|
116
126
|
this.rect = null; // geometry last sent to the X window
|
|
117
127
|
this._realizing = false;
|
|
@@ -150,11 +160,21 @@ export class GlAreaNode extends Node {
|
|
|
150
160
|
|
|
151
161
|
_setRoot(root) {
|
|
152
162
|
super._setRoot(root);
|
|
163
|
+
// Children mounted along with this node — React builds a subtree before
|
|
164
|
+
// it attaches it — are drawn from the first frame of the window it joins.
|
|
165
|
+
if (this.children.length) root?._overlaid?.add(this);
|
|
153
166
|
// the owning window may already exist (a <glarea> mounted into a live
|
|
154
167
|
// tree); otherwise WindowNode.realize picks the subtree up
|
|
155
168
|
if (root?.window) this.realize();
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
insertBefore(child, beforeChild) {
|
|
172
|
+
super.insertBefore(child, beforeChild);
|
|
173
|
+
// 2D content above the surface: the owning window's next frame gives it
|
|
174
|
+
// a pane (`_syncOverlay`), and the child-list claim asks for that frame
|
|
175
|
+
this.root?._overlaid?.add(this);
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
/** Create the GL child window. Async: the visual comes from the server. */
|
|
159
179
|
realize() {
|
|
160
180
|
if (this.window || this.destroyed || this._realizing) return;
|
|
@@ -199,6 +219,9 @@ export class GlAreaNode extends Node {
|
|
|
199
219
|
recordGlxFailure(this.app, err);
|
|
200
220
|
this.gl = null;
|
|
201
221
|
if (this.window) {
|
|
222
|
+
// the children's panes stay, over whatever the fallback draws, and
|
|
223
|
+
// stay hittable; with none, nothing of this node covers the rect
|
|
224
|
+
if (!this._overlay) this._leaveSurfaces();
|
|
202
225
|
this.window.destroy?.();
|
|
203
226
|
this.window = null;
|
|
204
227
|
this.rect = null;
|
|
@@ -222,26 +245,32 @@ export class GlAreaNode extends Node {
|
|
|
222
245
|
// GL draws into the window itself: no 2d backing pixmap, and the
|
|
223
246
|
// frame clock is ours to drive
|
|
224
247
|
backingStore: false,
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// takes it back for a scene that zooms instead, and the default action
|
|
233
|
-
// scrolls the nearest container the way it does anywhere else.
|
|
248
|
+
// No pointer input is selected here, and that is the whole of how the
|
|
249
|
+
// pointer over the surface reaches the tree. X reports a device event
|
|
250
|
+
// to the first window up the hierarchy that selected it, so a press,
|
|
251
|
+
// a motion or a wheel over this window arrives at the owning window
|
|
252
|
+
// instead — in its coordinates and under its implicit grab, so a drag
|
|
253
|
+
// that leaves the surface keeps coming — and its event manager takes
|
|
254
|
+
// it from there like any other (`hitSurface` names this node).
|
|
234
255
|
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
256
|
+
// Selecting one here takes it away from the tree, which is how the
|
|
257
|
+
// wheel alone used to be handled: the surface selected ButtonPress to
|
|
258
|
+
// hear it and handed it back, and so every *press* on the surface
|
|
259
|
+
// ended here too, with the drag and the release that should have
|
|
260
|
+
// followed it. A listener on `node.window` does the same — ntk selects
|
|
261
|
+
// what a window is listened to for — which is what `forwardsPointer`
|
|
262
|
+
// exists to tell an element built on this one.
|
|
240
263
|
});
|
|
241
264
|
this.window = wnd;
|
|
242
265
|
this.rect = rect;
|
|
243
266
|
this.config = config;
|
|
244
267
|
wnd._reactX11Node = this;
|
|
268
|
+
// Above everything 2D in the owning window on both backends, and above
|
|
269
|
+
// every surface made before this one: X stacks a new child window over
|
|
270
|
+
// its siblings, and Core Animation a layer added later over one at the
|
|
271
|
+
// same zPosition. The window's hit test reads the list in that order
|
|
272
|
+
// (`EventManager._surfaceAt`).
|
|
273
|
+
this._joinSurfaces();
|
|
245
274
|
this.gl = wnd.getContext('opengl', config);
|
|
246
275
|
// a buffer freed by the display is a frame that can be drawn again
|
|
247
276
|
if (typeof this.gl?.onFrameAvailable !== 'undefined') {
|
|
@@ -256,6 +285,9 @@ export class GlAreaNode extends Node {
|
|
|
256
285
|
});
|
|
257
286
|
wnd.on?.('expose', () => this.requestFrame());
|
|
258
287
|
wnd.map?.();
|
|
288
|
+
// made on top of its siblings — over the panes of children that were
|
|
289
|
+
// laid out and painted before the visual query answered
|
|
290
|
+
this._overlay?.restack();
|
|
259
291
|
this.requestFrame();
|
|
260
292
|
}
|
|
261
293
|
|
|
@@ -385,31 +417,117 @@ export class GlAreaNode extends Node {
|
|
|
385
417
|
}
|
|
386
418
|
|
|
387
419
|
/**
|
|
388
|
-
*
|
|
420
|
+
* `true`: pointer input over this surface is the tree's on this backend —
|
|
421
|
+
* dispatched through the owning window's event manager, at this node, and
|
|
422
|
+
* bubbling from it like anyone else's.
|
|
389
423
|
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
424
|
+
* It is here to be asked by an element built on `<glarea>` that listens on
|
|
425
|
+
* `node.window` for the pointer, which it had to do before core delivered
|
|
426
|
+
* it. That listener has to go wherever this is true: on X11 it selects the
|
|
427
|
+
* event on the surface's own window, and X then delivers it *there*
|
|
428
|
+
* instead of to the tree (see `_create`) — every press, and the wheel with
|
|
429
|
+
* them, since both are ButtonPress.
|
|
394
430
|
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
431
|
+
* A getter on the class rather than a flag on each instance, so it can be
|
|
432
|
+
* read without rendering anything: `GlAreaNode.prototype.forwardsPointer`,
|
|
433
|
+
* from `react-x11/node`.
|
|
398
434
|
*/
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
435
|
+
get forwardsPointer() {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* What a point in the owning window's space lands on, if it is over this
|
|
441
|
+
* surface: the child under it where there is one — the children are drawn
|
|
442
|
+
* above the surface (src/gloverlay.js) — and otherwise this node.
|
|
443
|
+
*
|
|
444
|
+
* A point that is over the surface is over it whatever the tree's own
|
|
445
|
+
* order says: X stacks the child window above the parent's drawing, and
|
|
446
|
+
* the Cocoa backend puts the layer at a zPosition over both presenters. So
|
|
447
|
+
* the window asks its surfaces before it hit-tests its tree
|
|
448
|
+
* (`EventManager._hit`), and a point inside lands here rather than on the
|
|
449
|
+
* box behind — which is all a tree walk can find, a window-owning node not
|
|
450
|
+
* being in its parent's paint order. It is the same answer X gives: the
|
|
451
|
+
* event it propagates to the owning window names this window, or a pane
|
|
452
|
+
* over it, as the child the pointer is in.
|
|
453
|
+
*
|
|
454
|
+
* The rect is the surface's own, in whole pixels, since the server decides
|
|
455
|
+
* by those. With no GL surface — not made yet, or given up after
|
|
456
|
+
* `onError` — only the children answer, and a point between them is the
|
|
457
|
+
* tree's. Hidden, or `pointerEvents: 'none'` here or above, lets the
|
|
458
|
+
* pointer through to what the tree has behind, as it does for any node.
|
|
459
|
+
*/
|
|
460
|
+
hitSurface(x, y) {
|
|
461
|
+
const panes = this._overlay?.panes.length ?? 0;
|
|
462
|
+
if (!this.window && panes === 0) return null;
|
|
463
|
+
const rect = this.rect ?? this._geometry();
|
|
464
|
+
if (
|
|
465
|
+
x < rect.x ||
|
|
466
|
+
y < rect.y ||
|
|
467
|
+
x >= rect.x + rect.width ||
|
|
468
|
+
y >= rect.y + rect.height
|
|
469
|
+
) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
for (let n = this; n; n = n.parent) {
|
|
473
|
+
if (n.destroyed || n.hidden) return null;
|
|
474
|
+
if (n.style?.display === 'none' || n.style?.pointerEvents === 'none') {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (n.isWindow) break;
|
|
478
|
+
}
|
|
479
|
+
if (panes !== 0) {
|
|
480
|
+
// front to back, as the tree's own hit test walks a box's children
|
|
481
|
+
const order = this.paintOrder();
|
|
482
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
483
|
+
const hit = order[i].hitTest(x, y);
|
|
484
|
+
if (hit) return hit;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return this.window ? this : null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Into the owning window's hit test, once: a GL window or a pane covers
|
|
491
|
+
* the rect now (`EventManager._surfaceAt`). */
|
|
492
|
+
_joinSurfaces() {
|
|
493
|
+
const surfaces = this.root?._surfaces;
|
|
494
|
+
if (surfaces && !surfaces.includes(this)) surfaces.push(this);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Out of the owning window's hit test: nothing covers the rect any more,
|
|
498
|
+
* and what the tree has behind it answers again. */
|
|
499
|
+
_leaveSurfaces() {
|
|
500
|
+
const surfaces = this.root?._surfaces;
|
|
501
|
+
const at = surfaces ? surfaces.indexOf(this) : -1;
|
|
502
|
+
if (at !== -1) surfaces.splice(at, 1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The owning window's frame, after layout (`WindowNode._syncOverlays`):
|
|
507
|
+
* panes for where the children are now. True when a pane was made,
|
|
508
|
+
* resized or dropped — a paint the frame then owes.
|
|
509
|
+
*/
|
|
510
|
+
_syncOverlay() {
|
|
511
|
+
if (!this._overlay) {
|
|
512
|
+
if (this.children.length === 0 || !canOverlay(this.app)) {
|
|
513
|
+
this.root?._overlaid?.delete(this);
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
this._overlay = new GlOverlay(this);
|
|
517
|
+
}
|
|
518
|
+
const changed = this._overlay.sync();
|
|
519
|
+
if (this._overlay.panes.length) this._joinSurfaces();
|
|
520
|
+
if (this.children.length === 0 && this._overlay.panes.length === 0) {
|
|
521
|
+
this._overlay = null;
|
|
522
|
+
this.root?._overlaid?.delete(this);
|
|
523
|
+
if (!this.window) this._leaveSurfaces();
|
|
524
|
+
}
|
|
525
|
+
return changed;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** …and the paint it owes them, with the frame's damage. */
|
|
529
|
+
_paintOverlay(damage) {
|
|
530
|
+
this._overlay?.paint(damage);
|
|
413
531
|
}
|
|
414
532
|
|
|
415
533
|
applyProps(newProps, oldProps) {
|
|
@@ -424,14 +542,27 @@ export class GlAreaNode extends Node {
|
|
|
424
542
|
super.setHidden(hidden);
|
|
425
543
|
if (hidden) this.window?.unmap?.();
|
|
426
544
|
else this.window?.map?.();
|
|
545
|
+
this._overlay?.setHidden(hidden);
|
|
427
546
|
}
|
|
428
547
|
|
|
429
|
-
//
|
|
430
|
-
// 2d context, and
|
|
548
|
+
// The surface covers this rect: nothing of this node is painted into the
|
|
549
|
+
// parent's 2d context, and its children are painted above the surface on
|
|
550
|
+
// panes of their own (src/gloverlay.js) rather than in the window's walk.
|
|
431
551
|
paint() {}
|
|
432
552
|
|
|
553
|
+
// …which is also why they are cut to its box: a pane never reaches past
|
|
554
|
+
// the surface, and the hit test and the damage model have to agree with
|
|
555
|
+
// the panes about where the children can be
|
|
556
|
+
clipsChildren() {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
433
560
|
destroySubtree() {
|
|
434
561
|
if (this.destroyed) return;
|
|
562
|
+
this._leaveSurfaces();
|
|
563
|
+
this.root?._overlaid?.delete(this);
|
|
564
|
+
this._overlay?.destroy();
|
|
565
|
+
this._overlay = null;
|
|
435
566
|
super.destroySubtree();
|
|
436
567
|
this._pacer.cancel();
|
|
437
568
|
this.gl?.destroy?.();
|
package/src/gloverlay.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// The children of a `<glarea>`: 2D content, drawn above the GL surface.
|
|
2
|
+
//
|
|
3
|
+
// A surface is stacked over everything 2D in its window — a child X window
|
|
4
|
+
// above the parent's drawing on X11, a layer at zPosition 1e7 over both
|
|
5
|
+
// presenters on the Cocoa backend — so the window's own paint walk can never
|
|
6
|
+
// put a pixel on it, and does not try: a `<glarea>` is in no paint order, and
|
|
7
|
+
// so is nothing under it. Its children are otherwise ordinary. They are laid
|
|
8
|
+
// out in its box like a `<box>`'s; they live in the owning window's tree, so
|
|
9
|
+
// their claims land in its damage list and their input comes through its
|
|
10
|
+
// event manager, the hit test asking them before the surface itself
|
|
11
|
+
// (`GlAreaNode.hitSurface`). What is theirs alone is where they are painted:
|
|
12
|
+
// into *panes* above the surface, from inside the owning window's frame and
|
|
13
|
+
// with that frame's damage (nodes/window/flush.js, `_syncOverlays` and
|
|
14
|
+
// `_paintOverlays`).
|
|
15
|
+
//
|
|
16
|
+
// A pane is the backend's answer to one question — can a window composite a
|
|
17
|
+
// translucent child over a GL surface?
|
|
18
|
+
//
|
|
19
|
+
// - **The Cocoa backend: yes.** Core Animation composites every layer, so
|
|
20
|
+
// one pane holds everything — a transparent bitmap layer over the whole
|
|
21
|
+
// surface, above the GL layer — and a translucent background, an
|
|
22
|
+
// antialiased edge or a shadow blends with the GL frame under it.
|
|
23
|
+
// `app.createOverlayPane` is how a backend says it composites.
|
|
24
|
+
// - **X11: no.** A child window is opaque without a compositor, and a
|
|
25
|
+
// compositor would not change that: it redirects top-level windows, not
|
|
26
|
+
// the children inside one. So a pane is a plain child window just big
|
|
27
|
+
// enough for what it holds, one per region the children reach, and the
|
|
28
|
+
// surface shows between them. Inside a pane the ground is the surface's
|
|
29
|
+
// `clearColor` — the colour its frames start from — so a pixel a child
|
|
30
|
+
// leaves unpainted there (a rounded corner, a translucent background, text
|
|
31
|
+
// with no box behind it) shows that colour, never the GL frame.
|
|
32
|
+
//
|
|
33
|
+
// A window per region rather than one window shaped by the SHAPE
|
|
34
|
+
// extension, for two reasons: SHAPE is near-universal but not universal —
|
|
35
|
+
// node-x11's in-process server, where this is tested, has none — and a
|
|
36
|
+
// window as big as the surface would keep a backing pixmap the size of a
|
|
37
|
+
// map to show a legend in its corner.
|
|
38
|
+
//
|
|
39
|
+
// A pane selects no input, so the pointer over one reaches the owning window
|
|
40
|
+
// by the same propagation that brings it the pointer over the surface
|
|
41
|
+
// (src/glnodes.js, `_create`), and lands on the child it is over.
|
|
42
|
+
//
|
|
43
|
+
// A leaf module, like src/embedding.js: `appcontext.js` asks `canOverlay`
|
|
44
|
+
// for `useSupports('glOverlay')`, and it imports nothing of ours.
|
|
45
|
+
import { cssColorStraight } from 'ntk';
|
|
46
|
+
|
|
47
|
+
// ConfigureWindow's stack mode: directly above the sibling it names
|
|
48
|
+
const STACK_ABOVE = 0;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether the children of a `<glarea>` can be drawn over its surface on this
|
|
52
|
+
* connection: a backend that composites a pane itself, or one that can make
|
|
53
|
+
* the plain child window a pane is on X11. One function for the element and
|
|
54
|
+
* for `useSupports('glOverlay')`, which have to agree — the rule `canEmbed`
|
|
55
|
+
* follows for `<foreign>`.
|
|
56
|
+
*/
|
|
57
|
+
export function canOverlay(app) {
|
|
58
|
+
return (
|
|
59
|
+
typeof app?.createOverlayPane === 'function' ||
|
|
60
|
+
typeof app?.createWindow === 'function'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A rect grown out to whole device pixels. */
|
|
65
|
+
function whole(r) {
|
|
66
|
+
const x = Math.floor(r.x);
|
|
67
|
+
const y = Math.floor(r.y);
|
|
68
|
+
return {
|
|
69
|
+
x,
|
|
70
|
+
y,
|
|
71
|
+
width: Math.ceil(r.x + r.width) - x,
|
|
72
|
+
height: Math.ceil(r.y + r.height) - y,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function intersect(a, b) {
|
|
77
|
+
const x = Math.max(a.x, b.x);
|
|
78
|
+
const y = Math.max(a.y, b.y);
|
|
79
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
80
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
81
|
+
return right > x && bottom > y
|
|
82
|
+
? { x, y, width: right - x, height: bottom - y }
|
|
83
|
+
: null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const overlaps = (a, b) =>
|
|
87
|
+
a.x < b.x + b.width &&
|
|
88
|
+
b.x < a.x + a.width &&
|
|
89
|
+
a.y < b.y + b.height &&
|
|
90
|
+
b.y < a.y + a.height;
|
|
91
|
+
|
|
92
|
+
function around(a, b) {
|
|
93
|
+
const x = Math.min(a.x, b.x);
|
|
94
|
+
const y = Math.min(a.y, b.y);
|
|
95
|
+
return {
|
|
96
|
+
x,
|
|
97
|
+
y,
|
|
98
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
99
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sameRect = (a, b) =>
|
|
104
|
+
a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
105
|
+
|
|
106
|
+
/** On screen: nothing from here to the window hidden or `display: 'none'`. */
|
|
107
|
+
function shown(node) {
|
|
108
|
+
for (let n = node; n; n = n.parent) {
|
|
109
|
+
if (n.destroyed || n.hidden || n.style?.display === 'none') return false;
|
|
110
|
+
if (n.isWindow) break;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Where the children put ink, as rects no two of which overlap: each drawn
|
|
117
|
+
* child's reach in whole pixels, cut to the surface, and any two that
|
|
118
|
+
* overlap merged into the box around both. A pane is a rectangle, and two
|
|
119
|
+
* panes over one pixel would each have to hold what the other draws there.
|
|
120
|
+
*
|
|
121
|
+
* The reach is `_subtreeBounds()`, the rect the damage model culls against —
|
|
122
|
+
* so a child's shadow, its outline and a descendant that sticks out of it
|
|
123
|
+
* are all inside its pane.
|
|
124
|
+
*/
|
|
125
|
+
export function overlayRegions(area, surface) {
|
|
126
|
+
const rects = [];
|
|
127
|
+
for (const child of area.paintOrder()) {
|
|
128
|
+
const reach = intersect(whole(child._subtreeBounds()), surface);
|
|
129
|
+
if (reach) rects.push(reach);
|
|
130
|
+
}
|
|
131
|
+
for (let merged = true; merged;) {
|
|
132
|
+
merged = false;
|
|
133
|
+
search: for (let i = 0; i < rects.length; i++) {
|
|
134
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
135
|
+
if (overlaps(rects[i], rects[j])) {
|
|
136
|
+
rects[i] = around(rects[i], rects[j]);
|
|
137
|
+
rects.splice(j, 1);
|
|
138
|
+
merged = true;
|
|
139
|
+
break search;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return rects;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The colour an opaque pane is filled with before its children paint: the
|
|
149
|
+
* surface's `clearColor`, what the GL frame under the pane starts from, at
|
|
150
|
+
* full alpha. The pane is opaque, and a translucent fill would pile up on
|
|
151
|
+
* the pane's own last frame rather than show anything under it.
|
|
152
|
+
*/
|
|
153
|
+
function groundOf(props) {
|
|
154
|
+
const value = props.clearColor ?? 'black';
|
|
155
|
+
const [r, g, b] = Array.isArray(value)
|
|
156
|
+
? value
|
|
157
|
+
: (cssColorStraight(value) ?? [0, 0, 0, 1]);
|
|
158
|
+
const byte = (c) => Math.round(Math.max(0, Math.min(1, c)) * 255);
|
|
159
|
+
return `rgb(${byte(r)}, ${byte(g)}, ${byte(b)})`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* One pane: the window — or on the Cocoa backend the layer, which speaks the
|
|
164
|
+
* same few verbs — and what the overlay knows about it: where it is, whether
|
|
165
|
+
* all of it is owed a paint, and its 2d context, made once, since ntk builds
|
|
166
|
+
* a fresh one with subscriptions of its own on every `getContext`.
|
|
167
|
+
*/
|
|
168
|
+
class Pane {
|
|
169
|
+
constructor(wnd, rect, transparent) {
|
|
170
|
+
this.wnd = wnd;
|
|
171
|
+
this.rect = rect;
|
|
172
|
+
this.transparent = transparent;
|
|
173
|
+
this.full = true;
|
|
174
|
+
this.ctx = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
context() {
|
|
178
|
+
if (!this.ctx && typeof this.wnd.getContext === 'function') {
|
|
179
|
+
this.ctx = this.wnd.getContext('2d');
|
|
180
|
+
}
|
|
181
|
+
return this.ctx;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Somewhere else. A new size is a new backing — a pixmap on X11, a
|
|
185
|
+
* bitmap on Cocoa — and has to be painted whole; a move keeps what the
|
|
186
|
+
* pane holds, which moved with the children it shows. */
|
|
187
|
+
place(rect) {
|
|
188
|
+
if (rect.width !== this.rect.width || rect.height !== this.rect.height) {
|
|
189
|
+
this.full = true;
|
|
190
|
+
}
|
|
191
|
+
this.rect = rect;
|
|
192
|
+
if (typeof this.wnd.setState === 'function') this.wnd.setState(rect);
|
|
193
|
+
else {
|
|
194
|
+
this.wnd.move?.(rect.x, rect.y);
|
|
195
|
+
this.wnd.resize?.(rect.width, rect.height);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
show(on) {
|
|
200
|
+
if (on) this.wnd.map?.();
|
|
201
|
+
else this.wnd.unmap?.();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
destroy() {
|
|
205
|
+
this.wnd.destroy?.();
|
|
206
|
+
this.ctx = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A `<glarea>`'s panes, from the owning window's frame. */
|
|
211
|
+
export class GlOverlay {
|
|
212
|
+
constructor(area) {
|
|
213
|
+
this.area = area;
|
|
214
|
+
this.app = area.app;
|
|
215
|
+
// one pane over the whole surface, where the backend composites it
|
|
216
|
+
this.composited = typeof this.app?.createOverlayPane === 'function';
|
|
217
|
+
this.panes = [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* After layout: panes for where the children are now. True when a pane was
|
|
222
|
+
* made, resized or dropped — a paint the frame then owes.
|
|
223
|
+
*
|
|
224
|
+
* Nothing is made for a surface that is hidden, or has no area, or has no
|
|
225
|
+
* child on screen: a pane with nothing on it would only hide the surface.
|
|
226
|
+
* The panes of one that goes are dropped rather than kept unmapped, and
|
|
227
|
+
* the frame that brings it back makes new ones and paints them whole.
|
|
228
|
+
*/
|
|
229
|
+
sync() {
|
|
230
|
+
const area = this.area;
|
|
231
|
+
const abs = area.abs;
|
|
232
|
+
let rects = [];
|
|
233
|
+
if (shown(area) && abs.width > 0 && abs.height > 0) {
|
|
234
|
+
// the surface's own rect, rounded the way its window's is
|
|
235
|
+
const surface = area._geometry();
|
|
236
|
+
if (!this.composited) rects = overlayRegions(area, surface);
|
|
237
|
+
else if (area.paintOrder().length) rects = [surface];
|
|
238
|
+
}
|
|
239
|
+
let changed = false;
|
|
240
|
+
while (this.panes.length > rects.length) {
|
|
241
|
+
this.panes.pop().destroy();
|
|
242
|
+
changed = true;
|
|
243
|
+
}
|
|
244
|
+
let made = false;
|
|
245
|
+
for (let i = 0; i < rects.length; i++) {
|
|
246
|
+
const pane = this.panes[i];
|
|
247
|
+
if (!pane) {
|
|
248
|
+
this.panes.push(this._makePane(rects[i]));
|
|
249
|
+
made = changed = true;
|
|
250
|
+
} else if (!sameRect(pane.rect, rects[i])) {
|
|
251
|
+
pane.place(rects[i]);
|
|
252
|
+
changed = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (made) this.restack();
|
|
256
|
+
return changed;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_makePane(rect) {
|
|
260
|
+
const owner = this.area.root.window;
|
|
261
|
+
const attributes = { parent: owner, ...rect };
|
|
262
|
+
const pane = this.composited
|
|
263
|
+
? new Pane(this.app.createOverlayPane(attributes), rect, true)
|
|
264
|
+
: // A plain child window on the owning window's visual, selecting
|
|
265
|
+
// nothing but what ntk selects for itself — the structure events and
|
|
266
|
+
// the exposures its backing store answers. The pointer is the tree's.
|
|
267
|
+
new Pane(this.app.createWindow(attributes), rect, false);
|
|
268
|
+
// ntk asks for a redraw when the backing no longer holds the picture — a
|
|
269
|
+
// resize it could not carry over — and a pane has nothing to redraw from
|
|
270
|
+
// but the children, on the next frame of the window they live in
|
|
271
|
+
pane.wnd.on?.('draw', () => this._lost(pane));
|
|
272
|
+
pane.show(true);
|
|
273
|
+
return pane;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A pane whose pixels are gone: all of it is owed, on the owning window's
|
|
277
|
+
* next frame, claimed as its rect so the rest of the window stays put. */
|
|
278
|
+
_lost(pane) {
|
|
279
|
+
pane.full = true;
|
|
280
|
+
const root = this.area.root;
|
|
281
|
+
if (root && !root.destroyed)
|
|
282
|
+
root.invalidate(false, { ...pane.rect }, 'expose');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Put the panes directly over the surface, bottom to top. Only X11 needs
|
|
287
|
+
* it: a child window is made on top of its siblings, which is right for a
|
|
288
|
+
* pane made after the surface and wrong for one made before it — the
|
|
289
|
+
* surface's window is made once its visual is known, which can be after
|
|
290
|
+
* the first frame has laid out and painted its children. So the surface
|
|
291
|
+
* restacks its panes when its own window is made, too (`_create`). On the
|
|
292
|
+
* Cocoa backend the layer's zPosition is the whole of it.
|
|
293
|
+
*/
|
|
294
|
+
restack() {
|
|
295
|
+
if (this.composited) return;
|
|
296
|
+
const X = this.app?.X;
|
|
297
|
+
if (typeof X?.ConfigureWindow !== 'function') return;
|
|
298
|
+
let below = this.area.window?.id ?? null;
|
|
299
|
+
for (const pane of this.panes) {
|
|
300
|
+
const id = pane.wnd.id;
|
|
301
|
+
if (id == null) continue;
|
|
302
|
+
if (below == null) X.ConfigureWindow(id, { stackMode: STACK_ABOVE });
|
|
303
|
+
else X.ConfigureWindow(id, { sibling: below, stackMode: STACK_ABOVE });
|
|
304
|
+
below = id;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Paint what the frame owes: every pane whole after it was made, resized
|
|
310
|
+
* or lost its pixels, and otherwise the frame's damage cut to each pane —
|
|
311
|
+
* `null` damage meaning the whole window, as it does for the paint walk.
|
|
312
|
+
*
|
|
313
|
+
* Each pass is the window's own paint walk over the children, translated
|
|
314
|
+
* so the pane's corner is the bitmap's, clipped to the pass, with
|
|
315
|
+
* `paintDamage()` naming it — the same culling, the same paint cache, the
|
|
316
|
+
* same everything as a pass over the window, and in its coordinates.
|
|
317
|
+
*/
|
|
318
|
+
paint(damage) {
|
|
319
|
+
for (const pane of this.panes) {
|
|
320
|
+
let passes;
|
|
321
|
+
if (pane.full || !damage) {
|
|
322
|
+
passes = [pane.rect];
|
|
323
|
+
} else {
|
|
324
|
+
passes = [];
|
|
325
|
+
for (const rect of damage) {
|
|
326
|
+
const hit = intersect(rect, pane.rect);
|
|
327
|
+
if (hit) passes.push(hit);
|
|
328
|
+
}
|
|
329
|
+
if (passes.length === 0) continue;
|
|
330
|
+
}
|
|
331
|
+
pane.full = false;
|
|
332
|
+
this._paintPane(pane, passes);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_paintPane(pane, passes) {
|
|
337
|
+
const area = this.area;
|
|
338
|
+
const root = area.root;
|
|
339
|
+
const ctx = pane.context();
|
|
340
|
+
if (!ctx || !root) return;
|
|
341
|
+
// an opaque pane is filled with what the surface starts its frames from;
|
|
342
|
+
// a composited one is cleared, and shows the frame itself
|
|
343
|
+
const ground = pane.transparent ? null : groundOf(area.props);
|
|
344
|
+
ctx.save();
|
|
345
|
+
try {
|
|
346
|
+
ctx.translate(-pane.rect.x, -pane.rect.y);
|
|
347
|
+
for (const pass of passes) {
|
|
348
|
+
ctx.save();
|
|
349
|
+
try {
|
|
350
|
+
ctx.beginPath();
|
|
351
|
+
ctx.rect(pass.x, pass.y, pass.width, pass.height);
|
|
352
|
+
ctx.clip();
|
|
353
|
+
if (ground) {
|
|
354
|
+
ctx.fillStyle = ground;
|
|
355
|
+
ctx.fillRect(pass.x, pass.y, pass.width, pass.height);
|
|
356
|
+
} else {
|
|
357
|
+
ctx.clearRect(pass.x, pass.y, pass.width, pass.height);
|
|
358
|
+
}
|
|
359
|
+
root._paintDamage = pass;
|
|
360
|
+
area._paintChildren(ctx);
|
|
361
|
+
} finally {
|
|
362
|
+
root._paintDamage = null;
|
|
363
|
+
ctx.restore();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} finally {
|
|
367
|
+
ctx.restore();
|
|
368
|
+
}
|
|
369
|
+
// A layer's contents are a copy the bitmap is pushed to; an X window's
|
|
370
|
+
// backing store is blitted by ntk on its own, from the paint above
|
|
371
|
+
pane.wnd.present?.();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Unmapped now; `sync` drops them on the frame the hide lays out. */
|
|
375
|
+
setHidden(hidden) {
|
|
376
|
+
for (const pane of this.panes) pane.show(!hidden);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
destroy() {
|
|
380
|
+
for (const pane of this.panes) pane.destroy();
|
|
381
|
+
this.panes = [];
|
|
382
|
+
}
|
|
383
|
+
}
|