react-x11 2.34.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.34.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++) {
@@ -29,6 +29,13 @@
29
29
  // ask for: on an older one nothing is asked and every part is drawn by its
30
30
  // element.
31
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
+ //
32
39
  // A part may be inside another of its element's (`parent`): a spinner in a
33
40
  // card that fades. Its layer is lifted inside the parent's, so it fades,
34
41
  // turns and is cut with it, and the parent's raster leaves it out — the
@@ -50,6 +57,11 @@ import { isVideoFrames } from '../videoframes.js';
50
57
  // upload for every new version.
51
58
  const MAX_SIDE = 8192;
52
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
+
53
65
  // Sorted after the element's children, which its own drawing comes after
54
66
  // (`Node.paint` paints the children first), and before its next sibling.
55
67
  const AFTER_CHILDREN = Number.MAX_SAFE_INTEGER;
@@ -61,6 +73,13 @@ const ORIGIN = Object.freeze({ x: 0, y: 0 });
61
73
  // what a part with no part lifted inside it is handed
62
74
  const NO_KIDS = Object.freeze(new Set());
63
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
+
64
83
  // what a part's raster holds before its first paint
65
84
  const UNPAINTED = Symbol('unpainted');
66
85
 
@@ -99,8 +118,13 @@ function isAnimation(a) {
99
118
  if (!finite(a.duration) || !(a.duration > 0)) return false;
100
119
  const values = a.values;
101
120
  if (!Array.isArray(values) || values.length < 2) return false;
102
- const value = a.property === 'opacity' ? finite : isMatrix;
103
- 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
+ }
104
128
  if (a.keyTimes !== undefined) {
105
129
  const t = a.keyTimes;
106
130
  if (!Array.isArray(t) || t.length !== values.length || t[0] !== 0) {
@@ -227,11 +251,38 @@ function unionRect(a, b) {
227
251
  function extentOf(sprite) {
228
252
  const reach = sprite.reach ?? sprite.rect;
229
253
  const origin = originOf(sprite);
230
- 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;
231
265
  for (const a of sprite.animations ?? []) {
232
266
  if (a.property !== 'transform') continue;
233
- for (const m of a.values) box = unionRect(box, mapRect(reach, m, origin));
267
+ turns = turns === null ? a.values : undefined;
234
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);
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
+ };
235
286
  const x = Math.floor(box.x) - RASTER_PAD;
236
287
  const y = Math.floor(box.y) - RASTER_PAD;
237
288
  return {
@@ -332,6 +383,8 @@ export class SpriteLayers {
332
383
  this.hosts.delete(host);
333
384
  if (!host.destroyed) changed.add(host);
334
385
  }
386
+ // the raster kept for parts that show nothing, this frame
387
+ let hidden = 0;
335
388
  for (const [host, list] of offers) {
336
389
  const was = this.hosts.get(host);
337
390
  const now = new Map();
@@ -378,17 +431,31 @@ export class SpriteLayers {
378
431
  };
379
432
  const extent = extentOf(sprite);
380
433
  const bounds = cut.clip ? intersectRects(extent, cut.clip) : extent;
381
- if (!bounds) continue;
382
434
  if (parent) {
383
435
  // the parent was asked about everywhere it can be, and its layer
384
436
  // carries this one: nothing more to ask where it stays inside the
385
437
  // parent's raster, and its element draws it where it does not
386
- if (!rectContains(rasterRectOf(parent.sprite), bounds)) continue;
438
+ if (bounds && !rectContains(rasterRectOf(parent.sprite), bounds)) {
439
+ continue;
440
+ }
387
441
  cut.bounds = bounds;
442
+ } else if (!bounds) {
443
+ // its own clip shows nothing of it
444
+ cut.bounds = null;
388
445
  } else if (!this.promotion._clear(host, bounds, cut)) {
389
446
  continue;
390
447
  }
391
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
+ }
392
459
  let state = was?.get(sprite.key);
393
460
  // a clip that comes or goes is a layer in a box or out of one: lifted
394
461
  // again, rather than a running layer moved between parents — and so
@@ -397,6 +464,7 @@ export class SpriteLayers {
397
464
  if (
398
465
  state &&
399
466
  (Boolean(state.clip) !== Boolean(cut.clip) ||
467
+ Boolean(state.outer) !== Boolean(cut.outer) ||
400
468
  state.contents !== contents ||
401
469
  state.parent !== parent)
402
470
  ) {
@@ -411,10 +479,12 @@ export class SpriteLayers {
411
479
  cut.clip !== null,
412
480
  contents,
413
481
  parent,
482
+ Boolean(cut.outer),
414
483
  );
415
484
  }
416
485
  state.sprite = sprite;
417
486
  state.clip = cut.clip;
487
+ state.outer = cut.outer ?? null;
418
488
  state.radius = cut.radius;
419
489
  state.order = parent ? null : [...place, AFTER_CHILDREN, index];
420
490
  state.kids = [];
@@ -423,7 +493,7 @@ export class SpriteLayers {
423
493
  // frame on; where it went since, it is the element's own claims
424
494
  // that move the hole. A child is under its parent's layer, whose
425
495
  // raster leaves it out instead (`sync`).
426
- if (lifting && !parent) this.promotion._claim(root, shown);
496
+ if (lifting && !parent && shown) this.promotion._claim(root, shown);
427
497
  state.extent = shown;
428
498
  now.set(sprite.key, state);
429
499
  }
@@ -473,16 +543,25 @@ export class SpriteLayers {
473
543
  return isVideoFrames(contents) && this.videos;
474
544
  }
475
545
 
476
- _lift(host, key, clipped, contents = null, parent = null) {
546
+ _lift(host, key, clipped, contents = null, parent = null, outered = false) {
477
547
  const layer = this.native.createLayer();
478
548
  // a part cut to a clip is in a box of the clip's size that masks to it,
479
549
  // and the box is what stands among the layers above the bitmap — or
480
- // among its parent's children, for a part inside another's layer
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`).
481
553
  const above = parent ? parent.layer : this.promotion.rootVisual.layer;
482
554
  let box = null;
555
+ let outerBox = null;
483
556
  if (clipped) {
484
557
  box = this.native.createLayer();
485
- this.native.addSublayer(above, 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
+ }
486
565
  this.native.addSublayer(box, layer);
487
566
  } else {
488
567
  this.native.addSublayer(above, layer);
@@ -492,6 +571,9 @@ export class SpriteLayers {
492
571
  key,
493
572
  layer,
494
573
  box,
574
+ outerBox,
575
+ outerProps: {},
576
+ outer: null, // the square clips around a rounded one, device pixels
495
577
  // the part whose layer this one is in, and the parts in this one's
496
578
  parent,
497
579
  kids: [],
@@ -528,7 +610,9 @@ export class SpriteLayers {
528
610
  * element drawing it again. */
529
611
  _drop(state, root) {
530
612
  state.lift?.release();
531
- this.native.removeFromSuperlayer(state.box ?? state.layer);
613
+ this.native.removeFromSuperlayer(
614
+ state.outerBox ?? state.box ?? state.layer,
615
+ );
532
616
  state.raster.release(this.native);
533
617
  for (const run of state.runs.values()) this._ends().delete(run.caId);
534
618
  state.runs.clear();
@@ -571,16 +655,40 @@ export class SpriteLayers {
571
655
  // where its layer's coordinates start: the window's corner, or the
572
656
  // corner of its parent's raster, for a part inside another's layer
573
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
+ }
574
679
  // in its box, where it is cut to a clip: placed from the clip's corner,
575
- // 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
576
682
  const clip = state.clip;
577
683
  if (state.box) {
684
+ const from = state.outerBox ? outer : base;
578
685
  const frame = [
579
- (clip.x - base.x) / s,
580
- (clip.y - base.y) / s,
686
+ (clip.x - from.x) / s,
687
+ (clip.y - from.y) / s,
581
688
  clip.width / s,
582
689
  clip.height / s,
583
690
  ];
691
+ const boxZ = state.outerBox ? 0 : z;
584
692
  // its corners rounded where the clip's are: a circle's arc, as wide
585
693
  // as the box allows
586
694
  const cornerRadius =
@@ -588,16 +696,16 @@ export class SpriteLayers {
588
696
  const was = state.boxProps;
589
697
  if (
590
698
  !sameMatrix(was.frame, frame) ||
591
- was.zPosition !== z ||
699
+ was.zPosition !== boxZ ||
592
700
  was.cornerRadius !== cornerRadius
593
701
  ) {
594
702
  this.native.setLayerProps(state.box, {
595
703
  frame,
596
704
  masksToBounds: true,
597
705
  cornerRadius,
598
- zPosition: z,
706
+ zPosition: boxZ,
599
707
  });
600
- state.boxProps = { frame, zPosition: z, cornerRadius };
708
+ state.boxProps = { frame, zPosition: boxZ, cornerRadius };
601
709
  }
602
710
  }
603
711
  const ox = state.box ? clip.x : base.x;
@@ -749,7 +857,9 @@ export class SpriteLayers {
749
857
  for (const lifted of this.hosts.values()) {
750
858
  for (const state of lifted.values()) {
751
859
  state.lift?.release();
752
- this.native.removeFromSuperlayer(state.box ?? state.layer);
860
+ this.native.removeFromSuperlayer(
861
+ state.outerBox ?? state.box ?? state.layer,
862
+ );
753
863
  state.raster.release(this.native);
754
864
  for (const run of state.runs.values()) this._ends().delete(run.caId);
755
865
  }