react-x11 2.27.0 → 2.28.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.27.0",
3
+ "version": "2.28.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -356,6 +356,9 @@ const ANIMATED_KEY_PATHS = Object.freeze({
356
356
  borderColor: { keyPath: 'borderColor', colour: true },
357
357
  borderWidth: { keyPath: 'borderWidth', scaled: true },
358
358
  borderRadius: { keyPath: 'cornerRadius', scaled: true },
359
+ // the subtree's, as CSS has it: a layer under an opacity is drawn with
360
+ // its sublayers and faded as one (`layerOpacity`)
361
+ opacity: { keyPath: 'opacity' },
359
362
  });
360
363
 
361
364
  // the ids the bridge reports an animation's end under: unique per process,
@@ -487,9 +490,11 @@ export function propBoxProps(node, app, scale, parentOrigin, order) {
487
490
  }
488
491
 
489
492
  /**
490
- * `opacity` as a layer's own. On the layer presenter a node's children are
491
- * its sublayers, so this fades the subtree with it; the bitmap paths draw
492
- * the group themselves and never promote a faded box.
493
+ * `opacity` as a layer's own. A node's children are its layer's sublayers —
494
+ * each its own on the layer presenter, one raster on a promoted box — and
495
+ * Core Animation draws a layer under an opacity below 1 with its sublayers
496
+ * and fades them as one (`allowsGroupOpacity`, YES by default on macOS): the
497
+ * group `NodePaint._paintGroup` draws on the bitmap.
493
498
  */
494
499
  export function layerOpacity(style) {
495
500
  const value = style?.opacity;
@@ -24,11 +24,12 @@
24
24
  // promoted only when nothing painted after it in the walk reaches into its
25
25
  // bounds — no later sibling at any level, no ancestor's border ring or
26
26
  // focus ring, no scrollbar — and only when every clipping ancestor holds
27
- // the whole of it, because the layer would not be clipped. Declining is
27
+ // the whole of it, because the layer would not be clipped, and no ancestor
28
+ // fades, because the layer would not be faded with it. Declining is
28
29
  // always safe: the frame clock runs the animation exactly as it does
29
30
  // without this file. And the same test runs again every frame, so a node
30
- // that becomes overlapped, hidden, clipped or non-plain returns to the
31
- // bitmap in the frame that finds it, the animation handed back to the
31
+ // that becomes overlapped, hidden, clipped, faded or non-plain returns to
32
+ // the bitmap in the frame that finds it, the animation handed back to the
32
33
  // clock. Overlays, toasts, drag ghosts, spinners and floating cards pass by
33
34
  // construction; a hover fade on a row in the middle of a list does not,
34
35
  // and stays on the clock. docs/macos.md §"Layer promotion" is the account.
@@ -157,12 +158,14 @@ function insideGlArea(node) {
157
158
  const faded = (style) => style?.opacity !== undefined && !(style.opacity >= 1);
158
159
 
159
160
  /**
160
- * A box whose `opacity` is below 1, now or on its way there, is a group the
161
- * bitmap composites (`NodePaint._paintGroup`) — the box and everything in it
162
- * drawn once and faded as one. On a layer of its own the box and its content
163
- * would be separate layers under the one opacity, fading one over the other,
164
- * and an opacity the frame clock is still animating would never reach the
165
- * layer at all.
161
+ * A box whose `opacity` is below 1, now or on its way there, is a group:
162
+ * the box and everything in it drawn once and faded as one. The bitmap
163
+ * composites one (`NodePaint._paintGroup`), and so does Core Animation — a
164
+ * layer under an opacity below 1 is drawn with its sublayers and faded as
165
+ * one (`allowsGroupOpacity`, YES by default on macOS), and a promoted box's
166
+ * children are a sublayer of its layer. So a faded box may be promoted;
167
+ * what may not is a box inside one, whose layer would be flat on the window
168
+ * root, outside the group and at full strength (`_clear`).
166
169
  */
167
170
  function fadesAsGroup(node) {
168
171
  return (
@@ -182,7 +185,6 @@ function fadesAsGroup(node) {
182
185
  function promotableNode(node) {
183
186
  if (node.destroyed || !plainBox(node)) return false;
184
187
  if (insideGlArea(node)) return false;
185
- if (fadesAsGroup(node)) return false;
186
188
  if (!stylePaintsPlain(node, node._targetStyle ?? node.style)) return false;
187
189
  if (node.isScroller?.()) return false;
188
190
  return !paintsOutline(node);
@@ -438,7 +440,9 @@ export class CocoaPromotion {
438
440
  dirty: { all: true, rects: [] },
439
441
  });
440
442
  node._promoted = true;
441
- // the bitmap under it repaints without it, from this frame on
443
+ // the bitmap under it repaints without it, from this frame on — and so
444
+ // does the raster of a promoted node it is in, which the claim that
445
+ // handed us its animation reached (`noteInvalidate`)
442
446
  this._claim(root, node.paintBounds());
443
447
  }
444
448
 
@@ -457,7 +461,32 @@ export class CocoaPromotion {
457
461
  this.animations.drop(node, reclaim && !node.destroyed);
458
462
  this._dropContent(p);
459
463
  p.visual.destroy();
460
- if (!node.destroyed && node.abs) this._claim(root, node.paintBounds());
464
+ if (!node.destroyed && node.abs) {
465
+ this._repaintHolder(node);
466
+ this._claim(root, node.paintBounds());
467
+ }
468
+ }
469
+
470
+ /**
471
+ * A node coming off its layer inside a promoted node: the bitmap holds a
472
+ * hole there, and the node's pixels belong in that node's raster — so the
473
+ * raster repaints where the node reaches, taking it back, in this frame.
474
+ * Nothing else would ask: `_claim` keeps our claims off our own books, a
475
+ * raster's layout key is compared only in a frame that ran layout, and
476
+ * what gives a node back claims its own layer (the grace ran out) or
477
+ * nothing (an ancestor began to fade, a sibling came over it). Going onto
478
+ * a layer needs no such call — the claim that handed us the animation
479
+ * reached the raster. The nearest promoted node alone, since a raster
480
+ * leaves out the whole subtree of a promoted child.
481
+ */
482
+ _repaintHolder(node) {
483
+ for (let n = node.parent; n && !n.isWindow; n = n.parent) {
484
+ const holder = this.promoted.get(n);
485
+ if (holder) {
486
+ this._dirtyRect(holder, node.paintBounds());
487
+ return;
488
+ }
489
+ }
461
490
  }
462
491
 
463
492
  _dropContent(p) {
@@ -511,11 +540,13 @@ export class CocoaPromotion {
511
540
  }
512
541
 
513
542
  /**
514
- * Is nothing painted over this node, and is all of it visible? Walked up
515
- * the chain: at every level, what the parent paints after the child on
516
- * the way to this node — later siblings, then the parent's own border and
517
- * ring, then its bars — must keep out of the node's reach, and the
518
- * parent's clip, if it has one, must hold the whole of it.
543
+ * Is nothing painted over this node, is all of it visible, and is it in
544
+ * no faded group? Walked up the chain: at every level, what the parent
545
+ * paints after the child on the way to this node — later siblings, then
546
+ * the parent's own border and ring, then its bars — must keep out of the
547
+ * node's reach, the parent's clip, if it has one, must hold the whole of
548
+ * it, and the parent must not fade: its group is drawn on the bitmap or on
549
+ * its own layer, and either way a layer of the node's would be outside it.
519
550
  */
520
551
  _clear(node) {
521
552
  // the exact reach, not `paintBounds()`: that one carries the damage
@@ -530,6 +561,7 @@ export class CocoaPromotion {
530
561
  if (this._reaches(order[j], bounds)) return false;
531
562
  }
532
563
  if (!parent.isWindow) {
564
+ if (fadesAsGroup(parent)) return false;
533
565
  const border = borderReach(parent);
534
566
  if (
535
567
  border > 0 &&
@@ -16,7 +16,7 @@ import {
16
16
  XK_END,
17
17
  XK_SPACE,
18
18
  } from '../keysyms.js';
19
- import { DAMAGE_SLOP, layoutDiff } from './damage.js';
19
+ import { DAMAGE_SLOP, NO_DAMAGE, layoutDiff } from './damage.js';
20
20
  import { describeSize, laidBox, offsetInParent } from './layout.js';
21
21
  import { insetRect, intersectRects, rectsOverlap } from './rects.js';
22
22
  import {
@@ -340,11 +340,23 @@ export const Scrollable = (Base) =>
340
340
  const askedY = this.scrollY;
341
341
  this.scrollY = clampScroll(this.scrollY, this._maxScroll('y'));
342
342
  this.scrollX = clampScroll(this.scrollX, this._maxScroll('x'));
343
- // Of those three moves the clamp's is the one no call claimed when it
344
- // was asked for — the diff below claims it
343
+ // Of those three moves the clamp's is the one nobody asked for, and
344
+ // where no blit is armed the diff below claims it
345
345
  const clamped = this.scrollX !== askedX || this.scrollY !== askedY;
346
346
  this._reportViewport();
347
347
  this._reportScrollTo(from);
348
+ // …and whichever of them moved the offsets claims the viewport the
349
+ // content moved in, once the pass is over. A `scrollIntoView` asks for
350
+ // its pass claiming nothing (issue #813), so this is the only claim
351
+ // its move gets, and the blit it armed reads it as the scroll's own,
352
+ // as it reads the one `scrollTo` makes. Not through the diff, where a
353
+ // claim reads as content laid out anew, which is not a pure scroll
354
+ // and declines that blit. A clamp under an armed blit owes it too:
355
+ // the walk below gives that frame's moves to the blit's ledger, and
356
+ // without a scrollTo nothing else has claimed the viewport.
357
+ if (this.scrollX !== from.x || this.scrollY !== from.y) {
358
+ this.root?._reflowed.add(this);
359
+ }
348
360
  // `scrollX` is how far the content has moved **from its start**, which
349
361
  // is the right-hand edge in RTL — so scrolling shifts the children the
350
362
  // other way. Keeping it a distance rather than a coordinate is what
@@ -588,43 +600,7 @@ export const Scrollable = (Base) =>
588
600
  // A pane no pass has placed arms nothing: its first pass is a layout
589
601
  // change rather than a pure scroll.
590
602
  if (root && this._childOrigin != null) {
591
- // Arming is the one moment the evidence still exists: the viewport
592
- // claim recorded below coalesces earlier claims into itself
593
- // (addDamageRect keeps the list disjoint), after which a change
594
- // inside the viewport is indistinguishable from the scroll's own
595
- // claim — the blind spot the claim-time cancel in
596
- // WindowNode.invalidate cannot cover (react-x11#295). The scroll
597
- // has not claimed yet, so damage already overlapping this viewport
598
- // is foreign by construction: poison the frame instead of arming,
599
- // and the full-viewport repaint below stays in force.
600
- const arming = this._pendingBlitFrom == null;
601
- // The ledger this frame's changes inside the viewport are written
602
- // to (issue #398). Opened with the blit and read by
603
- // _applyScrollBlits, which clears it beside the origin.
604
- if (arming) this._blitLedger = [];
605
- if (arming && Array.isArray(root._damage)) {
606
- const zone = insetRect(this.abs, -(DAMAGE_SLOP * 2 + 1));
607
- for (const rect of root._damage) {
608
- // Already coalesced, so these rects are as coarse as the frame
609
- // has made them — which the ledger reads conservatively: a blob
610
- // that swallowed the viewport says so and poisons, exactly as
611
- // this gate used to for every claim it saw.
612
- if (rectsOverlap(rect, zone) && !this._recordBlitClaim(rect)) {
613
- this._pendingBlitFrom = BLIT_POISONED;
614
- break;
615
- }
616
- }
617
- }
618
- // An element that also shifted its own drawing this frame
619
- // (`scrollContents`, issue #303) is two shifts of the same pixels,
620
- // and a frame can only have one.
621
- if (this._pendingBlitContents) this._pendingBlitFrom = BLIT_POISONED;
622
- // The offsets whose pixels are on screen, captured before the first
623
- // change of the frame: the frame's blit fast path (issue #138) shifts
624
- // from *these* to wherever layout settles, however many scrollTo
625
- // calls land in between.
626
- this._pendingBlitFrom ??= { x: this.scrollX, y: this.scrollY };
627
- (root._pendingScrolls ??= new Set()).add(this);
603
+ this._armScrollBlit(root);
628
604
  // ... and the claim about to be recorded is the scroll itself, not a
629
605
  // reason to un-blit it
630
606
  root._scrollClaim = this;
@@ -644,6 +620,52 @@ export const Scrollable = (Base) =>
644
620
  if (root) root._scrollClaim = null;
645
621
  }
646
622
 
623
+ /**
624
+ * Arm the frame's scroll blit (issue #138) for a move of this pane's
625
+ * offsets: a `scrollTo` about to make one, or a `scrollIntoView` whose
626
+ * pass may (`scrollIntoView`). Before the move, and before the claim
627
+ * that comes with it.
628
+ *
629
+ * Arming is the one moment the evidence still exists: the viewport
630
+ * claim coalesces earlier claims into itself (addDamageRect keeps the
631
+ * list disjoint), after which a change inside the viewport is
632
+ * indistinguishable from the scroll's own claim — the blind spot the
633
+ * claim-time cancel in WindowNode.invalidate cannot cover
634
+ * (react-x11#295). The scroll has not claimed yet, so damage already
635
+ * overlapping this viewport is foreign by construction: poison the
636
+ * frame instead of arming, and the full-viewport repaint stays in force.
637
+ */
638
+ _armScrollBlit(root) {
639
+ const arming = this._pendingBlitFrom == null;
640
+ // The ledger this frame's changes inside the viewport are written
641
+ // to (issue #398). Opened with the blit and read by
642
+ // _applyScrollBlits, which clears it beside the origin.
643
+ if (arming) this._blitLedger = [];
644
+ if (arming && Array.isArray(root._damage)) {
645
+ const zone = insetRect(this.abs, -(DAMAGE_SLOP * 2 + 1));
646
+ for (const rect of root._damage) {
647
+ // Already coalesced, so these rects are as coarse as the frame
648
+ // has made them — which the ledger reads conservatively: a blob
649
+ // that swallowed the viewport says so and poisons, exactly as
650
+ // this gate used to for every claim it saw.
651
+ if (rectsOverlap(rect, zone) && !this._recordBlitClaim(rect)) {
652
+ this._pendingBlitFrom = BLIT_POISONED;
653
+ break;
654
+ }
655
+ }
656
+ }
657
+ // An element that also shifted its own drawing this frame
658
+ // (`scrollContents`, issue #303) is two shifts of the same pixels,
659
+ // and a frame can only have one.
660
+ if (this._pendingBlitContents) this._pendingBlitFrom = BLIT_POISONED;
661
+ // The offsets whose pixels are on screen, captured before the first
662
+ // change of the frame: the frame's blit fast path (issue #138) shifts
663
+ // from *these* to wherever layout settles, however many scrollTo
664
+ // calls land in between.
665
+ this._pendingBlitFrom ??= { x: this.scrollX, y: this.scrollY };
666
+ (root._pendingScrolls ??= new Set()).add(this);
667
+ }
668
+
647
669
  /**
648
670
  * Keep the part of a request the extent in hand cannot answer, for the
649
671
  * pass that will measure one that can. `_scrollToDevice` clamps against
@@ -864,13 +886,29 @@ export const Scrollable = (Base) =>
864
886
  * that has no geometry yet. `absolutize` resolves it against freshly
865
887
  * computed yoga positions, and `onScroll` reports the move once that
866
888
  * pass is over (`_reportScrollTo`).
889
+ *
890
+ * Asking claims nothing (issue #813). Most of the time the node is in
891
+ * view already — every Tab between the fields of a form in a pane — and
892
+ * a claim made now, before anyone knows, repainted the pane's whole
893
+ * viewport for a frame that moved nothing. So the pass is asked for
894
+ * with no damage, and a request that finds its node in view costs what
895
+ * the focus change costs outside a pane. One that does move the pane
896
+ * moves it the way a scroll does: the blit is armed here, as `scrollTo`
897
+ * arms it, and the pass claims the viewport once the offsets have
898
+ * changed (`_absolutizeChildren`), which `_applyScrollBlits` narrows to
899
+ * the strip the move uncovered.
867
900
  */
868
901
  scrollIntoView(node) {
869
902
  if (!node || !this.isScroller()) return;
870
903
  this._scrollIntoViewTarget = node;
871
- // whatever the resolved scroll moves is inside this clipped viewport,
872
- // so the viewport's own before/after rects bound the frame
873
- this._invalidateLayout('scroll');
904
+ const root = this.root;
905
+ if (!root) return;
906
+ // a pane no pass has placed arms nothing, as for `scrollTo`
907
+ if (this._childOrigin != null) this._armScrollBlit(root);
908
+ // `'scroll'`: the walk goes everywhere, which is what reaches this
909
+ // pane however clean the boxes above it are, and the content floors
910
+ // owe nothing
911
+ root.invalidate(true, NO_DAMAGE, 'scroll', this);
874
912
  }
875
913
 
876
914
  _resolveScrollIntoView() {