react-x11 2.1.0 → 2.1.1
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/nodes.js +405 -40
package/package.json
CHANGED
package/src/nodes.js
CHANGED
|
@@ -254,6 +254,15 @@ const DAMAGE_SLOP = 1;
|
|
|
254
254
|
// pass, and always restored through `finally`.
|
|
255
255
|
let layoutDiffSink = null;
|
|
256
256
|
|
|
257
|
+
// The uniform translation the subtree currently being walked is riding: set
|
|
258
|
+
// while a scroll container whose blit is armed lays its children out, so the
|
|
259
|
+
// diff can tell "moved" from "scrolled" (issue #398). Every child of such a
|
|
260
|
+
// container lands at its old rect plus this shift, which is precisely what
|
|
261
|
+
// the blit is about to do to those pixels — so it is not a change, and the
|
|
262
|
+
// diff reports only the children that landed somewhere else. Null everywhere
|
|
263
|
+
// else, and restored through `finally` like the sink beside it.
|
|
264
|
+
let layoutDiffShift = null;
|
|
265
|
+
|
|
257
266
|
// What an invalidate() may name as its reason — a small closed set, so the
|
|
258
267
|
// frame log, the tracer and the full-repaint warning can print "why" next
|
|
259
268
|
// to "where". A typo'd reason would silently vanish from every report, so
|
|
@@ -314,6 +323,13 @@ const NO_SCROLL_BLIT = process.env.REACT_X11_NO_SCROLL_BLIT === '1';
|
|
|
314
323
|
// exposed strip is most of a repaint anyway.
|
|
315
324
|
const SCROLL_BLIT_MIN_KEEP = 0.5;
|
|
316
325
|
|
|
326
|
+
// …and how much of it the frame may end up repainting anyway. The strip and
|
|
327
|
+
// the scrollbar repair sit under this by a wide margin; what can push past
|
|
328
|
+
// it is a ledger repair (issue #398) that the damage cap had to merge with
|
|
329
|
+
// the scrollbar column, whose box then reaches back across the viewport.
|
|
330
|
+
// Past this the blit is buying a shift and paying for the viewport anyway.
|
|
331
|
+
const SCROLL_BLIT_MAX_REPAINT = 0.75;
|
|
332
|
+
|
|
317
333
|
// The server-side event mask every realized window ends up with. The
|
|
318
334
|
// subscriptions are a constant — the EventManager's pointer/key/focus
|
|
319
335
|
// listeners, the window's own resize/draw/expose pair, the backing store's
|
|
@@ -353,6 +369,14 @@ const BLIT_POISONED = Object.freeze({ poisoned: true });
|
|
|
353
369
|
// net delta in `_pendingBlitContents` beside it.
|
|
354
370
|
const BLIT_CONTENTS = Object.freeze({ contents: true });
|
|
355
371
|
|
|
372
|
+
// How much of what changed inside a blitting viewport the ledger will carry
|
|
373
|
+
// before the frame gives up and repaints the viewport instead (issue #398).
|
|
374
|
+
// A virtualized list's scroll frame changes a handful of regions — the two
|
|
375
|
+
// spacers and the entering rows — and past that the blit plus a scatter of
|
|
376
|
+
// repaints stops being cheaper than the one pass it replaced.
|
|
377
|
+
const BLIT_MAX_CLAIMS = 8;
|
|
378
|
+
const BLIT_MAX_CLAIM_AREA = 0.25;
|
|
379
|
+
|
|
356
380
|
const rectContains = (outer, inner) =>
|
|
357
381
|
outer.x <= inner.x &&
|
|
358
382
|
outer.y <= inner.y &&
|
|
@@ -1441,6 +1465,13 @@ function shallowEqual(a, b) {
|
|
|
1441
1465
|
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
1442
1466
|
}
|
|
1443
1467
|
|
|
1468
|
+
/** The half of `Node._joinsYoga` that is about the child alone — a real X
|
|
1469
|
+
* window (`<window>`, `<popup>`) or a node built without a box at all (a text
|
|
1470
|
+
* chunk) sits outside whatever parent it lands in. This is what
|
|
1471
|
+
* `_nonYogaKids` counts, so the count stays right for a parent that has no
|
|
1472
|
+
* box of its own either. */
|
|
1473
|
+
const outsideYoga = (child) => !child.yoga || child.isWindow;
|
|
1474
|
+
|
|
1444
1475
|
export class Node {
|
|
1445
1476
|
get ownerDocument() {
|
|
1446
1477
|
return DEVTOOLS_FAKE_DOCUMENT;
|
|
@@ -1465,6 +1496,20 @@ export class Node {
|
|
|
1465
1496
|
this.app = app;
|
|
1466
1497
|
this.parent = null;
|
|
1467
1498
|
this.children = [];
|
|
1499
|
+
// Where this node sits in `parent.children`, and how many of *this*
|
|
1500
|
+
// node's children sit outside its yoga tree. Both are bookkeeping that
|
|
1501
|
+
// turns the scans `insertBefore` used to do over the whole child list
|
|
1502
|
+
// into constant work, which is what stops a commit that mounts a
|
|
1503
|
+
// virtualized list's window from costing O(rows x pane) (issue #397).
|
|
1504
|
+
// The index is a hint — `_indexOfChild` proves it before using it — and
|
|
1505
|
+
// the count is exact, maintained by the three places `children` is
|
|
1506
|
+
// spliced.
|
|
1507
|
+
this._childIndex = -1;
|
|
1508
|
+
this._nonYogaKids = 0;
|
|
1509
|
+
// The pre-mutation bounds this frame already claimed for this node, so
|
|
1510
|
+
// that a second mutation reuses the rect instead of walking the subtree
|
|
1511
|
+
// again. Lives exactly as long as membership in `root._reflowed`.
|
|
1512
|
+
this._reflowBefore = null;
|
|
1468
1513
|
this.root = null; // owning WindowNode once attached
|
|
1469
1514
|
this.hidden = false;
|
|
1470
1515
|
this.destroyed = false;
|
|
@@ -2256,6 +2301,12 @@ export class Node {
|
|
|
2256
2301
|
/** Number of yoga-bearing children before `index` (window children and
|
|
2257
2302
|
* text spans/chunks do not join the parent's yoga tree). */
|
|
2258
2303
|
_yogaIndexAt(index) {
|
|
2304
|
+
// A list of ordinary boxes — a scroll pane's rows, which is the list
|
|
2305
|
+
// this is asked about a hundred times in one commit — has every child in
|
|
2306
|
+
// the yoga tree, and then the yoga index *is* the child index. Counting
|
|
2307
|
+
// the exceptions as they arrive turns that answer into a read instead of
|
|
2308
|
+
// a walk of every sibling in front of the new row (issue #397).
|
|
2309
|
+
if (this._nonYogaKids === 0) return index;
|
|
2259
2310
|
let n = 0;
|
|
2260
2311
|
for (let i = 0; i < index; i++) {
|
|
2261
2312
|
if (this._joinsYoga(this.children[i])) n++;
|
|
@@ -2267,6 +2318,24 @@ export class Node {
|
|
|
2267
2318
|
return Boolean(this.yoga && child.yoga && !child.isWindow);
|
|
2268
2319
|
}
|
|
2269
2320
|
|
|
2321
|
+
/**
|
|
2322
|
+
* Where `child` sits in `this.children`.
|
|
2323
|
+
*
|
|
2324
|
+
* The cached slot is checked rather than trusted: a node appears in the
|
|
2325
|
+
* list once, so `children[i] === child` *is* the proof that `i` is its
|
|
2326
|
+
* index, and a cache that has gone stale costs a scan rather than a wrong
|
|
2327
|
+
* answer. `_spliceChild` refreshes the two slots it knows — the child it
|
|
2328
|
+
* placed and the sibling it pushed along — which is what keeps a run of
|
|
2329
|
+
* inserts in front of the same trailing sibling (every virtualized list's
|
|
2330
|
+
* commit) off the scan entirely.
|
|
2331
|
+
*/
|
|
2332
|
+
_indexOfChild(child) {
|
|
2333
|
+
if (this.children[child._childIndex] === child) return child._childIndex;
|
|
2334
|
+
const i = this.children.indexOf(child);
|
|
2335
|
+
child._childIndex = i;
|
|
2336
|
+
return i;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2270
2339
|
/**
|
|
2271
2340
|
* Give this node's box a measure function, keeping a reference that can be
|
|
2272
2341
|
* asked again later.
|
|
@@ -2354,12 +2423,22 @@ export class Node {
|
|
|
2354
2423
|
* by calling insertBefore with a child that is *already* mounted here, and
|
|
2355
2424
|
* without the removal it would appear twice. Returns the new index. */
|
|
2356
2425
|
_spliceChild(child, beforeChild) {
|
|
2357
|
-
|
|
2426
|
+
// `parent === this` is the cheap form of "already in this list" — the
|
|
2427
|
+
// two are set and cleared together — so a child arriving for the first
|
|
2428
|
+
// time, which is every node of a freshly mounted subtree, pays no scan
|
|
2429
|
+
// at all for the question.
|
|
2430
|
+
const from = child.parent === this ? this._indexOfChild(child) : -1;
|
|
2358
2431
|
if (from !== -1) this.children.splice(from, 1);
|
|
2359
|
-
|
|
2360
|
-
|
|
2432
|
+
else if (outsideYoga(child)) this._nonYogaKids++;
|
|
2433
|
+
const before = beforeChild == null ? -1 : this._indexOfChild(beforeChild);
|
|
2361
2434
|
const index = before === -1 ? this.children.length : before;
|
|
2362
2435
|
this.children.splice(index, 0, child);
|
|
2436
|
+
// The two slots this splice knows. Every other cached index at or after
|
|
2437
|
+
// `index` has shifted by one and will be caught by the check in
|
|
2438
|
+
// `_indexOfChild`; these two are the ones a run of inserts in front of
|
|
2439
|
+
// the same sibling asks about again on the very next call.
|
|
2440
|
+
child._childIndex = index;
|
|
2441
|
+
if (beforeChild != null) beforeChild._childIndex = index + 1;
|
|
2363
2442
|
return index;
|
|
2364
2443
|
}
|
|
2365
2444
|
|
|
@@ -2407,11 +2486,13 @@ export class Node {
|
|
|
2407
2486
|
);
|
|
2408
2487
|
}
|
|
2409
2488
|
// captured before the child joins, so it covers the arrangement that is
|
|
2410
|
-
// about to be replaced (see _childListChanged)
|
|
2411
|
-
|
|
2489
|
+
// about to be replaced (see _childListChanged). A viewport mid-blit has
|
|
2490
|
+
// nothing vacating — the child being added had no pixels — and the
|
|
2491
|
+
// layout diff claims where it lands, so it names no region at all.
|
|
2492
|
+
const before = this._blitLedgerOpen() ? null : this._childListBefore();
|
|
2412
2493
|
// a move has to leave the yoga tree too — yoga aborts on insertChild of
|
|
2413
2494
|
// a node that still has a parent
|
|
2414
|
-
if (
|
|
2495
|
+
if (child.parent === this && this._joinsYoga(child)) {
|
|
2415
2496
|
this.yoga.removeChild(child.yoga);
|
|
2416
2497
|
}
|
|
2417
2498
|
const index = this._spliceChild(child, beforeChild);
|
|
@@ -2429,6 +2510,92 @@ export class Node {
|
|
|
2429
2510
|
a11yHooks.attached?.(this, child);
|
|
2430
2511
|
}
|
|
2431
2512
|
|
|
2513
|
+
/**
|
|
2514
|
+
* Is this node a scroll container that has a blit armed and still clean
|
|
2515
|
+
* this frame (issue #398)?
|
|
2516
|
+
*
|
|
2517
|
+
* While it is, the window keeps a *ledger* of the regions that actually
|
|
2518
|
+
* changed inside the viewport instead of cancelling the blit at the first
|
|
2519
|
+
* sign of one. The coarse claims this node would otherwise make — its own
|
|
2520
|
+
* box, which is all `paintBounds()` can say for a node that clips — would
|
|
2521
|
+
* cover the whole band the blit is about to move and throw that ledger
|
|
2522
|
+
* away, so the paths that make them take a finer route while this is true.
|
|
2523
|
+
*
|
|
2524
|
+
* `scrollContents` is out: an element blit already tests foreign claims
|
|
2525
|
+
* against the rect it handed over (issue #309), and its region is not a
|
|
2526
|
+
* viewport whose children *are* the scrolled content.
|
|
2527
|
+
*/
|
|
2528
|
+
_blitLedgerOpen() {
|
|
2529
|
+
const from = this._pendingBlitFrom;
|
|
2530
|
+
return (
|
|
2531
|
+
from != null &&
|
|
2532
|
+
from !== BLIT_POISONED &&
|
|
2533
|
+
!this._pendingBlitContents &&
|
|
2534
|
+
this._blitLedger != null
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* Write one changed region into this viewport's ledger, in the coordinates
|
|
2540
|
+
* it was named in. Returns false when the frame is better off repainting
|
|
2541
|
+
* the viewport — too many regions to be worth the bookkeeping, or one big
|
|
2542
|
+
* enough that there is nothing left for the blit to keep — which the
|
|
2543
|
+
* caller turns into the poison the gate used to apply unconditionally.
|
|
2544
|
+
*
|
|
2545
|
+
* Which side of the frame's layout pass the rect came from decides
|
|
2546
|
+
* whether it moves with the blit: a claim made during the commit names
|
|
2547
|
+
* where the content sits *now*, and the blit is about to shift it, so
|
|
2548
|
+
* `_applyScrollBlits` shifts the rect too. A claim raised once layout has
|
|
2549
|
+
* run — the diff's, the reflow queue's — already names where it landed.
|
|
2550
|
+
* Read off the window rather than passed in, so a claim from application
|
|
2551
|
+
* code reached during the layout pass is filed on the right side of it.
|
|
2552
|
+
*/
|
|
2553
|
+
_recordBlitClaim(rect) {
|
|
2554
|
+
const ledger = this._blitLedger;
|
|
2555
|
+
if (!ledger || ledger.length >= BLIT_MAX_CLAIMS) return false;
|
|
2556
|
+
const inside = intersectRects(rect, this.abs);
|
|
2557
|
+
// beside the band the blit moves: those pixels are painted the ordinary
|
|
2558
|
+
// way, out of the frame's own damage
|
|
2559
|
+
if (!inside) return true;
|
|
2560
|
+
// …and a claim that covers the viewport leaves the blit nothing to keep
|
|
2561
|
+
if (rectContains(inside, this.abs)) return false;
|
|
2562
|
+
ledger.push({ ...inside, pre: !this.root?._laidOut });
|
|
2563
|
+
return true;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* This node's paint bounds from before a child-list mutation — the `before`
|
|
2568
|
+
* half of `_childListChanged`'s protocol, captured while a departing child
|
|
2569
|
+
* is still attached.
|
|
2570
|
+
*
|
|
2571
|
+
* Walked once per node per frame rather than once per mutation. A commit
|
|
2572
|
+
* that mounts a virtualized list's window inserts a hundred rows into one
|
|
2573
|
+
* pane, one `insertBefore` at a time, and a walk of the whole pane per row
|
|
2574
|
+
* is what made that commit O(rows x pane) (issue #397).
|
|
2575
|
+
*
|
|
2576
|
+
* Reusing the first walk's answer is not an approximation. Nothing is laid
|
|
2577
|
+
* out or painted between two mutations in the same frame, so every child
|
|
2578
|
+
* still carries the rect it was last painted at, and a child that leaves
|
|
2579
|
+
* later in the frame was in the list — and so inside the rect — when the
|
|
2580
|
+
* first walk ran. `root._reflowed` is the marker for "this frame already
|
|
2581
|
+
* has one", which is exactly its lifetime: joined at the first claim,
|
|
2582
|
+
* cleared by `flush()`.
|
|
2583
|
+
*/
|
|
2584
|
+
_childListBefore() {
|
|
2585
|
+
const root = this.root;
|
|
2586
|
+
// A subtree still being built off-tree claims nothing — this is the
|
|
2587
|
+
// `appendInitialChild` path, which is most of a mount, and where the
|
|
2588
|
+
// walk used to be thrown away by `_childListChanged`'s `!root` return.
|
|
2589
|
+
if (!root) return null;
|
|
2590
|
+
if (root._reflowed.has(this) && this._reflowBefore) {
|
|
2591
|
+
return this._reflowBefore;
|
|
2592
|
+
}
|
|
2593
|
+
// NO_DAMAGE, not null, when a blitting viewport above clips this node
|
|
2594
|
+
// away entirely (issue #398): null here would read as "somewhere" and
|
|
2595
|
+
// repaint the window.
|
|
2596
|
+
return (this._reflowBefore = this._claimBounds() ?? NO_DAMAGE);
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2432
2599
|
/**
|
|
2433
2600
|
* A child was inserted or removed. `before` is this node's paint bounds from
|
|
2434
2601
|
* *before* the mutation, which the caller has to capture while the departing
|
|
@@ -2448,20 +2615,35 @@ export class Node {
|
|
|
2448
2615
|
this._clearHitBounds();
|
|
2449
2616
|
const root = this.root;
|
|
2450
2617
|
if (!root) return;
|
|
2618
|
+
// A viewport keeping a ledger this frame (issue #398) says both halves
|
|
2619
|
+
// of the protocol finer: `before` is the departing child's own rect
|
|
2620
|
+
// rather than this node's box, and the "after" half comes from the
|
|
2621
|
+
// shifted layout diff, which claims an entering child where it lands
|
|
2622
|
+
// and says nothing about the ones that only rode the scroll. Joining
|
|
2623
|
+
// `_reflowed` would undo both — its post-layout claim is this node's
|
|
2624
|
+
// box, the whole band the blit is about to move.
|
|
2625
|
+
if (this._blitLedgerOpen()) {
|
|
2626
|
+
root.invalidate(true, before ?? NO_DAMAGE, 'child-list');
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2451
2629
|
root.invalidate(true, before, 'child-list');
|
|
2452
2630
|
root._reflowed.add(this);
|
|
2453
2631
|
}
|
|
2454
2632
|
|
|
2455
2633
|
removeChild(child) {
|
|
2456
|
-
const index = this.
|
|
2634
|
+
const index = this._indexOfChild(child);
|
|
2457
2635
|
if (index === -1) return;
|
|
2458
2636
|
// told while the child is still wired, so the bridge can compute the
|
|
2459
2637
|
// index the AT will see the removal at
|
|
2460
2638
|
a11yHooks.detach?.(this, child);
|
|
2461
2639
|
// captured while the child is still attached, so it covers the rect the
|
|
2462
|
-
// child is about to stop occupying
|
|
2463
|
-
|
|
2640
|
+
// child is about to stop occupying — the child's own, for a viewport
|
|
2641
|
+
// mid-blit, where this node's box is the whole scrolled band
|
|
2642
|
+
const before = this._blitLedgerOpen()
|
|
2643
|
+
? (child._claimBounds() ?? NO_DAMAGE)
|
|
2644
|
+
: this._childListBefore();
|
|
2464
2645
|
this.children.splice(index, 1);
|
|
2646
|
+
if (outsideYoga(child)) this._nonYogaKids--;
|
|
2465
2647
|
if (this._joinsYoga(child)) {
|
|
2466
2648
|
this.yoga.removeChild(child.yoga);
|
|
2467
2649
|
}
|
|
@@ -3020,7 +3202,37 @@ export class Node {
|
|
|
3020
3202
|
this._clearHitBounds();
|
|
3021
3203
|
if (layoutDiffSink) {
|
|
3022
3204
|
const grow = this._outlineExtent() + DAMAGE_SLOP;
|
|
3023
|
-
|
|
3205
|
+
const shift = layoutDiffShift;
|
|
3206
|
+
const had = old.width > 0 && old.height > 0;
|
|
3207
|
+
if (shift) {
|
|
3208
|
+
// Riding a blit (issue #398): the rect this node *would* have had if
|
|
3209
|
+
// nothing but the scroll had happened. Landing there is the blit's
|
|
3210
|
+
// own translation and claims nothing — claiming it would repaint the
|
|
3211
|
+
// band the blit exists to keep. Landing anywhere else is a real move,
|
|
3212
|
+
// and both ends of it are claimed in post-blit coordinates, which is
|
|
3213
|
+
// where the frame will paint them.
|
|
3214
|
+
const was = {
|
|
3215
|
+
x: old.x + shift.x,
|
|
3216
|
+
y: old.y + shift.y,
|
|
3217
|
+
width: old.width,
|
|
3218
|
+
height: old.height,
|
|
3219
|
+
};
|
|
3220
|
+
if (
|
|
3221
|
+
had &&
|
|
3222
|
+
was.x === x &&
|
|
3223
|
+
was.y === y &&
|
|
3224
|
+
old.width === width &&
|
|
3225
|
+
old.height === height
|
|
3226
|
+
) {
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
if (had) layoutDiffSink(insetRect(was, -grow));
|
|
3230
|
+
if (width > 0 && height > 0) {
|
|
3231
|
+
layoutDiffSink(insetRect(this.abs, -grow));
|
|
3232
|
+
}
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
if (had) {
|
|
3024
3236
|
layoutDiffSink(insetRect(old, -grow));
|
|
3025
3237
|
}
|
|
3026
3238
|
if (width > 0 && height > 0) {
|
|
@@ -3199,10 +3411,48 @@ export class Node {
|
|
|
3199
3411
|
_invalidateLayout(reason) {
|
|
3200
3412
|
const root = this.root;
|
|
3201
3413
|
if (!root) return;
|
|
3202
|
-
|
|
3414
|
+
// Same walk, same frame, same answer — see `_childListBefore`, whose
|
|
3415
|
+
// record this shares so that a reflow and a child-list change on one
|
|
3416
|
+
// node in one frame walk the subtree once between them.
|
|
3417
|
+
root.invalidate(true, this._childListBefore(), reason);
|
|
3203
3418
|
root._reflowed.add(this);
|
|
3204
3419
|
}
|
|
3205
3420
|
|
|
3421
|
+
/**
|
|
3422
|
+
* `paintBounds()` with the one clip a damage claim must respect: a scroll
|
|
3423
|
+
* container above this node that is waiting to blit (issue #398).
|
|
3424
|
+
*
|
|
3425
|
+
* A viewport clips its children, so the part of a claim outside its box is
|
|
3426
|
+
* pixels that cannot appear — and leaving it in costs the blit the frame.
|
|
3427
|
+
* A virtualized list is the shape that makes this concrete: its spacers
|
|
3428
|
+
* are boxes thousands of pixels tall whose visible extent is a sliver or
|
|
3429
|
+
* nothing at all, and their unclipped claims, coalesced into the scroll's
|
|
3430
|
+
* own, leave `_blitKeptDamage` a damage rect many times the viewport to
|
|
3431
|
+
* refuse. Null when the clip left nothing.
|
|
3432
|
+
*
|
|
3433
|
+
* Only while a blit is pending — outside that this is `paintBounds()` and
|
|
3434
|
+
* one property read. Clipping every claim to every clipping ancestor
|
|
3435
|
+
* would be correct too, and is a bigger change than the frame this is
|
|
3436
|
+
* about.
|
|
3437
|
+
*/
|
|
3438
|
+
_claimBounds() {
|
|
3439
|
+
const bounds = this.paintBounds();
|
|
3440
|
+
const sv = this._blitViewport();
|
|
3441
|
+
return sv ? intersectRects(bounds, sv.paintBounds()) : bounds;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
/** The scroll container above this node that is waiting to blit, if there
|
|
3445
|
+
* is one — the viewport whose ledger this node's claims belong in, and
|
|
3446
|
+
* whose box clips them (issue #398). One property read when no blit is
|
|
3447
|
+
* pending, which is every frame that is not a scroll. */
|
|
3448
|
+
_blitViewport() {
|
|
3449
|
+
if (!this.root?._pendingScrolls?.size) return null;
|
|
3450
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
3451
|
+
if (n._blitLedgerOpen()) return n;
|
|
3452
|
+
}
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3206
3456
|
/**
|
|
3207
3457
|
* The region this node can put ink in: its own rect unioned with every
|
|
3208
3458
|
* descendant's. Not the same as `abs` — a child of a node that does not
|
|
@@ -5100,13 +5350,31 @@ export const Scrollable = (Base) =>
|
|
|
5100
5350
|
// real layout — claim it, but clipped to the viewport: ink below the
|
|
5101
5351
|
// fold never reaches the surface, and an unclipped claim would repaint
|
|
5102
5352
|
// whatever unrelated UI sits under this node's off-viewport extent.
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
(this._childOrigin.x !== ox || this._childOrigin.y !== oy);
|
|
5353
|
+
const wasOrigin = this._childOrigin;
|
|
5354
|
+
const shifted = wasOrigin && (wasOrigin.x !== ox || wasOrigin.y !== oy);
|
|
5106
5355
|
this._childOrigin = { x: ox, y: oy };
|
|
5107
5356
|
const outer = layoutDiffSink;
|
|
5357
|
+
const outerShift = layoutDiffShift;
|
|
5358
|
+
const ledger = shifted && this._blitLedgerOpen();
|
|
5108
5359
|
if (outer) {
|
|
5109
|
-
if (
|
|
5360
|
+
if (ledger) {
|
|
5361
|
+
// The blit's own ledger takes this walk (issue #398). The shift
|
|
5362
|
+
// below is what makes the diff worth running under a scroll at
|
|
5363
|
+
// all: without it every child reports the move the blit is about
|
|
5364
|
+
// to make for them, and the claims add up to the viewport. What
|
|
5365
|
+
// is left is the virtualized list's real frame — the rows that
|
|
5366
|
+
// entered, the ones that left, a spacer that resized — and it
|
|
5367
|
+
// goes to the ledger rather than to `outer`, whose claims are
|
|
5368
|
+
// what `layoutMoved` reads as "this frame is not a pure scroll".
|
|
5369
|
+
const vp = insetRect(this.abs, -DAMAGE_SLOP);
|
|
5370
|
+
layoutDiffSink = (rect) => {
|
|
5371
|
+
const clipped = intersectRects(rect, vp);
|
|
5372
|
+
if (clipped && !this._recordBlitClaim(clipped)) {
|
|
5373
|
+
this._pendingBlitFrom = BLIT_POISONED;
|
|
5374
|
+
}
|
|
5375
|
+
};
|
|
5376
|
+
layoutDiffShift = { x: ox - wasOrigin.x, y: oy - wasOrigin.y };
|
|
5377
|
+
} else if (shifted) {
|
|
5110
5378
|
layoutDiffSink = null;
|
|
5111
5379
|
} else {
|
|
5112
5380
|
const vp = insetRect(this.abs, -DAMAGE_SLOP);
|
|
@@ -5124,6 +5392,7 @@ export const Scrollable = (Base) =>
|
|
|
5124
5392
|
}
|
|
5125
5393
|
} finally {
|
|
5126
5394
|
layoutDiffSink = outer;
|
|
5395
|
+
layoutDiffShift = outerShift;
|
|
5127
5396
|
}
|
|
5128
5397
|
}
|
|
5129
5398
|
|
|
@@ -5274,10 +5543,19 @@ export const Scrollable = (Base) =>
|
|
|
5274
5543
|
// has not claimed yet, so damage already overlapping this viewport
|
|
5275
5544
|
// is foreign by construction: poison the frame instead of arming,
|
|
5276
5545
|
// and the full-viewport repaint below stays in force.
|
|
5277
|
-
|
|
5546
|
+
const arming = this._pendingBlitFrom == null;
|
|
5547
|
+
// The ledger this frame's changes inside the viewport are written
|
|
5548
|
+
// to (issue #398). Opened with the blit and read by
|
|
5549
|
+
// _applyScrollBlits, which clears it beside the origin.
|
|
5550
|
+
if (arming) this._blitLedger = [];
|
|
5551
|
+
if (arming && Array.isArray(root._damage)) {
|
|
5278
5552
|
const zone = insetRect(this.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
5279
5553
|
for (const rect of root._damage) {
|
|
5280
|
-
|
|
5554
|
+
// Already coalesced, so these rects are as coarse as the frame
|
|
5555
|
+
// has made them — which the ledger reads conservatively: a blob
|
|
5556
|
+
// that swallowed the viewport says so and poisons, exactly as
|
|
5557
|
+
// this gate used to for every claim it saw.
|
|
5558
|
+
if (rectsOverlap(rect, zone) && !this._recordBlitClaim(rect)) {
|
|
5281
5559
|
this._pendingBlitFrom = BLIT_POISONED;
|
|
5282
5560
|
break;
|
|
5283
5561
|
}
|
|
@@ -9488,8 +9766,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9488
9766
|
|
|
9489
9767
|
removeChild(child) {
|
|
9490
9768
|
if (child.isWindow) {
|
|
9491
|
-
const index = this.
|
|
9492
|
-
if (index !== -1)
|
|
9769
|
+
const index = this._indexOfChild(child);
|
|
9770
|
+
if (index !== -1) {
|
|
9771
|
+
this.children.splice(index, 1);
|
|
9772
|
+
this._nonYogaKids--;
|
|
9773
|
+
}
|
|
9493
9774
|
const id = child.window?.id;
|
|
9494
9775
|
child.parent = null;
|
|
9495
9776
|
child.destroySubtree();
|
|
@@ -9951,14 +10232,21 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9951
10232
|
// rects coalesce a change inside the viewport is indistinguishable from
|
|
9952
10233
|
// the scroll's own claim. (Unbounded claims need no check: FULL_DAMAGE
|
|
9953
10234
|
// fails the blit's damage gate by itself.)
|
|
10235
|
+
// The region this claim actually covers — a node's paint reach, clipped
|
|
10236
|
+
// to a blitting viewport above it (issue #398), or the bare rect a
|
|
10237
|
+
// caller handed over. Null when the clip left nothing (the node draws
|
|
10238
|
+
// where nothing can be seen, so it owes no pixels), and null on a frame
|
|
10239
|
+
// that is already unbounded, which owes neither a rect nor the subtree
|
|
10240
|
+
// walk that measures one — a blit cannot fire there either.
|
|
10241
|
+
const bounds =
|
|
10242
|
+
damage && damage !== NO_DAMAGE && this._damage !== FULL_DAMAGE
|
|
10243
|
+
? damage._claimBounds
|
|
10244
|
+
? damage._claimBounds()
|
|
10245
|
+
: damage
|
|
10246
|
+
: null;
|
|
9954
10247
|
const pendingScrolls = this._pendingScrolls;
|
|
9955
|
-
if (
|
|
9956
|
-
|
|
9957
|
-
damage &&
|
|
9958
|
-
damage !== NO_DAMAGE &&
|
|
9959
|
-
this._scrollClaim !== damage
|
|
9960
|
-
) {
|
|
9961
|
-
const rect = damage.paintBounds ? damage.paintBounds() : damage;
|
|
10248
|
+
if (pendingScrolls?.size && bounds && this._scrollClaim !== damage) {
|
|
10249
|
+
const rect = bounds;
|
|
9962
10250
|
for (const sv of pendingScrolls) {
|
|
9963
10251
|
// An element blitting a region of its own drawing (issue #303) is
|
|
9964
10252
|
// waiting on that region, not on the whole node it lives in — and
|
|
@@ -9979,6 +10267,15 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9979
10267
|
? contents.rect
|
|
9980
10268
|
: sv.abs && insetRect(sv.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
9981
10269
|
if (!waiting || rectsOverlap(rect, waiting)) {
|
|
10270
|
+
// …unless this viewport is keeping a ledger of what changed
|
|
10271
|
+
// inside it (issue #398): the region goes in the ledger and
|
|
10272
|
+
// `_applyScrollBlits` repaints it after the blit, which is the
|
|
10273
|
+
// same pixels on screen for a fraction of the drawing. The
|
|
10274
|
+
// ledger says no when the frame stops paying, and then this
|
|
10275
|
+
// falls through to the poison exactly as before.
|
|
10276
|
+
if (sv._blitLedgerOpen() && sv._recordBlitClaim(rect)) {
|
|
10277
|
+
continue;
|
|
10278
|
+
}
|
|
9982
10279
|
// Poison rather than disarm (react-x11#295): a null here would
|
|
9983
10280
|
// let a second scrollTo in the same frame re-arm from a
|
|
9984
10281
|
// mid-frame origin, and the blit would then move pixels that
|
|
@@ -9992,16 +10289,18 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9992
10289
|
}
|
|
9993
10290
|
if (layoutChanged && !damage) this._damage = FULL_DAMAGE;
|
|
9994
10291
|
else if (!layoutChanged && !damage) this._damage = FULL_DAMAGE;
|
|
9995
|
-
else if (
|
|
10292
|
+
else if (!bounds) {
|
|
10293
|
+
// A layout change that names no region: either NO_DAMAGE, from a
|
|
10294
|
+
// caller with a finer claim already in flight, or a node whose reach
|
|
10295
|
+
// a clipping ancestor left nothing of (issue #398). Unlike `!damage`
|
|
10296
|
+
// neither is "something, somewhere", so neither costs a full repaint.
|
|
10297
|
+
} else if (this._damage !== FULL_DAMAGE) {
|
|
9996
10298
|
// a node, or a bare rect for a caller that has a region rather than a
|
|
9997
10299
|
// node — a subtree that is about to be removed, say. Claims accumulate
|
|
9998
10300
|
// as a list of rects rather than one box around them all, so two changes
|
|
9999
10301
|
// at opposite corners of the window no longer repaint everything
|
|
10000
10302
|
// between them.
|
|
10001
|
-
this._damage = addDamageRect(
|
|
10002
|
-
this._damage,
|
|
10003
|
-
damage.paintBounds ? damage.paintBounds() : damage,
|
|
10004
|
-
);
|
|
10303
|
+
this._damage = addDamageRect(this._damage, bounds);
|
|
10005
10304
|
}
|
|
10006
10305
|
this.needsPaint = true;
|
|
10007
10306
|
// Recorded before the `_scheduled` gate, not inside it: the debt is
|
|
@@ -10056,6 +10355,10 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10056
10355
|
// not the flag's post-pass value
|
|
10057
10356
|
const layoutRan = this.needsLayout;
|
|
10058
10357
|
if (this.needsLayout) {
|
|
10358
|
+
// From here on a claim names where its content *landed*, not where it
|
|
10359
|
+
// sat before the scroll — which is what decides whether a blit
|
|
10360
|
+
// ledger's rect moves with the shift (issue #398).
|
|
10361
|
+
this._laidOut = true;
|
|
10059
10362
|
this._resolveSizeQueries(width, height);
|
|
10060
10363
|
this._applyContentFloors(width);
|
|
10061
10364
|
this.yoga.setWidth(width);
|
|
@@ -10091,14 +10394,25 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10091
10394
|
// replaced it. Claimed after layout because an inserted child has no
|
|
10092
10395
|
// rect before it.
|
|
10093
10396
|
for (const node of this._reflowed) {
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10397
|
+
// …and the pre-mutation walk this frame reused goes with it
|
|
10398
|
+
node._reflowBefore = null;
|
|
10399
|
+
if (node.destroyed || this._damage === FULL_DAMAGE) continue;
|
|
10400
|
+
// clipped to a blitting viewport above it, like every other claim
|
|
10401
|
+
// this frame, and written to that viewport's ledger too (issue
|
|
10402
|
+
// #398): the claim would otherwise coalesce into the scroll's own
|
|
10403
|
+
// and be dropped with it, leaving the band the blit kept holding
|
|
10404
|
+
// this node's pixels from before the reflow.
|
|
10405
|
+
const after = node._claimBounds();
|
|
10406
|
+
if (!after) continue;
|
|
10407
|
+
const sv = node._blitViewport();
|
|
10408
|
+
if (sv && !sv._recordBlitClaim(after)) {
|
|
10409
|
+
sv._pendingBlitFrom = BLIT_POISONED;
|
|
10410
|
+
}
|
|
10411
|
+
this._damage = addDamageRect(this._damage, after);
|
|
10099
10412
|
}
|
|
10100
10413
|
this._reflowed.clear();
|
|
10101
10414
|
} else if (this._reflowed.size) {
|
|
10415
|
+
for (const node of this._reflowed) node._reflowBefore = null;
|
|
10102
10416
|
this._reflowed.clear();
|
|
10103
10417
|
}
|
|
10104
10418
|
// any node this pass laid out may be what an open popup is anchored to
|
|
@@ -10107,6 +10421,9 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10107
10421
|
// a frame that turns out to be a pure scroll blits the surviving band
|
|
10108
10422
|
// and narrows its claim to the exposed strip
|
|
10109
10423
|
this._applyScrollBlits(width, height, layoutMoved);
|
|
10424
|
+
// …and the next commit's claims name the arrangement this frame leaves
|
|
10425
|
+
// behind again, from before whatever scroll comes with them
|
|
10426
|
+
this._laidOut = false;
|
|
10110
10427
|
if (!this.needsPaint) return;
|
|
10111
10428
|
this.needsPaint = false;
|
|
10112
10429
|
const damage = this._takeDamage(width, height);
|
|
@@ -10185,9 +10502,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10185
10502
|
pending.clear();
|
|
10186
10503
|
const from = nodes[0]._pendingBlitFrom;
|
|
10187
10504
|
const contents = nodes[0]._pendingBlitContents;
|
|
10505
|
+
const ledger = nodes[0]._blitLedger;
|
|
10188
10506
|
for (const n of nodes) {
|
|
10189
10507
|
n._pendingBlitFrom = null;
|
|
10190
10508
|
n._pendingBlitContents = null;
|
|
10509
|
+
n._blitLedger = null;
|
|
10191
10510
|
}
|
|
10192
10511
|
// two viewports scrolling in one frame is rare enough that sorting out
|
|
10193
10512
|
// whether their regions interact is not worth it
|
|
@@ -10267,11 +10586,39 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10267
10586
|
}
|
|
10268
10587
|
const keep = this._blitKeptDamage(vp);
|
|
10269
10588
|
if (!keep) return;
|
|
10589
|
+
// What changed inside the viewport while the blit was armed, in the
|
|
10590
|
+
// coordinates the frame is about to paint in (issue #398). A claim made
|
|
10591
|
+
// during the commit named where the content sat before the shift, and
|
|
10592
|
+
// the blit is about to move those pixels by the frame's delta, so it
|
|
10593
|
+
// moves with them; a claim from the layout diff already landed there.
|
|
10594
|
+
//
|
|
10595
|
+
// Repainting the result is what makes the blit honest about them: the
|
|
10596
|
+
// blit translates the previous frame's rendering, which is correct
|
|
10597
|
+
// everywhere the content did not change, and these are the places it
|
|
10598
|
+
// did. That is finer than the strip-only rule issue #398 asks for and
|
|
10599
|
+
// no more complicated, so a mid-viewport change — a row upgrading from
|
|
10600
|
+
// skeleton to content while the list scrolls — rides the fast path too
|
|
10601
|
+
// instead of falling back to the whole viewport.
|
|
10602
|
+
const repairs = [];
|
|
10603
|
+
let repairArea = 0;
|
|
10604
|
+
for (const claim of ledger ?? []) {
|
|
10605
|
+
const moved = claim.pre
|
|
10606
|
+
? {
|
|
10607
|
+
x: claim.x - dx,
|
|
10608
|
+
y: claim.y - dy,
|
|
10609
|
+
width: claim.width,
|
|
10610
|
+
height: claim.height,
|
|
10611
|
+
}
|
|
10612
|
+
: claim;
|
|
10613
|
+
const inside = intersectRects(moved, vp);
|
|
10614
|
+
if (!inside) continue;
|
|
10615
|
+
repairs.push(inside);
|
|
10616
|
+
repairArea += inside.width * inside.height;
|
|
10617
|
+
}
|
|
10618
|
+
// past this the blit plus a scatter of repaints is no longer cheaper
|
|
10619
|
+
// than the one full-viewport pass it replaced
|
|
10620
|
+
if (repairArea > area * BLIT_MAX_CLAIM_AREA) return;
|
|
10270
10621
|
if (!this._scrollBlitSafe(node, vp)) return;
|
|
10271
|
-
// scroll offsets grow down/right; the pixels move the other way
|
|
10272
|
-
// (0 - x rather than -x: negating +0 yields -0, which survives into
|
|
10273
|
-
// request buffers and test comparisons)
|
|
10274
|
-
if (!wnd.scrollRegion({ ...vp }, 0 - dx, 0 - dy)) return;
|
|
10275
10622
|
let rects = keep;
|
|
10276
10623
|
// the strip the shift exposed, full breadth — it also covers the corner
|
|
10277
10624
|
// gutter beside the bars, whose old pixels the blit did not overwrite
|
|
@@ -10326,6 +10673,24 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10326
10673
|
}
|
|
10327
10674
|
const crossBar = node._scrollbar(axis === 'y' ? 'x' : 'y');
|
|
10328
10675
|
if (crossBar) rects = addDamageRect(rects, scrollbarTrackRect(crossBar));
|
|
10676
|
+
for (const repair of repairs) rects = addDamageRect(rects, repair);
|
|
10677
|
+
// The last gate, and the only one that has to wait until the rects are
|
|
10678
|
+
// assembled: the frame carries at most MAX_DAMAGE_RECTS of them, so a
|
|
10679
|
+
// repair that does not sit beside the strip is merged with whatever is
|
|
10680
|
+
// nearest — the scrollbar column, most often — and the box of that
|
|
10681
|
+
// merge can reach back across the viewport. When it does, the blit is
|
|
10682
|
+
// buying a shift and paying for the viewport anyway, so let the plain
|
|
10683
|
+
// repaint scrollTo already claimed have the frame.
|
|
10684
|
+
let painted = 0;
|
|
10685
|
+
for (const rect of rects) {
|
|
10686
|
+
const inside = intersectRects(rect, vp);
|
|
10687
|
+
if (inside) painted += inside.width * inside.height;
|
|
10688
|
+
}
|
|
10689
|
+
if (painted > area * SCROLL_BLIT_MAX_REPAINT) return;
|
|
10690
|
+
// scroll offsets grow down/right; the pixels move the other way
|
|
10691
|
+
// (0 - x rather than -x: negating +0 yields -0, which survives into
|
|
10692
|
+
// request buffers and test comparisons)
|
|
10693
|
+
if (!wnd.scrollRegion({ ...vp }, 0 - dx, 0 - dy)) return;
|
|
10329
10694
|
this._damage = rects;
|
|
10330
10695
|
}
|
|
10331
10696
|
|