react-x11 2.18.2 → 2.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cocoa/overlay.js +40 -3
- package/src/glnodes.js +99 -4
- package/src/gloverlay.js +269 -15
- package/src/nodes/animation.js +2 -0
- package/src/nodes/damage.js +8 -0
- package/src/nodes/invalidate.js +126 -10
- package/src/nodes/node.js +24 -4
- package/src/nodes/position.js +11 -2
- package/src/nodes/scrollable.js +5 -2
- package/src/nodes/scrollblit.js +90 -24
- package/src/nodes/window/flush.js +40 -18
- package/src/nodes/window/window.js +4 -0
- package/src/styles.js +22 -0
package/package.json
CHANGED
package/src/cocoa/overlay.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// so where two surfaces overlap, both overlays are above both frames.
|
|
12
12
|
//
|
|
13
13
|
// It speaks the verbs of a window the overlay drives on X11 — `setState`,
|
|
14
|
-
// `map`, `unmap`, `getContext`, `destroy` — plus `present`,
|
|
15
|
-
// was painted on the layer: ntk blits an X window's backing
|
|
16
|
-
// own, where a layer's contents are a copy the bitmap has to be
|
|
14
|
+
// `map`, `unmap`, `getContext`, `scrollRegion`, `destroy` — plus `present`,
|
|
15
|
+
// which puts what was painted on the layer: ntk blits an X window's backing
|
|
16
|
+
// store on its own, where a layer's contents are a copy the bitmap has to be
|
|
17
|
+
// pushed to.
|
|
17
18
|
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
19
|
import { withoutActions } from './quiet.js';
|
|
19
20
|
|
|
@@ -135,6 +136,42 @@ export class CocoaOverlayPane {
|
|
|
135
136
|
return this._ctx;
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Move the pixels of `rect` (pane coordinates, device px) by (dx, dy)
|
|
141
|
+
* inside the bitmap — ntk `Window.scrollRegion`'s contract, which the
|
|
142
|
+
* overlay speaks to either backend: the band that survives the shift
|
|
143
|
+
* inside `rect` moves, the rest of it is left as it was, and false means
|
|
144
|
+
* nothing moved, so the caller repaints instead. A child that only moved
|
|
145
|
+
* is one of these rather than a repaint (src/gloverlay.js, issue #644).
|
|
146
|
+
*
|
|
147
|
+
* The bitmap is the pane's own and the layer holds a copy of it, so the
|
|
148
|
+
* move lands on screen with the frame's `present`, after the strips it
|
|
149
|
+
* uncovered are painted — never halfway.
|
|
150
|
+
*/
|
|
151
|
+
scrollRegion(rect, dx, dy) {
|
|
152
|
+
if (this.destroyed || !this._surface) return false;
|
|
153
|
+
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
154
|
+
// a bitmap of another size is one the next paint replaces, whole
|
|
155
|
+
const size = this._surfaceSize;
|
|
156
|
+
if (
|
|
157
|
+
size.width !== Math.max(1, this.rect.width) ||
|
|
158
|
+
size.height !== Math.max(1, this.rect.height)
|
|
159
|
+
) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const moved = this._native.scrollSurface(
|
|
163
|
+
this._surface,
|
|
164
|
+
Math.round(rect.x),
|
|
165
|
+
Math.round(rect.y),
|
|
166
|
+
Math.round(rect.width),
|
|
167
|
+
Math.round(rect.height),
|
|
168
|
+
dx,
|
|
169
|
+
dy,
|
|
170
|
+
);
|
|
171
|
+
if (moved) this._dirty = true;
|
|
172
|
+
return Boolean(moved);
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
/** What was painted, onto the layer — a copy, so the bitmap is free to be
|
|
139
176
|
* painted again at once. */
|
|
140
177
|
present() {
|
package/src/glnodes.js
CHANGED
|
@@ -11,6 +11,7 @@ import { cssColorStraight } from 'ntk';
|
|
|
11
11
|
export { directGLFailure, hasDirectGL } from './glbackend.js';
|
|
12
12
|
|
|
13
13
|
import { GlOverlay, canOverlay } from './gloverlay.js';
|
|
14
|
+
import { layoutDiff } from './nodes/damage.js';
|
|
14
15
|
import { Node } from './nodes/node.js';
|
|
15
16
|
import { FramePacer, resolveFrameRate } from './pacing.js';
|
|
16
17
|
|
|
@@ -320,11 +321,102 @@ export class GlAreaNode extends Node {
|
|
|
320
321
|
};
|
|
321
322
|
}
|
|
322
323
|
|
|
324
|
+
/**
|
|
325
|
+
* `Node.absolutize`, with the children walked one at a time where a pane
|
|
326
|
+
* holds them — the frame's layout diff running (a bounded frame), no
|
|
327
|
+
* scroll shift riding over this node, no layout algorithm of its own
|
|
328
|
+
* placing them, and this node's own rect where it was, so the panes are
|
|
329
|
+
* too. A child that moved and changed nothing else is then reported to
|
|
330
|
+
* the overlay instead of claimed (`_absolutizeChild`); anywhere else the
|
|
331
|
+
* walk is the plain one.
|
|
332
|
+
*/
|
|
323
333
|
absolutize(originX, originY) {
|
|
324
|
-
|
|
334
|
+
const overlay = this._overlay;
|
|
335
|
+
if (
|
|
336
|
+
!overlay?.panes.length ||
|
|
337
|
+
!layoutDiff.sink ||
|
|
338
|
+
layoutDiff.shift ||
|
|
339
|
+
this._host !== null ||
|
|
340
|
+
!this.yoga
|
|
341
|
+
) {
|
|
342
|
+
super.absolutize(originX, originY);
|
|
343
|
+
this._syncGeometry();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this._placed = true;
|
|
347
|
+
const was = this.abs;
|
|
348
|
+
this._assignAbs(
|
|
349
|
+
originX + this.yoga.getComputedLeft(),
|
|
350
|
+
originY + this.yoga.getComputedTop(),
|
|
351
|
+
this.yoga.getComputedWidth(),
|
|
352
|
+
this.yoga.getComputedHeight(),
|
|
353
|
+
);
|
|
354
|
+
if (this.props.onLayout) this._reportLayout();
|
|
355
|
+
// `_assignAbs` keeps the object when nothing changed
|
|
356
|
+
const still = this.abs === was;
|
|
357
|
+
const { x, y } = this.abs;
|
|
358
|
+
for (const child of this.children) {
|
|
359
|
+
if (child.isWindow) continue;
|
|
360
|
+
if (still) this._absolutizeChild(child, x, y, overlay);
|
|
361
|
+
else child.absolutize(x, y);
|
|
362
|
+
}
|
|
325
363
|
this._syncGeometry();
|
|
326
364
|
}
|
|
327
365
|
|
|
366
|
+
/**
|
|
367
|
+
* One child, laid out from (originX, originY). When all that changed is
|
|
368
|
+
* where it sits — the same size, somewhere else, a plain box drawn where
|
|
369
|
+
* layout put it — it is walked with the layout diff's `shift` set to the
|
|
370
|
+
* move (issue #644): every descendant that lands where it was plus the
|
|
371
|
+
* move claims nothing, exactly as under a scroll blit (issue #398), and
|
|
372
|
+
* one that lands anywhere else claims both ends. The child itself is one
|
|
373
|
+
* of the first kind, so the move claims nothing at all here; the overlay
|
|
374
|
+
* is told of it instead, with the reach its pixels had, and settles it
|
|
375
|
+
* after the panes are placed (`GlOverlay.settleMoves`) — moved on the
|
|
376
|
+
* pane, or claimed where it was and where it is.
|
|
377
|
+
*
|
|
378
|
+
* `layoutDiff.ride` marks the walk as this kind, for the one node that
|
|
379
|
+
* drops the diff under a shift: a scroll pane laid out again claims its
|
|
380
|
+
* own box there, since the move claimed nothing for it
|
|
381
|
+
* (nodes/scrollable.js).
|
|
382
|
+
*/
|
|
383
|
+
_absolutizeChild(child, originX, originY, overlay) {
|
|
384
|
+
const y = child.yoga;
|
|
385
|
+
const old = child.abs;
|
|
386
|
+
if (
|
|
387
|
+
!y ||
|
|
388
|
+
child.hidden ||
|
|
389
|
+
child.style?.display === 'none' ||
|
|
390
|
+
child.isScroller?.() ||
|
|
391
|
+
!(old.width > 0 && old.height > 0) ||
|
|
392
|
+
this.root?._placedNodes?.has(child)
|
|
393
|
+
) {
|
|
394
|
+
child.absolutize(originX, originY);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const dx = originX + y.getComputedLeft() - old.x;
|
|
398
|
+
const dy = originY + y.getComputedTop() - old.y;
|
|
399
|
+
if (
|
|
400
|
+
(dx === 0 && dy === 0) ||
|
|
401
|
+
y.getComputedWidth() !== old.width ||
|
|
402
|
+
y.getComputedHeight() !== old.height
|
|
403
|
+
) {
|
|
404
|
+
child.absolutize(originX, originY);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// before it moves: every pixel of it a pane holds
|
|
408
|
+
const reach = child.paintBounds();
|
|
409
|
+
layoutDiff.shift = { x: dx, y: dy };
|
|
410
|
+
layoutDiff.ride = true;
|
|
411
|
+
try {
|
|
412
|
+
child.absolutize(originX, originY);
|
|
413
|
+
} finally {
|
|
414
|
+
layoutDiff.shift = null;
|
|
415
|
+
layoutDiff.ride = false;
|
|
416
|
+
}
|
|
417
|
+
overlay.noteMove(child, reach, dx, dy);
|
|
418
|
+
}
|
|
419
|
+
|
|
328
420
|
// the scroll fast path moves `abs` without coming through absolutize
|
|
329
421
|
// (issue #405), and the real X window has to follow it all the same
|
|
330
422
|
_shiftAbs(dx, dy) {
|
|
@@ -537,8 +629,10 @@ export class GlAreaNode extends Node {
|
|
|
537
629
|
|
|
538
630
|
/**
|
|
539
631
|
* The owning window's frame, after layout (`WindowNode._syncOverlays`):
|
|
540
|
-
* panes for where the children are now
|
|
541
|
-
*
|
|
632
|
+
* panes for where the children are now, and the moves the layout pass
|
|
633
|
+
* reported settled against them — moved on a pane, or claimed. True when
|
|
634
|
+
* a pane was made, resized or dropped, or a move claimed something — a
|
|
635
|
+
* paint the frame then owes.
|
|
542
636
|
*/
|
|
543
637
|
_syncOverlay() {
|
|
544
638
|
if (!this._overlay) {
|
|
@@ -548,7 +642,8 @@ export class GlAreaNode extends Node {
|
|
|
548
642
|
}
|
|
549
643
|
this._overlay = new GlOverlay(this);
|
|
550
644
|
}
|
|
551
|
-
|
|
645
|
+
let changed = this._overlay.sync();
|
|
646
|
+
if (this._overlay.settleMoves(this.root)) changed = true;
|
|
552
647
|
if (this._overlay.panes.length) this._joinSurfaces();
|
|
553
648
|
if (this.children.length === 0 && this._overlay.panes.length === 0) {
|
|
554
649
|
this._overlay = null;
|
package/src/gloverlay.js
CHANGED
|
@@ -40,13 +40,41 @@
|
|
|
40
40
|
// by the same propagation that brings it the pointer over the surface
|
|
41
41
|
// (src/glnodes.js, `_create`), and lands on the child it is over.
|
|
42
42
|
//
|
|
43
|
+
// A pane keeps what it holds between frames, so a child that only moved is
|
|
44
|
+
// already painted, one shift away (issue #644). The layout pass reports such
|
|
45
|
+
// a child instead of claiming it (`GlAreaNode._absolutizeChild`), and the
|
|
46
|
+
// frame moves its pixels inside the pane — `scrollRegion`, the verb the
|
|
47
|
+
// scroll blit uses on a window — and repaints only what the move uncovered
|
|
48
|
+
// and what it now overlaps differently (`settleMoves`). A pan over a
|
|
49
|
+
// graph's mounted node bodies is that frame, sixty times a second.
|
|
50
|
+
//
|
|
43
51
|
// A leaf module, like src/embedding.js: `appcontext.js` asks `canOverlay`
|
|
44
|
-
// for `useSupports('glOverlay')`, and it imports nothing of ours
|
|
52
|
+
// for `useSupports('glOverlay')`, and it imports nothing of ours but the
|
|
53
|
+
// damage model's arithmetic, which imports nothing at all.
|
|
45
54
|
import { cssColorStraight } from 'ntk';
|
|
46
55
|
|
|
56
|
+
import { FULL_DAMAGE, addDamageRect } from './nodes/damage.js';
|
|
57
|
+
import {
|
|
58
|
+
innerPixels,
|
|
59
|
+
intersectRects,
|
|
60
|
+
rectArea,
|
|
61
|
+
rectContains,
|
|
62
|
+
unionRect,
|
|
63
|
+
} from './nodes/rects.js';
|
|
64
|
+
|
|
47
65
|
// ConfigureWindow's stack mode: directly above the sibling it names
|
|
48
66
|
const STACK_ABOVE = 0;
|
|
49
67
|
|
|
68
|
+
// Past this share of the pixels a move carries, the frame would repaint
|
|
69
|
+
// most of them anyway — around what it claims itself, or around the other
|
|
70
|
+
// children the copy reached — and the copy only adds to the bill.
|
|
71
|
+
const MOVE_BLIT_MAX_REPAINT = 0.75;
|
|
72
|
+
|
|
73
|
+
// The scroll blit's escape hatch (src/nodes/scrollblit.js), read the same
|
|
74
|
+
// way: every path that moves retained pixels instead of repainting them is
|
|
75
|
+
// one variable away from the plain repaint (docs/debugging.md).
|
|
76
|
+
const NO_BLIT = process.env.REACT_X11_NO_SCROLL_BLIT === '1';
|
|
77
|
+
|
|
50
78
|
/**
|
|
51
79
|
* Whether the children of a `<glarea>` can be drawn over its surface on this
|
|
52
80
|
* connection: a backend that composites a pane itself, or one that can make
|
|
@@ -73,16 +101,6 @@ function whole(r) {
|
|
|
73
101
|
};
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
function intersect(a, b) {
|
|
77
|
-
const x = Math.max(a.x, b.x);
|
|
78
|
-
const y = Math.max(a.y, b.y);
|
|
79
|
-
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
80
|
-
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
81
|
-
return right > x && bottom > y
|
|
82
|
-
? { x, y, width: right - x, height: bottom - y }
|
|
83
|
-
: null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
104
|
const overlaps = (a, b) =>
|
|
87
105
|
a.x < b.x + b.width &&
|
|
88
106
|
b.x < a.x + a.width &&
|
|
@@ -103,6 +121,48 @@ function around(a, b) {
|
|
|
103
121
|
const sameRect = (a, b) =>
|
|
104
122
|
a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
105
123
|
|
|
124
|
+
const shifted = (r, dx, dy) => ({
|
|
125
|
+
x: r.x + dx,
|
|
126
|
+
y: r.y + dy,
|
|
127
|
+
width: r.width,
|
|
128
|
+
height: r.height,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** `a` with `b` taken out of it: up to four rects — the full-width bands
|
|
132
|
+
* above and below `b`, and the pieces either side of it between them. */
|
|
133
|
+
function subtractRect(a, b) {
|
|
134
|
+
const cut = intersectRects(a, b);
|
|
135
|
+
if (!cut) return [a];
|
|
136
|
+
const out = [];
|
|
137
|
+
const right = a.x + a.width;
|
|
138
|
+
const bottom = a.y + a.height;
|
|
139
|
+
const cutRight = cut.x + cut.width;
|
|
140
|
+
const cutBottom = cut.y + cut.height;
|
|
141
|
+
if (cut.y > a.y) {
|
|
142
|
+
out.push({ x: a.x, y: a.y, width: a.width, height: cut.y - a.y });
|
|
143
|
+
}
|
|
144
|
+
if (cutBottom < bottom) {
|
|
145
|
+
out.push({
|
|
146
|
+
x: a.x,
|
|
147
|
+
y: cutBottom,
|
|
148
|
+
width: a.width,
|
|
149
|
+
height: bottom - cutBottom,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (cut.x > a.x) {
|
|
153
|
+
out.push({ x: a.x, y: cut.y, width: cut.x - a.x, height: cut.height });
|
|
154
|
+
}
|
|
155
|
+
if (cutRight < right) {
|
|
156
|
+
out.push({
|
|
157
|
+
x: cutRight,
|
|
158
|
+
y: cut.y,
|
|
159
|
+
width: right - cutRight,
|
|
160
|
+
height: cut.height,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
106
166
|
/** On screen: nothing from here to the window hidden or `display: 'none'`. */
|
|
107
167
|
function shown(node) {
|
|
108
168
|
for (let n = node; n; n = n.parent) {
|
|
@@ -125,7 +185,7 @@ function shown(node) {
|
|
|
125
185
|
export function overlayRegions(area, surface) {
|
|
126
186
|
const rects = [];
|
|
127
187
|
for (const child of area.paintOrder()) {
|
|
128
|
-
const reach =
|
|
188
|
+
const reach = intersectRects(whole(child._subtreeBounds()), surface);
|
|
129
189
|
if (reach) rects.push(reach);
|
|
130
190
|
}
|
|
131
191
|
for (let merged = true; merged;) {
|
|
@@ -172,6 +232,13 @@ class Pane {
|
|
|
172
232
|
this.transparent = transparent;
|
|
173
233
|
this.full = true;
|
|
174
234
|
this.ctx = null;
|
|
235
|
+
// placed this frame: what the pane holds is where the children were in
|
|
236
|
+
// the pane's own corner, not in the window, so nothing in it is a known
|
|
237
|
+
// shift away from where it goes
|
|
238
|
+
this.placed = false;
|
|
239
|
+
// moved pixels this frame, which has to reach the screen even when no
|
|
240
|
+
// pass follows it (`GlOverlay.paint`)
|
|
241
|
+
this.blitted = false;
|
|
175
242
|
}
|
|
176
243
|
|
|
177
244
|
context() {
|
|
@@ -188,7 +255,12 @@ class Pane {
|
|
|
188
255
|
if (rect.width !== this.rect.width || rect.height !== this.rect.height) {
|
|
189
256
|
this.full = true;
|
|
190
257
|
}
|
|
258
|
+
// pixels a scroll blit moved this frame (nodes/scrollblit.js, which runs
|
|
259
|
+
// before `sync`) were moved in the pane's own corner, and the pane has
|
|
260
|
+
// since gone somewhere else
|
|
261
|
+
if (this.blitted) this.full = true;
|
|
191
262
|
this.rect = rect;
|
|
263
|
+
this.placed = true;
|
|
192
264
|
if (typeof this.wnd.setState === 'function') this.wnd.setState(rect);
|
|
193
265
|
else {
|
|
194
266
|
this.wnd.move?.(rect.x, rect.y);
|
|
@@ -196,6 +268,37 @@ class Pane {
|
|
|
196
268
|
}
|
|
197
269
|
}
|
|
198
270
|
|
|
271
|
+
/** Can the pixels of `rect`, in window coordinates, move inside this
|
|
272
|
+
* pane? Not when it is owed a paint whole or was placed this frame, and
|
|
273
|
+
* not where the backend has no verb for it. */
|
|
274
|
+
canBlit(rect) {
|
|
275
|
+
return (
|
|
276
|
+
!this.full &&
|
|
277
|
+
!this.placed &&
|
|
278
|
+
typeof this.wnd.scrollRegion === 'function' &&
|
|
279
|
+
rectContains(this.rect, rect)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Move the pixels of `rect` — window coordinates — by (dx, dy) inside the
|
|
285
|
+
* pane, with the window verb's contract (ntk `Window.scrollRegion`, and
|
|
286
|
+
* the Cocoa pane's own): the band that survives inside `rect` moves, the
|
|
287
|
+
* rest of it is left as it was. False when the backend could not, which
|
|
288
|
+
* leaves the pane exactly as it was.
|
|
289
|
+
*/
|
|
290
|
+
blit(rect, dx, dy) {
|
|
291
|
+
const local = {
|
|
292
|
+
x: rect.x - this.rect.x,
|
|
293
|
+
y: rect.y - this.rect.y,
|
|
294
|
+
width: rect.width,
|
|
295
|
+
height: rect.height,
|
|
296
|
+
};
|
|
297
|
+
if (!this.wnd.scrollRegion(local, dx, dy)) return false;
|
|
298
|
+
this.blitted = true;
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
199
302
|
show(on) {
|
|
200
303
|
if (on) this.wnd.map?.();
|
|
201
304
|
else this.wnd.unmap?.();
|
|
@@ -215,6 +318,10 @@ export class GlOverlay {
|
|
|
215
318
|
// one pane over the whole surface, where the backend composites it
|
|
216
319
|
this.composited = typeof this.app?.createOverlayPane === 'function';
|
|
217
320
|
this.panes = [];
|
|
321
|
+
// the children this frame's layout moved and nothing else, reported
|
|
322
|
+
// rather than claimed (`GlAreaNode._absolutizeChild`) and settled after
|
|
323
|
+
// the panes are (`settleMoves`)
|
|
324
|
+
this.moves = [];
|
|
218
325
|
}
|
|
219
326
|
|
|
220
327
|
/**
|
|
@@ -229,6 +336,7 @@ export class GlOverlay {
|
|
|
229
336
|
sync() {
|
|
230
337
|
const area = this.area;
|
|
231
338
|
const abs = area.abs;
|
|
339
|
+
for (const pane of this.panes) pane.placed = false;
|
|
232
340
|
let rects = [];
|
|
233
341
|
if (shown(area) && abs.width > 0 && abs.height > 0) {
|
|
234
342
|
// the surface's own rect, rounded the way its window's is
|
|
@@ -279,7 +387,7 @@ export class GlOverlay {
|
|
|
279
387
|
pane.full = true;
|
|
280
388
|
const root = this.area.root;
|
|
281
389
|
if (root && !root.destroyed)
|
|
282
|
-
root.invalidate(false, { ...pane.rect }, 'expose');
|
|
390
|
+
root.invalidate(false, { ...pane.rect }, 'expose', this.area);
|
|
283
391
|
}
|
|
284
392
|
|
|
285
393
|
/**
|
|
@@ -305,6 +413,145 @@ export class GlOverlay {
|
|
|
305
413
|
}
|
|
306
414
|
}
|
|
307
415
|
|
|
416
|
+
/**
|
|
417
|
+
* A child the layout pass moved and nothing else — every descendant
|
|
418
|
+
* landed where it was plus (dx, dy) or claimed where it did not — and
|
|
419
|
+
* claimed nothing for (`GlAreaNode._absolutizeChild`). `reach` is its
|
|
420
|
+
* `paintBounds()` from before the move: every pixel of it the panes hold.
|
|
421
|
+
*/
|
|
422
|
+
noteMove(node, reach, dx, dy) {
|
|
423
|
+
this.moves.push({ node, reach, dx, dy });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The frame's moves, settled after `sync` has put the panes where the
|
|
428
|
+
* children are and before the frame takes its damage
|
|
429
|
+
* (`WindowNode._syncOverlays`). The one covering the most of the surface
|
|
430
|
+
* moves its pixels on its pane where that is safe and saves the repaint
|
|
431
|
+
* (`_blitMove`); every other one, and that one when not, is claimed where
|
|
432
|
+
* it was and where it is — the pixels the layout diff would have claimed
|
|
433
|
+
* for it and every descendant. True when there was anything to settle.
|
|
434
|
+
*
|
|
435
|
+
* The claims go to the panes' damage alone (`WindowNode._paneDamage`):
|
|
436
|
+
* the window paints none of a surface's children, and its pass under the
|
|
437
|
+
* surface is one nobody sees.
|
|
438
|
+
*/
|
|
439
|
+
settleMoves(root) {
|
|
440
|
+
const moves = this.moves;
|
|
441
|
+
if (moves.length === 0) return false;
|
|
442
|
+
this.moves = [];
|
|
443
|
+
// the panes repaint whole this frame: nothing is owed
|
|
444
|
+
if (!root || root.destroyed || root._paneDamage === FULL_DAMAGE) {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
const surface = this.area._geometry();
|
|
448
|
+
let best = null;
|
|
449
|
+
let most = 0;
|
|
450
|
+
for (const move of moves) {
|
|
451
|
+
const shown = intersectRects(whole(move.reach), surface);
|
|
452
|
+
const size = shown ? rectArea(shown) : 0;
|
|
453
|
+
if (size > most) {
|
|
454
|
+
best = move;
|
|
455
|
+
most = size;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// the others first: the pixels they claim are ones the copy may carry,
|
|
459
|
+
// and `_blitMove` repairs whatever the frame claims by then
|
|
460
|
+
for (const move of moves) if (move !== best) this._claimMove(root, move);
|
|
461
|
+
if (best && !this._blitMove(root, best)) this._claimMove(root, best);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Where a move's child was and where it is, claimed the ordinary way —
|
|
466
|
+
* cut to the surface's box, which clips the children (`clipsChildren`). */
|
|
467
|
+
_claimMove(root, { node, reach }) {
|
|
468
|
+
const box = this.area.abs;
|
|
469
|
+
for (const rect of [reach, node.paintBounds()]) {
|
|
470
|
+
const claim = intersectRects(rect, box);
|
|
471
|
+
if (claim) root._addPaneDamage(claim);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Move a child's pixels on its pane, and narrow what the frame owes the
|
|
477
|
+
* move to what the copy cannot supply. True when it did; false leaves
|
|
478
|
+
* the pane and the frame's damage as they were, for `_claimMove`.
|
|
479
|
+
*
|
|
480
|
+
* The copy is of what the child showed, moved as far as the child moved
|
|
481
|
+
* and kept where it is still on show (`dest`). That is right wherever
|
|
482
|
+
* the child's own drawing is the only thing that changed — and the frame
|
|
483
|
+
* repaints the rest:
|
|
484
|
+
*
|
|
485
|
+
* - what the child uncovered, and whatever of its new reach the copy did
|
|
486
|
+
* not land on: its reach before and after, less `dest`;
|
|
487
|
+
* - the other children: the pixels of theirs the copy carried off with
|
|
488
|
+
* the child's, and theirs that the copy covered — over the child or
|
|
489
|
+
* under it, their own pixels are not a shift away;
|
|
490
|
+
* - what the panes are owed already, where the copy would carry it: a
|
|
491
|
+
* claim is often a rect named before layout ran, and when it was
|
|
492
|
+
* inside the child its content went with the copy. Repainted shifted
|
|
493
|
+
* as well as where it stands, which is right whichever it was. A
|
|
494
|
+
* claim that cannot reach the panes is not in their list
|
|
495
|
+
* (`WindowNode._paneReach`) — a graph pane under the surface that
|
|
496
|
+
* claims itself whole on every step of the pan this copy is.
|
|
497
|
+
*
|
|
498
|
+
* Past `MOVE_BLIT_MAX_REPAINT` of `dest` those repaint most of what the
|
|
499
|
+
* copy saves, and the plain claims have the frame.
|
|
500
|
+
*/
|
|
501
|
+
_blitMove(root, { node, reach, dx, dy }) {
|
|
502
|
+
if (NO_BLIT || !Number.isInteger(dx) || !Number.isInteger(dy)) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
const area = this.area;
|
|
506
|
+
// a rounded surface clips its children to an arc, and the arc stays put
|
|
507
|
+
if ((area.style?.borderRadius ?? 0) > 0) return false;
|
|
508
|
+
// the whole pixels of the surface's box: a fractional edge is an
|
|
509
|
+
// antialiased clip, and that does not move either
|
|
510
|
+
const box = innerPixels(area.abs);
|
|
511
|
+
if (!box) return false;
|
|
512
|
+
const was = intersectRects(whole(reach), box);
|
|
513
|
+
const now = intersectRects(whole(node.paintBounds()), box);
|
|
514
|
+
// nothing of it was on show, so there is nothing to carry
|
|
515
|
+
if (!was) return false;
|
|
516
|
+
const pane = this.panes.find(
|
|
517
|
+
(p) => p.canBlit(was) && (!now || p.canBlit(now)),
|
|
518
|
+
);
|
|
519
|
+
if (!pane) return false;
|
|
520
|
+
const view = intersectRects(pane.rect, box);
|
|
521
|
+
const dest = intersectRects(shifted(was, dx, dy), view);
|
|
522
|
+
if (!dest) return false;
|
|
523
|
+
const cap = root._damageRectCap();
|
|
524
|
+
const claimed = root._paneDamage ?? [];
|
|
525
|
+
let rects = claimed;
|
|
526
|
+
const add = (rect) => {
|
|
527
|
+
if (rect) rects = addDamageRect(rects, rect, cap);
|
|
528
|
+
};
|
|
529
|
+
for (const claim of claimed)
|
|
530
|
+
add(intersectRects(shifted(claim, dx, dy), dest));
|
|
531
|
+
for (const piece of subtractRect(was, dest)) add(piece);
|
|
532
|
+
if (now) for (const piece of subtractRect(now, dest)) add(piece);
|
|
533
|
+
for (const other of area.paintOrder()) {
|
|
534
|
+
if (other === node) continue;
|
|
535
|
+
const theirs = whole(other.paintBounds());
|
|
536
|
+
add(intersectRects(theirs, dest));
|
|
537
|
+
add(intersectRects(shifted(theirs, dx, dy), dest));
|
|
538
|
+
}
|
|
539
|
+
// the list is disjoint, so the sum is the area it covers
|
|
540
|
+
let repainted = 0;
|
|
541
|
+
for (const rect of rects) {
|
|
542
|
+
const inside = intersectRects(rect, dest);
|
|
543
|
+
if (inside) repainted += rectArea(inside);
|
|
544
|
+
}
|
|
545
|
+
if (repainted > rectArea(dest) * MOVE_BLIT_MAX_REPAINT) return false;
|
|
546
|
+
// the verb moves the band that survives inside the rect it is handed,
|
|
547
|
+
// so handed where the pixels are and where they go, that band is `dest`
|
|
548
|
+
if (!pane.blit(unionRect(shifted(dest, -dx, -dy), dest), dx, dy)) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
root._paneDamage = rects;
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
308
555
|
/**
|
|
309
556
|
* Paint what the frame owes: every pane whole after it was made, resized
|
|
310
557
|
* or lost its pixels, and otherwise the frame's damage cut to each pane —
|
|
@@ -323,10 +570,17 @@ export class GlOverlay {
|
|
|
323
570
|
} else {
|
|
324
571
|
passes = [];
|
|
325
572
|
for (const rect of damage) {
|
|
326
|
-
const hit =
|
|
573
|
+
const hit = intersectRects(rect, pane.rect);
|
|
327
574
|
if (hit) passes.push(hit);
|
|
328
575
|
}
|
|
329
|
-
|
|
576
|
+
}
|
|
577
|
+
const blitted = pane.blitted;
|
|
578
|
+
pane.blitted = false;
|
|
579
|
+
pane.placed = false;
|
|
580
|
+
if (passes.length === 0) {
|
|
581
|
+
// pixels moved and nothing else owed: they still go out
|
|
582
|
+
if (blitted) pane.wnd.present?.();
|
|
583
|
+
continue;
|
|
330
584
|
}
|
|
331
585
|
pane.full = false;
|
|
332
586
|
this._paintPane(pane, passes);
|
package/src/nodes/animation.js
CHANGED
|
@@ -215,6 +215,7 @@ export class NodeAnimation {
|
|
|
215
215
|
false,
|
|
216
216
|
insetRect(this.paintBounds(), -shrank),
|
|
217
217
|
'shadow',
|
|
218
|
+
this,
|
|
218
219
|
);
|
|
219
220
|
}
|
|
220
221
|
}
|
|
@@ -235,6 +236,7 @@ export class NodeAnimation {
|
|
|
235
236
|
false,
|
|
236
237
|
insetRect(this.paintBounds(), -shrank),
|
|
237
238
|
'outline',
|
|
239
|
+
this,
|
|
238
240
|
);
|
|
239
241
|
}
|
|
240
242
|
}
|
package/src/nodes/damage.js
CHANGED
|
@@ -49,6 +49,14 @@ export const layoutDiff = {
|
|
|
49
49
|
// diff reports only the children that landed somewhere else. Null everywhere
|
|
50
50
|
// else, and restored through `finally` like the sink beside it.
|
|
51
51
|
shift: null,
|
|
52
|
+
|
|
53
|
+
// True when `shift` is a `<glarea>` child's own move rather than a scroll
|
|
54
|
+
// (src/glnodes.js, `_absolutizeChild`, issue #644): the overlay moves the
|
|
55
|
+
// child's pixels on its pane, and a node that lands where it was plus the
|
|
56
|
+
// shift is carried by that. A node that turns the diff off under a shift —
|
|
57
|
+
// a scroll pane laid out again, whose box would have claimed its children
|
|
58
|
+
// — claims its own box instead, since the move claimed nothing for it.
|
|
59
|
+
ride: false,
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
// What an invalidate() may name as its reason — a small closed set, so the
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -18,6 +18,20 @@ import { BLIT_POISONED } from './scrollblit.js';
|
|
|
18
18
|
import { DEV } from './util.js';
|
|
19
19
|
import { debugPaint } from './window/debugpaint.js';
|
|
20
20
|
|
|
21
|
+
// Which damage lists a claim belongs in (`WindowInvalidate._paneReach`).
|
|
22
|
+
const WINDOW_ONLY = 0;
|
|
23
|
+
const BOTH = 1;
|
|
24
|
+
const PANES_ONLY = 2;
|
|
25
|
+
|
|
26
|
+
/** Is `node` inside a `<glarea>` below `root` — drawn on its panes, and
|
|
27
|
+
* never by the window's own paint walk? */
|
|
28
|
+
function insideSurface(node, root) {
|
|
29
|
+
for (let n = node.parent; n && n !== root; n = n.parent) {
|
|
30
|
+
if (n.isGlArea) return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
/** Node's half of invalidation, installed onto `Node.prototype` by node.js. */
|
|
22
36
|
export class NodeInvalidate {
|
|
23
37
|
/**
|
|
@@ -40,7 +54,10 @@ export class NodeInvalidate {
|
|
|
40
54
|
// reaches — a shadow, an outline, a scene element's ink — so its cached
|
|
41
55
|
// paint reach goes with the claim
|
|
42
56
|
if (damage === this || damage === null) this._clearPaintBounds();
|
|
43
|
-
|
|
57
|
+
// …and names itself, so a rect it claims goes to the damage lists it can
|
|
58
|
+
// reach — the window's, a `<glarea>`'s panes', or both
|
|
59
|
+
// (`WindowNode._paneReach`)
|
|
60
|
+
this.root?.invalidate(layoutChanged, damage, reason, this);
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
/**
|
|
@@ -112,7 +129,7 @@ export class NodeInvalidate {
|
|
|
112
129
|
// Same walk, same frame, same answer — see `_childListBefore`, whose
|
|
113
130
|
// record this shares so that a reflow and a child-list change on one
|
|
114
131
|
// node in one frame walk the subtree once between them.
|
|
115
|
-
root.invalidate(true, this._childListBefore(), reason);
|
|
132
|
+
root.invalidate(true, this._childListBefore(), reason, this);
|
|
116
133
|
root._reflowed.add(this);
|
|
117
134
|
}
|
|
118
135
|
|
|
@@ -235,8 +252,14 @@ export class WindowInvalidate {
|
|
|
235
252
|
* diagnostic, collected per frame into `_lastReasons` so the frame log,
|
|
236
253
|
* REACT_X11_DEBUG_PAINT=full and the tracer can attribute a repaint.
|
|
237
254
|
* Omitting it costs nothing but attribution.
|
|
255
|
+
*
|
|
256
|
+
* `source` is the node making a claim that names a rect rather than a
|
|
257
|
+
* node — `Node.invalidate` passes itself — and is what decides which of
|
|
258
|
+
* the frame's two lists the claim goes to: the window's, the one the
|
|
259
|
+
* panes over its `<glarea>`s paint from, or both (`_paneReach`). Left
|
|
260
|
+
* out, both.
|
|
238
261
|
*/
|
|
239
|
-
invalidate(layoutChanged, damage = null, reason = null) {
|
|
262
|
+
invalidate(layoutChanged, damage = null, reason = null, source = null) {
|
|
240
263
|
if (this.destroyed || !this.window) return;
|
|
241
264
|
if (!layoutChanged && damage === NO_DAMAGE) {
|
|
242
265
|
// Nothing this node draws changed, so it contributes no region — and
|
|
@@ -274,6 +297,16 @@ export class WindowInvalidate {
|
|
|
274
297
|
this._scheduleFrame();
|
|
275
298
|
return;
|
|
276
299
|
}
|
|
300
|
+
// The panes over this window's `<glarea>`s paint from a list of their
|
|
301
|
+
// own (`_paneDamage`): a claim goes to the window's list, the panes',
|
|
302
|
+
// or both, by what its source can reach (`_paneReach`). One `size`
|
|
303
|
+
// read for a window with no surface children.
|
|
304
|
+
const reach =
|
|
305
|
+
this._overlaid.size !== 0
|
|
306
|
+
? this._paneReach(damage?.kind ? damage : source)
|
|
307
|
+
: WINDOW_ONLY;
|
|
308
|
+
const panes = reach !== WINDOW_ONLY;
|
|
309
|
+
const windowToo = reach !== PANES_ONLY;
|
|
277
310
|
if (layoutChanged) {
|
|
278
311
|
this.needsLayout = true;
|
|
279
312
|
// The content floors are measured from the tree, so anything that
|
|
@@ -314,8 +347,11 @@ export class WindowInvalidate {
|
|
|
314
347
|
// where nothing can be seen, so it owes no pixels), and null on a frame
|
|
315
348
|
// that is already unbounded, which owes neither a rect nor the subtree
|
|
316
349
|
// walk that measures one — a blit cannot fire there either.
|
|
350
|
+
const bounded =
|
|
351
|
+
(windowToo && this._damage !== FULL_DAMAGE) ||
|
|
352
|
+
(panes && this._paneDamage !== FULL_DAMAGE);
|
|
317
353
|
const bounds =
|
|
318
|
-
damage && damage !== NO_DAMAGE &&
|
|
354
|
+
damage && damage !== NO_DAMAGE && bounded
|
|
319
355
|
? damage._claimBounds
|
|
320
356
|
? damage._claimBounds()
|
|
321
357
|
: damage
|
|
@@ -324,6 +360,9 @@ export class WindowInvalidate {
|
|
|
324
360
|
if (pendingScrolls?.size && bounds && this._scrollClaim !== damage) {
|
|
325
361
|
const rect = bounds;
|
|
326
362
|
for (const sv of pendingScrolls) {
|
|
363
|
+
// A claim the window's list never sees is no change to the pixels
|
|
364
|
+
// a blit of the window's moves: a surface's children are on panes
|
|
365
|
+
if (!windowToo && !insideSurface(sv, this)) continue;
|
|
327
366
|
// An element blitting a region of its own drawing (issue #303) is
|
|
328
367
|
// waiting on that region, not on the whole node it lives in — and
|
|
329
368
|
// it is waiting on it *exactly* (issue #309). Its claim is the rect
|
|
@@ -363,25 +402,102 @@ export class WindowInvalidate {
|
|
|
363
402
|
}
|
|
364
403
|
}
|
|
365
404
|
}
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
405
|
+
if (!damage) {
|
|
406
|
+
// "something, somewhere" — in the panes alone, for a node whose
|
|
407
|
+
// pixels are only ever on one; everywhere, for anything else
|
|
408
|
+
if (windowToo) this._damage = FULL_DAMAGE;
|
|
409
|
+
else this._damage ??= [];
|
|
410
|
+
if (panes) this._paneDamage = FULL_DAMAGE;
|
|
411
|
+
} else if (!bounds) {
|
|
369
412
|
// A layout change that names no region: either NO_DAMAGE, from a
|
|
370
413
|
// caller with a finer claim already in flight, or a node whose reach
|
|
371
414
|
// a clipping ancestor left nothing of (issue #398). Unlike `!damage`
|
|
372
|
-
// neither is "something, somewhere", so neither costs a full repaint
|
|
373
|
-
|
|
415
|
+
// neither is "something, somewhere", so neither costs a full repaint
|
|
416
|
+
// — which a frame with no list at all would be, however little its
|
|
417
|
+
// layout pass goes on to claim (`_takeDamage`). A child of a
|
|
418
|
+
// `<glarea>` that only moved is this claim (issue #644), and a frame
|
|
419
|
+
// whose move is settled on a pane claims only the strips it uncovered.
|
|
420
|
+
this._damage ??= [];
|
|
421
|
+
} else {
|
|
374
422
|
// a node, or a bare rect for a caller that has a region rather than a
|
|
375
423
|
// node — a subtree that is about to be removed, say. Claims accumulate
|
|
376
424
|
// as a list of rects rather than one box around them all, so two changes
|
|
377
425
|
// at opposite corners of the window no longer repaint everything
|
|
378
426
|
// between them.
|
|
379
|
-
|
|
427
|
+
if (!windowToo) {
|
|
428
|
+
// the window owes nothing, and a frame with no list is unbounded
|
|
429
|
+
this._damage ??= [];
|
|
430
|
+
} else if (this._damage !== FULL_DAMAGE) {
|
|
431
|
+
this._damage = addDamageRect(
|
|
432
|
+
this._damage,
|
|
433
|
+
bounds,
|
|
434
|
+
this._damageRectCap(),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (panes) this._addPaneDamage(bounds);
|
|
380
438
|
}
|
|
381
439
|
this.needsPaint = true;
|
|
382
440
|
this._scheduleFrame();
|
|
383
441
|
}
|
|
384
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Which damage lists a claim from `node` belongs in: the window's, the
|
|
445
|
+
* panes' over its `<glarea>`s, or both. The panes hold the surfaces'
|
|
446
|
+
* children and nothing else (src/gloverlay.js), and the window paints
|
|
447
|
+
* none of those, so:
|
|
448
|
+
*
|
|
449
|
+
* - a node inside a surface is the panes' alone — the window's pass under
|
|
450
|
+
* the surface would repaint pixels no child of it ever had;
|
|
451
|
+
* - a node above a surface is both: its colour or font is what the
|
|
452
|
+
* children below it paint with. So is a surface itself, whose
|
|
453
|
+
* `clearColor` an X11 pane's ground is, and so is a claim with no
|
|
454
|
+
* source to go by;
|
|
455
|
+
* - anything else is the window's alone. It draws only into the window,
|
|
456
|
+
* where the surface covers it: a graph pane under the surface that its
|
|
457
|
+
* own pan claims whole every frame (issue #644) is the claim this keeps
|
|
458
|
+
* out of the panes' repaint.
|
|
459
|
+
*/
|
|
460
|
+
_paneReach(node) {
|
|
461
|
+
if (!node || node === this) return BOTH;
|
|
462
|
+
if (insideSurface(node, this)) return PANES_ONLY;
|
|
463
|
+
if (node.isGlArea) return BOTH;
|
|
464
|
+
for (const area of this._overlaid) {
|
|
465
|
+
for (let n = area.parent; n && n !== this; n = n.parent) {
|
|
466
|
+
if (n === node) return BOTH;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return WINDOW_ONLY;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** One rect more for the panes, unless they repaint whole already. */
|
|
473
|
+
_addPaneDamage(rect) {
|
|
474
|
+
if (this._paneDamage === FULL_DAMAGE) return;
|
|
475
|
+
this._paneDamage = addDamageRect(
|
|
476
|
+
this._paneDamage,
|
|
477
|
+
rect,
|
|
478
|
+
this._damageRectCap(),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* What the panes repaint this frame: null for all of them, and — unlike
|
|
484
|
+
* the window's list — an empty list when nothing reached them, which is
|
|
485
|
+
* most frames of a window whose surface children hold still.
|
|
486
|
+
*/
|
|
487
|
+
_takePaneDamage(width, height) {
|
|
488
|
+
const damage = this._paneDamage;
|
|
489
|
+
this._paneDamage = null;
|
|
490
|
+
if (damage === FULL_DAMAGE) return null;
|
|
491
|
+
if (!damage || damage.length === 0) return [];
|
|
492
|
+
const rects = [];
|
|
493
|
+
for (const claimed of damage) {
|
|
494
|
+
const clamped = this._clampDamage(claimed, width, height);
|
|
495
|
+
if (clamped === FULL_DAMAGE) return null;
|
|
496
|
+
if (clamped) rects.push(clamped);
|
|
497
|
+
}
|
|
498
|
+
return damageToPaint(rects);
|
|
499
|
+
}
|
|
500
|
+
|
|
385
501
|
/**
|
|
386
502
|
* The rects this frame will repaint, or null for the whole window.
|
|
387
503
|
*
|
package/src/nodes/node.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
applyLayoutStyle,
|
|
14
14
|
applyLayoutDefaults,
|
|
15
15
|
createLayoutNode,
|
|
16
|
+
onlyInsetsChanged,
|
|
16
17
|
paintPropsChanged,
|
|
17
18
|
isEventProp,
|
|
18
19
|
} from '../styles.js';
|
|
@@ -473,10 +474,10 @@ export class Node {
|
|
|
473
474
|
// `_reflowed` would undo both — its post-layout claim is this node's
|
|
474
475
|
// box, the whole band the blit is about to move.
|
|
475
476
|
if (this._blitLedgerOpen()) {
|
|
476
|
-
root.invalidate(true, before ?? NO_DAMAGE, 'child-list');
|
|
477
|
+
root.invalidate(true, before ?? NO_DAMAGE, 'child-list', this);
|
|
477
478
|
return;
|
|
478
479
|
}
|
|
479
|
-
root.invalidate(true, before, 'child-list');
|
|
480
|
+
root.invalidate(true, before, 'child-list', this);
|
|
480
481
|
root._reflowed.add(this);
|
|
481
482
|
}
|
|
482
483
|
|
|
@@ -604,7 +605,21 @@ export class Node {
|
|
|
604
605
|
// which is what keeps a commit from widening the region to every node
|
|
605
606
|
// it touched.
|
|
606
607
|
if (layoutChanged) {
|
|
607
|
-
|
|
608
|
+
// A child of a `<glarea>` that only moved (issue #644): its pixels are
|
|
609
|
+
// on a pane, one shift away, and the layout pass reports the move to
|
|
610
|
+
// the overlay rather than claiming it (src/glnodes.js). A claim of the
|
|
611
|
+
// subtree here, before and after, would be the repaint the overlay
|
|
612
|
+
// exists to skip — and the pass claims both ends of the move itself
|
|
613
|
+
// whenever the overlay cannot move the pixels.
|
|
614
|
+
if (
|
|
615
|
+
this.parent?.isGlArea &&
|
|
616
|
+
onlyInsetsChanged(style, prevStyle) &&
|
|
617
|
+
!this.paintChanged(newProps, prev)
|
|
618
|
+
) {
|
|
619
|
+
this.invalidate(true, NO_DAMAGE, 'props');
|
|
620
|
+
} else {
|
|
621
|
+
this._invalidateLayout('props');
|
|
622
|
+
}
|
|
608
623
|
} else {
|
|
609
624
|
// The style half is asked here rather than inside `paintChanged`, and
|
|
610
625
|
// stays core's answer: what a style change moves is the background,
|
|
@@ -632,12 +647,16 @@ export class Node {
|
|
|
632
647
|
* safe without knowing about subclasses: `<image src>`, `<canvas onDraw>`,
|
|
633
648
|
* a `value`, a `placeholder`, a `caretColor` — any prop a subclass paints
|
|
634
649
|
* from is a prop, so a change to it lands here as an inequality and damages
|
|
635
|
-
* the node.
|
|
650
|
+
* the node. Four kinds are skipped because they cannot affect this node's
|
|
636
651
|
* own drawing:
|
|
637
652
|
*
|
|
638
653
|
* - `children`, which the reconciler mutates through appendChild /
|
|
639
654
|
* removeChild / commitTextUpdate, each of which invalidates on its own;
|
|
640
655
|
* - event handlers, rebuilt every render and never painted;
|
|
656
|
+
* - `ref`, which is React's rather than the element's. Since React 19 it
|
|
657
|
+
* arrives as an ordinary prop, so an inline callback is a new function
|
|
658
|
+
* every render, like a handler. `key` would be the same if React ever
|
|
659
|
+
* passed it on; today React strips it;
|
|
641
660
|
* - `style`, compared by value by the caller — so a style object React
|
|
642
661
|
* rebuilt with the same contents costs nothing, which is the whole
|
|
643
662
|
* point, since React rebuilds sibling styles on every render and a
|
|
@@ -669,6 +688,7 @@ export class Node {
|
|
|
669
688
|
const keys = new Set([...Object.keys(newProps), ...Object.keys(prev)]);
|
|
670
689
|
for (const key of keys) {
|
|
671
690
|
if (key === 'children' || key === 'style' || isEventProp(key)) continue;
|
|
691
|
+
if (key === 'ref' || key === 'key') continue;
|
|
672
692
|
if (claimed.has(key)) continue;
|
|
673
693
|
if (newProps[key] !== prev[key]) return true;
|
|
674
694
|
}
|
package/src/nodes/position.js
CHANGED
|
@@ -317,6 +317,7 @@ export class WindowPosition {
|
|
|
317
317
|
nodes.sort((a, b) => depth.get(a) - depth.get(b));
|
|
318
318
|
}
|
|
319
319
|
const cap = this._damageRectCap();
|
|
320
|
+
const panes = this._overlaid.size !== 0;
|
|
320
321
|
const t = now();
|
|
321
322
|
let claimed = false;
|
|
322
323
|
for (const node of nodes) {
|
|
@@ -330,7 +331,12 @@ export class WindowPosition {
|
|
|
330
331
|
? node.paintBounds()
|
|
331
332
|
: null;
|
|
332
333
|
node._placedShown = after;
|
|
333
|
-
if (
|
|
334
|
+
if (
|
|
335
|
+
this._damage === FULL_DAMAGE &&
|
|
336
|
+
(!panes || this._paneDamage === FULL_DAMAGE)
|
|
337
|
+
) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
334
340
|
const sv = node._blitViewport();
|
|
335
341
|
let was = before;
|
|
336
342
|
if (was && sv) {
|
|
@@ -354,7 +360,10 @@ export class WindowPosition {
|
|
|
354
360
|
if (sv && !sv._recordBlitClaim(claim)) {
|
|
355
361
|
sv._pendingBlitFrom = BLIT_POISONED;
|
|
356
362
|
}
|
|
357
|
-
|
|
363
|
+
if (this._damage !== FULL_DAMAGE) {
|
|
364
|
+
this._damage = addDamageRect(this._damage, claim, cap);
|
|
365
|
+
}
|
|
366
|
+
if (panes) this._addPaneDamage(claim);
|
|
358
367
|
claimed = true;
|
|
359
368
|
}
|
|
360
369
|
}
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -297,8 +297,11 @@ export const Scrollable = (Base) =>
|
|
|
297
297
|
// …which holds for a scroll's shift and not for the clamp's: the
|
|
298
298
|
// content shrank, or the viewport grew, under the offset — a row
|
|
299
299
|
// collapsing at the end of a list scrolled to its end — and
|
|
300
|
-
// nothing claimed the viewport every child just moved in.
|
|
301
|
-
|
|
300
|
+
// nothing claimed the viewport every child just moved in. Nor
|
|
301
|
+
// for a `<glarea>` child's move that this pane rides (issue
|
|
302
|
+
// #644): its box claimed nothing, and a pane laid out again may
|
|
303
|
+
// have moved rows the move's copy carries where they were.
|
|
304
|
+
if (clamped || layoutDiff.ride) outer(vp);
|
|
302
305
|
} else {
|
|
303
306
|
layoutDiff.sink = (rect) => {
|
|
304
307
|
const clipped = intersectRects(rect, vp);
|
package/src/nodes/scrollblit.js
CHANGED
|
@@ -409,7 +409,7 @@ export class NodeScrollBlit {
|
|
|
409
409
|
// reason to un-blit it. `state.rect` by identity, so a second call this
|
|
410
410
|
// frame is recognised as the same claim rather than as foreign damage.
|
|
411
411
|
root._scrollClaim = state.rect;
|
|
412
|
-
root.invalidate(false, state.rect, 'scroll');
|
|
412
|
+
root.invalidate(false, state.rect, 'scroll', this);
|
|
413
413
|
root._scrollClaim = null;
|
|
414
414
|
return this._pendingBlitFrom !== BLIT_POISONED;
|
|
415
415
|
}
|
|
@@ -485,8 +485,10 @@ export class WindowScrollBlit {
|
|
|
485
485
|
// claim the scroll recorded, like every other declined gate here
|
|
486
486
|
if (from === BLIT_POISONED) return;
|
|
487
487
|
if (NO_SCROLL_BLIT || !from || node.destroyed || node.root !== this) return;
|
|
488
|
-
|
|
489
|
-
|
|
488
|
+
// the window's backing store, or the pane over a `<glarea>` that holds
|
|
489
|
+
// the pixels of a node inside it — none where neither can move them
|
|
490
|
+
const target = this._blitTarget(node);
|
|
491
|
+
if (!target) return;
|
|
490
492
|
// the debug overlays and the DevTools highlight draw over the whole
|
|
491
493
|
// window; a blit would drag shifted copies of them along
|
|
492
494
|
if (
|
|
@@ -497,7 +499,8 @@ export class WindowScrollBlit {
|
|
|
497
499
|
) {
|
|
498
500
|
return;
|
|
499
501
|
}
|
|
500
|
-
|
|
502
|
+
// unbounded already, on the surface the blit would move
|
|
503
|
+
if (!Array.isArray(target.damage)) return;
|
|
501
504
|
// the layout diff claimed real movement this pass — the frame is not a
|
|
502
505
|
// pure scroll, and a blit under rearranged content would shift stale
|
|
503
506
|
// pixels into place the repaint no longer covers
|
|
@@ -506,7 +509,7 @@ export class WindowScrollBlit {
|
|
|
506
509
|
// region and the shift itself (issue #303), where a scroll container is
|
|
507
510
|
// its whole viewport shifted by the change in its offsets.
|
|
508
511
|
if (contents) {
|
|
509
|
-
this._applyContentsBlit(node, contents, width, height);
|
|
512
|
+
this._applyContentsBlit(node, contents, width, height, target);
|
|
510
513
|
return;
|
|
511
514
|
}
|
|
512
515
|
// Back where the frame started: a burst of scrolls that went one way
|
|
@@ -518,14 +521,14 @@ export class WindowScrollBlit {
|
|
|
518
521
|
// (Every pixel-shift gate below is moot for a shift of nothing.)
|
|
519
522
|
if (from.x === node.scrollX && from.y === node.scrollY) {
|
|
520
523
|
const box = node.abs;
|
|
521
|
-
const kept = this._blitKeptDamage(box);
|
|
524
|
+
const kept = this._blitKeptDamage(box, false, target.damage);
|
|
522
525
|
if (!kept) return;
|
|
523
526
|
let rects = kept;
|
|
524
527
|
for (const claim of ledger ?? []) {
|
|
525
528
|
const inside = intersectRects(claim, box);
|
|
526
529
|
if (inside) rects = addDamageRect(rects, inside);
|
|
527
530
|
}
|
|
528
|
-
|
|
531
|
+
target.damage = rects;
|
|
529
532
|
return;
|
|
530
533
|
}
|
|
531
534
|
// children clip to the border box, so a border ring or rounded corner
|
|
@@ -575,7 +578,7 @@ export class WindowScrollBlit {
|
|
|
575
578
|
) {
|
|
576
579
|
return;
|
|
577
580
|
}
|
|
578
|
-
const keep = this._blitKeptDamage(vp);
|
|
581
|
+
const keep = this._blitKeptDamage(vp, false, target.damage);
|
|
579
582
|
if (!keep) return;
|
|
580
583
|
// What changed inside the viewport while the blit was armed, in the
|
|
581
584
|
// coordinates the frame is about to paint in (issue #398). A claim made
|
|
@@ -635,7 +638,7 @@ export class WindowScrollBlit {
|
|
|
635
638
|
? unionArea([strip, ...repairs]) - rectArea(strip)
|
|
636
639
|
: 0;
|
|
637
640
|
if (repairArea > area * BLIT_MAX_CLAIM_AREA) return;
|
|
638
|
-
if (!this._scrollBlitSafe(node, vp)) return;
|
|
641
|
+
if (!this._scrollBlitSafe(node, vp, target.top)) return;
|
|
639
642
|
// The band the scrolled bar's thumb travels in is repaired on its own.
|
|
640
643
|
// The thumb's rects are thin and run along the viewport's edge, and a
|
|
641
644
|
// rect reaching into the band from inside — a column held down the
|
|
@@ -730,8 +733,8 @@ export class WindowScrollBlit {
|
|
|
730
733
|
if (inside) painted += inside.width * inside.height;
|
|
731
734
|
}
|
|
732
735
|
if (painted > area * SCROLL_BLIT_MAX_REPAINT) return;
|
|
733
|
-
if (!
|
|
734
|
-
|
|
736
|
+
if (!target.blit({ ...vp }, dx, dy)) return;
|
|
737
|
+
target.damage = rects;
|
|
735
738
|
}
|
|
736
739
|
|
|
737
740
|
/**
|
|
@@ -751,12 +754,15 @@ export class WindowScrollBlit {
|
|
|
751
754
|
* damage cap merges neighbours on its own, without a claim-time gate to
|
|
752
755
|
* poison first. Requiring the rect back unchanged is what lets the
|
|
753
756
|
* claim-time gate narrow to `vp` itself and let furniture beside it live.
|
|
757
|
+
*
|
|
758
|
+
* `damage` is the list the blit's surface paints from: the window's, or
|
|
759
|
+
* its panes' for a node inside a `<glarea>` (`_blitTarget`).
|
|
754
760
|
*/
|
|
755
|
-
_blitKeptDamage(vp, exact = false) {
|
|
761
|
+
_blitKeptDamage(vp, exact = false, damage = this._damage) {
|
|
756
762
|
const keep = [];
|
|
757
763
|
let sawClaim = false;
|
|
758
764
|
const slopped = insetRect(vp, -(DAMAGE_SLOP + 1));
|
|
759
|
-
for (const rect of
|
|
765
|
+
for (const rect of damage) {
|
|
760
766
|
if (!rectsOverlap(rect, vp)) {
|
|
761
767
|
keep.push(rect);
|
|
762
768
|
continue;
|
|
@@ -795,7 +801,7 @@ export class WindowScrollBlit {
|
|
|
795
801
|
* balloon back towards the whole viewport. With no bars the L is two
|
|
796
802
|
* disjoint rects and stays two.
|
|
797
803
|
*/
|
|
798
|
-
_applyContentsBlit(node, { rect: vp, dx, dy }, width, height) {
|
|
804
|
+
_applyContentsBlit(node, { rect: vp, dx, dy }, width, height, target) {
|
|
799
805
|
// only a whole-pixel shift of a whole-pixel region is a copy
|
|
800
806
|
if (!isIntegerRect(vp)) return;
|
|
801
807
|
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return;
|
|
@@ -840,14 +846,14 @@ export class WindowScrollBlit {
|
|
|
840
846
|
if (child.style?.display === 'none') continue;
|
|
841
847
|
if (rectsOverlap(child._subtreeBounds(), vp)) return;
|
|
842
848
|
}
|
|
843
|
-
const keep = this._blitKeptDamage(vp, true);
|
|
849
|
+
const keep = this._blitKeptDamage(vp, true, target.damage);
|
|
844
850
|
if (!keep) return;
|
|
845
851
|
// An ancestor's rounded corners reach into the top and bottom rows of
|
|
846
852
|
// the region (a graph pane inside a rounded card is the common shape):
|
|
847
853
|
// those rows do not translate, so they leave the blit and get repainted
|
|
848
854
|
// as bands — the same carve the element does for its own furniture —
|
|
849
855
|
// and the band that shifts is what is left between them.
|
|
850
|
-
const bands = this._cornerBands(node, vp);
|
|
856
|
+
const bands = this._cornerBands(node, vp, target.top);
|
|
851
857
|
const shifted =
|
|
852
858
|
bands.top || bands.bottom
|
|
853
859
|
? {
|
|
@@ -864,11 +870,11 @@ export class WindowScrollBlit {
|
|
|
864
870
|
) {
|
|
865
871
|
return;
|
|
866
872
|
}
|
|
867
|
-
if (!this._scrollBlitSafe(node, shifted)) return;
|
|
873
|
+
if (!this._scrollBlitSafe(node, shifted, target.top)) return;
|
|
868
874
|
// the element's deltas are already how far the pixels moved, the sense
|
|
869
875
|
// scrollRegion takes (0 + x rather than x: a caller's -0 would survive
|
|
870
876
|
// into request buffers and test comparisons)
|
|
871
|
-
if (!
|
|
877
|
+
if (!target.blit({ ...shifted }, 0 + dx, 0 + dy)) return;
|
|
872
878
|
let rects = keep;
|
|
873
879
|
if (bands.top) {
|
|
874
880
|
rects = addDamageRect(rects, {
|
|
@@ -908,7 +914,7 @@ export class WindowScrollBlit {
|
|
|
908
914
|
height: vp.height - Math.abs(dy),
|
|
909
915
|
});
|
|
910
916
|
}
|
|
911
|
-
|
|
917
|
+
target.damage = rects;
|
|
912
918
|
}
|
|
913
919
|
|
|
914
920
|
/**
|
|
@@ -916,11 +922,13 @@ export class WindowScrollBlit {
|
|
|
916
922
|
* corners reach into — the rows an element blit has to leave behind and
|
|
917
923
|
* repaint, so that what shifts stays clear of every corner square
|
|
918
924
|
* (`_scrollBlitSafe`). Whole pixels, and zero when no corner reaches in.
|
|
925
|
+
* Ancestors below `upTo`, the node the blit's surface belongs to
|
|
926
|
+
* (`_blitTarget`): a pane is a rectangle no corner above it clips.
|
|
919
927
|
*/
|
|
920
|
-
_cornerBands(node, vp) {
|
|
928
|
+
_cornerBands(node, vp, upTo = this) {
|
|
921
929
|
let top = 0;
|
|
922
930
|
let bottom = 0;
|
|
923
|
-
for (let n = node.parent; n && n !==
|
|
931
|
+
for (let n = node.parent; n && n !== upTo; n = n.parent) {
|
|
924
932
|
const radius = n.style?.borderRadius ?? 0;
|
|
925
933
|
if (!(radius > 0) || !n.abs) continue;
|
|
926
934
|
if (!cornerSquaresOverlap(n.abs, radius, vp)) continue;
|
|
@@ -940,13 +948,18 @@ export class WindowScrollBlit {
|
|
|
940
948
|
* the viewport — an overlapping sibling, an ancestor's border ring or
|
|
941
949
|
* rounded corner, an enclosing viewport's scrollbar — would have its
|
|
942
950
|
* pixels dragged along by the blit, so any of them is a no.
|
|
951
|
+
*
|
|
952
|
+
* "Outside" is below `top`, the node whose subtree the blit's surface
|
|
953
|
+
* holds (`_blitTarget`): the window, or a `<glarea>` whose pane holds
|
|
954
|
+
* only its own children — what the window draws under the surface was
|
|
955
|
+
* never in the pane's pixels.
|
|
943
956
|
*/
|
|
944
|
-
_scrollBlitSafe(scroller, vp) {
|
|
957
|
+
_scrollBlitSafe(scroller, vp, top = this) {
|
|
945
958
|
// the window itself scrolls: everything inside it *is* the scrolled
|
|
946
959
|
// content, so there is nothing that could be dragged along
|
|
947
960
|
if (scroller === this) return true;
|
|
948
961
|
const ancestors = new Set();
|
|
949
|
-
for (let n = scroller.parent; n && n !==
|
|
962
|
+
for (let n = scroller.parent; n && n !== top; n = n.parent) {
|
|
950
963
|
ancestors.add(n);
|
|
951
964
|
}
|
|
952
965
|
const check = (parent) => {
|
|
@@ -993,7 +1006,60 @@ export class WindowScrollBlit {
|
|
|
993
1006
|
}
|
|
994
1007
|
return true;
|
|
995
1008
|
};
|
|
996
|
-
return check(
|
|
1009
|
+
return check(top);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Where a blit for `node` moves pixels, as `{ top, blit(rect, dx, dy),
|
|
1014
|
+
* damage }` — `blit` with `Window.scrollRegion`'s contract, `top` the node
|
|
1015
|
+
* whose subtree that surface holds, `damage` the list it paints from and
|
|
1016
|
+
* the blit narrows — or null when nothing can.
|
|
1017
|
+
*
|
|
1018
|
+
* The window's backing store, for a node the window paints. A node inside
|
|
1019
|
+
* a `<glarea>` is painted on the pane over the surface instead
|
|
1020
|
+
* (src/gloverlay.js), and the window's pixels under the surface are none
|
|
1021
|
+
* of its: shifting them moved nothing on screen while the strip-only
|
|
1022
|
+
* repaint left the pane holding the scroll's old frame (issue #644). So
|
|
1023
|
+
* the pane that holds the rect moves it, or the frame repaints — a pane
|
|
1024
|
+
* owed a paint whole, or none holding the whole rect, is a refusal like
|
|
1025
|
+
* any other here.
|
|
1026
|
+
*/
|
|
1027
|
+
_blitTarget(node) {
|
|
1028
|
+
for (let n = node.parent; n && n !== this; n = n.parent) {
|
|
1029
|
+
if (!n.isGlArea) continue;
|
|
1030
|
+
const overlay = n._overlay;
|
|
1031
|
+
if (!overlay) return null;
|
|
1032
|
+
const root = this;
|
|
1033
|
+
return {
|
|
1034
|
+
top: n,
|
|
1035
|
+
blit: (rect, dx, dy) => {
|
|
1036
|
+
const pane = overlay.panes.find((p) => p.canBlit(rect));
|
|
1037
|
+
return pane ? pane.blit(rect, dx, dy) : false;
|
|
1038
|
+
},
|
|
1039
|
+
// the panes' list: a scroll inside a surface claims nothing in the
|
|
1040
|
+
// window's, which paints none of its children (`_paneReach`)
|
|
1041
|
+
get damage() {
|
|
1042
|
+
return root._paneDamage;
|
|
1043
|
+
},
|
|
1044
|
+
set damage(rects) {
|
|
1045
|
+
root._paneDamage = rects;
|
|
1046
|
+
},
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const wnd = this.window;
|
|
1050
|
+
// an ntk without #139, or a presenter with no bitmap to move
|
|
1051
|
+
if (typeof wnd?.scrollRegion !== 'function') return null;
|
|
1052
|
+
const root = this;
|
|
1053
|
+
return {
|
|
1054
|
+
top: this,
|
|
1055
|
+
blit: (rect, dx, dy) => wnd.scrollRegion(rect, dx, dy),
|
|
1056
|
+
get damage() {
|
|
1057
|
+
return root._damage;
|
|
1058
|
+
},
|
|
1059
|
+
set damage(rects) {
|
|
1060
|
+
root._damage = rects;
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
997
1063
|
}
|
|
998
1064
|
|
|
999
1065
|
/**
|
|
@@ -101,8 +101,8 @@ export class WindowFlush {
|
|
|
101
101
|
return changed;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** …and their paint, with the
|
|
105
|
-
*
|
|
104
|
+
/** …and their paint, with the damage the panes are owed — their own
|
|
105
|
+
* list, which the claims that can reach them go to (`_paneReach`). */
|
|
106
106
|
_paintOverlays(damage) {
|
|
107
107
|
for (const area of this._overlaid) area._paintOverlay(damage);
|
|
108
108
|
}
|
|
@@ -195,13 +195,21 @@ export class WindowFlush {
|
|
|
195
195
|
// A bounded frame watches the walk: whatever this pass actually moved
|
|
196
196
|
// claims its old and new rects through the sink, and the frame stays
|
|
197
197
|
// a few rects instead of the whole window. An unbounded frame skips
|
|
198
|
-
// the bookkeeping — it repaints everything anyway
|
|
199
|
-
|
|
198
|
+
// the bookkeeping — it repaints everything anyway — unless the panes
|
|
199
|
+
// over a `<glarea>` are still bounded: they paint from a list of
|
|
200
|
+
// their own, and a move of the children they hold is theirs to claim.
|
|
201
|
+
const panes = this._overlaid.size !== 0;
|
|
202
|
+
if (
|
|
203
|
+
this._damage !== FULL_DAMAGE ||
|
|
204
|
+
(panes && this._paneDamage !== FULL_DAMAGE)
|
|
205
|
+
) {
|
|
200
206
|
const cap = this._damageRectCap();
|
|
201
207
|
layoutDiff.sink = (rect) => {
|
|
202
|
-
if (this._damage
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
if (this._damage !== FULL_DAMAGE) {
|
|
209
|
+
layoutMoved = true;
|
|
210
|
+
this._damage = addDamageRect(this._damage, rect, cap);
|
|
211
|
+
}
|
|
212
|
+
if (panes) this._addPaneDamage(rect);
|
|
205
213
|
};
|
|
206
214
|
}
|
|
207
215
|
try {
|
|
@@ -233,7 +241,13 @@ export class WindowFlush {
|
|
|
233
241
|
for (const node of this._reflowed) {
|
|
234
242
|
// …and the pre-mutation walk this frame reused goes with it
|
|
235
243
|
node._reflowBefore = null;
|
|
236
|
-
if (
|
|
244
|
+
if (
|
|
245
|
+
node.destroyed ||
|
|
246
|
+
(this._damage === FULL_DAMAGE &&
|
|
247
|
+
(!panes || this._paneDamage === FULL_DAMAGE))
|
|
248
|
+
) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
237
251
|
// clipped to a blitting viewport above it, like every other claim
|
|
238
252
|
// this frame, and written to that viewport's ledger too (issue
|
|
239
253
|
// #398): the claim would otherwise coalesce into the scroll's own
|
|
@@ -245,11 +259,14 @@ export class WindowFlush {
|
|
|
245
259
|
if (sv && !sv._recordBlitClaim(after)) {
|
|
246
260
|
sv._pendingBlitFrom = BLIT_POISONED;
|
|
247
261
|
}
|
|
248
|
-
this._damage
|
|
249
|
-
this._damage
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
262
|
+
if (this._damage !== FULL_DAMAGE) {
|
|
263
|
+
this._damage = addDamageRect(
|
|
264
|
+
this._damage,
|
|
265
|
+
after,
|
|
266
|
+
this._damageRectCap(),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (panes) this._addPaneDamage(after);
|
|
253
270
|
}
|
|
254
271
|
this._reflowed.clear();
|
|
255
272
|
} else if (this._reflowed.size) {
|
|
@@ -286,6 +303,10 @@ export class WindowFlush {
|
|
|
286
303
|
if (!this.needsPaint) return false;
|
|
287
304
|
this.needsPaint = false;
|
|
288
305
|
const damage = this._takeDamage(width, height);
|
|
306
|
+
// …and the panes' own, which they paint instead of the window's
|
|
307
|
+
const paneDamage =
|
|
308
|
+
this._overlaid.size !== 0 ? this._takePaneDamage(width, height) : null;
|
|
309
|
+
this._paneDamage = null;
|
|
289
310
|
if (debugPaint === 'full' && !damage && width > 0 && height > 0) {
|
|
290
311
|
// Silent full-window repaints are the perf bug class this renderer
|
|
291
312
|
// actually has (see AGENTS.md); this is what surfaces them. The stack
|
|
@@ -305,8 +326,8 @@ export class WindowFlush {
|
|
|
305
326
|
// list was still taken (its bookkeeping is what keeps the two paths one
|
|
306
327
|
// code) and is simply not consumed; the presenter diffs at the layer.
|
|
307
328
|
if (typeof this.window.presentFrame === 'function') {
|
|
308
|
-
// the panes are no presenter's: they paint from
|
|
309
|
-
if (this._overlaid.size !== 0) this._paintOverlays(
|
|
329
|
+
// the panes are no presenter's: they paint from their damage either way
|
|
330
|
+
if (this._overlaid.size !== 0) this._paintOverlays(paneDamage);
|
|
310
331
|
this.window.presentFrame(this, damage);
|
|
311
332
|
this.app._reactX11Startup?.painted();
|
|
312
333
|
return true;
|
|
@@ -328,10 +349,11 @@ export class WindowFlush {
|
|
|
328
349
|
for (const rect of damage ?? [null]) {
|
|
329
350
|
this._paintRegion(ctx, rect, width, height);
|
|
330
351
|
}
|
|
331
|
-
// …and the panes over the surfaces, with
|
|
352
|
+
// …and the panes over the surfaces, with their own damage: a claim from
|
|
332
353
|
// a `<glarea>`'s children is theirs to repaint — the window's pass under
|
|
333
|
-
// the surface is one nobody sees
|
|
334
|
-
|
|
354
|
+
// the surface is one nobody sees — and one from a node that cannot reach
|
|
355
|
+
// them is not. Inside the cache's frame, like a pass.
|
|
356
|
+
if (this._overlaid.size !== 0) this._paintOverlays(paneDamage);
|
|
335
357
|
// after every region: an entry drawn in one damage rect must not be
|
|
336
358
|
// evicted before the next rect of the same frame asks for it
|
|
337
359
|
this._paintCache?.endFrame();
|
|
@@ -205,6 +205,10 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
205
205
|
// and paints with its damage (nodes/window/flush.js, src/gloverlay.js).
|
|
206
206
|
// Empty is one `size` read a frame.
|
|
207
207
|
this._overlaid = new Set();
|
|
208
|
+
// …and the damage their panes repaint, a list beside the window's:
|
|
209
|
+
// each claim goes to one or both by what it can reach (`_paneReach`).
|
|
210
|
+
// Null with nothing owed.
|
|
211
|
+
this._paneDamage = null;
|
|
208
212
|
this.events = new EventManager(this);
|
|
209
213
|
// ids of the child windows in the order the *server* stacks them,
|
|
210
214
|
// bottom to top — see _restackWindowChildren
|
package/src/styles.js
CHANGED
|
@@ -2008,6 +2008,28 @@ function samePosition(a, b) {
|
|
|
2008
2008
|
return true;
|
|
2009
2009
|
}
|
|
2010
2010
|
|
|
2011
|
+
/**
|
|
2012
|
+
* Whether the only properties that differ between two resolved styles are
|
|
2013
|
+
* the insets — `left`, `top` and the rest: a box that moved, and drew
|
|
2014
|
+
* nothing differently (issue #644). Every other key counts, whether layout,
|
|
2015
|
+
* paint or text reads it, so a style that also changed anything else
|
|
2016
|
+
* answers false.
|
|
2017
|
+
*/
|
|
2018
|
+
export function onlyInsetsChanged(props, oldProps = {}) {
|
|
2019
|
+
let moved = false;
|
|
2020
|
+
for (const key of Object.keys(props)) {
|
|
2021
|
+
if (props[key] === oldProps[key]) continue;
|
|
2022
|
+
if (!INSETS.has(key)) return false;
|
|
2023
|
+
moved = true;
|
|
2024
|
+
}
|
|
2025
|
+
for (const key of Object.keys(oldProps)) {
|
|
2026
|
+
if (key in props) continue;
|
|
2027
|
+
if (!INSETS.has(key)) return false;
|
|
2028
|
+
moved = true;
|
|
2029
|
+
}
|
|
2030
|
+
return moved;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2011
2033
|
/** @returns true if any paint-only prop changed */
|
|
2012
2034
|
export function paintPropsChanged(props, oldProps = {}) {
|
|
2013
2035
|
for (const key of PAINT_PROPS) {
|