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 +1 -1
- package/src/cocoa/presenter.js +8 -3
- package/src/cocoa/promotion.js +49 -17
- package/src/nodes/scrollable.js +81 -43
package/package.json
CHANGED
package/src/cocoa/presenter.js
CHANGED
|
@@ -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.
|
|
491
|
-
* its
|
|
492
|
-
*
|
|
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;
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* layer
|
|
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)
|
|
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,
|
|
515
|
-
* the chain: at every level, what the parent
|
|
516
|
-
* the way to this node — later siblings, then
|
|
517
|
-
* ring, then its bars — must keep out of the
|
|
518
|
-
* parent's clip, if it has one, must hold the whole of
|
|
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 &&
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -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
|
|
344
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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() {
|