react-x11 2.22.4 → 2.22.5

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.22.4",
3
+ "version": "2.22.5",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -5,6 +5,7 @@
5
5
  import { Yoga } from '../yoga.js';
6
6
  import { callHandler } from '../errors.js';
7
7
  import { DAMAGE_SLOP, layoutDiff } from './damage.js';
8
+ import { resolveBorderWidths } from '../styles.js';
8
9
  import { insetRect, intersectRects, outerPixels, outside } from './rects.js';
9
10
  import { DEV } from './util.js';
10
11
 
@@ -520,12 +521,33 @@ export class NodeLayout {
520
521
  ) {
521
522
  return;
522
523
  }
524
+ // …and one that rode it and only grew or shrank changed the pixels
525
+ // along the edges that moved — the column a virtualized document's
526
+ // blocks sit in, whose spacers re-slice in the scroll's own frame
527
+ const bands =
528
+ had && was.x === x && was.y === y && width > 0 && height > 0
529
+ ? this._edgeBands(was, grow)
530
+ : null;
531
+ if (bands) {
532
+ for (const band of bands) layoutDiff.sink(band, this);
533
+ return;
534
+ }
523
535
  if (had) layoutDiff.sink(insetRect(was, -grow), this);
524
536
  if (width > 0 && height > 0) {
525
537
  layoutDiff.sink(insetRect(this.abs, -grow), this);
526
538
  }
527
539
  return;
528
540
  }
541
+ // A box that stayed where it was and only grew or shrank changed the
542
+ // pixels along the edges that moved, and no others (`_edgeBands`)
543
+ const bands =
544
+ had && old.x === x && old.y === y && width > 0 && height > 0
545
+ ? this._edgeBands(old, grow)
546
+ : null;
547
+ if (bands) {
548
+ for (const band of bands) layoutDiff.sink(band, this);
549
+ return;
550
+ }
529
551
  if (had) {
530
552
  layoutDiff.sink(insetRect(old, -grow), this);
531
553
  }
@@ -535,6 +557,64 @@ export class NodeLayout {
535
557
  }
536
558
  }
537
559
 
560
+ /**
561
+ * The pixels a resize changed, for a box whose origin held: a band along
562
+ * each edge that moved, from where it was to where it is, as deep as the
563
+ * box's paint reaches in from that edge — a rounded corner, a border, a
564
+ * shadow's or an outline's reach. Its own paint anywhere else is the same
565
+ * solid fill it was, and its children claim for themselves when they move.
566
+ *
567
+ * A tall box that grows at its far end is the case: a document or a log
568
+ * or a virtualized list whose column gained a block far below the fold
569
+ * claimed the column, clipped to its scroll box — the whole viewport — on
570
+ * every frame it grew, for pixels that were all out of sight.
571
+ *
572
+ * Null where the whole box has to be claimed: anything that is not a
573
+ * plain box, one that scrolls (its bars are sized by it), one whose
574
+ * gradient is stretched over it, and one drawn through a group surface.
575
+ */
576
+ _edgeBands(old, grow) {
577
+ if (this.kind !== 'box' || this.isScroller?.()) return null;
578
+ const style = this.style;
579
+ if (style.backgroundImage) return null;
580
+ if (style.opacity !== undefined && style.opacity < 1) return null;
581
+ const bw = resolveBorderWidths(style, this.direction);
582
+ const reach =
583
+ Math.max(
584
+ style.borderRadius ?? 0,
585
+ bw.top,
586
+ bw.right,
587
+ bw.bottom,
588
+ bw.left,
589
+ grow,
590
+ ) + DAMAGE_SLOP;
591
+ const now = this.abs;
592
+ const bands = [];
593
+ const oldBottom = old.y + old.height;
594
+ const newBottom = now.y + now.height;
595
+ if (oldBottom !== newBottom) {
596
+ const top = Math.min(oldBottom, newBottom) - reach;
597
+ bands.push({
598
+ x: now.x - reach,
599
+ y: top,
600
+ width: Math.max(old.width, now.width) + 2 * reach,
601
+ height: Math.max(oldBottom, newBottom) + reach - top,
602
+ });
603
+ }
604
+ const oldRight = old.x + old.width;
605
+ const newRight = now.x + now.width;
606
+ if (oldRight !== newRight) {
607
+ const left = Math.min(oldRight, newRight) - reach;
608
+ bands.push({
609
+ x: left,
610
+ y: now.y - reach,
611
+ width: Math.max(oldRight, newRight) + reach - left,
612
+ height: Math.max(old.height, now.height) + 2 * reach,
613
+ });
614
+ }
615
+ return bands;
616
+ }
617
+
538
618
  /**
539
619
  * Move an already-laid-out subtree by a constant, without asking yoga
540
620
  * anything — the scroll fast path's walk (issue #405).
package/src/nodes/node.js CHANGED
@@ -384,8 +384,10 @@ export class Node {
384
384
  // captured before the child joins, so it covers the arrangement that is
385
385
  // about to be replaced (see _childListChanged). A viewport mid-blit has
386
386
  // nothing vacating — the child being added had no pixels — and the
387
- // layout diff claims where it lands, so it names no region at all.
388
- const before = this._blitLedgerOpen() ? null : this._childListBefore();
387
+ // layout diff claims where it lands, so it names no region at all. A
388
+ // child already here is a move, and says so (`_childListFine`).
389
+ const fine = this._childListFine(child.parent !== this);
390
+ const before = fine ? null : this._childListBefore();
389
391
  // a move has to leave the yoga tree too — yoga aborts on insertChild of
390
392
  // a node that still has a parent
391
393
  if (child.parent === this && this._joinsYoga(child)) {
@@ -414,7 +416,7 @@ export class Node {
414
416
  // against the app's, and only now can see the `scale` props above it.
415
417
  child._rescaleSubtree(mounting);
416
418
  this._textContentChanged();
417
- this._childListChanged(before);
419
+ this._childListChanged(before, fine);
418
420
  a11yHooks.attached?.(this, child);
419
421
  }
420
422
 
@@ -464,7 +466,7 @@ export class Node {
464
466
  * through the layout diff in flush(), which is what lets this claim stay
465
467
  * bounded without requiring the node's own size to be pinned.
466
468
  */
467
- _childListChanged(before) {
469
+ _childListChanged(before, fine = this._childListFine(true)) {
468
470
  // belt for a subtree attached imperatively with its rect already laid
469
471
  // out — nothing then re-runs _assignAbs to notice the reach grew
470
472
  this._clearHitBounds();
@@ -477,7 +479,7 @@ export class Node {
477
479
  // and says nothing about the ones that only rode the scroll. Joining
478
480
  // `_reflowed` would undo both — its post-layout claim is this node's
479
481
  // box, the whole band the blit is about to move.
480
- if (this._blitLedgerOpen()) {
482
+ if (fine) {
481
483
  root.invalidate(true, before ?? NO_DAMAGE, 'child-list', this);
482
484
  return;
483
485
  }
@@ -485,6 +487,39 @@ export class Node {
485
487
  root._reflowed.add(this);
486
488
  }
487
489
 
490
+ /**
491
+ * Whether a child-list change here is claimed the fine way — the child
492
+ * that leaves claims its own rect, and the layout diff names where an
493
+ * entering child lands and whatever its arrival displaced — rather than
494
+ * as this node's box before and after the pass.
495
+ *
496
+ * Always for a viewport keeping a ledger this frame (issue #398). And,
497
+ * for a child that enters or leaves (`entersOrLeaves`), for any node
498
+ * inside a scroll box, scrolling or not: a document's blocks, the rows
499
+ * of a virtualized list one box down from its scroll box, a log that
500
+ * grows. Such a node is usually taller than the view, and its box
501
+ * clipped to the viewport is the whole viewport — so every re-slice of
502
+ * a list inside a column, all of it below the fold, repainted the view,
503
+ * and one landing in a scroll's frame poisoned its blit. The diff runs
504
+ * over every node that pass laid out, however deep, and a node that
505
+ * only grew claims the edge that moved (`_edgeBands`).
506
+ *
507
+ * Not for a move: a child moved within the list keeps its rect when
508
+ * what changed is only which sibling it is painted over, and no rect
509
+ * says that. Nor for a scroll box's own children outside a ledger frame:
510
+ * its box is also the claim that repaints its bars, which its content
511
+ * sizes.
512
+ */
513
+ _childListFine(entersOrLeaves) {
514
+ if (this._blitLedgerOpen()) return true;
515
+ if (!entersOrLeaves) return false;
516
+ for (let n = this.parent; n; n = n.parent) {
517
+ if (n.isWindow) return false;
518
+ if (n.isScroller?.()) return true;
519
+ }
520
+ return false;
521
+ }
522
+
488
523
  removeChild(child) {
489
524
  const index = this._indexOfChild(child);
490
525
  if (index === -1) return;
@@ -494,7 +529,8 @@ export class Node {
494
529
  // captured while the child is still attached, so it covers the rect the
495
530
  // child is about to stop occupying — the child's own, for a viewport
496
531
  // mid-blit, where this node's box is the whole scrolled band
497
- const before = this._blitLedgerOpen()
532
+ const fine = this._childListFine(true);
533
+ const before = fine
498
534
  ? (child._claimBounds() ?? NO_DAMAGE)
499
535
  : this._childListBefore();
500
536
  this.children.splice(index, 1);
@@ -515,7 +551,7 @@ export class Node {
515
551
  child.yoga = null;
516
552
  }
517
553
  this._textContentChanged();
518
- this._childListChanged(before);
554
+ this._childListChanged(before, fine);
519
555
  }
520
556
 
521
557
  /** Destroy real resources (X windows) in this subtree. Yoga nodes are
@@ -855,7 +855,15 @@ export class WindowScrollBlit {
855
855
  ? unionArea([strip, ...repairs]) - rectArea(strip)
856
856
  : 0;
857
857
  if (repairArea > area * BLIT_MAX_CLAIM_AREA) return;
858
- if (!this._scrollBlitSafe(node, vp, target.top)) return;
858
+ // An ancestor's rounded corners reach into the viewport: a scroll box
859
+ // filling a rounded card, the commonest place a scroll box is. Their
860
+ // arcs do not translate, but they live in four small squares and
861
+ // nowhere else, so they are pinned the way an element blit pins them
862
+ // (issue #691) and repaired below, where they are and where the copy
863
+ // dragged their image. Refused instead, a list or a document inside a
864
+ // rounded frame repainted its whole viewport on every step of a scroll.
865
+ const corners = this._cornerPins(node, vp, target.top);
866
+ if (!this._scrollBlitSafe(node, vp, target.top, null, corners)) return;
859
867
  // The band the scrolled bar's thumb travels in is repaired on its own.
860
868
  // The thumb's rects are thin and run along the viewport's edge, and a
861
869
  // rect reaching into the band from inside — a column held down the
@@ -932,10 +940,28 @@ export class WindowScrollBlit {
932
940
  if (dragged) add(dragged);
933
941
  }
934
942
  for (const repair of repairs) add(repair);
935
- rects = joinAlongEdges(rects).reduce(
936
- (capped, rect) => addDamageRect(capped, rect),
937
- [],
938
- );
943
+ // Each pinned corner where it is and where the copy put its image — one
944
+ // box, since a step moves it a few pixels, and less the strip, which
945
+ // repaints anyway.
946
+ for (const pin of corners ?? []) {
947
+ const image = { ...pin, x: pin.x + dx, y: pin.y + dy };
948
+ const repair = intersectRects(unionRect(pin, image), vp);
949
+ if (!repair) continue;
950
+ for (const piece of outside(repair, strip)) add(piece);
951
+ }
952
+ // Capped in one call where corners were pinned: two at each end of the
953
+ // viewport and the strip at one of them are more pieces than the cap
954
+ // holds, and squeezed under it one at a time a corner joined whatever
955
+ // box an earlier merge had grown — scrolling back up merged the bottom
956
+ // corners with the strip at the top, which is the viewport. Offered
957
+ // together, neighbours merge first (`addDamageRects`), with the room an
958
+ // element blit's pins get.
959
+ rects = corners
960
+ ? addDamageRects([], joinAlongEdges(rects), CONTENTS_BLIT_MAX_RECTS)
961
+ : joinAlongEdges(rects).reduce(
962
+ (capped, rect) => addDamageRect(capped, rect),
963
+ [],
964
+ );
939
965
  // The last gate, and the only one that has to wait until the rects are
940
966
  // assembled: damage rects must not overlap, and the frame carries at
941
967
  // most MAX_DAMAGE_RECTS of them, so repairs that meet — a column and a