react-x11 2.33.0 → 2.35.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.33.0",
3
+ "version": "2.35.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -109,7 +109,7 @@
109
109
  "yoga-layout": "^3.2.1"
110
110
  },
111
111
  "optionalDependencies": {
112
- "@windowkit/appkit": "^0.19.0",
112
+ "@windowkit/appkit": "^0.22.0",
113
113
  "@windowkit/wayland": "^3.1.1",
114
114
  "@windowkit/win32": "^0.0.6",
115
115
  "dbus-native": "^0.16.0",
@@ -79,6 +79,11 @@ const rectsOverlap = (a, b) =>
79
79
  a.y < b.y + b.height &&
80
80
  b.y < a.y + a.height;
81
81
 
82
+ // `a` cut to `b`: their overlap, or a clip of no size where they have none,
83
+ // which hides what it holds rather than holding it whole
84
+ const cutTo = (a, b) =>
85
+ intersectRects(a, b) ?? { x: b.x, y: b.y, width: 0, height: 0 };
86
+
82
87
  const containsRect = (outer, inner) =>
83
88
  inner.x >= outer.x &&
84
89
  inner.y >= outer.y &&
@@ -615,13 +620,23 @@ export class CocoaPromotion {
615
620
  !(parent.style?.borderRadius > 0)
616
621
  ) {
617
622
  const inner = insetRect(parent.abs, borderReach(parent));
618
- // a clip with round corners cut again is no box's shape
619
- if (cut.radius > 0 && cut.clip && !containsRect(inner, cut.clip)) {
620
- return false;
623
+ // a clip with round corners cut again is no one box's shape: the
624
+ // square ones go in a box of their own around it (`outer`)
625
+ if (cut.radius > 0 && cut.clip) {
626
+ if (cut.outer || !containsRect(inner, cut.clip)) {
627
+ cut.outer = cut.outer ? cutTo(cut.outer, inner) : inner;
628
+ }
629
+ } else {
630
+ cut.clip = cut.clip ? cutTo(cut.clip, inner) : inner;
631
+ }
632
+ bounds = intersectRects(bounds, cut.clip);
633
+ if (bounds && cut.outer) bounds = intersectRects(bounds, cut.outer);
634
+ // nothing of the part shows: its layer is kept, cut to show nothing,
635
+ // and there is nothing it could be under or over until it does
636
+ if (!bounds) {
637
+ cut.bounds = null;
638
+ return true;
621
639
  }
622
- cut.clip = cut.clip ? intersectRects(cut.clip, inner) : inner;
623
- bounds = cut.clip && intersectRects(bounds, cut.clip);
624
- if (!bounds) return false;
625
640
  }
626
641
  const order = parent.paintOrder();
627
642
  for (let j = order.indexOf(n) + 1; j < order.length; j++) {
@@ -28,11 +28,27 @@
28
28
  // delay a part joined half way through needs, so that is the bridge sprites
29
29
  // ask for: on an older one nothing is asked and every part is drawn by its
30
30
  // element.
31
+ //
32
+ // A part none of which shows — scrolled out of its pane, or outside its own
33
+ // clip — keeps its layer, cut to show nothing, up to a budget of raster for
34
+ // such parts: its element's clock would otherwise run for it, and it is
35
+ // already where its animation has it the frame a scroll brings it back. A
36
+ // clip with round corners that a square ancestor cuts again is a box of its
37
+ // own inside a box of the square one's.
38
+ //
39
+ // A part may be inside another of its element's (`parent`): a spinner in a
40
+ // card that fades. Its layer is lifted inside the parent's, so it fades,
41
+ // turns and is cut with it, and the parent's raster leaves it out — the
42
+ // presenter hands the parent's `paint` the keys of its children lifted with
43
+ // it, and paints it again whenever they change, in the frame they do. The
44
+ // parent was asked about everywhere it can be, so a child is asked about
45
+ // nothing more where it stays inside the parent's raster, and is its
46
+ // element's to draw where it does not, or where its parent is not lifted.
31
47
 
32
48
  import { RASTER_PAD, RasterState } from './presenter.js';
33
49
  import { CocoaPlayer, PlayerLift } from './player.js';
34
50
  import { VideoLift, canLiftVideo } from './video.js';
35
- import { intersectRects } from '../nodes/rects.js';
51
+ import { intersectRects, rectContains } from '../nodes/rects.js';
36
52
  import { DEV } from '../nodes/util.js';
37
53
  import { isVideoFrames } from '../videoframes.js';
38
54
 
@@ -41,12 +57,29 @@ import { isVideoFrames } from '../videoframes.js';
41
57
  // upload for every new version.
42
58
  const MAX_SIDE = 8192;
43
59
 
60
+ // The raster a window keeps for parts that show nothing — scrolled out of
61
+ // their pane, or outside their own clip — in device pixels: 32 MB. Past it
62
+ // such a part is its element's, which runs it on its own clock.
63
+ const HIDDEN_BUDGET = 8 * 1024 * 1024;
64
+
44
65
  // Sorted after the element's children, which its own drawing comes after
45
66
  // (`Node.paint` paints the children first), and before its next sibling.
46
67
  const AFTER_CHILDREN = Number.MAX_SAFE_INTEGER;
47
68
 
48
69
  const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
49
70
 
71
+ const ORIGIN = Object.freeze({ x: 0, y: 0 });
72
+
73
+ // what a part with no part lifted inside it is handed
74
+ const NO_KIDS = Object.freeze(new Set());
75
+
76
+ // keyframes already found good, by the property they were found good for
77
+ const CHECKED = new WeakMap();
78
+
79
+ // where a part can be about its origin, by its one transform animation's
80
+ // keyframes, and what that was worked out for (`extentOf`)
81
+ const EXTENTS = new WeakMap();
82
+
50
83
  // what a part's raster holds before its first paint
51
84
  const UNPAINTED = Symbol('unpainted');
52
85
 
@@ -85,8 +118,13 @@ function isAnimation(a) {
85
118
  if (!finite(a.duration) || !(a.duration > 0)) return false;
86
119
  const values = a.values;
87
120
  if (!Array.isArray(values) || values.length < 2) return false;
88
- const value = a.property === 'opacity' ? finite : isMatrix;
89
- if (!values.every(value)) return false;
121
+ // an element hands the same keyframes over from frame to frame: they
122
+ // are read once
123
+ if (CHECKED.get(values) !== a.property) {
124
+ const value = a.property === 'opacity' ? finite : isMatrix;
125
+ if (!values.every(value)) return false;
126
+ CHECKED.set(values, a.property);
127
+ }
90
128
  if (a.keyTimes !== undefined) {
91
129
  const t = a.keyTimes;
92
130
  if (!Array.isArray(t) || t.length !== values.length || t[0] !== 0) {
@@ -127,6 +165,9 @@ function isSprite(sprite) {
127
165
  }
128
166
  if (sprite.reach !== undefined && !isRect(sprite.reach)) return false;
129
167
  if (sprite.clip !== undefined && !isRect(sprite.clip)) return false;
168
+ if (sprite.parent !== undefined && typeof sprite.parent !== 'string') {
169
+ return false;
170
+ }
130
171
  if (
131
172
  sprite.clipRadius !== undefined &&
132
173
  !(finite(sprite.clipRadius) && sprite.clipRadius >= 0)
@@ -210,11 +251,38 @@ function unionRect(a, b) {
210
251
  function extentOf(sprite) {
211
252
  const reach = sprite.reach ?? sprite.rect;
212
253
  const origin = originOf(sprite);
213
- let box = mapRect(reach, sprite.transform ?? IDENTITY, origin);
254
+ const m0 = sprite.transform ?? IDENTITY;
255
+ // About its origin, where it can be is the same for a part that only
256
+ // moved: kept by its keyframes, which an element hands over from frame
257
+ // to frame, rather than every keyframe mapped every frame.
258
+ const rel = {
259
+ x: reach.x - origin.x,
260
+ y: reach.y - origin.y,
261
+ width: reach.width,
262
+ height: reach.height,
263
+ };
264
+ let turns = null;
214
265
  for (const a of sprite.animations ?? []) {
215
266
  if (a.property !== 'transform') continue;
216
- for (const m of a.values) box = unionRect(box, mapRect(reach, m, origin));
267
+ turns = turns === null ? a.values : undefined;
268
+ }
269
+ const key = `${rel.x},${rel.y},${rel.width},${rel.height},${m0.join(',')}`;
270
+ let about = turns ? EXTENTS.get(turns) : null;
271
+ if (about?.key !== key) {
272
+ let box = mapRect(rel, m0, ORIGIN);
273
+ for (const a of sprite.animations ?? []) {
274
+ if (a.property !== 'transform') continue;
275
+ for (const m of a.values) box = unionRect(box, mapRect(rel, m, ORIGIN));
276
+ }
277
+ about = { key, box };
278
+ if (turns) EXTENTS.set(turns, about);
217
279
  }
280
+ const box = {
281
+ x: about.box.x + origin.x,
282
+ y: about.box.y + origin.y,
283
+ width: about.box.width,
284
+ height: about.box.height,
285
+ };
218
286
  const x = Math.floor(box.x) - RASTER_PAD;
219
287
  const y = Math.floor(box.y) - RASTER_PAD;
220
288
  return {
@@ -315,6 +383,8 @@ export class SpriteLayers {
315
383
  this.hosts.delete(host);
316
384
  if (!host.destroyed) changed.add(host);
317
385
  }
386
+ // the raster kept for parts that show nothing, this frame
387
+ let hidden = 0;
318
388
  for (const [host, list] of offers) {
319
389
  const was = this.hosts.get(host);
320
390
  const now = new Map();
@@ -331,6 +401,15 @@ export class SpriteLayers {
331
401
  continue;
332
402
  }
333
403
  if (now.has(sprite.key)) continue;
404
+ // inside another of the element's parts: lifted with it or not at
405
+ // all, its parent earlier in the list, and neither of them a source
406
+ let parent = null;
407
+ if (sprite.parent !== undefined) {
408
+ parent = now.get(sprite.parent) ?? null;
409
+ if (!parent || parent.contents !== null || sprite.contents != null) {
410
+ continue;
411
+ }
412
+ }
334
413
  const contents = sprite.contents ?? null;
335
414
  // a source this bridge cannot show: the element draws it
336
415
  if (contents !== null && !this._canShow(contents)) continue;
@@ -352,32 +431,69 @@ export class SpriteLayers {
352
431
  };
353
432
  const extent = extentOf(sprite);
354
433
  const bounds = cut.clip ? intersectRects(extent, cut.clip) : extent;
355
- if (!bounds || !this.promotion._clear(host, bounds, cut)) continue;
434
+ if (parent) {
435
+ // the parent was asked about everywhere it can be, and its layer
436
+ // carries this one: nothing more to ask where it stays inside the
437
+ // parent's raster, and its element draws it where it does not
438
+ if (bounds && !rectContains(rasterRectOf(parent.sprite), bounds)) {
439
+ continue;
440
+ }
441
+ cut.bounds = bounds;
442
+ } else if (!bounds) {
443
+ // its own clip shows nothing of it
444
+ cut.bounds = null;
445
+ } else if (!this.promotion._clear(host, bounds, cut)) {
446
+ continue;
447
+ }
356
448
  const shown = cut.bounds;
449
+ // A part that shows nothing stays on its layer, cut to show nothing,
450
+ // so that its element's clock is not kept running for it, and is
451
+ // where it should be the frame a scroll brings it into view — but
452
+ // for a source, and past the budget of raster kept for such parts.
453
+ if (!shown) {
454
+ if (contents !== null) continue;
455
+ const px = raster.width * raster.height;
456
+ if (hidden + px > HIDDEN_BUDGET) continue;
457
+ hidden += px;
458
+ }
357
459
  let state = was?.get(sprite.key);
358
460
  // a clip that comes or goes is a layer in a box or out of one: lifted
359
461
  // again, rather than a running layer moved between parents — and so
360
- // is a part that shows another source, or stops painting to show one
462
+ // is a part that shows another source, or stops painting to show
463
+ // one, and one in another layer than it was
361
464
  if (
362
465
  state &&
363
466
  (Boolean(state.clip) !== Boolean(cut.clip) ||
364
- state.contents !== contents)
467
+ Boolean(state.outer) !== Boolean(cut.outer) ||
468
+ state.contents !== contents ||
469
+ state.parent !== parent)
365
470
  ) {
366
471
  this._drop(state, root);
367
472
  state = null;
368
473
  }
369
474
  const lifting = !state;
370
475
  if (lifting) {
371
- state = this._lift(host, sprite.key, cut.clip !== null, contents);
476
+ state = this._lift(
477
+ host,
478
+ sprite.key,
479
+ cut.clip !== null,
480
+ contents,
481
+ parent,
482
+ Boolean(cut.outer),
483
+ );
372
484
  }
373
485
  state.sprite = sprite;
374
486
  state.clip = cut.clip;
487
+ state.outer = cut.outer ?? null;
375
488
  state.radius = cut.radius;
376
- state.order = [...place, AFTER_CHILDREN, index];
489
+ state.order = parent ? null : [...place, AFTER_CHILDREN, index];
490
+ state.kids = [];
491
+ parent?.kids.push(state);
377
492
  // the bitmap under where it shows repaints without it, from this
378
493
  // frame on; where it went since, it is the element's own claims
379
- // that move the hole
380
- if (lifting) this.promotion._claim(root, shown);
494
+ // that move the hole. A child is under its parent's layer, whose
495
+ // raster leaves it out instead (`sync`).
496
+ if (lifting && !parent && shown) this.promotion._claim(root, shown);
381
497
  state.extent = shown;
382
498
  now.set(sprite.key, state);
383
499
  }
@@ -396,11 +512,12 @@ export class SpriteLayers {
396
512
  return changed;
397
513
  }
398
514
 
399
- /** Every lifted part, with its place in the paint order. */
515
+ /** Every lifted part above the bitmap, with its place in the paint
516
+ * order: not one inside another's layer, which goes with it (`sync`). */
400
517
  entries() {
401
518
  const out = [];
402
519
  for (const lifted of this.hosts.values()) {
403
- for (const state of lifted.values()) out.push(state);
520
+ for (const state of lifted.values()) if (!state.parent) out.push(state);
404
521
  }
405
522
  return out;
406
523
  }
@@ -426,23 +543,41 @@ export class SpriteLayers {
426
543
  return isVideoFrames(contents) && this.videos;
427
544
  }
428
545
 
429
- _lift(host, key, clipped, contents = null) {
546
+ _lift(host, key, clipped, contents = null, parent = null, outered = false) {
430
547
  const layer = this.native.createLayer();
431
548
  // a part cut to a clip is in a box of the clip's size that masks to it,
432
- // and the box is what stands among the layers above the bitmap
549
+ // and the box is what stands among the layers above the bitmap — or
550
+ // among its parent's children, for a part inside another's layer. A
551
+ // clip with round corners that a square one cuts again is in a box of
552
+ // its own inside a box of the square one's (`outer`).
553
+ const above = parent ? parent.layer : this.promotion.rootVisual.layer;
433
554
  let box = null;
555
+ let outerBox = null;
434
556
  if (clipped) {
435
557
  box = this.native.createLayer();
436
- this.native.addSublayer(this.promotion.rootVisual.layer, box);
558
+ if (outered) {
559
+ outerBox = this.native.createLayer();
560
+ this.native.addSublayer(above, outerBox);
561
+ this.native.addSublayer(outerBox, box);
562
+ } else {
563
+ this.native.addSublayer(above, box);
564
+ }
437
565
  this.native.addSublayer(box, layer);
438
566
  } else {
439
- this.native.addSublayer(this.promotion.rootVisual.layer, layer);
567
+ this.native.addSublayer(above, layer);
440
568
  }
441
569
  return {
442
570
  host,
443
571
  key,
444
572
  layer,
445
573
  box,
574
+ outerBox,
575
+ outerProps: {},
576
+ outer: null, // the square clips around a rounded one, device pixels
577
+ // the part whose layer this one is in, and the parts in this one's
578
+ parent,
579
+ kids: [],
580
+ paintedKids: '',
446
581
  boxProps: {},
447
582
  clip: null, // device pixels, window coordinates
448
583
  radius: 0, // the clip's corners, device pixels
@@ -475,11 +610,15 @@ export class SpriteLayers {
475
610
  * element drawing it again. */
476
611
  _drop(state, root) {
477
612
  state.lift?.release();
478
- this.native.removeFromSuperlayer(state.box ?? state.layer);
613
+ this.native.removeFromSuperlayer(
614
+ state.outerBox ?? state.box ?? state.layer,
615
+ );
479
616
  state.raster.release(this.native);
480
617
  for (const run of state.runs.values()) this._ends().delete(run.caId);
481
618
  state.runs.clear();
482
- if (!state.host.destroyed && state.extent) {
619
+ // a child's is under its parent's layer, which paints it again or goes
620
+ // with it
621
+ if (!state.host.destroyed && state.extent && !state.parent) {
483
622
  this.promotion._claim(root, state.extent);
484
623
  }
485
624
  }
@@ -491,28 +630,65 @@ export class SpriteLayers {
491
630
  const rect = rasterRectOf(sprite);
492
631
  const reach = sprite.reach ?? sprite.rect;
493
632
  const phase = `${reach.x - Math.floor(reach.x)},${reach.y - Math.floor(reach.y)}`;
633
+ // the parts lifted inside it, which its raster leaves out
634
+ const kids = state.kids.map((kid) => kid.key).join('\n');
494
635
  // painted again for a new version, and for a raster of another size or
495
- // with the part at another fraction of a pixel; a move by whole pixels
496
- // is the layer's alone. A source paints nothing: it shows itself.
636
+ // with the part at another fraction of a pixel, or with other parts
637
+ // lifted inside it; a move by whole pixels is the layer's alone. A
638
+ // source paints nothing: it shows itself.
497
639
  if (
498
640
  !state.lift &&
499
641
  (state.painted === UNPAINTED ||
500
642
  sprite.version !== state.painted ||
501
643
  state.rect?.width !== rect.width ||
502
644
  state.rect?.height !== rect.height ||
503
- state.phase !== phase)
645
+ state.phase !== phase ||
646
+ state.paintedKids !== kids)
504
647
  ) {
505
648
  this._paint(state, sprite, rect);
506
649
  state.phase = phase;
650
+ state.paintedKids = kids;
507
651
  }
508
652
  state.rect = rect;
509
653
  const origin = originOf(sprite);
510
654
  const m = sprite.transform ?? IDENTITY;
655
+ // where its layer's coordinates start: the window's corner, or the
656
+ // corner of its parent's raster, for a part inside another's layer
657
+ const base = state.parent?.rect ?? ORIGIN;
658
+ // in a box of the square clips around its own, where a square one cuts
659
+ // a rounded clip again: placed from the window's corner, at the part's
660
+ // place among the layers
661
+ const outer = state.outer;
662
+ if (state.outerBox) {
663
+ const frame = [
664
+ (outer.x - base.x) / s,
665
+ (outer.y - base.y) / s,
666
+ outer.width / s,
667
+ outer.height / s,
668
+ ];
669
+ const was = state.outerProps;
670
+ if (!sameMatrix(was.frame, frame) || was.zPosition !== z) {
671
+ this.native.setLayerProps(state.outerBox, {
672
+ frame,
673
+ masksToBounds: true,
674
+ zPosition: z,
675
+ });
676
+ state.outerProps = { frame, zPosition: z };
677
+ }
678
+ }
511
679
  // in its box, where it is cut to a clip: placed from the clip's corner,
512
- // and the box at the part's place among the layers
680
+ // and the box at the part's place among the layers, or from the outer
681
+ // box's corner inside it
513
682
  const clip = state.clip;
514
683
  if (state.box) {
515
- const frame = [clip.x / s, clip.y / s, clip.width / s, clip.height / s];
684
+ const from = state.outerBox ? outer : base;
685
+ const frame = [
686
+ (clip.x - from.x) / s,
687
+ (clip.y - from.y) / s,
688
+ clip.width / s,
689
+ clip.height / s,
690
+ ];
691
+ const boxZ = state.outerBox ? 0 : z;
516
692
  // its corners rounded where the clip's are: a circle's arc, as wide
517
693
  // as the box allows
518
694
  const cornerRadius =
@@ -520,20 +696,20 @@ export class SpriteLayers {
520
696
  const was = state.boxProps;
521
697
  if (
522
698
  !sameMatrix(was.frame, frame) ||
523
- was.zPosition !== z ||
699
+ was.zPosition !== boxZ ||
524
700
  was.cornerRadius !== cornerRadius
525
701
  ) {
526
702
  this.native.setLayerProps(state.box, {
527
703
  frame,
528
704
  masksToBounds: true,
529
705
  cornerRadius,
530
- zPosition: z,
706
+ zPosition: boxZ,
531
707
  });
532
- state.boxProps = { frame, zPosition: z, cornerRadius };
708
+ state.boxProps = { frame, zPosition: boxZ, cornerRadius };
533
709
  }
534
710
  }
535
- const ox = state.box ? clip.x : 0;
536
- const oy = state.box ? clip.y : 0;
711
+ const ox = state.box ? clip.x : base.x;
712
+ const oy = state.box ? clip.y : base.y;
537
713
  const next = {
538
714
  bounds: [0, 0, rect.width / s, rect.height / s],
539
715
  anchorPoint: [
@@ -570,6 +746,10 @@ export class SpriteLayers {
570
746
  state.lift?.layout?.(next.bounds);
571
747
  // after the model went out, in the same transaction
572
748
  this._syncAnimations(state, sprite);
749
+ // the parts inside it, in the element's order, over its raster
750
+ for (let i = 0; i < state.kids.length; i++) {
751
+ this.sync(state.kids[i], i + 1);
752
+ }
573
753
  }
574
754
 
575
755
  _paint(state, sprite, rect) {
@@ -586,7 +766,10 @@ export class SpriteLayers {
586
766
  ctx.translate(-rect.x, -rect.y);
587
767
  // a part paints whole: there is no pass to cull it by
588
768
  if (root) root._paintDamage = null;
589
- sprite.paint(ctx);
769
+ sprite.paint(
770
+ ctx,
771
+ state.kids.length ? new Set(state.kids.map((kid) => kid.key)) : NO_KIDS,
772
+ );
590
773
  } catch (err) {
591
774
  this._warn(
592
775
  state.host,
@@ -674,7 +857,9 @@ export class SpriteLayers {
674
857
  for (const lifted of this.hosts.values()) {
675
858
  for (const state of lifted.values()) {
676
859
  state.lift?.release();
677
- this.native.removeFromSuperlayer(state.box ?? state.layer);
860
+ this.native.removeFromSuperlayer(
861
+ state.outerBox ?? state.box ?? state.layer,
862
+ );
678
863
  state.raster.release(this.native);
679
864
  for (const run of state.runs.values()) this._ends().delete(run.caId);
680
865
  }
package/src/node.d.ts CHANGED
@@ -297,8 +297,16 @@ export interface Sprite {
297
297
  * cannot take both. */
298
298
  clipRadius?: number;
299
299
  /** Draws the part where it is in the window, as `paint` would, but at
300
- * opacity 1 and untransformed. A part with `contents` has none. */
301
- paint?(ctx: Context2D): void;
300
+ * opacity 1 and untransformed — and without the parts lifted inside it
301
+ * (`parent`), whose keys the presenter hands it as `children`. A part
302
+ * with `contents` has none. */
303
+ paint?(ctx: Context2D, children?: ReadonlySet<string>): void;
304
+ /** The key of another of the element's parts this one is inside, earlier
305
+ * in the list: its layer goes in that part's layer, fading, turning and
306
+ * cut with it, and is lifted only with it, and only where everywhere it
307
+ * can be is inside that part's raster. Its `rect`, `origin` and `clip`
308
+ * are the window's, with its parent untransformed. */
309
+ parent?: string;
302
310
  /**
303
311
  * A source that shows itself on the part's layer instead of a paint — a
304
312
  * `VideoFrames` sink, whose every push reaches the layer with no frame of