react-x11 2.1.0 → 2.1.2
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 +435 -49
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;
|
|
@@ -2199,8 +2244,19 @@ export class Node {
|
|
|
2199
2244
|
}
|
|
2200
2245
|
|
|
2201
2246
|
/** The theme above or on this node changed: drop the caches and restyle
|
|
2202
|
-
* the subtree, since a token can appear at any depth.
|
|
2203
|
-
|
|
2247
|
+
* the subtree, since a token can appear at any depth.
|
|
2248
|
+
*
|
|
2249
|
+
* `mounting` is `insertBefore` attaching a subtree that has never been in
|
|
2250
|
+
* the tree: the walk still resolves every token — the nodes can see their
|
|
2251
|
+
* ancestors now — but it claims no damage (issue #402). A node that has
|
|
2252
|
+
* never painted has no stale pixels to cover, and the rect it is about to
|
|
2253
|
+
* occupy is claimed by the child-list/layout-diff protocol like any other
|
|
2254
|
+
* inserted child's; the unbounded claims below would turn every commit
|
|
2255
|
+
* that mounts a token-styled node into a full-window repaint — which is
|
|
2256
|
+
* every re-slice of a virtualized list whose rows follow the palette. A
|
|
2257
|
+
* live theme *swap* is the other caller and keeps them: it moves pixels
|
|
2258
|
+
* that are already on screen, anywhere in the subtree. */
|
|
2259
|
+
_themeChanged(mounting = false) {
|
|
2204
2260
|
// This walk visits every node itself, so the per-node re-resolution below
|
|
2205
2261
|
// is enough — a style swap it causes must not start a second walk of the
|
|
2206
2262
|
// same subtree from halfway down.
|
|
@@ -2224,19 +2280,23 @@ export class Node {
|
|
|
2224
2280
|
if (localTextStyleChanged(this.style, before)) {
|
|
2225
2281
|
this._textContentChanged();
|
|
2226
2282
|
}
|
|
2227
|
-
this.root?.invalidate(true, null, 'theme');
|
|
2283
|
+
if (!mounting) this.root?.invalidate(true, null, 'theme');
|
|
2228
2284
|
}
|
|
2229
2285
|
// The palette is the floor under the cascade, so a theme swap moves the
|
|
2230
2286
|
// resolved style of every node that named none of its own — and none of
|
|
2231
2287
|
// that is in a style object, so nothing above would have noticed. A
|
|
2232
2288
|
// swap that only changes `fontFamily` is the case that made this worth
|
|
2233
2289
|
// having: nothing else about the node changes, and a cached layout
|
|
2234
|
-
// carries the face it was shaped with.
|
|
2235
|
-
|
|
2290
|
+
// carries the face it was shaped with. `_retext` runs on a mount too —
|
|
2291
|
+
// its own claims are bounded — but cannot answer non-zero there: a
|
|
2292
|
+
// node that was never attached has never resolved a text style.
|
|
2293
|
+
if (this._retext() !== 0 && !mounting) {
|
|
2294
|
+
this.root?.invalidate(true, null, 'theme');
|
|
2295
|
+
}
|
|
2236
2296
|
if (wasDirection !== undefined && this.direction !== wasDirection) {
|
|
2237
2297
|
this._directionMoved();
|
|
2238
2298
|
}
|
|
2239
|
-
for (const child of this.children) child._themeChanged();
|
|
2299
|
+
for (const child of this.children) child._themeChanged(mounting);
|
|
2240
2300
|
} finally {
|
|
2241
2301
|
inThemeWalk--;
|
|
2242
2302
|
}
|
|
@@ -2256,6 +2316,12 @@ export class Node {
|
|
|
2256
2316
|
/** Number of yoga-bearing children before `index` (window children and
|
|
2257
2317
|
* text spans/chunks do not join the parent's yoga tree). */
|
|
2258
2318
|
_yogaIndexAt(index) {
|
|
2319
|
+
// A list of ordinary boxes — a scroll pane's rows, which is the list
|
|
2320
|
+
// this is asked about a hundred times in one commit — has every child in
|
|
2321
|
+
// the yoga tree, and then the yoga index *is* the child index. Counting
|
|
2322
|
+
// the exceptions as they arrive turns that answer into a read instead of
|
|
2323
|
+
// a walk of every sibling in front of the new row (issue #397).
|
|
2324
|
+
if (this._nonYogaKids === 0) return index;
|
|
2259
2325
|
let n = 0;
|
|
2260
2326
|
for (let i = 0; i < index; i++) {
|
|
2261
2327
|
if (this._joinsYoga(this.children[i])) n++;
|
|
@@ -2267,6 +2333,24 @@ export class Node {
|
|
|
2267
2333
|
return Boolean(this.yoga && child.yoga && !child.isWindow);
|
|
2268
2334
|
}
|
|
2269
2335
|
|
|
2336
|
+
/**
|
|
2337
|
+
* Where `child` sits in `this.children`.
|
|
2338
|
+
*
|
|
2339
|
+
* The cached slot is checked rather than trusted: a node appears in the
|
|
2340
|
+
* list once, so `children[i] === child` *is* the proof that `i` is its
|
|
2341
|
+
* index, and a cache that has gone stale costs a scan rather than a wrong
|
|
2342
|
+
* answer. `_spliceChild` refreshes the two slots it knows — the child it
|
|
2343
|
+
* placed and the sibling it pushed along — which is what keeps a run of
|
|
2344
|
+
* inserts in front of the same trailing sibling (every virtualized list's
|
|
2345
|
+
* commit) off the scan entirely.
|
|
2346
|
+
*/
|
|
2347
|
+
_indexOfChild(child) {
|
|
2348
|
+
if (this.children[child._childIndex] === child) return child._childIndex;
|
|
2349
|
+
const i = this.children.indexOf(child);
|
|
2350
|
+
child._childIndex = i;
|
|
2351
|
+
return i;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2270
2354
|
/**
|
|
2271
2355
|
* Give this node's box a measure function, keeping a reference that can be
|
|
2272
2356
|
* asked again later.
|
|
@@ -2354,12 +2438,22 @@ export class Node {
|
|
|
2354
2438
|
* by calling insertBefore with a child that is *already* mounted here, and
|
|
2355
2439
|
* without the removal it would appear twice. Returns the new index. */
|
|
2356
2440
|
_spliceChild(child, beforeChild) {
|
|
2357
|
-
|
|
2441
|
+
// `parent === this` is the cheap form of "already in this list" — the
|
|
2442
|
+
// two are set and cleared together — so a child arriving for the first
|
|
2443
|
+
// time, which is every node of a freshly mounted subtree, pays no scan
|
|
2444
|
+
// at all for the question.
|
|
2445
|
+
const from = child.parent === this ? this._indexOfChild(child) : -1;
|
|
2358
2446
|
if (from !== -1) this.children.splice(from, 1);
|
|
2359
|
-
|
|
2360
|
-
|
|
2447
|
+
else if (outsideYoga(child)) this._nonYogaKids++;
|
|
2448
|
+
const before = beforeChild == null ? -1 : this._indexOfChild(beforeChild);
|
|
2361
2449
|
const index = before === -1 ? this.children.length : before;
|
|
2362
2450
|
this.children.splice(index, 0, child);
|
|
2451
|
+
// The two slots this splice knows. Every other cached index at or after
|
|
2452
|
+
// `index` has shifted by one and will be caught by the check in
|
|
2453
|
+
// `_indexOfChild`; these two are the ones a run of inserts in front of
|
|
2454
|
+
// the same sibling asks about again on the very next call.
|
|
2455
|
+
child._childIndex = index;
|
|
2456
|
+
if (beforeChild != null) beforeChild._childIndex = index + 1;
|
|
2363
2457
|
return index;
|
|
2364
2458
|
}
|
|
2365
2459
|
|
|
@@ -2368,9 +2462,10 @@ export class Node {
|
|
|
2368
2462
|
// popups live anywhere in the JSX tree but are independent
|
|
2369
2463
|
// override-redirect windows: bookkeeping only, no yoga, no paint —
|
|
2370
2464
|
// but they do inherit the theme of where they are written
|
|
2465
|
+
const mounting = child.parent == null;
|
|
2371
2466
|
this._spliceChild(child, beforeChild);
|
|
2372
2467
|
child.parent = this;
|
|
2373
|
-
if (this.theme || child.props.theme) child._themeChanged();
|
|
2468
|
+
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
2374
2469
|
a11yHooks.attached?.(this, child);
|
|
2375
2470
|
return;
|
|
2376
2471
|
}
|
|
@@ -2407,13 +2502,19 @@ export class Node {
|
|
|
2407
2502
|
);
|
|
2408
2503
|
}
|
|
2409
2504
|
// captured before the child joins, so it covers the arrangement that is
|
|
2410
|
-
// about to be replaced (see _childListChanged)
|
|
2411
|
-
|
|
2505
|
+
// about to be replaced (see _childListChanged). A viewport mid-blit has
|
|
2506
|
+
// nothing vacating — the child being added had no pixels — and the
|
|
2507
|
+
// layout diff claims where it lands, so it names no region at all.
|
|
2508
|
+
const before = this._blitLedgerOpen() ? null : this._childListBefore();
|
|
2412
2509
|
// a move has to leave the yoga tree too — yoga aborts on insertChild of
|
|
2413
2510
|
// a node that still has a parent
|
|
2414
|
-
if (
|
|
2511
|
+
if (child.parent === this && this._joinsYoga(child)) {
|
|
2415
2512
|
this.yoga.removeChild(child.yoga);
|
|
2416
2513
|
}
|
|
2514
|
+
// no parent means never attached: this insert is a mount, and the theme
|
|
2515
|
+
// walk resolves without claiming — a keyed reorder arrives here too, with
|
|
2516
|
+
// its parent still set, and that one keeps the claims (issue #402)
|
|
2517
|
+
const mounting = child.parent == null;
|
|
2417
2518
|
const index = this._spliceChild(child, beforeChild);
|
|
2418
2519
|
child.parent = this;
|
|
2419
2520
|
if (this._joinsYoga(child)) {
|
|
@@ -2423,12 +2524,98 @@ export class Node {
|
|
|
2423
2524
|
child._registerSizeQueries();
|
|
2424
2525
|
// it can see its ancestors now, so any token in its style can resolve.
|
|
2425
2526
|
// With no theme anywhere there is nothing to resolve and nothing to walk
|
|
2426
|
-
if (this.theme || child.props.theme) child._themeChanged();
|
|
2527
|
+
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
2427
2528
|
this._textContentChanged();
|
|
2428
2529
|
this._childListChanged(before);
|
|
2429
2530
|
a11yHooks.attached?.(this, child);
|
|
2430
2531
|
}
|
|
2431
2532
|
|
|
2533
|
+
/**
|
|
2534
|
+
* Is this node a scroll container that has a blit armed and still clean
|
|
2535
|
+
* this frame (issue #398)?
|
|
2536
|
+
*
|
|
2537
|
+
* While it is, the window keeps a *ledger* of the regions that actually
|
|
2538
|
+
* changed inside the viewport instead of cancelling the blit at the first
|
|
2539
|
+
* sign of one. The coarse claims this node would otherwise make — its own
|
|
2540
|
+
* box, which is all `paintBounds()` can say for a node that clips — would
|
|
2541
|
+
* cover the whole band the blit is about to move and throw that ledger
|
|
2542
|
+
* away, so the paths that make them take a finer route while this is true.
|
|
2543
|
+
*
|
|
2544
|
+
* `scrollContents` is out: an element blit already tests foreign claims
|
|
2545
|
+
* against the rect it handed over (issue #309), and its region is not a
|
|
2546
|
+
* viewport whose children *are* the scrolled content.
|
|
2547
|
+
*/
|
|
2548
|
+
_blitLedgerOpen() {
|
|
2549
|
+
const from = this._pendingBlitFrom;
|
|
2550
|
+
return (
|
|
2551
|
+
from != null &&
|
|
2552
|
+
from !== BLIT_POISONED &&
|
|
2553
|
+
!this._pendingBlitContents &&
|
|
2554
|
+
this._blitLedger != null
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
/**
|
|
2559
|
+
* Write one changed region into this viewport's ledger, in the coordinates
|
|
2560
|
+
* it was named in. Returns false when the frame is better off repainting
|
|
2561
|
+
* the viewport — too many regions to be worth the bookkeeping, or one big
|
|
2562
|
+
* enough that there is nothing left for the blit to keep — which the
|
|
2563
|
+
* caller turns into the poison the gate used to apply unconditionally.
|
|
2564
|
+
*
|
|
2565
|
+
* Which side of the frame's layout pass the rect came from decides
|
|
2566
|
+
* whether it moves with the blit: a claim made during the commit names
|
|
2567
|
+
* where the content sits *now*, and the blit is about to shift it, so
|
|
2568
|
+
* `_applyScrollBlits` shifts the rect too. A claim raised once layout has
|
|
2569
|
+
* run — the diff's, the reflow queue's — already names where it landed.
|
|
2570
|
+
* Read off the window rather than passed in, so a claim from application
|
|
2571
|
+
* code reached during the layout pass is filed on the right side of it.
|
|
2572
|
+
*/
|
|
2573
|
+
_recordBlitClaim(rect) {
|
|
2574
|
+
const ledger = this._blitLedger;
|
|
2575
|
+
if (!ledger || ledger.length >= BLIT_MAX_CLAIMS) return false;
|
|
2576
|
+
const inside = intersectRects(rect, this.abs);
|
|
2577
|
+
// beside the band the blit moves: those pixels are painted the ordinary
|
|
2578
|
+
// way, out of the frame's own damage
|
|
2579
|
+
if (!inside) return true;
|
|
2580
|
+
// …and a claim that covers the viewport leaves the blit nothing to keep
|
|
2581
|
+
if (rectContains(inside, this.abs)) return false;
|
|
2582
|
+
ledger.push({ ...inside, pre: !this.root?._laidOut });
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* This node's paint bounds from before a child-list mutation — the `before`
|
|
2588
|
+
* half of `_childListChanged`'s protocol, captured while a departing child
|
|
2589
|
+
* is still attached.
|
|
2590
|
+
*
|
|
2591
|
+
* Walked once per node per frame rather than once per mutation. A commit
|
|
2592
|
+
* that mounts a virtualized list's window inserts a hundred rows into one
|
|
2593
|
+
* pane, one `insertBefore` at a time, and a walk of the whole pane per row
|
|
2594
|
+
* is what made that commit O(rows x pane) (issue #397).
|
|
2595
|
+
*
|
|
2596
|
+
* Reusing the first walk's answer is not an approximation. Nothing is laid
|
|
2597
|
+
* out or painted between two mutations in the same frame, so every child
|
|
2598
|
+
* still carries the rect it was last painted at, and a child that leaves
|
|
2599
|
+
* later in the frame was in the list — and so inside the rect — when the
|
|
2600
|
+
* first walk ran. `root._reflowed` is the marker for "this frame already
|
|
2601
|
+
* has one", which is exactly its lifetime: joined at the first claim,
|
|
2602
|
+
* cleared by `flush()`.
|
|
2603
|
+
*/
|
|
2604
|
+
_childListBefore() {
|
|
2605
|
+
const root = this.root;
|
|
2606
|
+
// A subtree still being built off-tree claims nothing — this is the
|
|
2607
|
+
// `appendInitialChild` path, which is most of a mount, and where the
|
|
2608
|
+
// walk used to be thrown away by `_childListChanged`'s `!root` return.
|
|
2609
|
+
if (!root) return null;
|
|
2610
|
+
if (root._reflowed.has(this) && this._reflowBefore) {
|
|
2611
|
+
return this._reflowBefore;
|
|
2612
|
+
}
|
|
2613
|
+
// NO_DAMAGE, not null, when a blitting viewport above clips this node
|
|
2614
|
+
// away entirely (issue #398): null here would read as "somewhere" and
|
|
2615
|
+
// repaint the window.
|
|
2616
|
+
return (this._reflowBefore = this._claimBounds() ?? NO_DAMAGE);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2432
2619
|
/**
|
|
2433
2620
|
* A child was inserted or removed. `before` is this node's paint bounds from
|
|
2434
2621
|
* *before* the mutation, which the caller has to capture while the departing
|
|
@@ -2448,20 +2635,35 @@ export class Node {
|
|
|
2448
2635
|
this._clearHitBounds();
|
|
2449
2636
|
const root = this.root;
|
|
2450
2637
|
if (!root) return;
|
|
2638
|
+
// A viewport keeping a ledger this frame (issue #398) says both halves
|
|
2639
|
+
// of the protocol finer: `before` is the departing child's own rect
|
|
2640
|
+
// rather than this node's box, and the "after" half comes from the
|
|
2641
|
+
// shifted layout diff, which claims an entering child where it lands
|
|
2642
|
+
// and says nothing about the ones that only rode the scroll. Joining
|
|
2643
|
+
// `_reflowed` would undo both — its post-layout claim is this node's
|
|
2644
|
+
// box, the whole band the blit is about to move.
|
|
2645
|
+
if (this._blitLedgerOpen()) {
|
|
2646
|
+
root.invalidate(true, before ?? NO_DAMAGE, 'child-list');
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2451
2649
|
root.invalidate(true, before, 'child-list');
|
|
2452
2650
|
root._reflowed.add(this);
|
|
2453
2651
|
}
|
|
2454
2652
|
|
|
2455
2653
|
removeChild(child) {
|
|
2456
|
-
const index = this.
|
|
2654
|
+
const index = this._indexOfChild(child);
|
|
2457
2655
|
if (index === -1) return;
|
|
2458
2656
|
// told while the child is still wired, so the bridge can compute the
|
|
2459
2657
|
// index the AT will see the removal at
|
|
2460
2658
|
a11yHooks.detach?.(this, child);
|
|
2461
2659
|
// captured while the child is still attached, so it covers the rect the
|
|
2462
|
-
// child is about to stop occupying
|
|
2463
|
-
|
|
2660
|
+
// child is about to stop occupying — the child's own, for a viewport
|
|
2661
|
+
// mid-blit, where this node's box is the whole scrolled band
|
|
2662
|
+
const before = this._blitLedgerOpen()
|
|
2663
|
+
? (child._claimBounds() ?? NO_DAMAGE)
|
|
2664
|
+
: this._childListBefore();
|
|
2464
2665
|
this.children.splice(index, 1);
|
|
2666
|
+
if (outsideYoga(child)) this._nonYogaKids--;
|
|
2465
2667
|
if (this._joinsYoga(child)) {
|
|
2466
2668
|
this.yoga.removeChild(child.yoga);
|
|
2467
2669
|
}
|
|
@@ -3020,7 +3222,37 @@ export class Node {
|
|
|
3020
3222
|
this._clearHitBounds();
|
|
3021
3223
|
if (layoutDiffSink) {
|
|
3022
3224
|
const grow = this._outlineExtent() + DAMAGE_SLOP;
|
|
3023
|
-
|
|
3225
|
+
const shift = layoutDiffShift;
|
|
3226
|
+
const had = old.width > 0 && old.height > 0;
|
|
3227
|
+
if (shift) {
|
|
3228
|
+
// Riding a blit (issue #398): the rect this node *would* have had if
|
|
3229
|
+
// nothing but the scroll had happened. Landing there is the blit's
|
|
3230
|
+
// own translation and claims nothing — claiming it would repaint the
|
|
3231
|
+
// band the blit exists to keep. Landing anywhere else is a real move,
|
|
3232
|
+
// and both ends of it are claimed in post-blit coordinates, which is
|
|
3233
|
+
// where the frame will paint them.
|
|
3234
|
+
const was = {
|
|
3235
|
+
x: old.x + shift.x,
|
|
3236
|
+
y: old.y + shift.y,
|
|
3237
|
+
width: old.width,
|
|
3238
|
+
height: old.height,
|
|
3239
|
+
};
|
|
3240
|
+
if (
|
|
3241
|
+
had &&
|
|
3242
|
+
was.x === x &&
|
|
3243
|
+
was.y === y &&
|
|
3244
|
+
old.width === width &&
|
|
3245
|
+
old.height === height
|
|
3246
|
+
) {
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
if (had) layoutDiffSink(insetRect(was, -grow));
|
|
3250
|
+
if (width > 0 && height > 0) {
|
|
3251
|
+
layoutDiffSink(insetRect(this.abs, -grow));
|
|
3252
|
+
}
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
if (had) {
|
|
3024
3256
|
layoutDiffSink(insetRect(old, -grow));
|
|
3025
3257
|
}
|
|
3026
3258
|
if (width > 0 && height > 0) {
|
|
@@ -3199,10 +3431,48 @@ export class Node {
|
|
|
3199
3431
|
_invalidateLayout(reason) {
|
|
3200
3432
|
const root = this.root;
|
|
3201
3433
|
if (!root) return;
|
|
3202
|
-
|
|
3434
|
+
// Same walk, same frame, same answer — see `_childListBefore`, whose
|
|
3435
|
+
// record this shares so that a reflow and a child-list change on one
|
|
3436
|
+
// node in one frame walk the subtree once between them.
|
|
3437
|
+
root.invalidate(true, this._childListBefore(), reason);
|
|
3203
3438
|
root._reflowed.add(this);
|
|
3204
3439
|
}
|
|
3205
3440
|
|
|
3441
|
+
/**
|
|
3442
|
+
* `paintBounds()` with the one clip a damage claim must respect: a scroll
|
|
3443
|
+
* container above this node that is waiting to blit (issue #398).
|
|
3444
|
+
*
|
|
3445
|
+
* A viewport clips its children, so the part of a claim outside its box is
|
|
3446
|
+
* pixels that cannot appear — and leaving it in costs the blit the frame.
|
|
3447
|
+
* A virtualized list is the shape that makes this concrete: its spacers
|
|
3448
|
+
* are boxes thousands of pixels tall whose visible extent is a sliver or
|
|
3449
|
+
* nothing at all, and their unclipped claims, coalesced into the scroll's
|
|
3450
|
+
* own, leave `_blitKeptDamage` a damage rect many times the viewport to
|
|
3451
|
+
* refuse. Null when the clip left nothing.
|
|
3452
|
+
*
|
|
3453
|
+
* Only while a blit is pending — outside that this is `paintBounds()` and
|
|
3454
|
+
* one property read. Clipping every claim to every clipping ancestor
|
|
3455
|
+
* would be correct too, and is a bigger change than the frame this is
|
|
3456
|
+
* about.
|
|
3457
|
+
*/
|
|
3458
|
+
_claimBounds() {
|
|
3459
|
+
const bounds = this.paintBounds();
|
|
3460
|
+
const sv = this._blitViewport();
|
|
3461
|
+
return sv ? intersectRects(bounds, sv.paintBounds()) : bounds;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
/** The scroll container above this node that is waiting to blit, if there
|
|
3465
|
+
* is one — the viewport whose ledger this node's claims belong in, and
|
|
3466
|
+
* whose box clips them (issue #398). One property read when no blit is
|
|
3467
|
+
* pending, which is every frame that is not a scroll. */
|
|
3468
|
+
_blitViewport() {
|
|
3469
|
+
if (!this.root?._pendingScrolls?.size) return null;
|
|
3470
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
3471
|
+
if (n._blitLedgerOpen()) return n;
|
|
3472
|
+
}
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3206
3476
|
/**
|
|
3207
3477
|
* The region this node can put ink in: its own rect unioned with every
|
|
3208
3478
|
* descendant's. Not the same as `abs` — a child of a node that does not
|
|
@@ -5100,13 +5370,31 @@ export const Scrollable = (Base) =>
|
|
|
5100
5370
|
// real layout — claim it, but clipped to the viewport: ink below the
|
|
5101
5371
|
// fold never reaches the surface, and an unclipped claim would repaint
|
|
5102
5372
|
// whatever unrelated UI sits under this node's off-viewport extent.
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
(this._childOrigin.x !== ox || this._childOrigin.y !== oy);
|
|
5373
|
+
const wasOrigin = this._childOrigin;
|
|
5374
|
+
const shifted = wasOrigin && (wasOrigin.x !== ox || wasOrigin.y !== oy);
|
|
5106
5375
|
this._childOrigin = { x: ox, y: oy };
|
|
5107
5376
|
const outer = layoutDiffSink;
|
|
5377
|
+
const outerShift = layoutDiffShift;
|
|
5378
|
+
const ledger = shifted && this._blitLedgerOpen();
|
|
5108
5379
|
if (outer) {
|
|
5109
|
-
if (
|
|
5380
|
+
if (ledger) {
|
|
5381
|
+
// The blit's own ledger takes this walk (issue #398). The shift
|
|
5382
|
+
// below is what makes the diff worth running under a scroll at
|
|
5383
|
+
// all: without it every child reports the move the blit is about
|
|
5384
|
+
// to make for them, and the claims add up to the viewport. What
|
|
5385
|
+
// is left is the virtualized list's real frame — the rows that
|
|
5386
|
+
// entered, the ones that left, a spacer that resized — and it
|
|
5387
|
+
// goes to the ledger rather than to `outer`, whose claims are
|
|
5388
|
+
// what `layoutMoved` reads as "this frame is not a pure scroll".
|
|
5389
|
+
const vp = insetRect(this.abs, -DAMAGE_SLOP);
|
|
5390
|
+
layoutDiffSink = (rect) => {
|
|
5391
|
+
const clipped = intersectRects(rect, vp);
|
|
5392
|
+
if (clipped && !this._recordBlitClaim(clipped)) {
|
|
5393
|
+
this._pendingBlitFrom = BLIT_POISONED;
|
|
5394
|
+
}
|
|
5395
|
+
};
|
|
5396
|
+
layoutDiffShift = { x: ox - wasOrigin.x, y: oy - wasOrigin.y };
|
|
5397
|
+
} else if (shifted) {
|
|
5110
5398
|
layoutDiffSink = null;
|
|
5111
5399
|
} else {
|
|
5112
5400
|
const vp = insetRect(this.abs, -DAMAGE_SLOP);
|
|
@@ -5124,6 +5412,7 @@ export const Scrollable = (Base) =>
|
|
|
5124
5412
|
}
|
|
5125
5413
|
} finally {
|
|
5126
5414
|
layoutDiffSink = outer;
|
|
5415
|
+
layoutDiffShift = outerShift;
|
|
5127
5416
|
}
|
|
5128
5417
|
}
|
|
5129
5418
|
|
|
@@ -5274,10 +5563,19 @@ export const Scrollable = (Base) =>
|
|
|
5274
5563
|
// has not claimed yet, so damage already overlapping this viewport
|
|
5275
5564
|
// is foreign by construction: poison the frame instead of arming,
|
|
5276
5565
|
// and the full-viewport repaint below stays in force.
|
|
5277
|
-
|
|
5566
|
+
const arming = this._pendingBlitFrom == null;
|
|
5567
|
+
// The ledger this frame's changes inside the viewport are written
|
|
5568
|
+
// to (issue #398). Opened with the blit and read by
|
|
5569
|
+
// _applyScrollBlits, which clears it beside the origin.
|
|
5570
|
+
if (arming) this._blitLedger = [];
|
|
5571
|
+
if (arming && Array.isArray(root._damage)) {
|
|
5278
5572
|
const zone = insetRect(this.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
5279
5573
|
for (const rect of root._damage) {
|
|
5280
|
-
|
|
5574
|
+
// Already coalesced, so these rects are as coarse as the frame
|
|
5575
|
+
// has made them — which the ledger reads conservatively: a blob
|
|
5576
|
+
// that swallowed the viewport says so and poisons, exactly as
|
|
5577
|
+
// this gate used to for every claim it saw.
|
|
5578
|
+
if (rectsOverlap(rect, zone) && !this._recordBlitClaim(rect)) {
|
|
5281
5579
|
this._pendingBlitFrom = BLIT_POISONED;
|
|
5282
5580
|
break;
|
|
5283
5581
|
}
|
|
@@ -9465,6 +9763,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9465
9763
|
return;
|
|
9466
9764
|
}
|
|
9467
9765
|
if (child.isWindow) {
|
|
9766
|
+
const mounting = child.parent == null;
|
|
9468
9767
|
this._spliceChild(child, beforeChild);
|
|
9469
9768
|
child.parent = this;
|
|
9470
9769
|
// Initial children are realized when this window realizes; a child
|
|
@@ -9476,7 +9775,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9476
9775
|
child.realize(this.window);
|
|
9477
9776
|
if (child.window) this._xStack.push(child.window.id);
|
|
9478
9777
|
}
|
|
9479
|
-
if (this.theme || child.props.theme) child._themeChanged();
|
|
9778
|
+
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
9480
9779
|
// React reorders a keyed list with one insertBefore per moved child;
|
|
9481
9780
|
// restacking once at the end of the commit skips the intermediate
|
|
9482
9781
|
// orders, which nobody ever sees.
|
|
@@ -9488,8 +9787,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9488
9787
|
|
|
9489
9788
|
removeChild(child) {
|
|
9490
9789
|
if (child.isWindow) {
|
|
9491
|
-
const index = this.
|
|
9492
|
-
if (index !== -1)
|
|
9790
|
+
const index = this._indexOfChild(child);
|
|
9791
|
+
if (index !== -1) {
|
|
9792
|
+
this.children.splice(index, 1);
|
|
9793
|
+
this._nonYogaKids--;
|
|
9794
|
+
}
|
|
9493
9795
|
const id = child.window?.id;
|
|
9494
9796
|
child.parent = null;
|
|
9495
9797
|
child.destroySubtree();
|
|
@@ -9951,14 +10253,21 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9951
10253
|
// rects coalesce a change inside the viewport is indistinguishable from
|
|
9952
10254
|
// the scroll's own claim. (Unbounded claims need no check: FULL_DAMAGE
|
|
9953
10255
|
// fails the blit's damage gate by itself.)
|
|
10256
|
+
// The region this claim actually covers — a node's paint reach, clipped
|
|
10257
|
+
// to a blitting viewport above it (issue #398), or the bare rect a
|
|
10258
|
+
// caller handed over. Null when the clip left nothing (the node draws
|
|
10259
|
+
// where nothing can be seen, so it owes no pixels), and null on a frame
|
|
10260
|
+
// that is already unbounded, which owes neither a rect nor the subtree
|
|
10261
|
+
// walk that measures one — a blit cannot fire there either.
|
|
10262
|
+
const bounds =
|
|
10263
|
+
damage && damage !== NO_DAMAGE && this._damage !== FULL_DAMAGE
|
|
10264
|
+
? damage._claimBounds
|
|
10265
|
+
? damage._claimBounds()
|
|
10266
|
+
: damage
|
|
10267
|
+
: null;
|
|
9954
10268
|
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;
|
|
10269
|
+
if (pendingScrolls?.size && bounds && this._scrollClaim !== damage) {
|
|
10270
|
+
const rect = bounds;
|
|
9962
10271
|
for (const sv of pendingScrolls) {
|
|
9963
10272
|
// An element blitting a region of its own drawing (issue #303) is
|
|
9964
10273
|
// waiting on that region, not on the whole node it lives in — and
|
|
@@ -9979,6 +10288,15 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9979
10288
|
? contents.rect
|
|
9980
10289
|
: sv.abs && insetRect(sv.abs, -(DAMAGE_SLOP * 2 + 1));
|
|
9981
10290
|
if (!waiting || rectsOverlap(rect, waiting)) {
|
|
10291
|
+
// …unless this viewport is keeping a ledger of what changed
|
|
10292
|
+
// inside it (issue #398): the region goes in the ledger and
|
|
10293
|
+
// `_applyScrollBlits` repaints it after the blit, which is the
|
|
10294
|
+
// same pixels on screen for a fraction of the drawing. The
|
|
10295
|
+
// ledger says no when the frame stops paying, and then this
|
|
10296
|
+
// falls through to the poison exactly as before.
|
|
10297
|
+
if (sv._blitLedgerOpen() && sv._recordBlitClaim(rect)) {
|
|
10298
|
+
continue;
|
|
10299
|
+
}
|
|
9982
10300
|
// Poison rather than disarm (react-x11#295): a null here would
|
|
9983
10301
|
// let a second scrollTo in the same frame re-arm from a
|
|
9984
10302
|
// mid-frame origin, and the blit would then move pixels that
|
|
@@ -9992,16 +10310,18 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
9992
10310
|
}
|
|
9993
10311
|
if (layoutChanged && !damage) this._damage = FULL_DAMAGE;
|
|
9994
10312
|
else if (!layoutChanged && !damage) this._damage = FULL_DAMAGE;
|
|
9995
|
-
else if (
|
|
10313
|
+
else if (!bounds) {
|
|
10314
|
+
// A layout change that names no region: either NO_DAMAGE, from a
|
|
10315
|
+
// caller with a finer claim already in flight, or a node whose reach
|
|
10316
|
+
// a clipping ancestor left nothing of (issue #398). Unlike `!damage`
|
|
10317
|
+
// neither is "something, somewhere", so neither costs a full repaint.
|
|
10318
|
+
} else if (this._damage !== FULL_DAMAGE) {
|
|
9996
10319
|
// a node, or a bare rect for a caller that has a region rather than a
|
|
9997
10320
|
// node — a subtree that is about to be removed, say. Claims accumulate
|
|
9998
10321
|
// as a list of rects rather than one box around them all, so two changes
|
|
9999
10322
|
// at opposite corners of the window no longer repaint everything
|
|
10000
10323
|
// between them.
|
|
10001
|
-
this._damage = addDamageRect(
|
|
10002
|
-
this._damage,
|
|
10003
|
-
damage.paintBounds ? damage.paintBounds() : damage,
|
|
10004
|
-
);
|
|
10324
|
+
this._damage = addDamageRect(this._damage, bounds);
|
|
10005
10325
|
}
|
|
10006
10326
|
this.needsPaint = true;
|
|
10007
10327
|
// Recorded before the `_scheduled` gate, not inside it: the debt is
|
|
@@ -10056,6 +10376,10 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10056
10376
|
// not the flag's post-pass value
|
|
10057
10377
|
const layoutRan = this.needsLayout;
|
|
10058
10378
|
if (this.needsLayout) {
|
|
10379
|
+
// From here on a claim names where its content *landed*, not where it
|
|
10380
|
+
// sat before the scroll — which is what decides whether a blit
|
|
10381
|
+
// ledger's rect moves with the shift (issue #398).
|
|
10382
|
+
this._laidOut = true;
|
|
10059
10383
|
this._resolveSizeQueries(width, height);
|
|
10060
10384
|
this._applyContentFloors(width);
|
|
10061
10385
|
this.yoga.setWidth(width);
|
|
@@ -10091,14 +10415,25 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10091
10415
|
// replaced it. Claimed after layout because an inserted child has no
|
|
10092
10416
|
// rect before it.
|
|
10093
10417
|
for (const node of this._reflowed) {
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10418
|
+
// …and the pre-mutation walk this frame reused goes with it
|
|
10419
|
+
node._reflowBefore = null;
|
|
10420
|
+
if (node.destroyed || this._damage === FULL_DAMAGE) continue;
|
|
10421
|
+
// clipped to a blitting viewport above it, like every other claim
|
|
10422
|
+
// this frame, and written to that viewport's ledger too (issue
|
|
10423
|
+
// #398): the claim would otherwise coalesce into the scroll's own
|
|
10424
|
+
// and be dropped with it, leaving the band the blit kept holding
|
|
10425
|
+
// this node's pixels from before the reflow.
|
|
10426
|
+
const after = node._claimBounds();
|
|
10427
|
+
if (!after) continue;
|
|
10428
|
+
const sv = node._blitViewport();
|
|
10429
|
+
if (sv && !sv._recordBlitClaim(after)) {
|
|
10430
|
+
sv._pendingBlitFrom = BLIT_POISONED;
|
|
10431
|
+
}
|
|
10432
|
+
this._damage = addDamageRect(this._damage, after);
|
|
10099
10433
|
}
|
|
10100
10434
|
this._reflowed.clear();
|
|
10101
10435
|
} else if (this._reflowed.size) {
|
|
10436
|
+
for (const node of this._reflowed) node._reflowBefore = null;
|
|
10102
10437
|
this._reflowed.clear();
|
|
10103
10438
|
}
|
|
10104
10439
|
// any node this pass laid out may be what an open popup is anchored to
|
|
@@ -10107,6 +10442,9 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10107
10442
|
// a frame that turns out to be a pure scroll blits the surviving band
|
|
10108
10443
|
// and narrows its claim to the exposed strip
|
|
10109
10444
|
this._applyScrollBlits(width, height, layoutMoved);
|
|
10445
|
+
// …and the next commit's claims name the arrangement this frame leaves
|
|
10446
|
+
// behind again, from before whatever scroll comes with them
|
|
10447
|
+
this._laidOut = false;
|
|
10110
10448
|
if (!this.needsPaint) return;
|
|
10111
10449
|
this.needsPaint = false;
|
|
10112
10450
|
const damage = this._takeDamage(width, height);
|
|
@@ -10185,9 +10523,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10185
10523
|
pending.clear();
|
|
10186
10524
|
const from = nodes[0]._pendingBlitFrom;
|
|
10187
10525
|
const contents = nodes[0]._pendingBlitContents;
|
|
10526
|
+
const ledger = nodes[0]._blitLedger;
|
|
10188
10527
|
for (const n of nodes) {
|
|
10189
10528
|
n._pendingBlitFrom = null;
|
|
10190
10529
|
n._pendingBlitContents = null;
|
|
10530
|
+
n._blitLedger = null;
|
|
10191
10531
|
}
|
|
10192
10532
|
// two viewports scrolling in one frame is rare enough that sorting out
|
|
10193
10533
|
// whether their regions interact is not worth it
|
|
@@ -10267,11 +10607,39 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10267
10607
|
}
|
|
10268
10608
|
const keep = this._blitKeptDamage(vp);
|
|
10269
10609
|
if (!keep) return;
|
|
10610
|
+
// What changed inside the viewport while the blit was armed, in the
|
|
10611
|
+
// coordinates the frame is about to paint in (issue #398). A claim made
|
|
10612
|
+
// during the commit named where the content sat before the shift, and
|
|
10613
|
+
// the blit is about to move those pixels by the frame's delta, so it
|
|
10614
|
+
// moves with them; a claim from the layout diff already landed there.
|
|
10615
|
+
//
|
|
10616
|
+
// Repainting the result is what makes the blit honest about them: the
|
|
10617
|
+
// blit translates the previous frame's rendering, which is correct
|
|
10618
|
+
// everywhere the content did not change, and these are the places it
|
|
10619
|
+
// did. That is finer than the strip-only rule issue #398 asks for and
|
|
10620
|
+
// no more complicated, so a mid-viewport change — a row upgrading from
|
|
10621
|
+
// skeleton to content while the list scrolls — rides the fast path too
|
|
10622
|
+
// instead of falling back to the whole viewport.
|
|
10623
|
+
const repairs = [];
|
|
10624
|
+
let repairArea = 0;
|
|
10625
|
+
for (const claim of ledger ?? []) {
|
|
10626
|
+
const moved = claim.pre
|
|
10627
|
+
? {
|
|
10628
|
+
x: claim.x - dx,
|
|
10629
|
+
y: claim.y - dy,
|
|
10630
|
+
width: claim.width,
|
|
10631
|
+
height: claim.height,
|
|
10632
|
+
}
|
|
10633
|
+
: claim;
|
|
10634
|
+
const inside = intersectRects(moved, vp);
|
|
10635
|
+
if (!inside) continue;
|
|
10636
|
+
repairs.push(inside);
|
|
10637
|
+
repairArea += inside.width * inside.height;
|
|
10638
|
+
}
|
|
10639
|
+
// past this the blit plus a scatter of repaints is no longer cheaper
|
|
10640
|
+
// than the one full-viewport pass it replaced
|
|
10641
|
+
if (repairArea > area * BLIT_MAX_CLAIM_AREA) return;
|
|
10270
10642
|
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
10643
|
let rects = keep;
|
|
10276
10644
|
// the strip the shift exposed, full breadth — it also covers the corner
|
|
10277
10645
|
// gutter beside the bars, whose old pixels the blit did not overwrite
|
|
@@ -10326,6 +10694,24 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10326
10694
|
}
|
|
10327
10695
|
const crossBar = node._scrollbar(axis === 'y' ? 'x' : 'y');
|
|
10328
10696
|
if (crossBar) rects = addDamageRect(rects, scrollbarTrackRect(crossBar));
|
|
10697
|
+
for (const repair of repairs) rects = addDamageRect(rects, repair);
|
|
10698
|
+
// The last gate, and the only one that has to wait until the rects are
|
|
10699
|
+
// assembled: the frame carries at most MAX_DAMAGE_RECTS of them, so a
|
|
10700
|
+
// repair that does not sit beside the strip is merged with whatever is
|
|
10701
|
+
// nearest — the scrollbar column, most often — and the box of that
|
|
10702
|
+
// merge can reach back across the viewport. When it does, the blit is
|
|
10703
|
+
// buying a shift and paying for the viewport anyway, so let the plain
|
|
10704
|
+
// repaint scrollTo already claimed have the frame.
|
|
10705
|
+
let painted = 0;
|
|
10706
|
+
for (const rect of rects) {
|
|
10707
|
+
const inside = intersectRects(rect, vp);
|
|
10708
|
+
if (inside) painted += inside.width * inside.height;
|
|
10709
|
+
}
|
|
10710
|
+
if (painted > area * SCROLL_BLIT_MAX_REPAINT) return;
|
|
10711
|
+
// scroll offsets grow down/right; the pixels move the other way
|
|
10712
|
+
// (0 - x rather than -x: negating +0 yields -0, which survives into
|
|
10713
|
+
// request buffers and test comparisons)
|
|
10714
|
+
if (!wnd.scrollRegion({ ...vp }, 0 - dx, 0 - dy)) return;
|
|
10329
10715
|
this._damage = rects;
|
|
10330
10716
|
}
|
|
10331
10717
|
|