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/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 2D
109
- * content cannot overlap it — put HUD content in a sibling `<popup>`.
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
- // The wheel, and only the wheel. A GL surface owns a real X window, so
226
- // the pointer events over it are delivered *there* rather than to the
227
- // window the rest of the tree is hit-tested in which is why nothing
228
- // over a `<glarea>` reached an application before. Selecting it here
229
- // and handing it back to the owning window's manager (`_onWheel`
230
- // below) is the whole of it: from there the event is an ordinary
231
- // synthetic `Wheel` at this node, so it bubbles, `preventDefault()`
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
- // Selected unconditionally rather than when a handler is declared: the
236
- // default action is what a reader expects from a wheel over a page,
237
- // and an element that wants it back has `preventDefault()`. One
238
- // ButtonPress per notch is not a cost worth a conditional.
239
- onWheel: (ev) => this._onWheel(ev),
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
- * A wheel over the surface, handed to the window the tree lives in.
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
- * ntk reports the position inside *this* window; the manager hit-tests in
391
- * the owning window's space, so the node's own origin goes back on. Both
392
- * are device pixels the scale is applied at the far end, where a handler
393
- * reads `ev.x` (src/events.js).
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
- * Smooth deltas are not part of this yet: XI2 is selected on the window
396
- * the manager owns, not on this child, so a touchpad's fractions arrive
397
- * here as whole notches from buttons 4-7.
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
- _onWheel(native) {
400
- const events = this.root?.events;
401
- if (!events || this.destroyed) return;
402
- events._onWheel(
403
- {
404
- ...native,
405
- x: (native.x ?? 0) + this.abs.x,
406
- y: (native.y ?? 0) + this.abs.y,
407
- },
408
- // named rather than hit-tested: a window-owning child is not in its
409
- // parent's paint order, so the hit test would answer with the box
410
- // behind this surface
411
- this,
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
- // the child window covers this rect: nothing to paint into the parent's
430
- // 2d context, and no drawn children are allowed under it
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?.();
@@ -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
+ }
package/src/host.d.ts CHANGED
@@ -21,7 +21,6 @@ import type { NtkApp } from './types/nodes.js';
21
21
  export interface HostContext {
22
22
  isInsideText: boolean;
23
23
  isInsideSvg: boolean;
24
- isInside3d: boolean;
25
24
  }
26
25
 
27
26
  export interface ElementDefinition {