react-x11 2.22.8 → 2.22.10

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/gloverlay.js CHANGED
@@ -24,17 +24,38 @@
24
24
  // - **X11: no.** A child window is opaque without a compositor, and a
25
25
  // compositor would not change that: it redirects top-level windows, not
26
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.
27
+ // enough for what it holds, one per child, and the surface shows between
28
+ // them. Inside a pane the ground is the surface's `clearColor` — the
29
+ // colour its frames start from — so a pixel a child leaves unpainted there
30
+ // (a rounded corner, a translucent background, text with no box behind
31
+ // it) shows that colour, never the GL frame.
32
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.
33
+ // One pane per child, kept by the child, and holding that child and what
34
+ // is under it where the two meet — never what is over it (`_paintPane`).
35
+ // Two children that overlap keep a pane each, stacked in their order, the
36
+ // upper one holding the lower one's pixels where they meet. So a card
37
+ // dragged across another moves its own pane and leaves the other one's
38
+ // alone: nothing the other holds changed. Panes used to be one per region
39
+ // the children reached, overlapping ones merged, handed out by position in
40
+ // a list — and a card dragged into a neighbour's reach re-cut the regions,
41
+ // so every pane after it in the list went to someone else's place with
42
+ // that someone's pixels in it, for a frame, on every step.
43
+ //
44
+ // A pane's window takes a new place and the pixels that go there together
45
+ // (`X11PaneWindow`), and keeps its contents in the server while it is
46
+ // mapped, as the surface's window does (src/glnodes.js): a pane or a
47
+ // frame another pane moves off is put back by the server at once, not
48
+ // after an Expose and a repaint.
49
+ //
50
+ // Each pane is cut with the SHAPE extension to what its child painted
51
+ // (`X11PaneWindow.cutToInk`), so the frame shows around a child and not
52
+ // only between the children's reaches — a card's handles and rounded
53
+ // corners, not the rectangle its canvas reaches. A window per child
54
+ // rather than one window over the surface cut to all of them keeps each
55
+ // pixmap as small as what it holds: one the size of the surface would
56
+ // hold a map's worth of pixels to show a legend in its corner. Where the
57
+ // server has no SHAPE — node-x11's in-process one — a pane is the
58
+ // rectangle of its child's reach, as it always was.
38
59
  // - **XQuartz: no, and not opaquely either.** Every GL surface there, the
39
60
  // direct ones Apple-DRI exports and indirect GLX's alike, is a surface of
40
61
  // the macOS window server, composited above everything the X server draws
@@ -51,11 +72,13 @@
51
72
  //
52
73
  // A pane keeps what it holds between frames, so a child that only moved is
53
74
  // already painted, one shift away (issue #644). The layout pass reports such
54
- // a child instead of claiming it (`GlAreaNode._absolutizeChild`), and the
55
- // frame moves its pixels inside the pane — `scrollRegion`, the verb the
56
- // scroll blit uses on a window — and repaints only what the move uncovered
57
- // and what it now overlaps differently (`settleMoves`). A pan over a
58
- // graph's mounted node bodies is that frame, sixty times a second.
75
+ // a child instead of claiming it (`GlAreaNode._absolutizeChild`). On X11 its
76
+ // pane goes with it, and the window's move carries its pixels; on a
77
+ // composited backend the frame moves them inside the one pane —
78
+ // `scrollRegion`, the verb the scroll blit uses on a window. Either way the
79
+ // frame repaints only what the move uncovered and what it now overlaps
80
+ // differently (`settleMoves`). A pan over a graph's mounted node bodies is
81
+ // that frame, sixty times a second.
59
82
  //
60
83
  // A leaf module, like src/embedding.js: `appcontext.js` asks `canOverlay`
61
84
  // for `useSupports('glOverlay')`, and it imports nothing of ours but the
@@ -76,6 +99,10 @@ import {
76
99
  // ConfigureWindow's stack mode: directly above the sibling it names
77
100
  const STACK_ABOVE = 0;
78
101
 
102
+ // How many rects one pane's own list keeps before merging the closest pair
103
+ // (`Pane.owe`): each is a pass over that pane alone, which is small.
104
+ const OWED_CAP = 4;
105
+
79
106
  // Past this share of the pixels a move carries, the frame would repaint
80
107
  // most of them anyway — around what it claims itself, or around the other
81
108
  // children the copy reached — and the copy only adds to the bill.
@@ -86,6 +113,10 @@ const MOVE_BLIT_MAX_REPAINT = 0.75;
86
113
  // one variable away from the plain repaint (docs/debugging.md).
87
114
  const NO_BLIT = process.env.REACT_X11_NO_SCROLL_BLIT === '1';
88
115
 
116
+ // …and the one for cutting an X11 pane to its ink (`X11PaneWindow.cutToInk`):
117
+ // with it set, every pane is the rectangle of its child's reach
118
+ const NO_PANE_SHAPE = process.env.REACT_X11_NO_PANE_SHAPE === '1';
119
+
89
120
  // Connections on which nothing can be drawn over a GL surface, each with the
90
121
  // reason (`beginGlOverlay`), and the probe that finds out, once per app.
91
122
  const refusals = new WeakMap();
@@ -185,17 +216,6 @@ const overlaps = (a, b) =>
185
216
  a.y < b.y + b.height &&
186
217
  b.y < a.y + a.height;
187
218
 
188
- function around(a, b) {
189
- const x = Math.min(a.x, b.x);
190
- const y = Math.min(a.y, b.y);
191
- return {
192
- x,
193
- y,
194
- width: Math.max(a.x + a.width, b.x + b.width) - x,
195
- height: Math.max(a.y + a.height, b.y + b.height) - y,
196
- };
197
- }
198
-
199
219
  const sameRect = (a, b) =>
200
220
  a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
201
221
 
@@ -209,35 +229,464 @@ function shown(node) {
209
229
  }
210
230
 
211
231
  /**
212
- * Where the children put ink, as rects no two of which overlap: each drawn
213
- * child's reach in whole pixels, cut to the surface, and any two that
214
- * overlap merged into the box around both. A pane is a rectangle, and two
215
- * panes over one pixel would each have to hold what the other draws there.
232
+ * Where each child puts ink, in paint order: its reach in whole pixels, cut
233
+ * to the surface — the rect of the X11 pane that is that child's. Two that
234
+ * overlap keep a rect each (see the header).
216
235
  *
217
236
  * The reach is `_subtreeBounds()`, the rect the damage model culls against —
218
237
  * so a child's shadow, its outline and a descendant that sticks out of it
219
238
  * are all inside its pane.
220
239
  */
221
- export function overlayRegions(area, surface) {
222
- const rects = [];
240
+ export function childRegions(area, surface) {
241
+ const regions = [];
223
242
  for (const child of area.paintOrder()) {
224
- const reach = intersectRects(whole(child._subtreeBounds()), surface);
225
- if (reach) rects.push(reach);
226
- }
227
- for (let merged = true; merged;) {
228
- merged = false;
229
- search: for (let i = 0; i < rects.length; i++) {
230
- for (let j = i + 1; j < rects.length; j++) {
231
- if (overlaps(rects[i], rects[j])) {
232
- rects[i] = around(rects[i], rects[j]);
233
- rects.splice(j, 1);
234
- merged = true;
235
- break search;
236
- }
243
+ const rect = intersectRects(whole(child._subtreeBounds()), surface);
244
+ if (rect) regions.push({ node: child, rect });
245
+ }
246
+ return regions;
247
+ }
248
+
249
+ // ChangeWindowAttributes' backing-store value: the server keeps what the
250
+ // window shows while it is mapped
251
+ const BACKING_STORE_WHEN_MAPPED = 1;
252
+ // a resize keeps what the window shows at its top-left corner rather than
253
+ // discarding it
254
+ const NORTH_WEST_GRAVITY = 1;
255
+ // a background of None: an exposure shows what was there, not a fill colour
256
+ const NO_BACKGROUND = 0;
257
+ // The step an X11 pane's pixmap grows in. A pane whose child is zooming is
258
+ // a new size every frame, and a pixmap and its picture made per frame per
259
+ // pane is a round of requests for nothing; within the step the one it has
260
+ // does.
261
+ const PIXMAP_STEP = 64;
262
+
263
+ /**
264
+ * An X11 pane's drawable: a plain child window of the owning window, and a
265
+ * pixmap the pane is painted in — its double buffer — with the verbs a pane
266
+ * asks of its window (`Pane`).
267
+ *
268
+ * Not an ntk window's own backing store, because of *when* that shows. ntk
269
+ * presents a backing store on its own schedule — the next vblank, through
270
+ * Present — while a window moves and resizes the moment its ConfigureWindow
271
+ * reaches the server. So a pane that went somewhere else showed its old
272
+ * pixels in the new place for a frame, and one that grew was shown cut to
273
+ * the size ntk had not heard back about yet. Here the geometry waits for
274
+ * the paint: `present` sends the ConfigureWindow and the CopyArea of what
275
+ * was painted back to back, and the server applies the move and the pixels
276
+ * that go with it together.
277
+ *
278
+ * The window keeps its contents in the server while it is mapped (X backing
279
+ * store), so what a pane moving off another uncovers is put back at once;
280
+ * its background is None, so an exposure it does get is filled from the
281
+ * pixmap with nothing flashed first; and a resize keeps what it shows at
282
+ * the corner until the copy lands.
283
+ *
284
+ * **Cut to its ink** where the server has SHAPE (`cutToInk`). A pane is a
285
+ * rectangle as big as its child's reach, and a child's reach is more than
286
+ * it paints: a card's canvas reaches past the card for its handles, a
287
+ * rounded box leaves its corners, a shadow its falloff. As a rectangle, all
288
+ * of that covered the GL frame with the pane's ground — a graph's edges
289
+ * stopped short of every card and lost their arrowheads, which the 2D
290
+ * renderer of the same graph drew. Cut, the children are painted on a
291
+ * transparent ground, and the window's bounding shape is where they left
292
+ * half their alpha or more: the frame shows everywhere they did not paint.
293
+ * What stays is the X11 limit on translucency — a pixel inside the shape
294
+ * blends with the ground, not with the frame.
295
+ */
296
+ export class X11PaneWindow {
297
+ /** Whether the app is one this can be made on: an ntk connection. */
298
+ static usable(app) {
299
+ return (
300
+ typeof app?.X?.CopyArea === 'function' &&
301
+ typeof app.createPixmap === 'function' &&
302
+ typeof app.createWindow === 'function'
303
+ );
304
+ }
305
+
306
+ /**
307
+ * @param shape the SHAPE extension to cut the pane to its ink with, or
308
+ * null for a rectangle (`cutToInk` can still cut it later)
309
+ */
310
+ constructor(app, owner, rect, shape = null) {
311
+ this.app = app;
312
+ this.X = app.X;
313
+ // where the window is on the server, and where the next present puts it
314
+ this.rect = { ...rect };
315
+ this._next = null;
316
+ // mapped once there is something to show, and while the pane is shown
317
+ this._mapped = false;
318
+ this._shown = false;
319
+ this._painted = false;
320
+ // pane-local rects painted since the last present
321
+ this._owed = [];
322
+ // cut to its ink: the SHAPE extension, and the ground its alpha is laid
323
+ // over, straight floats (`_paintPane` sets it)
324
+ this._shape = null;
325
+ this.ground = [0, 0, 0];
326
+ this.window = app.createWindow({
327
+ parent: owner,
328
+ x: rect.x,
329
+ y: rect.y,
330
+ width: rect.width,
331
+ height: rect.height,
332
+ // ntk's double buffer, which this replaces — not X's backing store
333
+ backingStore: false,
334
+ backgroundPixmap: NO_BACKGROUND,
335
+ bitGravity: NORTH_WEST_GRAVITY,
336
+ });
337
+ this.id = this.window.id;
338
+ this.depth =
339
+ this.window.depth || owner.depth || app.display.screen[0].root_depth;
340
+ this.X.ChangeWindowAttributes(this.id, {
341
+ backingStore: BACKING_STORE_WHEN_MAPPED,
342
+ });
343
+ this._gc = this.X.AllocID();
344
+ this.X.CreateGC(this._gc, this.id, { graphicsExposures: 0 });
345
+ // the pixmaps: `buffer`, what the window is copied from; cut to its
346
+ // ink, `argb`, what the children paint in, and `mask`, a bit a pixel
347
+ // of where they did
348
+ this._store = null;
349
+ this._ctx = null;
350
+ if (!this.cutToInk(shape)) this._grow(rect.width, rect.height);
351
+ // An exposure the server could not fill from its own copy — one with no
352
+ // backing store, or one that let it go — is filled from the pixmap.
353
+ // Adding the listener is what selects Exposure (ntk).
354
+ this.window.on('expose', (ev) => {
355
+ if (this._painted) this._copy([ev]);
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Paint on a transparent ground from now on, and show only where the
361
+ * children painted: the window's bounding shape follows their alpha. True
362
+ * when it changed — the pane's pixels are gone, and it is painted whole
363
+ * next — and false where it cannot: no SHAPE, no 1-bit picture format,
364
+ * or already cut.
365
+ */
366
+ cutToInk(shape) {
367
+ if (NO_PANE_SHAPE || this._shape || !shape) return false;
368
+ const R = this.app.display.Render;
369
+ if (!R?.mono1 || !R.rgba32) return false;
370
+ this._shape = shape;
371
+ this._free(this._store);
372
+ this._store = null;
373
+ this._grow(this.rect.width, this.rect.height);
374
+ return true;
375
+ }
376
+
377
+ /** What a pane asks of its window's event selection: none of the pointer. */
378
+ get eventMask() {
379
+ return this.window.eventMask;
380
+ }
381
+
382
+ /** A pane asks for the context every pass: it is the pixmap's, and a pane
383
+ * that grew past its pixmap has a new one. */
384
+ get ownsContext() {
385
+ return true;
386
+ }
387
+
388
+ /** Painted on a transparent ground, which the pane lays over `ground`. */
389
+ get paintsAlpha() {
390
+ return this._shape !== null;
391
+ }
392
+
393
+ /** Whether a present is owed without a paint: a place to take, or pixels
394
+ * a blit moved. */
395
+ get presentOwed() {
396
+ return this._next !== null || this._owed.length !== 0 || !this._mapped;
397
+ }
398
+
399
+ getContext(name) {
400
+ if (name !== '2d') return null;
401
+ const store = this._store;
402
+ return (this._ctx ??= (store.argb ?? store.buffer).getContext('2d'));
403
+ }
404
+
405
+ /** The pixmaps, at least `width`×`height`, grown in steps and never
406
+ * shrunk. */
407
+ _grow(width, height) {
408
+ const cur = this._store;
409
+ if (cur && cur.width >= width && cur.height >= height) return;
410
+ const up = (v) =>
411
+ Math.max(PIXMAP_STEP, Math.ceil(v / PIXMAP_STEP) * PIXMAP_STEP);
412
+ const w = up(Math.max(width, cur?.width ?? 0));
413
+ const h = up(Math.max(height, cur?.height ?? 0));
414
+ const app = this.app;
415
+ const X = this.X;
416
+ const pixmap = (depth, visual) =>
417
+ app.createPixmap({
418
+ parent: this.window,
419
+ width: w,
420
+ height: h,
421
+ depth,
422
+ visual,
423
+ });
424
+ // the window's pixels, read through the window's visual
425
+ const store = {
426
+ width: w,
427
+ height: h,
428
+ buffer: pixmap(this.depth, this.window.visualId),
429
+ };
430
+ if (this._shape) {
431
+ const R = app.display.Render;
432
+ const picture = (drawable, format) => {
433
+ const id = X.AllocID();
434
+ R.CreatePicture(id, drawable.id, format);
435
+ return id;
436
+ };
437
+ store.bufferPicture = picture(
438
+ store.buffer,
439
+ this.depth === 32 ? R.rgba32 : R.rgb24,
440
+ );
441
+ store.argb = pixmap(32);
442
+ store.argbPicture = picture(store.argb, R.rgba32);
443
+ store.argbGc = X.AllocID();
444
+ X.CreateGC(store.argbGc, store.argb.id, { graphicsExposures: 0 });
445
+ store.mask = pixmap(1);
446
+ store.maskPicture = picture(store.mask, R.mono1);
447
+ // nothing painted yet is nothing shown
448
+ R.FillRectangles(
449
+ R.PictOp.Src,
450
+ store.maskPicture,
451
+ [0, 0, 0, 0],
452
+ [0, 0, w, h],
453
+ );
454
+ }
455
+ this._free(cur);
456
+ this._store = store;
457
+ this._ctx = null;
458
+ // what the old pixmaps held is gone: the pane is painted whole next
459
+ this._painted = false;
460
+ }
461
+
462
+ _free(store) {
463
+ if (!store) return;
464
+ const R = this.app.display.Render;
465
+ try {
466
+ for (const id of [
467
+ store.bufferPicture,
468
+ store.argbPicture,
469
+ store.maskPicture,
470
+ ]) {
471
+ if (id) R.FreePicture(id);
237
472
  }
473
+ if (store.argbGc) this.X.FreeGC(store.argbGc);
474
+ } catch {
475
+ // the connection is closing, and takes them with it
476
+ }
477
+ store.buffer.destroy();
478
+ store.argb?.destroy();
479
+ store.mask?.destroy();
480
+ }
481
+
482
+ /** Where the next present puts the window. A new size may be new
483
+ * pixmaps, which the pane paints whole anyway (`Pane.place`). */
484
+ setState(rect) {
485
+ this._next = { ...rect };
486
+ this._grow(rect.width, rect.height);
487
+ }
488
+
489
+ /** Pixels painted at `rect`, pane-local, for the next present to show. */
490
+ owe(rect) {
491
+ this._owed.push(rect);
492
+ this._painted = true;
493
+ }
494
+
495
+ /** `rect` in whole pixels, cut to the pane's size — or null. */
496
+ _clamp(rect) {
497
+ const { width, height } = this._next ?? this.rect;
498
+ const x0 = Math.max(0, Math.floor(rect.x));
499
+ const y0 = Math.max(0, Math.floor(rect.y));
500
+ const x1 = Math.min(width, Math.ceil(rect.x + rect.width));
501
+ const y1 = Math.min(height, Math.ceil(rect.y + rect.height));
502
+ return x1 > x0 && y1 > y0 ? { x0, y0, x1, y1 } : null;
503
+ }
504
+
505
+ /**
506
+ * `Window.scrollRegion`'s contract, on the pixmap the children are
507
+ * painted in: the band of `rect` (pane-local) that survives the shift
508
+ * moves, and all of `rect` is owed to the window at the next present.
509
+ */
510
+ scrollRegion(rect, dx, dy) {
511
+ if (!this._painted) return false;
512
+ if (
513
+ !Number.isInteger(dx) ||
514
+ !Number.isInteger(dy) ||
515
+ (dx === 0 && dy === 0)
516
+ ) {
517
+ return false;
518
+ }
519
+ const r = this._clamp(rect);
520
+ if (!r) return false;
521
+ const { x0, y0, x1, y1 } = r;
522
+ const dstX0 = Math.max(x0, x0 + dx);
523
+ const dstY0 = Math.max(y0, y0 + dy);
524
+ const dstX1 = Math.min(x1, x1 + dx);
525
+ const dstY1 = Math.min(y1, y1 + dy);
526
+ if (dstX1 <= dstX0 || dstY1 <= dstY0) return false;
527
+ const store = this._store;
528
+ const target = store.argb ?? store.buffer;
529
+ this.X.CopyArea(
530
+ target.id,
531
+ target.id,
532
+ store.argbGc ?? this._gc,
533
+ dstX0 - dx,
534
+ dstY0 - dy,
535
+ dstX0,
536
+ dstY0,
537
+ dstX1 - dstX0,
538
+ dstY1 - dstY0,
539
+ );
540
+ this.owe({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
541
+ return true;
542
+ }
543
+
544
+ /**
545
+ * Show what was painted, where it goes: the ConfigureWindow of a new
546
+ * place; where the pane is cut to its ink, the painted rects laid over the
547
+ * ground and their alpha into the mask, and the shape set from it; the
548
+ * map of a pane shown for the first time; and the copies of what was
549
+ * painted since — back to back, one batch the server applies together.
550
+ * The shape goes before the copies: a pixel it takes in shows what was
551
+ * under it, never the pane's old one, until its copy lands.
552
+ */
553
+ present() {
554
+ const X = this.X;
555
+ const next = this._next;
556
+ if (next) {
557
+ this._next = null;
558
+ const change = {};
559
+ if (next.x !== this.rect.x) change.x = next.x;
560
+ if (next.y !== this.rect.y) change.y = next.y;
561
+ if (next.width !== this.rect.width) change.width = next.width;
562
+ if (next.height !== this.rect.height) change.height = next.height;
563
+ this.rect = next;
564
+ if (Object.keys(change).length !== 0) X.ConfigureWindow(this.id, change);
565
+ }
566
+ if (!this._painted) return;
567
+ const owed = [];
568
+ for (const rect of this._owed) {
569
+ const r = this._clamp(rect);
570
+ if (r) owed.push(r);
571
+ }
572
+ this._owed = [];
573
+ const store = this._store;
574
+ if (this._shape && owed.length !== 0) {
575
+ const R = this.app.display.Render;
576
+ const [r, g, b] = this.ground;
577
+ for (const { x0, y0, x1, y1 } of owed) {
578
+ const w = x1 - x0;
579
+ const h = y1 - y0;
580
+ R.FillRectangles(
581
+ R.PictOp.Src,
582
+ store.bufferPicture,
583
+ [r, g, b, 1],
584
+ [x0, y0, w, h],
585
+ );
586
+ R.Composite(
587
+ R.PictOp.Over,
588
+ store.argbPicture,
589
+ 0,
590
+ store.bufferPicture,
591
+ x0,
592
+ y0,
593
+ 0,
594
+ 0,
595
+ x0,
596
+ y0,
597
+ w,
598
+ h,
599
+ );
600
+ R.Composite(
601
+ R.PictOp.Src,
602
+ store.argbPicture,
603
+ 0,
604
+ store.maskPicture,
605
+ x0,
606
+ y0,
607
+ 0,
608
+ 0,
609
+ x0,
610
+ y0,
611
+ w,
612
+ h,
613
+ );
614
+ }
615
+ const S = this._shape;
616
+ S.Mask(S.Op.Set, S.Kind.Bounding, this.id, 0, 0, store.mask.id);
617
+ }
618
+ if (this._shown && !this._mapped) {
619
+ X.MapWindow(this.id);
620
+ this._mapped = true;
621
+ }
622
+ for (const { x0, y0, x1, y1 } of owed) {
623
+ X.CopyArea(
624
+ store.buffer.id,
625
+ this.id,
626
+ this._gc,
627
+ x0,
628
+ y0,
629
+ x0,
630
+ y0,
631
+ x1 - x0,
632
+ y1 - y0,
633
+ );
634
+ }
635
+ }
636
+
637
+ /** The buffer's pixels at `rects` (pane-local), onto the window. */
638
+ _copy(rects) {
639
+ const { width, height } = this.rect;
640
+ for (const r of rects) {
641
+ const x0 = Math.max(0, Math.floor(r.x));
642
+ const y0 = Math.max(0, Math.floor(r.y));
643
+ const x1 = Math.min(width, Math.ceil(r.x + r.width));
644
+ const y1 = Math.min(height, Math.ceil(r.y + r.height));
645
+ if (x1 <= x0 || y1 <= y0) continue;
646
+ this.X.CopyArea(
647
+ this._store.buffer.id,
648
+ this.id,
649
+ this._gc,
650
+ x0,
651
+ y0,
652
+ x0,
653
+ y0,
654
+ x1 - x0,
655
+ y1 - y0,
656
+ );
657
+ }
658
+ }
659
+
660
+ map() {
661
+ this._shown = true;
662
+ // a pane with nothing painted yet is mapped by the present that shows it
663
+ if (this._painted && !this._mapped) {
664
+ this.X.MapWindow(this.id);
665
+ this._mapped = true;
666
+ this._copy([
667
+ { x: 0, y: 0, width: this.rect.width, height: this.rect.height },
668
+ ]);
669
+ }
670
+ }
671
+
672
+ unmap() {
673
+ this._shown = false;
674
+ if (!this._mapped) return;
675
+ this.X.UnmapWindow(this.id);
676
+ this._mapped = false;
677
+ }
678
+
679
+ destroy() {
680
+ this.window.destroy?.();
681
+ this._free(this._store);
682
+ this._store = null;
683
+ this._ctx = null;
684
+ try {
685
+ this.X.FreeGC(this._gc);
686
+ } catch {
687
+ // the connection is closing, and takes the GC with it
238
688
  }
239
689
  }
240
- return rects;
241
690
  }
242
691
 
243
692
  /**
@@ -247,12 +696,45 @@ export function overlayRegions(area, surface) {
247
696
  * the pane's own last frame rather than show anything under it.
248
697
  */
249
698
  function groundOf(props) {
699
+ const [r, g, b] = groundRgb(props);
700
+ const byte = (c) => Math.round(c * 255);
701
+ return `rgb(${byte(r)}, ${byte(g)}, ${byte(b)})`;
702
+ }
703
+
704
+ /** `groundOf` as straight [r, g, b] floats, for a pane that lays its
705
+ * children over it itself (`X11PaneWindow.present`). */
706
+ function groundRgb(props) {
250
707
  const value = props.clearColor ?? 'black';
251
708
  const [r, g, b] = Array.isArray(value)
252
709
  ? value
253
710
  : (cssColorStraight(value) ?? [0, 0, 0, 1]);
254
- const byte = (c) => Math.round(Math.max(0, Math.min(1, c)) * 255);
255
- return `rgb(${byte(r)}, ${byte(g)}, ${byte(b)})`;
711
+ const clamp = (c) => Math.max(0, Math.min(1, c));
712
+ return [clamp(r), clamp(g), clamp(b)];
713
+ }
714
+
715
+ /**
716
+ * The SHAPE extension of an app's connection, asked once: resolves to it,
717
+ * or to null where the server has none — node-x11's in-process server, for
718
+ * one. Asked by the first overlay that makes X11 panes rather than at
719
+ * startup, so an app with no `<glarea>` children never asks.
720
+ */
721
+ const shapeQueries = new WeakMap();
722
+ function askShape(app) {
723
+ let pending = shapeQueries.get(app);
724
+ if (!pending) {
725
+ pending = new Promise((resolve) => {
726
+ try {
727
+ app.X.require('shape', (err, ext) =>
728
+ resolve(err ? null : (ext ?? null)),
729
+ );
730
+ } catch {
731
+ // a connection that cannot ask has none to give
732
+ resolve(null);
733
+ }
734
+ });
735
+ shapeQueries.set(app, pending);
736
+ }
737
+ return pending;
256
738
  }
257
739
 
258
740
  /**
@@ -262,10 +744,17 @@ function groundOf(props) {
262
744
  * a fresh one with subscriptions of its own on every `getContext`.
263
745
  */
264
746
  class Pane {
265
- constructor(wnd, rect, transparent) {
747
+ /**
748
+ * @param node the child an X11 pane is kept by and holds, up to — null for
749
+ * a composited pane, which holds every child
750
+ * @param overlay the panes it is one of, which `canBlit` asks about
751
+ */
752
+ constructor(wnd, rect, transparent, node = null, overlay = null) {
266
753
  this.wnd = wnd;
267
754
  this.rect = rect;
268
755
  this.transparent = transparent;
756
+ this.node = node;
757
+ this.overlay = overlay;
269
758
  this.full = true;
270
759
  this.ctx = null;
271
760
  // placed this frame: what the pane holds is where the children were in
@@ -275,15 +764,30 @@ class Pane {
275
764
  // moved pixels this frame, which has to reach the screen even when no
276
765
  // pass follows it (`GlOverlay.paint`)
277
766
  this.blitted = false;
767
+ // what this pane alone is owed this frame, beside the damage every pane
768
+ // is cut from: the pixels a moved child's pane carried wrongly, or the
769
+ // ones of it a pane over it holds (`_settleMove`)
770
+ this.owed = [];
771
+ // X11: placed this frame by exactly its child's move, which the window's
772
+ // move carried (`_syncChildren`, `_settleCarried`)
773
+ this.carried = false;
278
774
  }
279
775
 
280
776
  context() {
777
+ // an X11 pane's is its pixmap's, which a pane that grew has a new one of
778
+ if (this.wnd.ownsContext) return this.wnd.getContext('2d');
281
779
  if (!this.ctx && typeof this.wnd.getContext === 'function') {
282
780
  this.ctx = this.wnd.getContext('2d');
283
781
  }
284
782
  return this.ctx;
285
783
  }
286
784
 
785
+ /** One rect more for this pane alone, window coordinates. */
786
+ owe(rect) {
787
+ const hit = rect && intersectRects(rect, this.rect);
788
+ if (hit) this.owed = addDamageRect(this.owed, hit, OWED_CAP);
789
+ }
790
+
287
791
  /** Somewhere else. A new size is a new backing — a pixmap on X11, a
288
792
  * bitmap on Cocoa — and has to be painted whole; a move keeps what the
289
793
  * pane holds, which moved with the children it shows. */
@@ -322,6 +826,11 @@ class Pane {
322
826
  * the Cocoa pane's own): the band that survives inside `rect` moves, the
323
827
  * rest of it is left as it was. False when the backend could not, which
324
828
  * leaves the pane exactly as it was.
829
+ *
830
+ * A child's pane on X11 is not the only one holding those pixels: every
831
+ * pane over it that reaches into `rect` holds a copy, which the move
832
+ * leaves where it was. Those are owed `rect` (`owe`), so they repaint
833
+ * what they hold of it.
325
834
  */
326
835
  blit(rect, dx, dy) {
327
836
  const local = {
@@ -332,6 +841,12 @@ class Pane {
332
841
  };
333
842
  if (!this.wnd.scrollRegion(local, dx, dy)) return false;
334
843
  this.blitted = true;
844
+ const panes = this.node ? this.overlay?.panes : null;
845
+ if (panes) {
846
+ for (let i = panes.indexOf(this) + 1; i < panes.length; i++) {
847
+ panes[i].owe(rect);
848
+ }
849
+ }
335
850
  return true;
336
851
  }
337
852
 
@@ -353,7 +868,23 @@ export class GlOverlay {
353
868
  this.app = area.app;
354
869
  // one pane over the whole surface, where the backend composites it
355
870
  this.composited = typeof this.app?.createOverlayPane === 'function';
871
+ // bottom to top: the children's order on X11, one pane each
356
872
  this.panes = [];
873
+ // X11: each child's pane, kept by the child for as long as it has one
874
+ this.byNode = new Map();
875
+ // X11: the SHAPE extension the panes are cut to their ink with — null
876
+ // until the server has answered, and for good where it has none
877
+ this.shape = null;
878
+ if (!this.composited && X11PaneWindow.usable(this.app)) {
879
+ askShape(this.app).then((shape) => {
880
+ if (!shape || this.destroyed) return;
881
+ this.shape = shape;
882
+ // panes made before the answer: cut now, painted whole next frame
883
+ for (const pane of this.panes) {
884
+ if (pane.wnd.cutToInk?.(shape)) this._lost(pane);
885
+ }
886
+ });
887
+ }
357
888
  // the children this frame's layout moved and nothing else, reported
358
889
  // rather than claimed (`GlAreaNode._absolutizeChild`) and settled after
359
890
  // the panes are (`settleMoves`)
@@ -373,42 +904,115 @@ export class GlOverlay {
373
904
  const area = this.area;
374
905
  const abs = area.abs;
375
906
  for (const pane of this.panes) pane.placed = false;
376
- let rects = [];
377
- if (shown(area) && abs.width > 0 && abs.height > 0) {
378
- // the surface's own rect, rounded the way its window's is
379
- const surface = area._geometry();
380
- if (!this.composited) rects = overlayRegions(area, surface);
381
- else if (area.paintOrder().length) rects = [surface];
382
- }
907
+ const visible = shown(area) && abs.width > 0 && abs.height > 0;
908
+ // the surface's own rect, rounded the way its window's is
909
+ const surface = visible ? area._geometry() : null;
910
+ if (!this.composited) return this._syncChildren(surface);
911
+ const rects = surface && area.paintOrder().length ? [surface] : [];
383
912
  let changed = false;
384
913
  while (this.panes.length > rects.length) {
385
914
  this.panes.pop().destroy();
386
915
  changed = true;
387
916
  }
388
- let made = false;
389
917
  for (let i = 0; i < rects.length; i++) {
390
918
  const pane = this.panes[i];
391
919
  if (!pane) {
392
- this.panes.push(this._makePane(rects[i]));
393
- made = changed = true;
920
+ this.panes.push(this._makePane(rects[i], null));
921
+ changed = true;
394
922
  } else if (!sameRect(pane.rect, rects[i])) {
395
923
  pane.place(rects[i]);
396
924
  changed = true;
397
925
  }
398
926
  }
399
- if (made) this.restack();
400
927
  return changed;
401
928
  }
402
929
 
403
- _makePane(rect) {
930
+ /**
931
+ * `sync` on X11: a pane per child, kept by the child. A child that is new
932
+ * gets one, a child that went takes its with it, and one whose reach moved
933
+ * has its pane placed there — and painted whole, unless the move was the
934
+ * child's own and nothing else, which the window's move carries
935
+ * (`_settleMove` repaints what it does not). A change of order restacks
936
+ * them, and repaints whichever overlap another: what a pane holds is what
937
+ * is under its child.
938
+ */
939
+ _syncChildren(surface) {
940
+ const regions = surface ? childRegions(this.area, surface) : [];
941
+ const moved = new Map();
942
+ for (const move of this.moves) moved.set(move.node, move);
943
+ const next = [];
944
+ let changed = false;
945
+ let made = false;
946
+ const kept = new Set();
947
+ for (const pane of this.panes) pane.carried = false;
948
+ for (const { node, rect } of regions) {
949
+ let pane = this.byNode.get(node);
950
+ if (!pane) {
951
+ pane = this._makePane(rect, node);
952
+ this.byNode.set(node, pane);
953
+ made = changed = true;
954
+ } else if (!sameRect(pane.rect, rect)) {
955
+ const move = moved.get(node);
956
+ const carried =
957
+ move !== undefined &&
958
+ !pane.full &&
959
+ rect.width === pane.rect.width &&
960
+ rect.height === pane.rect.height &&
961
+ rect.x === pane.rect.x + move.dx &&
962
+ rect.y === pane.rect.y + move.dy;
963
+ pane.place(rect);
964
+ if (carried) pane.carried = true;
965
+ else pane.full = true;
966
+ changed = true;
967
+ }
968
+ kept.add(node);
969
+ next.push(pane);
970
+ }
971
+ for (const [node, pane] of this.byNode) {
972
+ if (kept.has(node)) continue;
973
+ pane.destroy();
974
+ this.byNode.delete(node);
975
+ changed = true;
976
+ }
977
+ const reordered =
978
+ next.length !== this.panes.length ||
979
+ next.some((pane, i) => pane !== this.panes[i]);
980
+ this.panes = next;
981
+ if (reordered) {
982
+ this.restack();
983
+ if (!made) {
984
+ // what each pane holds is its child and what is under it: a new
985
+ // order is new contents wherever two of them meet
986
+ for (const pane of next) {
987
+ if (
988
+ next.some(
989
+ (other) => other !== pane && overlaps(other.rect, pane.rect),
990
+ )
991
+ ) {
992
+ pane.full = true;
993
+ }
994
+ }
995
+ }
996
+ changed = true;
997
+ }
998
+ return changed;
999
+ }
1000
+
1001
+ _makePane(rect, node) {
404
1002
  const owner = this.area.root.window;
405
1003
  const attributes = { parent: owner, ...rect };
406
- const pane = this.composited
407
- ? new Pane(this.app.createOverlayPane(attributes), rect, true)
408
- : // A plain child window on the owning window's visual, selecting
409
- // nothing but what ntk selects for itself — the structure events and
410
- // the exposures its backing store answers. The pointer is the tree's.
411
- new Pane(this.app.createWindow(attributes), rect, false);
1004
+ let wnd;
1005
+ if (this.composited) wnd = this.app.createOverlayPane(attributes);
1006
+ else if (X11PaneWindow.usable(this.app)) {
1007
+ // a child window and its own double buffer, selecting nothing but the
1008
+ // exposures a pixmap answers — the pointer is the tree's
1009
+ wnd = new X11PaneWindow(this.app, owner, rect, this.shape);
1010
+ } else {
1011
+ // an app with no X connection to speak for itself — the headless mock
1012
+ // — makes its panes as plain windows
1013
+ wnd = this.app.createWindow(attributes);
1014
+ }
1015
+ const pane = new Pane(wnd, rect, this.composited, node, this);
412
1016
  // ntk asks for a redraw when the backing no longer holds the picture — a
413
1017
  // resize it could not carry over — and a pane has nothing to redraw from
414
1018
  // but the children, on the next frame of the window they live in
@@ -480,6 +1084,22 @@ export class GlOverlay {
480
1084
  if (!root || root.destroyed || root._paneDamage === FULL_DAMAGE) {
481
1085
  return true;
482
1086
  }
1087
+ if (!this.composited) {
1088
+ // Each child's move is its own pane's to settle: carried by the
1089
+ // window's move where its pane went with it, moved on the pane where
1090
+ // the pane stayed — cut to the surface, say, which a box bigger than
1091
+ // it is — or claimed.
1092
+ const reaches = new Map();
1093
+ for (const move of moves) reaches.set(move.node, move.reach);
1094
+ for (const move of moves) {
1095
+ const own = this.byNode.get(move.node);
1096
+ if (own?.carried) this._settleCarried(move, reaches);
1097
+ else if (!own || !this._blitMove(root, move, own)) {
1098
+ this._claimMove(root, move);
1099
+ }
1100
+ }
1101
+ return true;
1102
+ }
483
1103
  const surface = this.area._geometry();
484
1104
  let best = null;
485
1105
  let most = 0;
@@ -498,6 +1118,46 @@ export class GlOverlay {
498
1118
  return true;
499
1119
  }
500
1120
 
1121
+ /**
1122
+ * A child that only moved, on X11, whose pane went with it (`sync`): what
1123
+ * each pane is owed for it, and nothing in the frame's damage, which every
1124
+ * pane is cut from.
1125
+ *
1126
+ * - Its own pane: the window's move brought the child's pixels and the
1127
+ * ground around them, which is one colour, and took along whatever of
1128
+ * the children under it the pane held. Those are owed where they are
1129
+ * now and where the move put their old pixels — and when nothing is
1130
+ * under it, nothing is, which is a card dragged through open graph: one
1131
+ * ConfigureWindow a step.
1132
+ * - The panes over it, which hold its pixels where they meet it: where it
1133
+ * was, and where it is.
1134
+ * - The panes under it hold nothing of it, and are owed nothing.
1135
+ */
1136
+ _settleCarried({ node, reach, dx, dy }, reaches) {
1137
+ const panes = this.panes;
1138
+ const at = panes.indexOf(this.byNode.get(node));
1139
+ const own = panes[at];
1140
+ const order = this.area.paintOrder();
1141
+ const upTo = order.indexOf(node);
1142
+ for (let i = 0; i < upTo; i++) {
1143
+ const under = order[i];
1144
+ const bounds = [whole(under.paintBounds())];
1145
+ // one that moved this frame as well was somewhere else in the pane
1146
+ const before = reaches.get(under);
1147
+ if (before) bounds.push(whole(before));
1148
+ for (const b of bounds) {
1149
+ own.owe(b);
1150
+ own.owe(shiftRect(b, dx, dy));
1151
+ }
1152
+ }
1153
+ const was = whole(reach);
1154
+ const now = whole(node.paintBounds());
1155
+ for (let i = at + 1; i < panes.length; i++) {
1156
+ panes[i].owe(was);
1157
+ panes[i].owe(now);
1158
+ }
1159
+ }
1160
+
501
1161
  /** Where a move's child was and where it is, claimed the ordinary way —
502
1162
  * cut to the surface's box, which clips the children (`clipsChildren`). */
503
1163
  _claimMove(root, { node, reach }) {
@@ -534,7 +1194,7 @@ export class GlOverlay {
534
1194
  * Past `MOVE_BLIT_MAX_REPAINT` of `dest` those repaint most of what the
535
1195
  * copy saves, and the plain claims have the frame.
536
1196
  */
537
- _blitMove(root, { node, reach, dx, dy }) {
1197
+ _blitMove(root, { node, reach, dx, dy }, own = null) {
538
1198
  if (NO_BLIT || !Number.isInteger(dx) || !Number.isInteger(dy)) {
539
1199
  return false;
540
1200
  }
@@ -549,9 +1209,12 @@ export class GlOverlay {
549
1209
  const now = intersectRects(whole(node.paintBounds()), box);
550
1210
  // nothing of it was on show, so there is nothing to carry
551
1211
  if (!was) return false;
552
- const pane = this.panes.find(
553
- (p) => p.canBlit(was) && (!now || p.canBlit(now)),
554
- );
1212
+ // the child's own pane on X11, the one over the whole surface elsewhere
1213
+ const pane = own
1214
+ ? own.canBlit(was) && (!now || own.canBlit(now))
1215
+ ? own
1216
+ : null
1217
+ : this.panes.find((p) => p.canBlit(was) && (!now || p.canBlit(now)));
555
1218
  if (!pane) return false;
556
1219
  const view = intersectRects(pane.rect, box);
557
1220
  const dest = intersectRects(shiftRect(was, dx, dy), view);
@@ -566,7 +1229,13 @@ export class GlOverlay {
566
1229
  add(intersectRects(shiftRect(claim, dx, dy), dest));
567
1230
  for (const piece of outside(was, dest)) add(piece);
568
1231
  if (now) for (const piece of outside(now, dest)) add(piece);
569
- for (const other of area.paintOrder()) {
1232
+ // the children whose pixels the pane holds: every other one on a pane
1233
+ // over the whole surface, and on X11 the ones under this child — the
1234
+ // panes over it are owed the copy by `Pane.blit`
1235
+ const order = area.paintOrder();
1236
+ const upTo = own ? order.indexOf(node) : order.length;
1237
+ for (let i = 0; i < upTo; i++) {
1238
+ const other = order[i];
570
1239
  if (other === node) continue;
571
1240
  const theirs = whole(other.paintBounds());
572
1241
  add(intersectRects(theirs, dest));
@@ -607,15 +1276,20 @@ export class GlOverlay {
607
1276
  passes = [];
608
1277
  for (const rect of damage) {
609
1278
  const hit = intersectRects(rect, pane.rect);
610
- if (hit) passes.push(hit);
1279
+ if (hit) passes = addDamageRect(passes, hit, OWED_CAP);
1280
+ }
1281
+ for (const rect of pane.owed) {
1282
+ passes = addDamageRect(passes, rect, OWED_CAP);
611
1283
  }
612
1284
  }
1285
+ pane.owed = [];
613
1286
  const blitted = pane.blitted;
614
1287
  pane.blitted = false;
615
1288
  pane.placed = false;
616
1289
  if (passes.length === 0) {
617
- // pixels moved and nothing else owed: they still go out
618
- if (blitted) pane.wnd.present?.();
1290
+ // pixels moved, or a place to take, and nothing else owed: they
1291
+ // still go out
1292
+ if (blitted || pane.wnd.presentOwed) pane.wnd.present?.();
619
1293
  continue;
620
1294
  }
621
1295
  pane.full = false;
@@ -628,8 +1302,12 @@ export class GlOverlay {
628
1302
  const root = area.root;
629
1303
  if (!root) return;
630
1304
  // an opaque pane is filled with what the surface starts its frames from;
631
- // a composited one is cleared, and shows the frame itself
632
- const ground = pane.transparent ? null : groundOf(area.props);
1305
+ // a composited one is cleared, and shows the frame itself; and one cut
1306
+ // to its ink is cleared too, and lays the ground under what the children
1307
+ // painted itself, where the frame is not shown (`X11PaneWindow`)
1308
+ const alpha = pane.transparent || pane.wnd.paintsAlpha === true;
1309
+ const ground = alpha ? null : groundOf(area.props);
1310
+ if (pane.wnd.paintsAlpha) pane.wnd.ground = groundRgb(area.props);
633
1311
  // one pass, drawn by a context in window coordinates
634
1312
  const paintPass = (ctx, pass) => {
635
1313
  ctx.save();
@@ -644,7 +1322,9 @@ export class GlOverlay {
644
1322
  ctx.clearRect(pass.x, pass.y, pass.width, pass.height);
645
1323
  }
646
1324
  root._paintDamage = pass;
647
- area._paintChildren(ctx);
1325
+ // an X11 pane holds its child and what is under it, and never
1326
+ // what is over it — the pane over it holds that (see the header)
1327
+ area._paintChildren(ctx, pane.node);
648
1328
  } finally {
649
1329
  root._paintDamage = null;
650
1330
  ctx.restore();
@@ -667,8 +1347,19 @@ export class GlOverlay {
667
1347
  ctx.restore();
668
1348
  }
669
1349
  }
670
- // A layer's contents are a copy the bitmap is pushed to; an X window's
1350
+ // What was painted goes out with the place it goes to (`X11PaneWindow`);
1351
+ // a layer's contents are a copy the bitmap is pushed to; an ntk window's
671
1352
  // backing store is blitted by ntk on its own, from the paint above
1353
+ if (typeof pane.wnd.owe === 'function') {
1354
+ for (const pass of passes) {
1355
+ pane.wnd.owe({
1356
+ x: pass.x - pane.rect.x,
1357
+ y: pass.y - pane.rect.y,
1358
+ width: pass.width,
1359
+ height: pass.height,
1360
+ });
1361
+ }
1362
+ }
672
1363
  pane.wnd.present?.();
673
1364
  }
674
1365
 
@@ -678,7 +1369,9 @@ export class GlOverlay {
678
1369
  }
679
1370
 
680
1371
  destroy() {
1372
+ this.destroyed = true;
681
1373
  for (const pane of this.panes) pane.destroy();
682
1374
  this.panes = [];
1375
+ this.byNode.clear();
683
1376
  }
684
1377
  }