react-x11 2.21.2 → 2.22.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 +2 -2
- package/src/gloverlay.js +8 -48
- package/src/node.d.ts +11 -1
- package/src/nodes/damage.js +52 -29
- package/src/nodes/invalidate.js +13 -1
- package/src/nodes/layout.js +24 -49
- package/src/nodes/moveblit.js +610 -0
- package/src/nodes/node.js +14 -15
- package/src/nodes/paint.js +103 -8
- package/src/nodes/position.js +1 -0
- package/src/nodes/rects.js +101 -6
- package/src/nodes/roundclip.js +214 -0
- package/src/nodes/scrollable.js +9 -4
- package/src/nodes/scrollbars.js +9 -3
- package/src/nodes/scrollblit.js +285 -92
- package/src/nodes/window/flush.js +12 -0
- package/src/nodes/window/window.js +8 -0
package/src/nodes/scrollblit.js
CHANGED
|
@@ -8,20 +8,28 @@
|
|
|
8
8
|
// viewport, so the fast path can cost correctness nothing.
|
|
9
9
|
|
|
10
10
|
import { EMPTY_STYLE, resolveBorderWidths } from '../styles.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DAMAGE_SLOP,
|
|
13
|
+
MAX_DAMAGE_RECTS,
|
|
14
|
+
addDamageRect,
|
|
15
|
+
addDamageRects,
|
|
16
|
+
} from './damage.js';
|
|
12
17
|
import {
|
|
13
18
|
rectContains,
|
|
14
19
|
isIntegerRect,
|
|
15
20
|
insetRect,
|
|
16
21
|
innerPixels,
|
|
22
|
+
outerPixels,
|
|
17
23
|
intersectRects,
|
|
18
24
|
unionRect,
|
|
19
25
|
rectArea,
|
|
20
|
-
|
|
26
|
+
cornerSquares,
|
|
21
27
|
rectsOverlap,
|
|
28
|
+
outside,
|
|
22
29
|
} from './rects.js';
|
|
23
30
|
import { isPaintedColor } from './boxpaint.js';
|
|
24
31
|
import { scrollbarTrackRect } from './scrollbars.js';
|
|
32
|
+
import { DEV } from './util.js';
|
|
25
33
|
import { debugPaint } from './window/debugpaint.js';
|
|
26
34
|
|
|
27
35
|
// --- scroll blitting (issue #138) ---------------------------------------
|
|
@@ -58,6 +66,15 @@ const SCROLL_BLIT_MIN_KEEP = 0.5;
|
|
|
58
66
|
// Past this the blit is buying a shift and paying for the viewport anyway.
|
|
59
67
|
const SCROLL_BLIT_MAX_REPAINT = 0.75;
|
|
60
68
|
|
|
69
|
+
// The rects an element blit may leave its frame to paint: the strips the
|
|
70
|
+
// shift exposed, on top of a frame's usual allowance. A diagonal pan of a
|
|
71
|
+
// rounded pane with furniture in two corners owes two strips, two pieces of
|
|
72
|
+
// furniture and four corner repairs; held to four rects they merged into
|
|
73
|
+
// boxes a third of the pane, and a graph panned diagonally at 41fps on
|
|
74
|
+
// XQuartz and 52 on macOS where two more rects make it 60 and 78 (issue
|
|
75
|
+
// #691). Eight bought nothing past six.
|
|
76
|
+
const CONTENTS_BLIT_MAX_RECTS = MAX_DAMAGE_RECTS + 2;
|
|
77
|
+
|
|
61
78
|
// A scroll that must not blit this frame: content inside the viewport
|
|
62
79
|
// already changed (see the arming check in scrollTo, and the claim-time
|
|
63
80
|
// cancel in WindowNode.invalidate — react-x11#295). Truthy on purpose, so
|
|
@@ -201,6 +218,98 @@ function unionIsBox(a, b) {
|
|
|
201
218
|
);
|
|
202
219
|
}
|
|
203
220
|
|
|
221
|
+
/** Is `rect` inside one of `pins`, the furniture an element blit pinned —
|
|
222
|
+
* repaired whole after the copy, whatever changed in it (issue #682)? */
|
|
223
|
+
function insidePinned(rect, pins) {
|
|
224
|
+
if (!pins) return false;
|
|
225
|
+
for (const pin of pins) {
|
|
226
|
+
if (rectContains(pin, rect)) return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Does every corner square of a rounded `box` that reaches into `vp` lie
|
|
232
|
+
* inside one of `pins` — repaired after the copy rather than dragged along
|
|
233
|
+
* by it (issue #691)? True when none reaches in. */
|
|
234
|
+
function cornersPinned(box, radius, vp, pins) {
|
|
235
|
+
for (const square of cornerSquares(box, radius)) {
|
|
236
|
+
if (!rectsOverlap(square, vp)) continue;
|
|
237
|
+
const inside = intersectRects(outerPixels(square), vp);
|
|
238
|
+
if (inside && !insidePinned(inside, pins)) return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** One of `scrollContents`' lists as an array, read once — an iterable may
|
|
244
|
+
* be a generator — null when it was left out, and undefined when it is not
|
|
245
|
+
* a list at all. */
|
|
246
|
+
function asList(value) {
|
|
247
|
+
if (value == null) return null;
|
|
248
|
+
return typeof value[Symbol.iterator] === 'function' ? [...value] : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The first entry of `list` that `ok` refuses, the value itself when it is
|
|
252
|
+
* not a list, or undefined when there is nothing wrong with it. */
|
|
253
|
+
function strayIn(value, list, ok) {
|
|
254
|
+
if (list === undefined) return value;
|
|
255
|
+
return list?.find((entry) => entry != null && !ok(entry));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const isNode = (value) => typeof value.paintBounds === 'function';
|
|
259
|
+
|
|
260
|
+
const isRect = (value) =>
|
|
261
|
+
Number.isFinite(value.x) &&
|
|
262
|
+
Number.isFinite(value.y) &&
|
|
263
|
+
Number.isFinite(value.width) &&
|
|
264
|
+
Number.isFinite(value.height) &&
|
|
265
|
+
value.width >= 0 &&
|
|
266
|
+
value.height >= 0;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The furniture one `scrollContents` call pins: the whole pixels each rect
|
|
270
|
+
* touches inside `rect`, rounded outward because repairing a pixel more
|
|
271
|
+
* costs nothing and a pixel less is a sliver of stale furniture the pan
|
|
272
|
+
* drags along. A rect beside `rect` has no pixels the copy moves, and is
|
|
273
|
+
* left out, as is an empty one. Null when nothing is left.
|
|
274
|
+
*/
|
|
275
|
+
function pinnedRects(rect, list) {
|
|
276
|
+
let out = null;
|
|
277
|
+
for (const pin of list ?? []) {
|
|
278
|
+
if (pin == null || pin.width === 0 || pin.height === 0) continue;
|
|
279
|
+
const inside = intersectRects(outerPixels(pin), rect);
|
|
280
|
+
if (inside) (out ??= []).push(inside);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A value from a caller, short enough for a warning. An object that will
|
|
286
|
+
* not serialize — a node's tree is a cycle — is named by its keys. */
|
|
287
|
+
function describe(value) {
|
|
288
|
+
if (typeof value?.paintBounds === 'function') {
|
|
289
|
+
return `the node <${value.kind}>`;
|
|
290
|
+
}
|
|
291
|
+
// the likeliest mistake of all, from a React component
|
|
292
|
+
if (typeof value?.current?.paintBounds === 'function') {
|
|
293
|
+
return `a ref to <${value.current.kind}> rather than the node, its .current`;
|
|
294
|
+
}
|
|
295
|
+
if (value === null || typeof value !== 'object') return String(value);
|
|
296
|
+
try {
|
|
297
|
+
const json = JSON.stringify(value);
|
|
298
|
+
return json.length > 120 ? `${json.slice(0, 117)}...` : json;
|
|
299
|
+
} catch {
|
|
300
|
+
return `an object with ${Object.keys(value).join(', ') || 'no keys'}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const warned = new Set();
|
|
305
|
+
/** A development warning, once per kind of mistake rather than per value:
|
|
306
|
+
* the value is new on every frame of a pan. */
|
|
307
|
+
function warnOnce(key, message) {
|
|
308
|
+
if (!DEV || warned.has(key)) return;
|
|
309
|
+
warned.add(key);
|
|
310
|
+
console.warn(message);
|
|
311
|
+
}
|
|
312
|
+
|
|
204
313
|
/** Node's half of the scroll blit, installed onto `Node.prototype` by node.js. */
|
|
205
314
|
export class NodeScrollBlit {
|
|
206
315
|
/**
|
|
@@ -215,8 +324,10 @@ export class NodeScrollBlit {
|
|
|
215
324
|
* away, so the paths that make them take a finer route while this is true.
|
|
216
325
|
*
|
|
217
326
|
* `scrollContents` is out: an element blit already tests foreign claims
|
|
218
|
-
* against the rect it handed over (issue #309),
|
|
219
|
-
* viewport whose children *are* the scrolled content
|
|
327
|
+
* against the rect it handed over (issue #309), its region is not a
|
|
328
|
+
* viewport whose children *are* the scrolled content, and what changes
|
|
329
|
+
* inside it on a pan frame is furniture the element names up front as
|
|
330
|
+
* pinned (issue #682, `_pinnedHolds`).
|
|
220
331
|
*/
|
|
221
332
|
_blitLedgerOpen() {
|
|
222
333
|
const from = this._pendingBlitFrom;
|
|
@@ -277,6 +388,18 @@ export class NodeScrollBlit {
|
|
|
277
388
|
return true;
|
|
278
389
|
}
|
|
279
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Is `rect` inside furniture this node's pending element blit pinned
|
|
393
|
+
* (issue #682)? A claim there is the element's own and costs the blit
|
|
394
|
+
* nothing: the frame repaints the pinned rect after the copy, and the
|
|
395
|
+
* rect the copy dragged its old pixels to, so the change is covered
|
|
396
|
+
* whichever side of the shift the claim was named on — where the
|
|
397
|
+
* furniture stays, or where the content under it was before it moved.
|
|
398
|
+
*/
|
|
399
|
+
_pinnedHolds(rect) {
|
|
400
|
+
return insidePinned(rect, this._pendingBlitContents?.pinned);
|
|
401
|
+
}
|
|
402
|
+
|
|
280
403
|
/**
|
|
281
404
|
* How far the blit this viewport has pending moves the pixels it keeps:
|
|
282
405
|
* the delta `_applyScrollBlits` hands `scrollRegion`, from the offsets
|
|
@@ -330,12 +453,24 @@ export class NodeScrollBlit {
|
|
|
330
453
|
* moved something — and each of them falls back to repainting the rect,
|
|
331
454
|
* which is the behaviour without this call at all.
|
|
332
455
|
*
|
|
333
|
-
* All of those are about `rect`, not about this node (issue #309)
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
456
|
+
* All of those are about `rect`, not about this node (issue #309): a
|
|
457
|
+
* claim beside the rect leaves the frame a blit, so an element can carve
|
|
458
|
+
* a HUD strip along the pane's edge out of the region it shifts and claim
|
|
459
|
+
* the strip as ordinary damage.
|
|
460
|
+
*
|
|
461
|
+
* Furniture in a corner does better as `pinned` (issue #682): rects inside
|
|
462
|
+
* `rect`, in the same coordinates, that stay put while everything around
|
|
463
|
+
* them moves — a minimap, zoom controls, drawn by the element over its
|
|
464
|
+
* own drawing or laid out over it as nodes. Carved out instead, a corner
|
|
465
|
+
* takes the rows beside it too, since what shifts is one rectangle, and
|
|
466
|
+
* two bottom corners carve a band the pane's full width, repainted every
|
|
467
|
+
* frame. Pinned, the copy drags a stale image of each piece of furniture
|
|
468
|
+
* by (dx, dy), and the frame repaints the pinned rect and that image —
|
|
469
|
+
* two small rects, whether the furniture changed or not, so a minimap's
|
|
470
|
+
* viewport box can follow the pan without claiming a thing. A claim
|
|
471
|
+
* inside a pinned rect is the element's own rather than a reason to
|
|
472
|
+
* decline, and so is a node that lies inside one: its pixels are
|
|
473
|
+
* repaired with the rest of the rect.
|
|
339
474
|
*
|
|
340
475
|
* `riders` are the other way round: nodes that are not this one whose
|
|
341
476
|
* pixels in `rect` are part of what moves — a layer of widgets an element
|
|
@@ -352,7 +487,7 @@ export class NodeScrollBlit {
|
|
|
352
487
|
* arrives as `paintDamage()`, because most of the gates cannot be decided
|
|
353
488
|
* until the frame closes; a `false` here is only the ones that can.
|
|
354
489
|
*/
|
|
355
|
-
scrollContents(rect, dx, dy, riders) {
|
|
490
|
+
scrollContents(rect, dx, dy, riders, pinned) {
|
|
356
491
|
const root = this.root;
|
|
357
492
|
if (
|
|
358
493
|
!root ||
|
|
@@ -367,6 +502,33 @@ export class NodeScrollBlit {
|
|
|
367
502
|
// rounding a gesture to whole pixels lands here on most events.
|
|
368
503
|
if (!dx && !dy) return true;
|
|
369
504
|
const pending = this._pendingBlitContents;
|
|
505
|
+
// Two lists, one of nodes and one of rects, and the cost of mixing them
|
|
506
|
+
// up is not a slow frame but a wrong one: furniture handed over as a
|
|
507
|
+
// rider is dragged along by the pan and repaired by nobody. So each has
|
|
508
|
+
// to be what it says, or the frame repaints.
|
|
509
|
+
const riderList = asList(riders);
|
|
510
|
+
const pinList = asList(pinned);
|
|
511
|
+
const strayRider = strayIn(riders, riderList, isNode);
|
|
512
|
+
const strayPin = strayIn(pinned, pinList, isRect);
|
|
513
|
+
if (strayRider !== undefined) {
|
|
514
|
+
warnOnce(
|
|
515
|
+
'riders',
|
|
516
|
+
'react-x11: scrollContents() takes the nodes a pan carries as a ' +
|
|
517
|
+
`list of riders, and got ${describe(strayRider)}. Furniture that ` +
|
|
518
|
+
'stays put while the pan moves around it is pinned — a list of ' +
|
|
519
|
+
'rects, the argument after riders. This frame repaints the region ' +
|
|
520
|
+
'instead of blitting it.',
|
|
521
|
+
);
|
|
522
|
+
} else if (strayPin !== undefined) {
|
|
523
|
+
warnOnce(
|
|
524
|
+
'pinned',
|
|
525
|
+
'react-x11: scrollContents() takes the furniture it pins as a list ' +
|
|
526
|
+
'of rects — { x, y, width, height }, in window coordinates like ' +
|
|
527
|
+
`the region — and got ${describe(strayPin)}. This frame repaints ` +
|
|
528
|
+
'the region instead of blitting it, since a blit would drag the ' +
|
|
529
|
+
'furniture along with the pan.',
|
|
530
|
+
);
|
|
531
|
+
}
|
|
370
532
|
// The rect has to be the same one all frame: two different regions of
|
|
371
533
|
// one node shifting by different deltas is not one CopyArea, and the
|
|
372
534
|
// second claim would coalesce into the first past the point either can
|
|
@@ -374,12 +536,14 @@ export class NodeScrollBlit {
|
|
|
374
536
|
// offsets this frame — two shifts of the same pixels, and a frame can
|
|
375
537
|
// only have one.
|
|
376
538
|
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
pending
|
|
381
|
-
|
|
382
|
-
|
|
539
|
+
strayRider !== undefined ||
|
|
540
|
+
strayPin !== undefined ||
|
|
541
|
+
(this._pendingBlitFrom != null &&
|
|
542
|
+
(!pending ||
|
|
543
|
+
pending.rect.x !== rect.x ||
|
|
544
|
+
pending.rect.y !== rect.y ||
|
|
545
|
+
pending.rect.width !== rect.width ||
|
|
546
|
+
pending.rect.height !== rect.height))
|
|
383
547
|
) {
|
|
384
548
|
this._pendingBlitFrom = BLIT_POISONED;
|
|
385
549
|
} else if (this._pendingBlitFrom == null && Array.isArray(root._damage)) {
|
|
@@ -394,11 +558,13 @@ export class NodeScrollBlit {
|
|
|
394
558
|
// claim it could swallow has to overlap it, and every claim carries
|
|
395
559
|
// its own slop already — `paintBounds` inflates a node's region by
|
|
396
560
|
// `DAMAGE_SLOP` on every side, so ink that bleeds into `rect` is
|
|
397
|
-
// claimed overlapping it. Furniture *beside* the rect — a
|
|
398
|
-
//
|
|
399
|
-
//
|
|
561
|
+
// claimed overlapping it. Furniture *beside* the rect — a strip the
|
|
562
|
+
// element carved out and repaints itself — is not a change to the
|
|
563
|
+
// pixels about to move, and nor is furniture this call pins inside
|
|
564
|
+
// it (issue #682): the frame repaints that whole, blit or no blit.
|
|
565
|
+
const pins = pinnedRects(rect, pinList);
|
|
400
566
|
for (const claimed of root._damage) {
|
|
401
|
-
if (rectsOverlap(claimed, rect)) {
|
|
567
|
+
if (rectsOverlap(claimed, rect) && !insidePinned(claimed, pins)) {
|
|
402
568
|
this._pendingBlitFrom = BLIT_POISONED;
|
|
403
569
|
break;
|
|
404
570
|
}
|
|
@@ -415,13 +581,22 @@ export class NodeScrollBlit {
|
|
|
415
581
|
// coalescing scrollTo gets from recording an origin
|
|
416
582
|
state.dx += dx;
|
|
417
583
|
state.dy += dy;
|
|
418
|
-
if (
|
|
419
|
-
for (const rider of
|
|
584
|
+
if (strayRider === undefined) {
|
|
585
|
+
for (const rider of riderList ?? []) {
|
|
420
586
|
if (!rider || rider === this || rider.destroyed) continue;
|
|
421
587
|
(state.riders ??= new Set()).add(rider);
|
|
422
588
|
rider._ridesBlitOf = this;
|
|
423
589
|
}
|
|
424
590
|
}
|
|
591
|
+
// …and the furniture of every call this frame: a pan that moved its
|
|
592
|
+
// minimap names both places, and the frame repairs both. Kept in whole
|
|
593
|
+
// pixels inside the rect, once each.
|
|
594
|
+
if (strayPin === undefined) {
|
|
595
|
+
for (const pin of pinnedRects(state.rect, pinList) ?? []) {
|
|
596
|
+
const pins = (state.pinned ??= []);
|
|
597
|
+
if (!pins.some((held) => rectContains(held, pin))) pins.push(pin);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
425
600
|
this._pendingBlitFrom ??= BLIT_CONTENTS;
|
|
426
601
|
(root._pendingScrolls ??= new Set()).add(this);
|
|
427
602
|
// ...and the claim about to be recorded is the shift itself, not a
|
|
@@ -842,10 +1017,15 @@ export class WindowScrollBlit {
|
|
|
842
1017
|
* the L of exposed strips overlaps the scrollbar rects and the merges
|
|
843
1018
|
* balloon back towards the whole viewport. With no bars the L is two
|
|
844
1019
|
* disjoint rects and stays two.
|
|
1020
|
+
*
|
|
1021
|
+
* Furniture the element pinned (issue #682) is the one thing inside the
|
|
1022
|
+
* region that does not move: the copy drags a stale image of it along,
|
|
1023
|
+
* so each pinned rect is repaired where it is and where the image landed,
|
|
1024
|
+
* and a node inside one is furniture too rather than a reason to refuse.
|
|
845
1025
|
*/
|
|
846
1026
|
_applyContentsBlit(
|
|
847
1027
|
node,
|
|
848
|
-
{ rect: vp, dx, dy, riders },
|
|
1028
|
+
{ rect: vp, dx, dy, riders, pinned },
|
|
849
1029
|
width,
|
|
850
1030
|
height,
|
|
851
1031
|
target,
|
|
@@ -888,66 +1068,39 @@ export class WindowScrollBlit {
|
|
|
888
1068
|
// A scroll container's children *are* the scrolled content, which is
|
|
889
1069
|
// why _scrollBlitSafe skips that subtree. An element's are not: it
|
|
890
1070
|
// draws the region in paintContent and its children are laid out on
|
|
891
|
-
// top, so one reaching in would have its pixels dragged along
|
|
1071
|
+
// top, so one reaching in would have its pixels dragged along — unless
|
|
1072
|
+
// it lies inside furniture the element pinned, which is repaired after
|
|
1073
|
+
// the copy whoever drew it.
|
|
892
1074
|
for (const child of node.children) {
|
|
893
1075
|
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
894
1076
|
if (child.style?.display === 'none') continue;
|
|
895
|
-
|
|
1077
|
+
const reach = child._subtreeBounds();
|
|
1078
|
+
if (rectsOverlap(reach, vp) && !insidePinned(reach, pinned)) return;
|
|
896
1079
|
}
|
|
897
1080
|
const keep = this._blitKeptDamage(vp, true, target.damage);
|
|
898
1081
|
if (!keep) return;
|
|
899
|
-
// An ancestor's rounded corners reach into the
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
: vp;
|
|
914
|
-
if (shifted.height <= 0 || Math.abs(dy) >= shifted.height) return;
|
|
915
|
-
if (
|
|
916
|
-
(shifted.width - Math.abs(dx)) * (shifted.height - Math.abs(dy)) <
|
|
917
|
-
area * SCROLL_BLIT_MIN_KEEP
|
|
918
|
-
) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
if (!this._scrollBlitSafe(node, shifted, target.top, riders)) return;
|
|
922
|
-
// the element's deltas are already how far the pixels moved, the sense
|
|
923
|
-
// scrollRegion takes (0 + x rather than x: a caller's -0 would survive
|
|
924
|
-
// into request buffers and test comparisons)
|
|
925
|
-
if (!target.blit({ ...shifted }, 0 + dx, 0 + dy)) return;
|
|
926
|
-
let rects = keep;
|
|
927
|
-
if (bands.top) {
|
|
928
|
-
rects = addDamageRect(rects, {
|
|
929
|
-
x: vp.x,
|
|
930
|
-
y: vp.y,
|
|
931
|
-
width: vp.width,
|
|
932
|
-
height: bands.top,
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
if (bands.bottom) {
|
|
936
|
-
rects = addDamageRect(rects, {
|
|
937
|
-
x: vp.x,
|
|
938
|
-
y: shifted.y + shifted.height,
|
|
939
|
-
width: vp.width,
|
|
940
|
-
height: bands.bottom,
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
vp = shifted;
|
|
1082
|
+
// An ancestor's rounded corners reach into the region (a graph pane
|
|
1083
|
+
// inside a rounded card is the common shape). Their arcs do not
|
|
1084
|
+
// translate, but they live in four small squares and nowhere else, so
|
|
1085
|
+
// they are pinned like the element's own furniture (issue #691): the
|
|
1086
|
+
// whole region shifts, and each square is repaired where it is and where
|
|
1087
|
+
// the copy dragged its image. Carved out as the rows they reach into,
|
|
1088
|
+
// they took the region's full width at the top and at the bottom —
|
|
1089
|
+
// every card and edge in those rows repainted through the rounded clip
|
|
1090
|
+
// on every frame — and next to furniture in two corners, one rect more
|
|
1091
|
+
// than the damage cap has room for.
|
|
1092
|
+
const corners = this._cornerPins(node, vp, target.top);
|
|
1093
|
+
const pins =
|
|
1094
|
+
corners && pinned ? [...pinned, ...corners] : (corners ?? pinned);
|
|
1095
|
+
if (!this._scrollBlitSafe(node, vp, target.top, riders, pins)) return;
|
|
944
1096
|
// The strips the shift exposed, on the sides the pixels came from. The
|
|
945
1097
|
// horizontal one takes the full width and the vertical one takes what
|
|
946
1098
|
// is left, so a diagonal shift claims two rects that do not overlap —
|
|
947
1099
|
// overlapping claims merge into their box, and the box of an L is the
|
|
948
1100
|
// whole region again.
|
|
1101
|
+
const strips = [];
|
|
949
1102
|
if (dy !== 0) {
|
|
950
|
-
|
|
1103
|
+
strips.push({
|
|
951
1104
|
x: vp.x,
|
|
952
1105
|
y: dy > 0 ? vp.y : vp.y + vp.height + dy,
|
|
953
1106
|
width: vp.width,
|
|
@@ -955,38 +1108,73 @@ export class WindowScrollBlit {
|
|
|
955
1108
|
});
|
|
956
1109
|
}
|
|
957
1110
|
if (dx !== 0) {
|
|
958
|
-
|
|
1111
|
+
strips.push({
|
|
959
1112
|
x: dx > 0 ? vp.x : vp.x + vp.width + dx,
|
|
960
1113
|
y: dy > 0 ? vp.y + dy : vp.y,
|
|
961
1114
|
width: Math.abs(dx),
|
|
962
1115
|
height: vp.height - Math.abs(dy),
|
|
963
1116
|
});
|
|
964
1117
|
}
|
|
1118
|
+
const pieces = [...strips];
|
|
1119
|
+
if (pins) {
|
|
1120
|
+
// Each piece of furniture where it stays, and where the copy put the
|
|
1121
|
+
// image of it it dragged along — one box, since a pan step moves it a
|
|
1122
|
+
// few pixels, and inside the region, since the copy wrote nothing
|
|
1123
|
+
// outside. Less the strips, which repaint anyway: furniture flush
|
|
1124
|
+
// against the side the pixels came from would otherwise merge with
|
|
1125
|
+
// the strip down that side into the box of the two, most of the
|
|
1126
|
+
// region's height.
|
|
1127
|
+
for (const pin of pins) {
|
|
1128
|
+
const image = { ...pin, x: pin.x + dx, y: pin.y + dy };
|
|
1129
|
+
const repair = intersectRects(unionRect(pin, image), vp);
|
|
1130
|
+
if (!repair) continue;
|
|
1131
|
+
let cut = [repair];
|
|
1132
|
+
for (const strip of strips) {
|
|
1133
|
+
cut = cut.flatMap((piece) => outside(piece, strip));
|
|
1134
|
+
}
|
|
1135
|
+
pieces.push(...cut);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// in one call, so the small pieces find their neighbours before the
|
|
1139
|
+
// cap makes anything merge (`addDamageRects`)
|
|
1140
|
+
const rects = addDamageRects(keep, pieces, CONTENTS_BLIT_MAX_RECTS);
|
|
1141
|
+
if (pins) {
|
|
1142
|
+
// Furniture can be most of a pane, and the damage cap can merge a
|
|
1143
|
+
// repair with a strip into the box of the two: past this, the blit is
|
|
1144
|
+
// buying a shift and paying for the region anyway.
|
|
1145
|
+
let painted = 0;
|
|
1146
|
+
for (const rect of rects) {
|
|
1147
|
+
const inside = intersectRects(rect, vp);
|
|
1148
|
+
if (inside) painted += rectArea(inside);
|
|
1149
|
+
}
|
|
1150
|
+
if (painted > area * SCROLL_BLIT_MAX_REPAINT) return;
|
|
1151
|
+
}
|
|
1152
|
+
// the element's deltas are already how far the pixels moved, the sense
|
|
1153
|
+
// scrollRegion takes (0 + x rather than x: a caller's -0 would survive
|
|
1154
|
+
// into request buffers and test comparisons)
|
|
1155
|
+
if (!target.blit({ ...vp }, 0 + dx, 0 + dy)) return;
|
|
965
1156
|
target.damage = rects;
|
|
966
1157
|
}
|
|
967
1158
|
|
|
968
1159
|
/**
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
* (`_scrollBlitSafe`). Whole pixels, and zero when no corner reaches in.
|
|
1160
|
+
* The squares of `vp` an ancestor's rounded corners reach into — the only
|
|
1161
|
+
* pixels of a rounded outline a translation cannot keep — in whole pixels,
|
|
1162
|
+
* for an element blit to pin (issue #691). Null when no corner reaches in.
|
|
973
1163
|
* Ancestors below `upTo`, the node the blit's surface belongs to
|
|
974
1164
|
* (`_blitTarget`): a pane is a rectangle no corner above it clips.
|
|
975
1165
|
*/
|
|
976
|
-
|
|
977
|
-
let
|
|
978
|
-
let bottom = 0;
|
|
1166
|
+
_cornerPins(node, vp, upTo = this) {
|
|
1167
|
+
let out = null;
|
|
979
1168
|
for (let n = node.parent; n && n !== upTo; n = n.parent) {
|
|
980
1169
|
const radius = n.style?.borderRadius ?? 0;
|
|
981
1170
|
if (!(radius > 0) || !n.abs) continue;
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
);
|
|
1171
|
+
for (const square of cornerSquares(n.abs, radius)) {
|
|
1172
|
+
if (!rectsOverlap(square, vp)) continue;
|
|
1173
|
+
const inside = intersectRects(outerPixels(square), vp);
|
|
1174
|
+
if (inside) (out ??= []).push(inside);
|
|
1175
|
+
}
|
|
988
1176
|
}
|
|
989
|
-
return
|
|
1177
|
+
return out;
|
|
990
1178
|
}
|
|
991
1179
|
|
|
992
1180
|
/**
|
|
@@ -1002,7 +1190,7 @@ export class WindowScrollBlit {
|
|
|
1002
1190
|
* only its own children — what the window draws under the surface was
|
|
1003
1191
|
* never in the pane's pixels.
|
|
1004
1192
|
*/
|
|
1005
|
-
_scrollBlitSafe(scroller, vp, top = this, riders = null) {
|
|
1193
|
+
_scrollBlitSafe(scroller, vp, top = this, riders = null, pinned = null) {
|
|
1006
1194
|
// the window itself scrolls: everything inside it *is* the scrolled
|
|
1007
1195
|
// content, so there is nothing that could be dragged along
|
|
1008
1196
|
if (scroller === this) return true;
|
|
@@ -1041,10 +1229,11 @@ export class WindowScrollBlit {
|
|
|
1041
1229
|
// radius-sized squares at the corners, and the straight run of
|
|
1042
1230
|
// the edge between them is the border ring already excluded
|
|
1043
1231
|
// above. A viewport that reaches the edge but stays clear of the
|
|
1044
|
-
// squares
|
|
1045
|
-
//
|
|
1232
|
+
// squares is translation-safe, and so is an element blit that
|
|
1233
|
+
// pins the squares it reaches into (`_cornerPins`) — repaired
|
|
1234
|
+
// where they are and where the copy dragged them.
|
|
1046
1235
|
const radius = child.style?.borderRadius ?? 0;
|
|
1047
|
-
if (radius > 0 &&
|
|
1236
|
+
if (radius > 0 && !cornersPinned(child.abs, radius, vp, pinned)) {
|
|
1048
1237
|
return false;
|
|
1049
1238
|
}
|
|
1050
1239
|
if (typeof child._scrollbars === 'function') {
|
|
@@ -1063,7 +1252,11 @@ export class WindowScrollBlit {
|
|
|
1063
1252
|
// …and a node the blit carries has pixels that move with it
|
|
1064
1253
|
// (`scrollContents`'s riders)
|
|
1065
1254
|
if (riders?.has(child)) continue;
|
|
1066
|
-
|
|
1255
|
+
const reach = child._subtreeBounds();
|
|
1256
|
+
if (!rectsOverlap(reach, vp)) continue;
|
|
1257
|
+
// …and one inside furniture an element blit pinned is repaired
|
|
1258
|
+
// where it is and where the copy dragged it, like the element's own
|
|
1259
|
+
if (insidePinned(reach, pinned)) continue;
|
|
1067
1260
|
// A box that paints nothing of its own is its children, and as
|
|
1068
1261
|
// safe as they are: the box that clips a rider to its element.
|
|
1069
1262
|
if (child._paintsNothingOwn() && check(child)) continue;
|
|
@@ -210,6 +210,8 @@ export class WindowFlush {
|
|
|
210
210
|
if (this._claimLayoutMove(rect, node, cap)) layoutMoved = true;
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
|
+
// the moves this walk hands over to be copied are this frame's alone
|
|
214
|
+
this._rigidMoves = null;
|
|
213
215
|
try {
|
|
214
216
|
// `_absolutizeChildren`, not the loop it wraps: a `<window
|
|
215
217
|
// style={{overflow: 'scroll'}}>` is a scroll container like any box,
|
|
@@ -279,6 +281,16 @@ export class WindowFlush {
|
|
|
279
281
|
this.window.prepareFrame?.(this, layoutRan);
|
|
280
282
|
// any node this pass laid out may be what an open popup is anchored to
|
|
281
283
|
if (layoutRan) this._notifyAnchorChange();
|
|
284
|
+
// A subtree that only moved and covers its box opaquely is copied where
|
|
285
|
+
// it went, and the frame paints around the copy — or its move is
|
|
286
|
+
// claimed, where it was and where it is (nodes/moveblit.js). Once every
|
|
287
|
+
// other claim of the frame is in: what the copy leaves to paint depends
|
|
288
|
+
// on what else changed around it.
|
|
289
|
+
const moves = layoutRan ? this._rigidMoves : null;
|
|
290
|
+
this._rigidMoves = null;
|
|
291
|
+
if (moves !== null && this._settleRigidMoves(moves, width, height)) {
|
|
292
|
+
layoutMoved = true;
|
|
293
|
+
}
|
|
282
294
|
// after layout (the claims above included), before the damage is taken:
|
|
283
295
|
// a frame that turns out to be a pure scroll blits the surviving band
|
|
284
296
|
// and narrows its claim to the exposed strip
|
|
@@ -18,6 +18,7 @@ import { NO_DAMAGE } from '../damage.js';
|
|
|
18
18
|
import { installMethods } from '../install.js';
|
|
19
19
|
import { WindowInvalidate } from '../invalidate.js';
|
|
20
20
|
import { WindowLayoutHost } from '../layouthost.js';
|
|
21
|
+
import { WindowMoveBlit } from '../moveblit.js';
|
|
21
22
|
import { DEVTOOLS_FAKE_DOCUMENT, Node } from '../node.js';
|
|
22
23
|
import { WindowPaint } from '../paint.js';
|
|
23
24
|
import { WindowPosition } from '../position.js';
|
|
@@ -209,6 +210,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
209
210
|
// each claim goes to one or both by what it can reach (`_paneReach`).
|
|
210
211
|
// Null with nothing owed.
|
|
211
212
|
this._paneDamage = null;
|
|
213
|
+
// The claims the window's list took this frame, each with the node that
|
|
214
|
+
// made it — null once there are too many to be worth keeping — and the
|
|
215
|
+
// subtrees this frame's layout moved that the window may copy rather
|
|
216
|
+
// than repaint, settled once the pass is over (nodes/moveblit.js).
|
|
217
|
+
this._claimLog = [];
|
|
218
|
+
this._rigidMoves = null;
|
|
212
219
|
this.events = new EventManager(this);
|
|
213
220
|
// ids of the child windows in the order the *server* stacks them,
|
|
214
221
|
// bottom to top — see _restackWindowChildren
|
|
@@ -979,6 +986,7 @@ installMethods(
|
|
|
979
986
|
WindowPosition,
|
|
980
987
|
WindowInvalidate,
|
|
981
988
|
WindowScrollBlit,
|
|
989
|
+
WindowMoveBlit,
|
|
982
990
|
WindowPaint,
|
|
983
991
|
WindowListeners,
|
|
984
992
|
WindowHints,
|