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
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
// The move blit (issue #681). A subtree that only moved — the same size,
|
|
2
|
+
// somewhere else, nothing inside it changed — is already painted, one shift
|
|
3
|
+
// away. Where something in it covers its box with opaque pixels, what was
|
|
4
|
+
// under it cannot show through, so its pixels are copied from where they
|
|
5
|
+
// were to where they went (`Window.scrollRegion`, the verb the scroll blit
|
|
6
|
+
// moves a viewport's band with), and the frame paints only what the copy
|
|
7
|
+
// cannot supply: what the move uncovered, the parts of the subtree the cover
|
|
8
|
+
// does not reach, and whatever is drawn over it. A card of widgets dragged
|
|
9
|
+
// across a pane is that frame, once a step.
|
|
10
|
+
//
|
|
11
|
+
// The layout pass finds the move (`_beginRigidMove`, nodes/layout.js) and
|
|
12
|
+
// hands it here instead of claiming it, and the frame settles it once every
|
|
13
|
+
// other claim is in (`_settleRigidMoves`, from flush). Every gate falls back
|
|
14
|
+
// to the two claims the move would have made — where its pixels were and
|
|
15
|
+
// where they went — so the fast path can cost correctness nothing: the worst
|
|
16
|
+
// mistake it can make is not firing.
|
|
17
|
+
|
|
18
|
+
import { cssColorStraight } from 'ntk/color';
|
|
19
|
+
import { EMPTY_STYLE, resolveBorderWidths } from '../styles.js';
|
|
20
|
+
import { isPaintedColor } from './boxpaint.js';
|
|
21
|
+
import { DAMAGE_SLOP, FULL_DAMAGE, addDamageRect } from './damage.js';
|
|
22
|
+
import { NodePaint } from './paint.js';
|
|
23
|
+
import {
|
|
24
|
+
innerPixels,
|
|
25
|
+
insetRect,
|
|
26
|
+
intersectRects,
|
|
27
|
+
isIntegerRect,
|
|
28
|
+
outerPixels,
|
|
29
|
+
outside,
|
|
30
|
+
rectArea,
|
|
31
|
+
rectContains,
|
|
32
|
+
rectsOverlap,
|
|
33
|
+
shiftRect,
|
|
34
|
+
unionRect,
|
|
35
|
+
} from './rects.js';
|
|
36
|
+
import { scrollbarTrackRect } from './scrollbars.js';
|
|
37
|
+
import { debugPaint } from './window/debugpaint.js';
|
|
38
|
+
|
|
39
|
+
// Below this many pixels the copy is not worth its bookkeeping: the frame
|
|
40
|
+
// still repaints a strip along each side of it, a pass apiece, where a small
|
|
41
|
+
// box repainted whole is a pass or two.
|
|
42
|
+
const MOVE_BLIT_MIN_AREA = 32 * 32;
|
|
43
|
+
|
|
44
|
+
// Past this share of the copy, the frame would repaint most of the pixels it
|
|
45
|
+
// copied — around whatever else changed over the subtree — and the copy only
|
|
46
|
+
// adds to the bill. The `<glarea>` pane's copy stops at the same share
|
|
47
|
+
// (src/gloverlay.js).
|
|
48
|
+
const MOVE_BLIT_MAX_REPAINT = 0.75;
|
|
49
|
+
|
|
50
|
+
// The verb moves the band of the rect it is handed, and the window presents
|
|
51
|
+
// all of that rect: handed the box around both ends of a move, a jump across
|
|
52
|
+
// the window presents everything between them. Past this multiple of the
|
|
53
|
+
// copy, the two ends are repainted where they are instead.
|
|
54
|
+
const MOVE_BLIT_MAX_SPAN = 2;
|
|
55
|
+
|
|
56
|
+
// How many claims a frame logs with their nodes (`_logClaim`). A drag step
|
|
57
|
+
// makes a few — the element that draws the card a body sits on, a hover —
|
|
58
|
+
// and a frame past this is a commit the copy would not pay for anyway. Such
|
|
59
|
+
// a frame is judged from its damage list alone, every rect of it as if drawn
|
|
60
|
+
// over the subtree (`_claimsBySource`).
|
|
61
|
+
const CLAIM_LOG_MAX = 32;
|
|
62
|
+
|
|
63
|
+
// The scroll blit's escape hatch (nodes/scrollblit.js), read the same way:
|
|
64
|
+
// every path that moves retained pixels instead of repainting them is one
|
|
65
|
+
// variable away from the plain repaint (docs/debugging.md).
|
|
66
|
+
const NO_BLIT = process.env.REACT_X11_NO_SCROLL_BLIT === '1';
|
|
67
|
+
|
|
68
|
+
// Where a claim's node paints against the subtree being copied
|
|
69
|
+
// (`_claimSide`).
|
|
70
|
+
const BELOW = 0;
|
|
71
|
+
const INSIDE = 1;
|
|
72
|
+
const ABOVE = 2;
|
|
73
|
+
|
|
74
|
+
/** A colour nothing shows through. */
|
|
75
|
+
function isOpaqueColor(color) {
|
|
76
|
+
if (!isPaintedColor(color)) return false;
|
|
77
|
+
const rgba = cssColorStraight(color);
|
|
78
|
+
return rgba !== null && rgba[3] >= 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The squares at the corners of a box that a radius gives up, on the
|
|
82
|
+
* whole pixels they touch: where a rounded fill or clip is not opaque. */
|
|
83
|
+
function cornerSquares(box, radius) {
|
|
84
|
+
const r = Math.min(radius, box.width / 2, box.height / 2);
|
|
85
|
+
if (!(r > 0)) return [];
|
|
86
|
+
const right = box.x + box.width - r;
|
|
87
|
+
const bottom = box.y + box.height - r;
|
|
88
|
+
return [
|
|
89
|
+
{ x: box.x, y: box.y, width: r, height: r },
|
|
90
|
+
{ x: right, y: box.y, width: r, height: r },
|
|
91
|
+
{ x: box.x, y: bottom, width: r, height: r },
|
|
92
|
+
{ x: right, y: bottom, width: r, height: r },
|
|
93
|
+
].map(outerPixels);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The whole pixels a node that clips lets its children draw in: its box,
|
|
98
|
+
* less its radius all round when it is rounded — the corner squares are
|
|
99
|
+
* the pixels a rounded clip gives up — as `WindowNode._coverFor` has it.
|
|
100
|
+
* Null when nothing is left.
|
|
101
|
+
*/
|
|
102
|
+
function clipBoxOf(node) {
|
|
103
|
+
const radius = node.style?.borderRadius ?? 0;
|
|
104
|
+
return innerPixels(radius > 0 ? insetRect(node.abs, radius) : node.abs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether what `node` draws over its children is all core's to know the
|
|
109
|
+
* reach of: its border, its outline and its scrollbars. An element that
|
|
110
|
+
* overrides `paint` draws after `super.paint` — over its children, where
|
|
111
|
+
* only it knows.
|
|
112
|
+
*/
|
|
113
|
+
function drawsKnownOverChildren(node) {
|
|
114
|
+
if (node.paint === NodePaint.prototype.paint) return true;
|
|
115
|
+
let proto = Object.getPrototypeOf(node);
|
|
116
|
+
while (proto && !Object.hasOwn(proto, 'paint')) {
|
|
117
|
+
proto = Object.getPrototypeOf(proto);
|
|
118
|
+
}
|
|
119
|
+
// the Scrollable mixin's: `super.paint`, then the scrollbars
|
|
120
|
+
return proto !== null && Object.hasOwn(proto, '_paintScrollbars');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sameRects(a, b) {
|
|
124
|
+
if (a.length !== b.length) return false;
|
|
125
|
+
for (let i = 0; i < a.length; i++) {
|
|
126
|
+
if (
|
|
127
|
+
a[i].x !== b[i].x ||
|
|
128
|
+
a[i].y !== b[i].y ||
|
|
129
|
+
a[i].width !== b[i].width ||
|
|
130
|
+
a[i].height !== b[i].height
|
|
131
|
+
) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Node's half of the move blit, installed onto `Node.prototype` by node.js. */
|
|
139
|
+
export class NodeMoveBlit {
|
|
140
|
+
/**
|
|
141
|
+
* Where this subtree covers its box with opaque pixels on every paint, as
|
|
142
|
+
* `{ rect, holes }` — window coordinates, whole pixels — or null when it
|
|
143
|
+
* promises nothing. `rect` is the box, and `holes` the squares in it that
|
|
144
|
+
* a radius gives up, where what is under the box shows through its
|
|
145
|
+
* antialiased corners. Everywhere else in `rect` whatever was painted
|
|
146
|
+
* before the subtree is hidden, which is what makes a copy of its pixels
|
|
147
|
+
* exact there (`WindowNode._blitRigidMove`).
|
|
148
|
+
*
|
|
149
|
+
* Three things cover: a background colour nothing shows through; an
|
|
150
|
+
* element's own promise (`opaqueRect()`); and, for a node that makes
|
|
151
|
+
* neither, its one drawn child's cover, cut to the box when it clips — the
|
|
152
|
+
* wrappers a card is positioned and scaled by. A node with several
|
|
153
|
+
* children answers null rather than the biggest of them: this is asked on
|
|
154
|
+
* every step of a drag, and a walk of the subtree is the cost the copy
|
|
155
|
+
* exists to save.
|
|
156
|
+
*
|
|
157
|
+
* Nothing in a group composited at an alpha below one is opaque, so an
|
|
158
|
+
* `opacity` on the way down answers null; so does a node on a layer of its
|
|
159
|
+
* own, whose pixels are not the bitmap's (src/cocoa/promotion.js).
|
|
160
|
+
*/
|
|
161
|
+
_opaqueCover() {
|
|
162
|
+
let clip = null;
|
|
163
|
+
const holes = [];
|
|
164
|
+
for (let node = this; ;) {
|
|
165
|
+
if (node.hidden || node._promoted) return null;
|
|
166
|
+
const style = node.style ?? EMPTY_STYLE;
|
|
167
|
+
if (style.display === 'none') return null;
|
|
168
|
+
if (style.opacity !== undefined && !(style.opacity >= 1)) return null;
|
|
169
|
+
const radius = style.borderRadius ?? 0;
|
|
170
|
+
let own = null;
|
|
171
|
+
if (isOpaqueColor(style.backgroundColor)) {
|
|
172
|
+
own = node.abs;
|
|
173
|
+
holes.push(...cornerSquares(node.abs, radius));
|
|
174
|
+
} else {
|
|
175
|
+
own = node.opaqueRect();
|
|
176
|
+
}
|
|
177
|
+
if (own) {
|
|
178
|
+
const cut = clip ? intersectRects(own, clip) : own;
|
|
179
|
+
const rect = cut && innerPixels(cut);
|
|
180
|
+
if (!rect) return null;
|
|
181
|
+
return {
|
|
182
|
+
rect,
|
|
183
|
+
holes: holes
|
|
184
|
+
.map((hole) => intersectRects(hole, rect))
|
|
185
|
+
.filter(Boolean),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (node.clipsChildren()) {
|
|
189
|
+
// a rounded clip gives up its corners the way a rounded fill does
|
|
190
|
+
const box = innerPixels(node.abs);
|
|
191
|
+
clip = box && (clip ? intersectRects(clip, box) : box);
|
|
192
|
+
if (!clip) return null;
|
|
193
|
+
holes.push(...cornerSquares(node.abs, radius));
|
|
194
|
+
}
|
|
195
|
+
const order = node.paintOrder();
|
|
196
|
+
if (order.length !== 1) return null;
|
|
197
|
+
node = order[0];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The box this subtree's pixels are all inside, on whole pixels, with
|
|
203
|
+
* nothing to spare — or null when that cannot be promised, and the damage
|
|
204
|
+
* model's reach, a pixel of slop all round, is what has to be claimed.
|
|
205
|
+
*
|
|
206
|
+
* A claim is grown by a pixel because a fractional edge and a glyph's
|
|
207
|
+
* antialiasing put ink just outside a box. A box on whole pixels with no
|
|
208
|
+
* shadow and no outline inks nothing outside itself, and neither do its
|
|
209
|
+
* children when it clips them or none comes within a pixel of its edge.
|
|
210
|
+
* For a copy that pixel is not free: around a card it is four strips a
|
|
211
|
+
* pixel wide, a paint pass apiece, where the copy's own frame is the
|
|
212
|
+
* strip the move uncovered.
|
|
213
|
+
*/
|
|
214
|
+
_inkBox() {
|
|
215
|
+
const box = this.abs;
|
|
216
|
+
if (!isIntegerRect(box)) return null;
|
|
217
|
+
if (this._outlineExtent() > 0 || this._shadowExtent() > 0) return null;
|
|
218
|
+
if (!this.clipsChildren() && !inksWithin(this, box)) return null;
|
|
219
|
+
return box;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether everything `node`'s children draw stays inside `box`, whole
|
|
225
|
+
* pixels. A child's reach (`_subtreeBounds`, its shadow and outline in it)
|
|
226
|
+
* has to be inside the box; one that comes within a pixel of its edge has
|
|
227
|
+
* to be a box on whole pixels, whose fills and borders end where it does —
|
|
228
|
+
* text puts antialiased ink a pixel past its glyphs — and so, unless it
|
|
229
|
+
* clips them, do its own children: the wrapper a card's content is laid out
|
|
230
|
+
* in fills the card to its edge and draws nothing there.
|
|
231
|
+
*/
|
|
232
|
+
function inksWithin(node, box) {
|
|
233
|
+
const inner = insetRect(box, DAMAGE_SLOP);
|
|
234
|
+
for (const child of node.children) {
|
|
235
|
+
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
236
|
+
if (child.style?.display === 'none') continue;
|
|
237
|
+
const reach = child._subtreeBounds();
|
|
238
|
+
if (!rectContains(box, reach)) return false;
|
|
239
|
+
if (rectContains(inner, reach)) continue;
|
|
240
|
+
if (child.kind !== 'box' || !isIntegerRect(child.abs)) return false;
|
|
241
|
+
if (!child.clipsChildren() && !inksWithin(child, box)) return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** WindowNode's half of the move blit, installed onto `WindowNode.prototype` by window/window.js. */
|
|
247
|
+
export class WindowMoveBlit {
|
|
248
|
+
/**
|
|
249
|
+
* One rect this frame's list took (`invalidate`, `_claimLayoutMove`,
|
|
250
|
+
* `_placeNodes`), and the node whose claim it was: what tells a copy of a
|
|
251
|
+
* subtree a claim under it, which the copy covers, from one over it,
|
|
252
|
+
* which it has to repaint (`_claimSide`). A frame past `CLAIM_LOG_MAX`
|
|
253
|
+
* claims stops keeping them, and `_takeDamage` starts the next frame's.
|
|
254
|
+
*/
|
|
255
|
+
_logClaim(rect, source) {
|
|
256
|
+
const log = this._claimLog;
|
|
257
|
+
if (log === null) return;
|
|
258
|
+
if (log.length >= CLAIM_LOG_MAX) {
|
|
259
|
+
this._claimLog = null;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
log.push({
|
|
263
|
+
// a caller's rect can be a live one — a `contentBox()`, a pan's region
|
|
264
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
265
|
+
source,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Whether the move of `node` by `shift` could be copied rather than
|
|
271
|
+
* repainted, as far as the layout pass can tell before it walks the
|
|
272
|
+
* subtree (`_beginRigidMove`). A frame still bounded, with no scroll blit
|
|
273
|
+
* armed — two copies in one frame would each carry pixels the other made
|
|
274
|
+
* stale, which is why a scroll blit declines a frame the layout moved —
|
|
275
|
+
* and nothing drawn over the whole window that a copy would drag along. A
|
|
276
|
+
* whole-pixel shift, and a backing the window can move pixels in. And a
|
|
277
|
+
* subtree whose pixels are the window's to move: not inside a `<glarea>`,
|
|
278
|
+
* whose panes hold them (src/gloverlay.js), nor holding one with panes;
|
|
279
|
+
* not on a layer of its own or inside one; not inside a group faded
|
|
280
|
+
* through a surface of its own. Everything that needs the frame's other
|
|
281
|
+
* claims waits for the pass to end (`_settleRigidMoves`).
|
|
282
|
+
*/
|
|
283
|
+
_mayBlitMove(node, shift) {
|
|
284
|
+
if (NO_BLIT || this._damage === FULL_DAMAGE) return false;
|
|
285
|
+
if (this._pendingScrolls?.size) return false;
|
|
286
|
+
if (!Number.isInteger(shift.x) || !Number.isInteger(shift.y)) return false;
|
|
287
|
+
if (typeof this.window?.scrollRegion !== 'function') return false;
|
|
288
|
+
if (
|
|
289
|
+
debugPaint ||
|
|
290
|
+
process.env.REACT_X11_DEBUG_LAYOUT ||
|
|
291
|
+
this._highlight ||
|
|
292
|
+
this._traceUpdates
|
|
293
|
+
) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
if (node._promoted) return false;
|
|
297
|
+
for (let n = node.parent; n && n !== this; n = n.parent) {
|
|
298
|
+
if (n.isGlArea || n._promoted) return false;
|
|
299
|
+
const opacity = n.style?.opacity;
|
|
300
|
+
if (opacity !== undefined && !(opacity >= 1)) return false;
|
|
301
|
+
}
|
|
302
|
+
for (const area of this._overlaid) {
|
|
303
|
+
for (let n = area; n && n !== this; n = n.parent) {
|
|
304
|
+
if (n === node) return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A move `_mayBlitMove` let through, queued for `_settleRigidMoves`. */
|
|
311
|
+
_deferRigidMove(node, was, shift) {
|
|
312
|
+
(this._rigidMoves ??= []).push({ node, was, shift });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The moves this frame's layout pass queued, once every other claim of
|
|
317
|
+
* the frame is in. The one whose copy is biggest is copied where that is
|
|
318
|
+
* safe and pays (`_blitRigidMove`); every other one, and that one when
|
|
319
|
+
* not, is claimed as the pass would have claimed it. True when the
|
|
320
|
+
* window's list took one of those claims — a frame that is not a pure
|
|
321
|
+
* scroll (`_applyScrollBlits`).
|
|
322
|
+
*/
|
|
323
|
+
_settleRigidMoves(moves, width, height) {
|
|
324
|
+
let best = null;
|
|
325
|
+
let most = 0;
|
|
326
|
+
// a scroll armed while the pass ran is the one copy the frame gets
|
|
327
|
+
if (Array.isArray(this._damage) && !this._pendingScrolls?.size) {
|
|
328
|
+
for (const move of moves) {
|
|
329
|
+
const region = this._moveBlitRegion(move, width, height);
|
|
330
|
+
const size = region ? rectArea(region.dest) : 0;
|
|
331
|
+
if (size > most) {
|
|
332
|
+
best = { move, region };
|
|
333
|
+
most = size;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
let took = false;
|
|
338
|
+
// the others first: the copy has to account for what they claim
|
|
339
|
+
for (const move of moves) {
|
|
340
|
+
if (move !== best?.move && this._claimMove(move)) took = true;
|
|
341
|
+
}
|
|
342
|
+
if (best && !this._blitRigidMove(best.move, best.region)) {
|
|
343
|
+
if (this._claimMove(best.move)) took = true;
|
|
344
|
+
}
|
|
345
|
+
return took;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* A move claimed the plain way, as `Node._claimRigidMove` claims one no
|
|
350
|
+
* copy is waiting on: where the subtree's pixels were and where they went,
|
|
351
|
+
* each cut to what its clipping ancestors let reach the surface.
|
|
352
|
+
*/
|
|
353
|
+
_claimMove({ node, was, shift }) {
|
|
354
|
+
const cap = this._damageRectCap();
|
|
355
|
+
let took = false;
|
|
356
|
+
for (const end of [was, shiftRect(was, shift.x, shift.y)]) {
|
|
357
|
+
const clipped = node._clippedByAncestors(end);
|
|
358
|
+
if (clipped && this._claimLayoutMove(clipped, node, cap)) took = true;
|
|
359
|
+
}
|
|
360
|
+
return took;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Where a move's copy would go, or null when there is none to make.
|
|
365
|
+
* `dest` is the part of the subtree's opaque cover (`_opaqueCover`) that
|
|
366
|
+
* its clipping ancestors let through where it is and, moved back, where
|
|
367
|
+
* it was; `src` is `dest` moved back. A clip cuts a whole pixel whole only
|
|
368
|
+
* on a whole pixel and away from a rounded corner, so each clip is taken
|
|
369
|
+
* that way (`clipBoxOf`), and the window's edge is one more.
|
|
370
|
+
*
|
|
371
|
+
* A box with nothing drawn inside it is left to its repaint: that is one
|
|
372
|
+
* fill, where the copy is a CopyArea and a strip on each side.
|
|
373
|
+
*/
|
|
374
|
+
_moveBlitRegion({ node, shift }, width, height) {
|
|
375
|
+
if (node.destroyed || node.root !== this || node.hidden) return null;
|
|
376
|
+
if (node.kind === 'box' && node.paintOrder().length === 0) return null;
|
|
377
|
+
const cover = node._opaqueCover();
|
|
378
|
+
if (!cover) return null;
|
|
379
|
+
let clip = { x: 0, y: 0, width, height };
|
|
380
|
+
for (let n = node.parent; n && n !== this && clip; n = n.parent) {
|
|
381
|
+
if (!n.clipsChildren()) continue;
|
|
382
|
+
const box = clipBoxOf(n);
|
|
383
|
+
clip = box && intersectRects(clip, box);
|
|
384
|
+
}
|
|
385
|
+
if (!clip) return null;
|
|
386
|
+
const here = intersectRects(cover.rect, clip);
|
|
387
|
+
const dest =
|
|
388
|
+
here && intersectRects(here, shiftRect(clip, shift.x, shift.y));
|
|
389
|
+
if (!dest || rectArea(dest) < MOVE_BLIT_MIN_AREA) return null;
|
|
390
|
+
const src = shiftRect(dest, -shift.x, -shift.y);
|
|
391
|
+
if (rectArea(unionRect(src, dest)) > rectArea(dest) * MOVE_BLIT_MAX_SPAN) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const holes = [];
|
|
395
|
+
for (const hole of cover.holes) {
|
|
396
|
+
const inside = intersectRects(hole, dest);
|
|
397
|
+
if (inside) holes.push(inside);
|
|
398
|
+
}
|
|
399
|
+
return { dest, src, holes };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Copy a moved subtree's pixels where they went, and narrow what the
|
|
404
|
+
* frame owes the move to what the copy cannot supply. True when it did;
|
|
405
|
+
* false leaves the frame's damage as it was, for `_claimMove`.
|
|
406
|
+
*
|
|
407
|
+
* The copy is of what the subtree showed where it covers, moved as far as
|
|
408
|
+
* it moved (`dest`). Nothing under it shows through there, so the copy is
|
|
409
|
+
* right wherever its own drawing is all that is there — and the frame
|
|
410
|
+
* repaints the rest:
|
|
411
|
+
*
|
|
412
|
+
* - what the move uncovered, and the parts of the subtree the copy did
|
|
413
|
+
* not land on — its rounded corners, a shadow, a child reaching out of
|
|
414
|
+
* it: where its pixels were and where they went, less `dest`;
|
|
415
|
+
* - what is drawn over it: inside `dest` where it is, and where the copy
|
|
416
|
+
* carried what it covered (`_drawnOverMove`);
|
|
417
|
+
* - the frame's other claims, by whose they are (`_claimSide`). One from
|
|
418
|
+
* under the subtree is repainted outside `dest` alone, since nothing of
|
|
419
|
+
* it shows inside: the element that draws the card a mounted body sits
|
|
420
|
+
* on claims the box the card moved through, and most of that box is the
|
|
421
|
+
* body's. One from inside the subtree is repainted where it stands and
|
|
422
|
+
* moved with the copy, which covers it whichever side of the layout
|
|
423
|
+
* pass it was named on. One from over it, or from anywhere the log
|
|
424
|
+
* cannot say, is repainted where it stands and where the copy carried
|
|
425
|
+
* what it covered.
|
|
426
|
+
*
|
|
427
|
+
* Past `MOVE_BLIT_MAX_REPAINT` of `dest` those repaint most of what the
|
|
428
|
+
* copy saves, and the plain claims have the frame.
|
|
429
|
+
*/
|
|
430
|
+
_blitRigidMove({ node, was, shift }, { src, dest, holes }) {
|
|
431
|
+
const dx = shift.x;
|
|
432
|
+
const dy = shift.y;
|
|
433
|
+
const target = this._blitTarget(node);
|
|
434
|
+
if (target?.top !== this) return false;
|
|
435
|
+
const over = this._drawnOverMove(node, src, dest, dx, dy);
|
|
436
|
+
if (!over) return false;
|
|
437
|
+
// each ancestor of the subtree, with its child on the way down to it
|
|
438
|
+
const chain = new Map();
|
|
439
|
+
for (let n = node.parent, child = node; n; child = n, n = n.parent) {
|
|
440
|
+
chain.set(n, child);
|
|
441
|
+
if (n === this) break;
|
|
442
|
+
}
|
|
443
|
+
// Assembled uncapped: a cap merge would pick two pieces either side of
|
|
444
|
+
// the copy and put it back. Pieces cut around the same `dest` nest —
|
|
445
|
+
// bands above and below it full width, pieces beside it its rows only —
|
|
446
|
+
// so what overlaps coalesces without reaching into it.
|
|
447
|
+
let rects = [];
|
|
448
|
+
const add = (rect) => {
|
|
449
|
+
if (rect) rects = addDamageRect(rects, rect, Infinity);
|
|
450
|
+
};
|
|
451
|
+
for (const { rect, source } of this._claimsBySource()) {
|
|
452
|
+
const side = this._claimSide(source, node, chain);
|
|
453
|
+
if (side === BELOW) {
|
|
454
|
+
for (const piece of outside(rect, dest)) add(piece);
|
|
455
|
+
} else if (side === INSIDE) {
|
|
456
|
+
add(rect);
|
|
457
|
+
add(shiftRect(rect, dx, dy));
|
|
458
|
+
} else {
|
|
459
|
+
add(rect);
|
|
460
|
+
add(intersectRects(shiftRect(rect, dx, dy), dest));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// where the subtree's pixels were and where they went: its box when it
|
|
464
|
+
// inks nothing outside it, which leaves the strips the move uncovered
|
|
465
|
+
const ink = node._inkBox();
|
|
466
|
+
const ends = ink
|
|
467
|
+
? [shiftRect(ink, -dx, -dy), ink]
|
|
468
|
+
: [was, shiftRect(was, dx, dy)];
|
|
469
|
+
for (const end of ends) {
|
|
470
|
+
const clipped = node._clippedByAncestors(end);
|
|
471
|
+
if (!clipped) continue;
|
|
472
|
+
for (const piece of outside(clipped, dest)) add(piece);
|
|
473
|
+
}
|
|
474
|
+
for (const rect of over) add(rect);
|
|
475
|
+
// …and where the copy is not opaque: a rounded corner shows what was
|
|
476
|
+
// under it where it was, and has to show what is under it here
|
|
477
|
+
for (const hole of holes) add(hole);
|
|
478
|
+
// Each rect is a paint pass, and the frame's cap on them is the window's
|
|
479
|
+
// price for one (`_damageRectCap`). Past it the cap merges pieces either
|
|
480
|
+
// side of the copy into the box around them, and that box reaches into
|
|
481
|
+
// the copied content and repaints the widgets along its edge — on X11,
|
|
482
|
+
// four passes, a rounded card's four corners and the strip it uncovered
|
|
483
|
+
// cost more requests and more bytes than the card they spare. The plain
|
|
484
|
+
// claims have that frame.
|
|
485
|
+
if (rects.length > this._damageRectCap()) return false;
|
|
486
|
+
// the list is disjoint, so the sum is the area it covers
|
|
487
|
+
let repainted = 0;
|
|
488
|
+
for (const rect of rects) {
|
|
489
|
+
const inside = intersectRects(rect, dest);
|
|
490
|
+
if (inside) repainted += rectArea(inside);
|
|
491
|
+
}
|
|
492
|
+
if (repainted > rectArea(dest) * MOVE_BLIT_MAX_REPAINT) return false;
|
|
493
|
+
// the verb moves the band that survives inside the rect it is handed,
|
|
494
|
+
// so handed where the pixels are and where they go, that band is `dest`
|
|
495
|
+
if (!target.blit(unionRect(src, dest), dx, dy)) return false;
|
|
496
|
+
this._damage = rects;
|
|
497
|
+
// what the list holds now is no longer the claims it was built from
|
|
498
|
+
this._claimLog = null;
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* This frame's claims, each with the node that made it: the log, when it
|
|
504
|
+
* holds exactly what the list does. Replayed through the same merge it has
|
|
505
|
+
* to come out as the list itself — anything else means a writer of the
|
|
506
|
+
* list that did not log, and a log that says nothing reliable. Otherwise
|
|
507
|
+
* the list's own rects with no node, which `_claimSide` reads as drawn
|
|
508
|
+
* over the subtree.
|
|
509
|
+
*/
|
|
510
|
+
_claimsBySource() {
|
|
511
|
+
const log = this._claimLog;
|
|
512
|
+
const damage = this._damage;
|
|
513
|
+
if (log !== null) {
|
|
514
|
+
const cap = this._damageRectCap();
|
|
515
|
+
let replay = [];
|
|
516
|
+
for (const { rect } of log) replay = addDamageRect(replay, rect, cap);
|
|
517
|
+
if (sameRects(replay, damage)) return log;
|
|
518
|
+
}
|
|
519
|
+
return damage.map((rect) => ({ rect, source: null }));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Where a claim's node paints against the subtree being copied — inside
|
|
524
|
+
* it, under it or over it — found at the nearest ancestor the two share,
|
|
525
|
+
* by which of its children on the way down comes first in its paint
|
|
526
|
+
* order. An ancestor's own claim is over: its border, its outline and its
|
|
527
|
+
* scrollbars are drawn after its children. So is a claim with no node,
|
|
528
|
+
* the window's, or one whose node has left the tree.
|
|
529
|
+
*/
|
|
530
|
+
_claimSide(source, node, chain) {
|
|
531
|
+
if (
|
|
532
|
+
!source ||
|
|
533
|
+
source === this ||
|
|
534
|
+
source.destroyed ||
|
|
535
|
+
source.root !== this
|
|
536
|
+
) {
|
|
537
|
+
return ABOVE;
|
|
538
|
+
}
|
|
539
|
+
for (let n = source, branch = null; n; branch = n, n = n.parent) {
|
|
540
|
+
if (n === node) return INSIDE;
|
|
541
|
+
const mine = chain.get(n);
|
|
542
|
+
if (mine === undefined) continue;
|
|
543
|
+
if (branch === null) return ABOVE;
|
|
544
|
+
const order = n.paintOrder();
|
|
545
|
+
const at = order.indexOf(mine);
|
|
546
|
+
const theirs = order.indexOf(branch);
|
|
547
|
+
return at !== -1 && theirs !== -1 && theirs < at ? BELOW : ABOVE;
|
|
548
|
+
}
|
|
549
|
+
return ABOVE;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* What is drawn over a moved subtree, as the rects inside `dest` a copy
|
|
554
|
+
* leaves wrong: where each thing is, and where the copy carried what it
|
|
555
|
+
* covered of `src`. The later siblings in paint order — the subtree's,
|
|
556
|
+
* and each ancestor's up to the window — and each ancestor's own drawing
|
|
557
|
+
* over its children: the border, drawn inside its box and over them, as
|
|
558
|
+
* far in as a rounded one bends; the outline, outside the box, which a
|
|
559
|
+
* child reaches only past an ancestor that does not clip; the scrollbars.
|
|
560
|
+
*
|
|
561
|
+
* Null when an ancestor draws anything else over its children — an
|
|
562
|
+
* element that overrides `paint` — whose reach nothing here knows.
|
|
563
|
+
*/
|
|
564
|
+
_drawnOverMove(node, src, dest, dx, dy) {
|
|
565
|
+
const out = [];
|
|
566
|
+
const reach = unionRect(src, dest);
|
|
567
|
+
const over = (rect) => {
|
|
568
|
+
if (!rect || !rectsOverlap(rect, reach)) return;
|
|
569
|
+
const box = outerPixels(rect);
|
|
570
|
+
const here = intersectRects(box, dest);
|
|
571
|
+
if (here) out.push(here);
|
|
572
|
+
const carried = intersectRects(shiftRect(box, dx, dy), dest);
|
|
573
|
+
if (carried) out.push(carried);
|
|
574
|
+
};
|
|
575
|
+
for (let n = node.parent, child = node; n; child = n, n = n.parent) {
|
|
576
|
+
if (n !== this) {
|
|
577
|
+
if (!drawsKnownOverChildren(n)) return null;
|
|
578
|
+
const style = n.style ?? EMPTY_STYLE;
|
|
579
|
+
const bw = resolveBorderWidths(style, n.direction);
|
|
580
|
+
const ring = Math.max(bw.top, bw.right, bw.bottom, bw.left);
|
|
581
|
+
if (ring > 0) {
|
|
582
|
+
const inner = insetRect(
|
|
583
|
+
n.abs,
|
|
584
|
+
Math.max(ring, style.borderRadius ?? 0) + DAMAGE_SLOP,
|
|
585
|
+
);
|
|
586
|
+
const outer = insetRect(n.abs, -DAMAGE_SLOP);
|
|
587
|
+
for (const piece of outside(outer, inner)) over(piece);
|
|
588
|
+
}
|
|
589
|
+
const outline = n._outlineExtent();
|
|
590
|
+
if (outline > 0 && !n.clipsChildren()) {
|
|
591
|
+
const around = insetRect(n.abs, -(outline + DAMAGE_SLOP));
|
|
592
|
+
for (const piece of outside(around, n.abs)) over(piece);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (typeof n._scrollbars === 'function') {
|
|
596
|
+
for (const bar of n._scrollbars()) {
|
|
597
|
+
over(insetRect(scrollbarTrackRect(bar), -DAMAGE_SLOP));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const order = n.paintOrder();
|
|
601
|
+
const at = order.indexOf(child);
|
|
602
|
+
for (let i = at === -1 ? 0 : at + 1; i < order.length; i++) {
|
|
603
|
+
// on a layer of its own: nothing of it is in the bitmap
|
|
604
|
+
if (!order[i]._promoted) over(order[i].paintBounds());
|
|
605
|
+
}
|
|
606
|
+
if (n === this) break;
|
|
607
|
+
}
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
}
|
package/src/nodes/node.js
CHANGED
|
@@ -38,6 +38,7 @@ import { NodeInvalidate } from './invalidate.js';
|
|
|
38
38
|
import { CUSTOM_SELF_DAMAGED, THEME_SCOPE } from './kinds.js';
|
|
39
39
|
import { NodeLayout } from './layout.js';
|
|
40
40
|
import { NodeLayoutHost } from './layouthost.js';
|
|
41
|
+
import { NodeMoveBlit } from './moveblit.js';
|
|
41
42
|
import { NodePaint } from './paint.js';
|
|
42
43
|
import { NodePosition } from './position.js';
|
|
43
44
|
import { NodeQueries } from './queries.js';
|
|
@@ -620,21 +621,18 @@ export class Node {
|
|
|
620
621
|
typeof style.height === 'number'
|
|
621
622
|
? 'position'
|
|
622
623
|
: 'props';
|
|
623
|
-
// A
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
// riders)
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
moved &&
|
|
636
|
-
!this.paintChanged(newProps, prev)
|
|
637
|
-
) {
|
|
624
|
+
// A node that only moved claims nothing here: the layout pass claims
|
|
625
|
+
// both ends of the move itself — where the subtree's pixels were and
|
|
626
|
+
// where they went, once (`_beginRigidMove`), or node by node through
|
|
627
|
+
// the diff when the move turns out to be more than a move — and a
|
|
628
|
+
// claim of the subtree here, before and after, would be the repaint
|
|
629
|
+
// that three copies exist to skip. A child of a `<glarea>` is moved
|
|
630
|
+
// on its pane (issue #644, src/glnodes.js); a node an element's pan
|
|
631
|
+
// carries rides the region the frame blits (`scrollContents`'s
|
|
632
|
+
// riders, src/nodes/layout.js); and anything else that covers its
|
|
633
|
+
// box opaquely is copied in the window (issue #681,
|
|
634
|
+
// src/nodes/moveblit.js). Each claims what it cannot copy.
|
|
635
|
+
if (moved && !this.paintChanged(newProps, prev)) {
|
|
638
636
|
this.invalidate(true, NO_DAMAGE, reason);
|
|
639
637
|
} else {
|
|
640
638
|
this._invalidateLayout(reason);
|
|
@@ -920,6 +918,7 @@ installMethods(
|
|
|
920
918
|
NodeHitTest,
|
|
921
919
|
NodeInvalidate,
|
|
922
920
|
NodeScrollBlit,
|
|
921
|
+
NodeMoveBlit,
|
|
923
922
|
NodePaint,
|
|
924
923
|
NodeBoxPaint,
|
|
925
924
|
NodeSelectable,
|