react-x11 2.18.6 → 2.20.0
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/backend/context2d.js +14 -0
- package/src/glnodes.js +3 -1
- package/src/node.d.ts +23 -1
- package/src/nodes/animation.js +1 -0
- package/src/nodes/invalidate.js +5 -0
- package/src/nodes/layout.js +237 -12
- package/src/nodes/layouthost.js +2 -0
- package/src/nodes/node.js +8 -1
- package/src/nodes/queries.js +6 -1
- package/src/nodes/rects.js +13 -0
- package/src/nodes/scrollable.js +28 -10
- package/src/nodes/scrollblit.js +65 -10
- package/src/nodes/window/floors.js +90 -6
- package/src/nodes/window/size.js +214 -10
- package/src/nodes/window/window.js +6 -0
- package/src/paintcache.js +15 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"optionalDependencies": {
|
|
111
111
|
"@windowkit/appkit": "^0.12.0",
|
|
112
112
|
"@windowkit/wayland": "^3.1.1",
|
|
113
|
-
"@windowkit/win32": "^0.0.
|
|
113
|
+
"@windowkit/win32": "^0.0.3",
|
|
114
114
|
"dbus-native": "^0.15.1",
|
|
115
115
|
"x11-dri": "^0.9.0"
|
|
116
116
|
},
|
package/src/backend/context2d.js
CHANGED
|
@@ -577,6 +577,20 @@ export class BackendContext2D {
|
|
|
577
577
|
* dashes of an animated edge were repainted sixteen times a second and
|
|
578
578
|
* never moved.
|
|
579
579
|
*/
|
|
580
|
+
/**
|
|
581
|
+
* Text drawn under a transform that scales is drawn scaled, glyphs and
|
|
582
|
+
* all: both bridges lay a text layout out once and draw its outlines
|
|
583
|
+
* through the context's matrix (Direct2D's `DrawTextLayout`, CoreText's
|
|
584
|
+
* `CTFrameDraw`). ntk's context on X11 cannot — its glyphs come out of a
|
|
585
|
+
* cache rasterized at the size they were shaped at, and a transform
|
|
586
|
+
* moves where each lands, not how big it is — and says nothing, which is
|
|
587
|
+
* the same answer. So an element that would rather draw a layout it has
|
|
588
|
+
* than shape a new one at every step of a zoom asks this first.
|
|
589
|
+
*/
|
|
590
|
+
get scalesText() {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
580
594
|
get lineDashOffset() {
|
|
581
595
|
return this._state.dashOffset;
|
|
582
596
|
}
|
package/src/glnodes.js
CHANGED
|
@@ -379,8 +379,10 @@ export class GlAreaNode extends Node {
|
|
|
379
379
|
// `_assignAbs` keeps the object when nothing changed
|
|
380
380
|
const still = this.abs === was;
|
|
381
381
|
const { x, y } = this.abs;
|
|
382
|
+
const dx = x - was.x;
|
|
383
|
+
const dy = y - was.y;
|
|
382
384
|
for (const child of this.children) {
|
|
383
|
-
if (child.isWindow) continue;
|
|
385
|
+
if (child.isWindow || child._followParent(dx, dy)) continue;
|
|
384
386
|
if (still) this._absolutizeChild(child, x, y, overlay);
|
|
385
387
|
else child.absolutize(x, y);
|
|
386
388
|
}
|
package/src/node.d.ts
CHANGED
|
@@ -142,6 +142,15 @@ export interface Context2D {
|
|
|
142
142
|
strokeRect(x: number, y: number, width: number, height: number): void;
|
|
143
143
|
clearRect(x: number, y: number, width: number, height: number): void;
|
|
144
144
|
fillText(text: string, x: number, y: number): void;
|
|
145
|
+
/**
|
|
146
|
+
* `true` where text drawn under a transform that scales is drawn scaled,
|
|
147
|
+
* glyphs and all — the Windows and macOS contexts. Absent on X11, where
|
|
148
|
+
* ntk rasterizes glyphs at the size the text was shaped at and a
|
|
149
|
+
* transform moves only where each one lands. An element that would draw
|
|
150
|
+
* a layout it already has at another size, rather than shape a new one,
|
|
151
|
+
* asks this first.
|
|
152
|
+
*/
|
|
153
|
+
readonly scalesText?: boolean;
|
|
145
154
|
measureText(text: string): { width: number; [key: string]: unknown };
|
|
146
155
|
/**
|
|
147
156
|
* Composite an offscreen `Surface` (`react-x11/ntk`) — whole at a point,
|
|
@@ -536,8 +545,21 @@ export declare class Node {
|
|
|
536
545
|
* coordinates and inside this node. The return says whether the frame is
|
|
537
546
|
* still a blit candidate; the gates that matter close at frame time, and
|
|
538
547
|
* every one of them falls back to repainting `rect`.
|
|
548
|
+
*
|
|
549
|
+
* `riders` are other nodes whose pixels in `rect` are part of what moves —
|
|
550
|
+
* a layer of widgets the element lays out over its own drawing and pans
|
|
551
|
+
* with it. Each is to move by exactly (dx, dy) in this frame's layout pass
|
|
552
|
+
* and change nothing else; then it does not decline the blit, and only
|
|
553
|
+
* what it leaves or reaches outside `rect` is repainted, so keep what it
|
|
554
|
+
* can paint inside the element (a box that clips it). Anything else it
|
|
555
|
+
* does is claimed as usual, which declines the blit.
|
|
539
556
|
*/
|
|
540
|
-
scrollContents(
|
|
557
|
+
scrollContents(
|
|
558
|
+
rect: Rect,
|
|
559
|
+
dx: number,
|
|
560
|
+
dy: number,
|
|
561
|
+
riders?: Iterable<Node>,
|
|
562
|
+
): boolean;
|
|
541
563
|
/**
|
|
542
564
|
* Did anything this node draws change? True damages the whole node, false
|
|
543
565
|
* contributes no damage at all, and the default answers true for any prop
|
package/src/nodes/animation.js
CHANGED
package/src/nodes/invalidate.js
CHANGED
|
@@ -322,6 +322,11 @@ export class WindowInvalidate {
|
|
|
322
322
|
// panned — was a full measurement of the content floors a step.
|
|
323
323
|
if (reason !== 'scroll' && reason !== 'position') {
|
|
324
324
|
this._floorsDirty = true;
|
|
325
|
+
// …and which node changed, so that a change inside a box that sizes
|
|
326
|
+
// itself measures that box and nothing around it (`_floorsScope`).
|
|
327
|
+
// A claim that names no node, or names the window, is the tree's.
|
|
328
|
+
if (source && source !== this) this._floorsSources.add(source);
|
|
329
|
+
else this._floorsUnscoped = true;
|
|
325
330
|
}
|
|
326
331
|
if (reason !== 'scroll' && reason !== 'resize' && reason !== 'position') {
|
|
327
332
|
this._floorsContentDirty = true;
|
package/src/nodes/layout.js
CHANGED
|
@@ -5,9 +5,54 @@
|
|
|
5
5
|
import { Yoga } from '../yoga.js';
|
|
6
6
|
import { callHandler } from '../errors.js';
|
|
7
7
|
import { DAMAGE_SLOP, layoutDiff } from './damage.js';
|
|
8
|
-
import { insetRect } from './rects.js';
|
|
8
|
+
import { insetRect, intersectRects, outerPixels } from './rects.js';
|
|
9
9
|
import { DEV } from './util.js';
|
|
10
10
|
|
|
11
|
+
/** The parts of `rect` outside `hole`: up to four bands, full width above
|
|
12
|
+
* and below, the rest beside. */
|
|
13
|
+
function outside(rect, hole) {
|
|
14
|
+
const cut = intersectRects(rect, hole);
|
|
15
|
+
if (!cut) return [rect];
|
|
16
|
+
const out = [];
|
|
17
|
+
const right = rect.x + rect.width;
|
|
18
|
+
const bottom = rect.y + rect.height;
|
|
19
|
+
if (cut.y > rect.y) {
|
|
20
|
+
out.push({
|
|
21
|
+
x: rect.x,
|
|
22
|
+
y: rect.y,
|
|
23
|
+
width: rect.width,
|
|
24
|
+
height: cut.y - rect.y,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const cutBottom = cut.y + cut.height;
|
|
28
|
+
if (cutBottom < bottom) {
|
|
29
|
+
out.push({
|
|
30
|
+
x: rect.x,
|
|
31
|
+
y: cutBottom,
|
|
32
|
+
width: rect.width,
|
|
33
|
+
height: bottom - cutBottom,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (cut.x > rect.x) {
|
|
37
|
+
out.push({
|
|
38
|
+
x: rect.x,
|
|
39
|
+
y: cut.y,
|
|
40
|
+
width: cut.x - rect.x,
|
|
41
|
+
height: cut.height,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const cutRight = cut.x + cut.width;
|
|
45
|
+
if (cutRight < right) {
|
|
46
|
+
out.push({
|
|
47
|
+
x: cutRight,
|
|
48
|
+
y: cut.y,
|
|
49
|
+
width: right - cutRight,
|
|
50
|
+
height: cut.height,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
11
56
|
export const MEASURE_MODES = [];
|
|
12
57
|
MEASURE_MODES[Yoga.MEASURE_MODE_UNDEFINED] = 'unconstrained';
|
|
13
58
|
MEASURE_MODES[Yoga.MEASURE_MODE_EXACTLY] = 'exactly';
|
|
@@ -198,20 +243,200 @@ export class NodeLayout {
|
|
|
198
243
|
// its own — counts as on screen too
|
|
199
244
|
this._placed = true;
|
|
200
245
|
if (!this.yoga) return;
|
|
201
|
-
this.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.
|
|
246
|
+
const x = originX + this.yoga.getComputedLeft();
|
|
247
|
+
const y = originY + this.yoga.getComputedTop();
|
|
248
|
+
const width = this.yoga.getComputedWidth();
|
|
249
|
+
const height = this.yoga.getComputedHeight();
|
|
250
|
+
const wasX = this.abs.x;
|
|
251
|
+
const wasY = this.abs.y;
|
|
252
|
+
const move = this._beginRigidMove(x, y, width, height);
|
|
253
|
+
try {
|
|
254
|
+
this._assignAbs(x, y, width, height);
|
|
255
|
+
if (this.props.onLayout) this._reportLayout();
|
|
256
|
+
if (this._host !== null) {
|
|
257
|
+
this._absolutizeHostChildren();
|
|
258
|
+
} else {
|
|
259
|
+
const dx = this.abs.x - wasX;
|
|
260
|
+
const dy = this.abs.y - wasY;
|
|
261
|
+
for (const child of this.children) {
|
|
262
|
+
if (child.isWindow || child._followParent(dx, dy)) continue;
|
|
263
|
+
child.absolutize(this.abs.x, this.abs.y);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
if (move) layoutDiff.shift = null;
|
|
268
|
+
}
|
|
269
|
+
if (move) this._claimRigidMove(move);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A child of a node that moved this pass, which yoga did not lay out
|
|
274
|
+
* again: its box and everything under it sit exactly where the last pass
|
|
275
|
+
* put them relative to their parents, so the whole subtree moves by
|
|
276
|
+
* however far its parent did (`dx`, `dy`) — `_shiftAbs`, the scroll fast
|
|
277
|
+
* path's translation (issue #405), instead of the walk. Answers whether
|
|
278
|
+
* it was handled; false means walk it.
|
|
279
|
+
*
|
|
280
|
+
* The witness is yoga's has-new-layout flag, the one that fast path
|
|
281
|
+
* reads: a pass sets it on every node it lays out and on every child of
|
|
282
|
+
* one, cached or not, so a child it is clear on was not reached, nor was
|
|
283
|
+
* anything under it. Cleared here when it is found set, just before the
|
|
284
|
+
* walk that reads what the pass left — no pass runs in between — so the
|
|
285
|
+
* next move of the same subtree finds it clear. Nothing else pays for
|
|
286
|
+
* it: a child of a node that did not move is walked as it always was,
|
|
287
|
+
* and a scroll pane always is, since its flag is its own (and a scroll
|
|
288
|
+
* moves its children with no pass at all).
|
|
289
|
+
*
|
|
290
|
+
* Walking such a subtree read four numbers out of yoga per node and wrote
|
|
291
|
+
* them back unchanged: the bodies of a graph's cards, fifty widgets deep,
|
|
292
|
+
* on every step of a pan that moved the one box they sit in.
|
|
293
|
+
*
|
|
294
|
+
* A move it rode — the diff's `shift`, where the pixels are carried —
|
|
295
|
+
* claims nothing, as the walk would not have. A move of its own claims
|
|
296
|
+
* where the subtree's pixels were and where they are, which is what the
|
|
297
|
+
* walk's claims came to.
|
|
298
|
+
*/
|
|
299
|
+
_followParent(dx, dy) {
|
|
300
|
+
if (dx === 0 && dy === 0) return false;
|
|
301
|
+
const yoga = this.yoga;
|
|
302
|
+
if (!yoga || !this._placed || this.isScroller?.()) return false;
|
|
303
|
+
if (yoga.hasNewLayout()) {
|
|
304
|
+
yoga.markLayoutSeen();
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const sink = layoutDiff.sink;
|
|
308
|
+
const shift = layoutDiff.shift;
|
|
309
|
+
if (
|
|
310
|
+
sink &&
|
|
311
|
+
!(shift && shift.x === dx && shift.y === dy) &&
|
|
312
|
+
!this.hidden &&
|
|
313
|
+
this.style?.display !== 'none'
|
|
314
|
+
) {
|
|
315
|
+
const reach = this.paintBounds();
|
|
316
|
+
// where the walk's `_assignAbs` puts the near end: after the shift a
|
|
317
|
+
// scroll blit is about to make, when one is riding (issue #398)
|
|
318
|
+
sink(
|
|
319
|
+
shift
|
|
320
|
+
? {
|
|
321
|
+
x: reach.x + shift.x,
|
|
322
|
+
y: reach.y + shift.y,
|
|
323
|
+
width: reach.width,
|
|
324
|
+
height: reach.height,
|
|
325
|
+
}
|
|
326
|
+
: reach,
|
|
327
|
+
this,
|
|
328
|
+
);
|
|
329
|
+
sink(
|
|
330
|
+
{
|
|
331
|
+
x: reach.x + dx,
|
|
332
|
+
y: reach.y + dy,
|
|
333
|
+
width: reach.width,
|
|
334
|
+
height: reach.height,
|
|
335
|
+
},
|
|
336
|
+
this,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
this._shiftAbs(dx, dy);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The start of a move that changed nothing but where this subtree sits:
|
|
345
|
+
* the same size, somewhere else, in a bounded frame's layout diff that no
|
|
346
|
+
* shift is riding already. The walk below then runs with the diff's
|
|
347
|
+
* `shift` set to the move, so every descendant that lands where it was
|
|
348
|
+
* plus the move claims nothing — and one that lands anywhere else claims
|
|
349
|
+
* both ends of its own, as under a scroll blit (issue #398).
|
|
350
|
+
*
|
|
351
|
+
* What the subtree is owed instead is two claims: where its pixels were,
|
|
352
|
+
* and where they went (`_claimRigidMove`). Walked node by node, a moved
|
|
353
|
+
* card of fifty widgets claimed a hundred rects, the frame's cap merged
|
|
354
|
+
* them into their box, and the box of pieces reaching past every clipping
|
|
355
|
+
* ancestor was the whole window — a panned layer of such cards made every
|
|
356
|
+
* step of the pan a full repaint, header and all.
|
|
357
|
+
*
|
|
358
|
+
* Null when the move is not of that kind, or no diff is listening; the
|
|
359
|
+
* walk is then the plain one.
|
|
360
|
+
*/
|
|
361
|
+
_beginRigidMove(x, y, width, height) {
|
|
362
|
+
if (!layoutDiff.sink || layoutDiff.shift) return null;
|
|
363
|
+
const old = this.abs;
|
|
364
|
+
if (width !== old.width || height !== old.height) return null;
|
|
365
|
+
if (x === old.x && y === old.y) return null;
|
|
366
|
+
if (!(old.width > 0 && old.height > 0)) return null;
|
|
367
|
+
if (this.hidden || this.style?.display === 'none') return null;
|
|
368
|
+
// a placement moves it again after the walk (src/nodes/position.js)
|
|
369
|
+
if (this.root?._placedNodes?.has(this)) return null;
|
|
370
|
+
const shift = { x: x - old.x, y: y - old.y };
|
|
371
|
+
// before anything under it moves: every pixel it had
|
|
372
|
+
const was = this.paintBounds();
|
|
373
|
+
layoutDiff.shift = shift;
|
|
374
|
+
// Carried by an element's blit (`scrollContents`'s riders), when the
|
|
375
|
+
// move is the blit's own: then the pixels inside its rect are moved
|
|
376
|
+
// for it, and only what lies outside is owed.
|
|
377
|
+
const contents = this._ridesBlitOf?._pendingBlitContents;
|
|
378
|
+
const carried =
|
|
379
|
+
contents && contents.dx === shift.x && contents.dy === shift.y
|
|
380
|
+
? contents.rect
|
|
381
|
+
: null;
|
|
382
|
+
return { sink: layoutDiff.sink, was, shift, carried };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Both ends of a rigid move, each clipped to what the clipping ancestors
|
|
387
|
+
* let reach the surface. The far end is the near one moved: every
|
|
388
|
+
* descendant that rode the move is inside it, and one that did not has
|
|
389
|
+
* claimed where it went itself.
|
|
390
|
+
*
|
|
391
|
+
* Named for the node that moved, which is what sends each claim to the
|
|
392
|
+
* lists that paint it (`_paneReach`): the panes alone inside a
|
|
393
|
+
* `<glarea>`, both for a subtree holding one.
|
|
394
|
+
*/
|
|
395
|
+
_claimRigidMove({ sink, was, shift, carried }) {
|
|
396
|
+
const root = this.root;
|
|
397
|
+
const to = {
|
|
398
|
+
x: was.x + shift.x,
|
|
399
|
+
y: was.y + shift.y,
|
|
400
|
+
width: was.width,
|
|
401
|
+
height: was.height,
|
|
402
|
+
};
|
|
403
|
+
if (carried) {
|
|
404
|
+
// Straight to the window's list rather than through the diff, whose
|
|
405
|
+
// claims read as "this frame is not a pure scroll": what is outside
|
|
406
|
+
// the rect is beside it, and the blit stands.
|
|
407
|
+
const cap = root._damageRectCap();
|
|
408
|
+
for (const end of [was, to]) {
|
|
409
|
+
const clipped = this._clippedByAncestors(end);
|
|
410
|
+
if (!clipped) continue;
|
|
411
|
+
for (const piece of outside(clipped, carried)) {
|
|
412
|
+
root._claimLayoutMove(piece, this, cap);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
210
415
|
return;
|
|
211
416
|
}
|
|
212
|
-
|
|
213
|
-
|
|
417
|
+
const from = this._clippedByAncestors(was);
|
|
418
|
+
if (from) sink(from, this);
|
|
419
|
+
const at = this._clippedByAncestors(to);
|
|
420
|
+
if (at) sink(at, this);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* `rect` less whatever the ancestors that clip their children cut away,
|
|
425
|
+
* up to the window: pixels outside one never reach the surface, so a
|
|
426
|
+
* claim of them repaints what cannot change. Null when nothing is left.
|
|
427
|
+
* Each clip is the whole pixels the ancestor's box touches — its clip
|
|
428
|
+
* antialiases a fractional edge over the pixel it cuts, and on a box on
|
|
429
|
+
* whole pixels cuts exactly; a clip it skips is one nothing inside
|
|
430
|
+
* reaches (`_childrenCanOverflow`).
|
|
431
|
+
*/
|
|
432
|
+
_clippedByAncestors(rect) {
|
|
433
|
+
let out = rect;
|
|
434
|
+
for (let n = this.parent; n && out; n = n.parent) {
|
|
435
|
+
if (n.isWindow) break;
|
|
436
|
+
if (!n.clipsChildren()) continue;
|
|
437
|
+
out = intersectRects(out, outerPixels(n.abs));
|
|
214
438
|
}
|
|
439
|
+
return out;
|
|
215
440
|
}
|
|
216
441
|
|
|
217
442
|
/**
|
package/src/nodes/layouthost.js
CHANGED
|
@@ -415,6 +415,7 @@ export class NodeLayoutHost {
|
|
|
415
415
|
// content floors were measured from, as any style change is
|
|
416
416
|
root._floorsDirty = true;
|
|
417
417
|
root._floorsContentDirty = true;
|
|
418
|
+
root._floorsUnscoped = true;
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
|
|
@@ -944,6 +945,7 @@ export class WindowLayoutHost {
|
|
|
944
945
|
this._failedHosts.clear();
|
|
945
946
|
this._floorsDirty = true;
|
|
946
947
|
this._floorsContentDirty = true;
|
|
948
|
+
this._floorsUnscoped = true;
|
|
947
949
|
return true;
|
|
948
950
|
}
|
|
949
951
|
}
|
package/src/nodes/node.js
CHANGED
|
@@ -140,6 +140,9 @@ export class Node {
|
|
|
140
140
|
// palette with one against the app's own — and none of those is a
|
|
141
141
|
// *change* the user saw, so no transition may start from it.
|
|
142
142
|
this._placed = false;
|
|
143
|
+
// the element whose pending `scrollContents` carries this node's pixels
|
|
144
|
+
// this frame (src/nodes/scrollblit.js), or null
|
|
145
|
+
this._ridesBlitOf = null;
|
|
143
146
|
// the loops this node's style declares, whether or not they are running
|
|
144
147
|
this._loops = null;
|
|
145
148
|
// `resolvedTextStyle()`'s cache: this node's own text style over what it
|
|
@@ -623,8 +626,12 @@ export class Node {
|
|
|
623
626
|
// subtree here, before and after, would be the repaint the overlay
|
|
624
627
|
// exists to skip — and the pass claims both ends of the move itself
|
|
625
628
|
// whenever the overlay cannot move the pixels.
|
|
629
|
+
//
|
|
630
|
+
// …and so is a node an element's pan carries (`scrollContents`'s
|
|
631
|
+
// riders): its pixels move with the region the frame blits, and the
|
|
632
|
+
// pass claims what they leave outside it (src/nodes/layout.js).
|
|
626
633
|
if (
|
|
627
|
-
this.parent?.isGlArea &&
|
|
634
|
+
(this.parent?.isGlArea || this._ridesBlitOf !== null) &&
|
|
628
635
|
moved &&
|
|
629
636
|
!this.paintChanged(newProps, prev)
|
|
630
637
|
) {
|
package/src/nodes/queries.js
CHANGED
|
@@ -196,6 +196,7 @@ export class NodeQueries {
|
|
|
196
196
|
if (applyLayoutStyle(this.yoga, this.style, before) && this.root) {
|
|
197
197
|
this.root._floorsDirty = true;
|
|
198
198
|
this.root._floorsContentDirty = true;
|
|
199
|
+
this.root._floorsSources.add(this);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
}
|
|
@@ -233,6 +234,7 @@ export class WindowQueries {
|
|
|
233
234
|
// a query block may carry layout properties, so the floors measured from
|
|
234
235
|
// the styles it is replacing are not the answer any more
|
|
235
236
|
this._floorsDirty = true;
|
|
237
|
+
this._floorsUnscoped = true;
|
|
236
238
|
for (const node of [...this._sizeQueryNodes]) {
|
|
237
239
|
if (node.destroyed) this._sizeQueryNodes.delete(node);
|
|
238
240
|
else node._sizeQueriesChanged();
|
|
@@ -324,7 +326,10 @@ export class WindowQueries {
|
|
|
324
326
|
// a block may carry layout properties, so the floors measured from the
|
|
325
327
|
// styles it is replacing are not the answer any more — the same debt a
|
|
326
328
|
// window query leaves (`_resolveSizeQueries`)
|
|
327
|
-
if (changed)
|
|
329
|
+
if (changed) {
|
|
330
|
+
this._floorsDirty = true;
|
|
331
|
+
this._floorsUnscoped = true;
|
|
332
|
+
}
|
|
328
333
|
return changed;
|
|
329
334
|
}
|
|
330
335
|
}
|
package/src/nodes/rects.js
CHANGED
|
@@ -34,6 +34,19 @@ export function innerPixels(rect) {
|
|
|
34
34
|
return { x, y, width: right - x, height: bottom - y };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** The whole pixels `rect` touches — a fractional edge taken whole, which is
|
|
38
|
+
* where antialiasing can put ink. */
|
|
39
|
+
export function outerPixels(rect) {
|
|
40
|
+
const x = Math.floor(rect.x);
|
|
41
|
+
const y = Math.floor(rect.y);
|
|
42
|
+
return {
|
|
43
|
+
x,
|
|
44
|
+
y,
|
|
45
|
+
width: Math.ceil(rect.x + rect.width) - x,
|
|
46
|
+
height: Math.ceil(rect.y + rect.height) - y,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
/** The overlap of two rects, or null when they have none. */
|
|
38
51
|
export function intersectRects(a, b) {
|
|
39
52
|
const x = Math.max(a.x, b.x);
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -144,14 +144,28 @@ export const Scrollable = (Base) =>
|
|
|
144
144
|
absolutize(originX, originY) {
|
|
145
145
|
this._placed = true;
|
|
146
146
|
if (!this.yoga) return;
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
const x = originX + this.yoga.getComputedLeft();
|
|
148
|
+
const y = originY + this.yoga.getComputedTop();
|
|
149
|
+
const width = this.yoga.getComputedWidth();
|
|
150
|
+
const height = this.yoga.getComputedHeight();
|
|
151
|
+
// a box that moved and changed nothing else claims where it was and
|
|
152
|
+
// where it went, once — not once per node inside it (`Node`'s)
|
|
153
|
+
const wasX = this.abs.x;
|
|
154
|
+
const wasY = this.abs.y;
|
|
155
|
+
const move = this._beginRigidMove(x, y, width, height);
|
|
156
|
+
try {
|
|
157
|
+
this._assignAbs(x, y, width, height);
|
|
158
|
+
if (this.props.onLayout) this._reportLayout();
|
|
159
|
+
this._absolutizeChildren(
|
|
160
|
+
this.abs.x,
|
|
161
|
+
this.abs.y,
|
|
162
|
+
this.abs.x - wasX,
|
|
163
|
+
this.abs.y - wasY,
|
|
164
|
+
);
|
|
165
|
+
} finally {
|
|
166
|
+
if (move) layoutDiff.shift = null;
|
|
167
|
+
}
|
|
168
|
+
if (move) this._claimRigidMove(move);
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
/**
|
|
@@ -159,8 +173,11 @@ export const Scrollable = (Base) =>
|
|
|
159
173
|
* Split out of `absolutize` because a `<window>` writes its own `abs`
|
|
160
174
|
* during flush and then walks its children from (0, 0) — the same walk,
|
|
161
175
|
* reached by a different route.
|
|
176
|
+
*
|
|
177
|
+
* `dx`/`dy` is how far this box moved in this pass, which is how far a
|
|
178
|
+
* child yoga did not lay out again moves with it (`_followParent`).
|
|
162
179
|
*/
|
|
163
|
-
_absolutizeChildren(originX, originY) {
|
|
180
|
+
_absolutizeChildren(originX, originY, dx = 0, dy = 0) {
|
|
164
181
|
if (!this.isScroller()) {
|
|
165
182
|
// a layout host's children go where its algorithm put them
|
|
166
183
|
if (this._host !== null) {
|
|
@@ -168,7 +185,8 @@ export const Scrollable = (Base) =>
|
|
|
168
185
|
return;
|
|
169
186
|
}
|
|
170
187
|
for (const child of this.children) {
|
|
171
|
-
if (
|
|
188
|
+
if (child.isWindow || child._followParent(dx, dy)) continue;
|
|
189
|
+
child.absolutize(originX, originY);
|
|
172
190
|
}
|
|
173
191
|
return;
|
|
174
192
|
}
|
package/src/nodes/scrollblit.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
cornerSquaresOverlap,
|
|
21
21
|
rectsOverlap,
|
|
22
22
|
} from './rects.js';
|
|
23
|
+
import { isPaintedColor } from './boxpaint.js';
|
|
23
24
|
import { scrollbarTrackRect } from './scrollbars.js';
|
|
24
25
|
import { debugPaint } from './window/debugpaint.js';
|
|
25
26
|
|
|
@@ -336,11 +337,22 @@ export class NodeScrollBlit {
|
|
|
336
337
|
* and claims it as ordinary damage. Those claims land beside the rect and
|
|
337
338
|
* the frame stays a blit.
|
|
338
339
|
*
|
|
340
|
+
* `riders` are the other way round: nodes that are not this one whose
|
|
341
|
+
* pixels in `rect` are part of what moves — a layer of widgets an element
|
|
342
|
+
* lays out over its own drawing and pans with it (a graph's node bodies).
|
|
343
|
+
* Each is to move by exactly (dx, dy) in this frame's layout pass and to
|
|
344
|
+
* change nothing else. Doing that, it does not decline the blit by being
|
|
345
|
+
* over the rect, its commit claims nothing, and its move claims only
|
|
346
|
+
* what it leaves or reaches outside the rect (`_claimRigidMove`) — so
|
|
347
|
+
* keep what it can paint inside the element's box, a box that clips it
|
|
348
|
+
* being the way. Doing anything else, it is claimed like any other node,
|
|
349
|
+
* which declines the blit; the rect then repaints, riders and all.
|
|
350
|
+
*
|
|
339
351
|
* Returns whether the frame is still a blit candidate. The real answer
|
|
340
352
|
* arrives as `paintDamage()`, because most of the gates cannot be decided
|
|
341
353
|
* until the frame closes; a `false` here is only the ones that can.
|
|
342
354
|
*/
|
|
343
|
-
scrollContents(rect, dx, dy) {
|
|
355
|
+
scrollContents(rect, dx, dy, riders) {
|
|
344
356
|
const root = this.root;
|
|
345
357
|
if (
|
|
346
358
|
!root ||
|
|
@@ -403,6 +415,13 @@ export class NodeScrollBlit {
|
|
|
403
415
|
// coalescing scrollTo gets from recording an origin
|
|
404
416
|
state.dx += dx;
|
|
405
417
|
state.dy += dy;
|
|
418
|
+
if (riders) {
|
|
419
|
+
for (const rider of riders) {
|
|
420
|
+
if (!rider || rider === this || rider.destroyed) continue;
|
|
421
|
+
(state.riders ??= new Set()).add(rider);
|
|
422
|
+
rider._ridesBlitOf = this;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
406
425
|
this._pendingBlitFrom ??= BLIT_CONTENTS;
|
|
407
426
|
(root._pendingScrolls ??= new Set()).add(this);
|
|
408
427
|
// ...and the claim about to be recorded is the shift itself, not a
|
|
@@ -426,15 +445,34 @@ export class NodeScrollBlit {
|
|
|
426
445
|
* own, leave `_blitKeptDamage` a damage rect many times the viewport to
|
|
427
446
|
* refuse. Null when the clip left nothing.
|
|
428
447
|
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
448
|
+
* And clipped to every other clipping ancestor too, for the same reason
|
|
449
|
+
* without the blit: a claim past an `overflow: 'hidden'` box repaints
|
|
450
|
+
* pixels nothing inside it can reach. A layer of cards panned inside a
|
|
451
|
+
* clipped graph pane reaches past the pane on every side, and its
|
|
452
|
+
* claims, unclipped, were the whole window.
|
|
433
453
|
*/
|
|
434
454
|
_claimBounds() {
|
|
435
455
|
const bounds = this.paintBounds();
|
|
436
456
|
const sv = this._blitViewport();
|
|
437
|
-
|
|
457
|
+
const clipped = sv ? intersectRects(bounds, sv.paintBounds()) : bounds;
|
|
458
|
+
return clipped && this._clippedByAncestors(clipped);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Whether this node puts no pixels of its own on the surface — a plain
|
|
463
|
+
* box with no fill, border, shadow or outline, holding what is under it
|
|
464
|
+
* and nothing more. Conservative: anything but a box answers false.
|
|
465
|
+
*/
|
|
466
|
+
_paintsNothingOwn() {
|
|
467
|
+
if (this.kind !== 'box' || this.isScroller?.()) return false;
|
|
468
|
+
const style = this.style ?? EMPTY_STYLE;
|
|
469
|
+
if (isPaintedColor(style.backgroundColor) || style.backgroundImage) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
if (style.opacity !== undefined && style.opacity < 1) return false;
|
|
473
|
+
if (this._outlineExtent() > 0 || this._shadowExtent() > 0) return false;
|
|
474
|
+
const bw = resolveBorderWidths(style, this.direction);
|
|
475
|
+
return !(bw.top > 0 || bw.right > 0 || bw.bottom > 0 || bw.left > 0);
|
|
438
476
|
}
|
|
439
477
|
|
|
440
478
|
/** The scroll container above this node that is waiting to blit, if there
|
|
@@ -473,6 +511,10 @@ export class WindowScrollBlit {
|
|
|
473
511
|
const contents = nodes[0]._pendingBlitContents;
|
|
474
512
|
const ledger = nodes[0]._blitLedger;
|
|
475
513
|
for (const n of nodes) {
|
|
514
|
+
// the pass that needed them is over: a rider is a rider for a frame
|
|
515
|
+
for (const rider of n._pendingBlitContents?.riders ?? []) {
|
|
516
|
+
if (rider._ridesBlitOf === n) rider._ridesBlitOf = null;
|
|
517
|
+
}
|
|
476
518
|
n._pendingBlitFrom = null;
|
|
477
519
|
n._pendingBlitContents = null;
|
|
478
520
|
n._blitLedger = null;
|
|
@@ -801,7 +843,13 @@ export class WindowScrollBlit {
|
|
|
801
843
|
* balloon back towards the whole viewport. With no bars the L is two
|
|
802
844
|
* disjoint rects and stays two.
|
|
803
845
|
*/
|
|
804
|
-
_applyContentsBlit(
|
|
846
|
+
_applyContentsBlit(
|
|
847
|
+
node,
|
|
848
|
+
{ rect: vp, dx, dy, riders },
|
|
849
|
+
width,
|
|
850
|
+
height,
|
|
851
|
+
target,
|
|
852
|
+
) {
|
|
805
853
|
// only a whole-pixel shift of a whole-pixel region is a copy
|
|
806
854
|
if (!isIntegerRect(vp)) return;
|
|
807
855
|
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return;
|
|
@@ -870,7 +918,7 @@ export class WindowScrollBlit {
|
|
|
870
918
|
) {
|
|
871
919
|
return;
|
|
872
920
|
}
|
|
873
|
-
if (!this._scrollBlitSafe(node, shifted, target.top)) return;
|
|
921
|
+
if (!this._scrollBlitSafe(node, shifted, target.top, riders)) return;
|
|
874
922
|
// the element's deltas are already how far the pixels moved, the sense
|
|
875
923
|
// scrollRegion takes (0 + x rather than x: a caller's -0 would survive
|
|
876
924
|
// into request buffers and test comparisons)
|
|
@@ -954,7 +1002,7 @@ export class WindowScrollBlit {
|
|
|
954
1002
|
* only its own children — what the window draws under the surface was
|
|
955
1003
|
* never in the pane's pixels.
|
|
956
1004
|
*/
|
|
957
|
-
_scrollBlitSafe(scroller, vp, top = this) {
|
|
1005
|
+
_scrollBlitSafe(scroller, vp, top = this, riders = null) {
|
|
958
1006
|
// the window itself scrolls: everything inside it *is* the scrolled
|
|
959
1007
|
// content, so there is nothing that could be dragged along
|
|
960
1008
|
if (scroller === this) return true;
|
|
@@ -1012,7 +1060,14 @@ export class WindowScrollBlit {
|
|
|
1012
1060
|
// be dragged along — a pulsing toast over a list is what promotion
|
|
1013
1061
|
// is for, and this is the half of it that keeps the pan a blit.
|
|
1014
1062
|
if (child._promoted) continue;
|
|
1015
|
-
|
|
1063
|
+
// …and a node the blit carries has pixels that move with it
|
|
1064
|
+
// (`scrollContents`'s riders)
|
|
1065
|
+
if (riders?.has(child)) continue;
|
|
1066
|
+
if (!rectsOverlap(child._subtreeBounds(), vp)) continue;
|
|
1067
|
+
// A box that paints nothing of its own is its children, and as
|
|
1068
|
+
// safe as they are: the box that clips a rider to its element.
|
|
1069
|
+
if (child._paintsNothingOwn() && check(child)) continue;
|
|
1070
|
+
return false;
|
|
1016
1071
|
}
|
|
1017
1072
|
return true;
|
|
1018
1073
|
};
|
|
@@ -197,6 +197,88 @@ export function collectFloorStale(node, stale, found, sweep) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Whether `node` is a box its content cannot size and its surroundings cannot
|
|
202
|
+
* size either — an edge a floor measurement may stop at
|
|
203
|
+
* (`WindowNode._floorsScope`).
|
|
204
|
+
*
|
|
205
|
+
* Absolutely positioned, with a width and a height its style names as
|
|
206
|
+
* numbers: it is out of its parent's flow, so nothing measured inside it
|
|
207
|
+
* reaches anything above it (`contentSpan` skips it, and no floor is written
|
|
208
|
+
* on it), and every pass lays it out at those numbers wherever it sits, so
|
|
209
|
+
* what is inside it is measured the same in a pass over the window and in a
|
|
210
|
+
* pass over it alone. Nothing in its own box resolves against its parent —
|
|
211
|
+
* a percentage of padding would — and neither it nor its parent is a layout
|
|
212
|
+
* host, whose children are measured tree by tree.
|
|
213
|
+
*
|
|
214
|
+
* A card on a graph, a popover laid out inside the window, a sized overlay:
|
|
215
|
+
* the boxes a change inside is most often confined to. An in-flow box with a
|
|
216
|
+
* size of its own would do as well along its cross axis, but on its main
|
|
217
|
+
* axis a flex line can grow it past its width or shrink it under a minimum
|
|
218
|
+
* the author named, and that is a question about its siblings.
|
|
219
|
+
*/
|
|
220
|
+
export function isFloorBoundary(node) {
|
|
221
|
+
const style = node.style;
|
|
222
|
+
return (
|
|
223
|
+
node.yoga != null &&
|
|
224
|
+
!node.isWindow &&
|
|
225
|
+
style.position === 'absolute' &&
|
|
226
|
+
style.display !== 'none' &&
|
|
227
|
+
typeof style.width === 'number' &&
|
|
228
|
+
typeof style.height === 'number' &&
|
|
229
|
+
typeof style.minWidth !== 'string' &&
|
|
230
|
+
typeof style.minHeight !== 'string' &&
|
|
231
|
+
typeof style.maxWidth !== 'string' &&
|
|
232
|
+
typeof style.maxHeight !== 'string' &&
|
|
233
|
+
!PADDINGS.some((key) => typeof style[key] === 'string') &&
|
|
234
|
+
node._host === null &&
|
|
235
|
+
(node.parent?._host ?? null) === null
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const PADDINGS = [
|
|
240
|
+
'padding',
|
|
241
|
+
'paddingTop',
|
|
242
|
+
'paddingRight',
|
|
243
|
+
'paddingBottom',
|
|
244
|
+
'paddingLeft',
|
|
245
|
+
'paddingHorizontal',
|
|
246
|
+
'paddingVertical',
|
|
247
|
+
'paddingStart',
|
|
248
|
+
'paddingEnd',
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The box that holds a change to `node` in (`isFloorBoundary`), or null when
|
|
253
|
+
* none does below `root`. Looked for from the parent up: a node that changed
|
|
254
|
+
* may have changed its own size, and it is what is *around* the change that
|
|
255
|
+
* has to hold still.
|
|
256
|
+
*/
|
|
257
|
+
export function floorBoundaryOf(node, root) {
|
|
258
|
+
for (let n = node.parent; n && n !== root; n = n.parent) {
|
|
259
|
+
if (n.isWindow) return null;
|
|
260
|
+
if (isFloorBoundary(n)) return n;
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Whether yoga has a change on record anywhere but inside `boundaries` and on
|
|
267
|
+
* the way down to them (`paths`) — something that changed with no node
|
|
268
|
+
* saying so, which a measurement confined to the boundaries would miss. A
|
|
269
|
+
* dirty node's ancestors are dirty too, so the walk goes down dirty nodes
|
|
270
|
+
* only, and a clean one has nothing under it to find.
|
|
271
|
+
*/
|
|
272
|
+
export function dirtyOutside(node, boundaries, paths) {
|
|
273
|
+
for (const child of node.children) {
|
|
274
|
+
if (!child.yoga || child.isWindow || !child.yoga.isDirty()) continue;
|
|
275
|
+
if (boundaries.has(child)) continue;
|
|
276
|
+
if (!paths.has(child)) return true;
|
|
277
|
+
if (dirtyOutside(child, boundaries, paths)) return true;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
200
282
|
/**
|
|
201
283
|
* After a pass that settled the widths: which of the height floors, each a
|
|
202
284
|
* height *for a width*, were measured for a width their node no longer has.
|
|
@@ -216,7 +298,7 @@ export function collectFloorStale(node, stale, found, sweep) {
|
|
|
216
298
|
* the pixel grid off and the real one with it on, so the same layout reads
|
|
217
299
|
* a fraction apart between them.
|
|
218
300
|
*/
|
|
219
|
-
export function probeHeightFloors(node, root, hit) {
|
|
301
|
+
export function probeHeightFloors(node, root, hit, stop = root) {
|
|
220
302
|
for (const child of node.children) {
|
|
221
303
|
if (!child.yoga || child.isWindow) continue;
|
|
222
304
|
if (child.style.display === 'none') continue;
|
|
@@ -230,10 +312,10 @@ export function probeHeightFloors(node, root, hit) {
|
|
|
230
312
|
at !== undefined &&
|
|
231
313
|
child._heightForWidth(width) !== child._heightForWidth(at)
|
|
232
314
|
) {
|
|
233
|
-
markHeightStale(child, root, hit);
|
|
315
|
+
markHeightStale(child, root, hit, stop);
|
|
234
316
|
}
|
|
235
317
|
} else {
|
|
236
|
-
probeHeightFloors(child, root, hit);
|
|
318
|
+
probeHeightFloors(child, root, hit, stop);
|
|
237
319
|
}
|
|
238
320
|
// whatever the extent is, it is the extent for this width now — a
|
|
239
321
|
// stale one is about to be measured here, a clean one was just checked
|
|
@@ -241,10 +323,12 @@ export function probeHeightFloors(node, root, hit) {
|
|
|
241
323
|
}
|
|
242
324
|
}
|
|
243
325
|
|
|
244
|
-
/** A leaf whose height moved takes every extent above it with it
|
|
245
|
-
|
|
326
|
+
/** A leaf whose height moved takes every extent above it with it — up to
|
|
327
|
+
* `stop`, a box that sizes itself (`isFloorBoundary`), whose extent is its
|
|
328
|
+
* style's whatever moved inside it. */
|
|
329
|
+
function markHeightStale(node, root, hit, stop = root) {
|
|
246
330
|
hit.marked = true;
|
|
247
|
-
for (let n = node; n && n !==
|
|
331
|
+
for (let n = node; n && n !== stop; n = n.parent) {
|
|
248
332
|
if (n._floorH === undefined) continue;
|
|
249
333
|
n._floorH = undefined;
|
|
250
334
|
root._floorsStale.add(n);
|
package/src/nodes/window/size.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { measuringExactly } from '../../styles.js';
|
|
6
6
|
import { availableArea } from '../../screens.js';
|
|
7
|
+
import { Yoga } from '../../yoga.js';
|
|
7
8
|
import {
|
|
8
9
|
captureLeafHeights,
|
|
9
10
|
collectFloorStale,
|
|
11
|
+
dirtyOutside,
|
|
12
|
+
floorBoundaryOf,
|
|
10
13
|
probeHeightFloors,
|
|
11
14
|
setMeasuringShrink,
|
|
12
15
|
restoreShrink,
|
|
@@ -27,6 +30,26 @@ import {
|
|
|
27
30
|
scaleWindowGeometry,
|
|
28
31
|
} from './hints.js';
|
|
29
32
|
|
|
33
|
+
// Escape hatch, read once like the others: for measuring the scoped floors
|
|
34
|
+
// against the whole-tree ones on the same build, and as first aid if a
|
|
35
|
+
// change inside a card is ever measured wrong. `=== '1'` for the reason
|
|
36
|
+
// REACT_X11_NO_SCROLL_BLIT gives.
|
|
37
|
+
const NO_SCOPED_FLOORS = process.env.REACT_X11_NO_SCOPED_FLOORS === '1';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mark a box laid out alone (`_measureBoundary`) for the window's pass to lay
|
|
41
|
+
* out again. It was laid out at the size that pass will give it, so yoga
|
|
42
|
+
* would answer from its cache and keep what the measurement left inside —
|
|
43
|
+
* placed off the pixel grid, with the shrink borrowed. A style set to
|
|
44
|
+
* something else and back is how a box with no measure function says it
|
|
45
|
+
* changed.
|
|
46
|
+
*/
|
|
47
|
+
function relayOut(node) {
|
|
48
|
+
const shrink = node.style.flexShrink ?? 1;
|
|
49
|
+
node.yoga.setFlexShrink(shrink + 1);
|
|
50
|
+
node.yoga.setFlexShrink(shrink);
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/** Content-sized windows, installed onto `WindowNode.prototype` by window.js. */
|
|
31
54
|
export class WindowSize {
|
|
32
55
|
/**
|
|
@@ -102,6 +125,21 @@ export class WindowSize {
|
|
|
102
125
|
this._sweepLayoutHosts();
|
|
103
126
|
const found = { width: false, height: false };
|
|
104
127
|
this._floorsStale.clear();
|
|
128
|
+
// Decided afresh on every call rather than once a frame: something
|
|
129
|
+
// between the two calls a frame makes — a size query resolving — can
|
|
130
|
+
// turn a scoped change into one that is not, and the wider collection
|
|
131
|
+
// is the one that is always right.
|
|
132
|
+
const scope = this._floorsScope();
|
|
133
|
+
this._floorsScopeNow = scope;
|
|
134
|
+
if (scope !== null) {
|
|
135
|
+
for (const boundary of scope) {
|
|
136
|
+
// its children's floors are written from here; its own extent is
|
|
137
|
+
// its style's, and stands
|
|
138
|
+
this._floorsStale.add(boundary);
|
|
139
|
+
collectFloorStale(boundary, this._floorsStale, found, false);
|
|
140
|
+
}
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
105
143
|
// the root's own children are written from here too, and its direction
|
|
106
144
|
// can move like any node's
|
|
107
145
|
this._floorsStale.add(this);
|
|
@@ -109,6 +147,123 @@ export class WindowSize {
|
|
|
109
147
|
return found;
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The boxes the changes since the last measurement are confined to, or
|
|
152
|
+
* null when a measurement has to look at the whole tree.
|
|
153
|
+
*
|
|
154
|
+
* Every change to a box tree announces itself through `invalidate` with
|
|
155
|
+
* the node it changed (`_floorsSources`), and every node above it is
|
|
156
|
+
* dirty in yoga for it — which is all `collectFloorStale` can see, so a
|
|
157
|
+
* text that changed inside a card took every extent from the card up to
|
|
158
|
+
* the window with it, and the measurement that followed laid the whole
|
|
159
|
+
* tree out three times to read them back. A box that sizes itself
|
|
160
|
+
* (`isFloorBoundary`) holds the change in: its extent is its style's, the
|
|
161
|
+
* extents above it are what they were, and what is inside it can be
|
|
162
|
+
* measured by laying it out alone.
|
|
163
|
+
*
|
|
164
|
+
* Scoped only when all of that is known to hold: every change has such a
|
|
165
|
+
* box around it, yoga has nothing dirty outside them and the way down to
|
|
166
|
+
* them (a change nobody named), the tree has been measured once already,
|
|
167
|
+
* the window is the width it was measured at and sizes itself from no
|
|
168
|
+
* content, and no layout host or container query is in play. Anything
|
|
169
|
+
* else is the whole tree, as it always was.
|
|
170
|
+
*/
|
|
171
|
+
_floorsScope() {
|
|
172
|
+
const sources = this._floorsSources;
|
|
173
|
+
if (
|
|
174
|
+
NO_SCOPED_FLOORS ||
|
|
175
|
+
this._floorsUnscoped ||
|
|
176
|
+
sources.size === 0 ||
|
|
177
|
+
!this._floorsSwept ||
|
|
178
|
+
this._layoutHosts.size !== 0 ||
|
|
179
|
+
this._containerQueryNodes.size !== 0 ||
|
|
180
|
+
this._floorsWidth !== (this.window?.width ?? this._requestedSize?.width)
|
|
181
|
+
) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const props = this.props;
|
|
185
|
+
if (
|
|
186
|
+
isAutoSize(props.width) ||
|
|
187
|
+
isAutoSize(props.height) ||
|
|
188
|
+
CONTENT_BOUND_PROPS.some((key) => isContentBound(props[key]))
|
|
189
|
+
) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const boundaries = new Set();
|
|
193
|
+
for (const source of sources) {
|
|
194
|
+
// gone, or moved to another window: its old parent's child list is
|
|
195
|
+
// the change that counts, and that parent said so itself
|
|
196
|
+
if (source.destroyed || source.root !== this) continue;
|
|
197
|
+
const boundary = floorBoundaryOf(source, this);
|
|
198
|
+
if (boundary === null) return null;
|
|
199
|
+
boundaries.add(boundary);
|
|
200
|
+
}
|
|
201
|
+
if (boundaries.size === 0) return null;
|
|
202
|
+
// the outermost of nested ones: measuring it measures what it holds
|
|
203
|
+
const paths = new Set();
|
|
204
|
+
for (const boundary of [...boundaries]) {
|
|
205
|
+
for (let n = boundary.parent; n && n !== this; n = n.parent) {
|
|
206
|
+
if (boundaries.has(n)) {
|
|
207
|
+
boundaries.delete(boundary);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const boundary of boundaries) {
|
|
213
|
+
for (let n = boundary.parent; n && n !== this; n = n.parent) {
|
|
214
|
+
paths.add(n);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (dirtyOutside(this, boundaries, paths)) return null;
|
|
218
|
+
return boundaries;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* One boundary's floors, measured with it laid out alone — the passes
|
|
223
|
+
* `_measureContentSpans` runs over the whole tree, run over the box: its
|
|
224
|
+
* size is its style's in either, so the space everything inside it is
|
|
225
|
+
* laid out in is the same. `probe` is the height measurement's check for
|
|
226
|
+
* leaves whose width moved, stopped at the box, whose own extent stands.
|
|
227
|
+
*/
|
|
228
|
+
_measureBoundary(boundary, axis, probe = false) {
|
|
229
|
+
const yoga = boundary.yoga;
|
|
230
|
+
const dir =
|
|
231
|
+
boundary.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
|
|
232
|
+
if (axis === 'width') {
|
|
233
|
+
const shrunk = [];
|
|
234
|
+
setMeasuringShrink(boundary, axis, shrunk);
|
|
235
|
+
this._scopedFloorPasses += 1;
|
|
236
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
237
|
+
contentSpan(boundary, axis, null, this);
|
|
238
|
+
restoreShrink(shrunk);
|
|
239
|
+
relayOut(boundary);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this._scopedFloorPasses += 1;
|
|
243
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
244
|
+
if (probe) {
|
|
245
|
+
const hit = { marked: false, owed: false };
|
|
246
|
+
probeHeightFloors(boundary, this, hit, boundary);
|
|
247
|
+
if (hit.marked) {
|
|
248
|
+
this._writeFloors('height');
|
|
249
|
+
this._scopedFloorPasses += 1;
|
|
250
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const intrinsic = new Map();
|
|
254
|
+
captureLeafHeights(boundary, intrinsic);
|
|
255
|
+
const frozen = [];
|
|
256
|
+
freezeWidths(boundary, frozen);
|
|
257
|
+
const shrunk = [];
|
|
258
|
+
setMeasuringShrink(boundary, axis, shrunk);
|
|
259
|
+
this._scopedFloorPasses += 1;
|
|
260
|
+
yoga.calculateLayout(undefined, 0, dir);
|
|
261
|
+
contentSpan(boundary, axis, intrinsic, this);
|
|
262
|
+
restoreWidths(frozen);
|
|
263
|
+
restoreShrink(shrunk);
|
|
264
|
+
relayOut(boundary);
|
|
265
|
+
}
|
|
266
|
+
|
|
112
267
|
/** The floors on the children of every node found stale, from the extents
|
|
113
268
|
* they carry — `writeFloors` for each. */
|
|
114
269
|
_writeFloors(axis) {
|
|
@@ -203,28 +358,77 @@ export class WindowSize {
|
|
|
203
358
|
*/
|
|
204
359
|
_applyContentFloors(width, height) {
|
|
205
360
|
const found = this._collectFloorStale();
|
|
361
|
+
const scope = this._floorsScopeNow;
|
|
362
|
+
if (scope !== null) {
|
|
363
|
+
this._applyScopedFloors(scope, found, width, height);
|
|
364
|
+
} else {
|
|
365
|
+
measuringExactly(() => {
|
|
366
|
+
if (found.width) this._measureWidthFloors();
|
|
367
|
+
else this._writeFloors('width');
|
|
368
|
+
// from the extents on hand; a stale one writes the style's minimum,
|
|
369
|
+
// which is the floor coming off ahead of its measurement below
|
|
370
|
+
this._writeFloors('height');
|
|
371
|
+
});
|
|
372
|
+
let heights = found.height;
|
|
373
|
+
let probed = false;
|
|
374
|
+
if (!heights) {
|
|
375
|
+
this._layoutRoot(width, height);
|
|
376
|
+
heights = this._probeHeightFloors().owed;
|
|
377
|
+
probed = true;
|
|
378
|
+
}
|
|
379
|
+
if (heights) {
|
|
380
|
+
measuringExactly(() => this._measureHeightFloors(width, !probed));
|
|
381
|
+
this._layoutRoot(width, height);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this._floorsDirty = false;
|
|
385
|
+
this._floorsContentDirty = false;
|
|
386
|
+
this._floorsSwept = true;
|
|
387
|
+
this._floorsWidth = width;
|
|
388
|
+
this._floorsSources.clear();
|
|
389
|
+
this._floorsUnscoped = false;
|
|
390
|
+
this._floorsScopeNow = null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* `_applyContentFloors` for changes confined to boxes that size
|
|
395
|
+
* themselves (`_floorsScope`): the same measurements in the same order,
|
|
396
|
+
* each over one box laid out alone rather than over the window, and the
|
|
397
|
+
* window's one real pass after them — which yoga answers from its cache
|
|
398
|
+
* everywhere but the boxes and the way down to them.
|
|
399
|
+
*/
|
|
400
|
+
_applyScopedFloors(boundaries, found, width, height) {
|
|
206
401
|
measuringExactly(() => {
|
|
207
|
-
if (found.width)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
402
|
+
if (found.width) {
|
|
403
|
+
this._writeFloors('width');
|
|
404
|
+
for (const boundary of boundaries) {
|
|
405
|
+
this._measureBoundary(boundary, 'width');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this._writeFloors('width');
|
|
211
409
|
this._writeFloors('height');
|
|
212
410
|
});
|
|
213
411
|
let heights = found.height;
|
|
214
412
|
let probed = false;
|
|
215
413
|
if (!heights) {
|
|
216
414
|
this._layoutRoot(width, height);
|
|
217
|
-
|
|
415
|
+
const hit = { marked: false, owed: false };
|
|
416
|
+
for (const boundary of boundaries) {
|
|
417
|
+
probeHeightFloors(boundary, this, hit, boundary);
|
|
418
|
+
}
|
|
419
|
+
heights = hit.owed;
|
|
218
420
|
probed = true;
|
|
219
421
|
}
|
|
220
422
|
if (heights) {
|
|
221
|
-
measuringExactly(() =>
|
|
423
|
+
measuringExactly(() => {
|
|
424
|
+
this._writeFloors('height');
|
|
425
|
+
for (const boundary of boundaries) {
|
|
426
|
+
this._measureBoundary(boundary, 'height', !probed);
|
|
427
|
+
}
|
|
428
|
+
this._writeFloors('height');
|
|
429
|
+
});
|
|
222
430
|
this._layoutRoot(width, height);
|
|
223
431
|
}
|
|
224
|
-
this._floorsDirty = false;
|
|
225
|
-
this._floorsContentDirty = false;
|
|
226
|
-
this._floorsSwept = true;
|
|
227
|
-
this._floorsWidth = width;
|
|
228
432
|
}
|
|
229
433
|
|
|
230
434
|
/**
|
|
@@ -267,6 +267,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
267
267
|
this._floorsDirty = true;
|
|
268
268
|
this._floorsWidth = null;
|
|
269
269
|
this._floorsStale = new Set();
|
|
270
|
+
// the nodes whose changes the next measurement owes, and whether one
|
|
271
|
+
// came with no node to name (`_floorsScope`)
|
|
272
|
+
this._floorsSources = new Set();
|
|
273
|
+
this._floorsUnscoped = false;
|
|
274
|
+
this._floorsScopeNow = null;
|
|
275
|
+
this._scopedFloorPasses = 0;
|
|
270
276
|
// whether the first floors pass has run (`collectFloorStale`'s sweep)
|
|
271
277
|
this._floorsSwept = false;
|
|
272
278
|
this._floorsMeasured = 0;
|
package/src/paintcache.js
CHANGED
|
@@ -83,19 +83,26 @@ const MAX_ITEM_PIXELS = 256 * 256;
|
|
|
83
83
|
* grow this forever; the cost of forgetting is one extra live paint. */
|
|
84
84
|
const MAX_PENDING = 4096;
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
86
|
+
/**
|
|
87
|
+
* Whether `ctx` draws on the device's pixel grid, wherever its origin is: a
|
|
88
|
+
* surface rendered at device resolution composites through it pixel for
|
|
89
|
+
* pixel. An integer translation keeps that — and it is how an overlay pane
|
|
90
|
+
* over a `<glarea>` draws, from the window's coordinates into its layer's
|
|
91
|
+
* (src/win32/overlay.js) — so declining it painted every cached drawing on a
|
|
92
|
+
* surface's children live, every time a pane repainted: every card of a
|
|
93
|
+
* graph, on every step of a pan. A scale, a rotation or a fraction of a
|
|
94
|
+
* pixel does not, and paints live.
|
|
93
95
|
*/
|
|
94
96
|
function isDeviceSpace(ctx) {
|
|
95
97
|
const m = ctx.getTransform?.();
|
|
96
98
|
if (!m) return true;
|
|
97
99
|
return (
|
|
98
|
-
m.a === 1 &&
|
|
100
|
+
m.a === 1 &&
|
|
101
|
+
m.b === 0 &&
|
|
102
|
+
m.c === 0 &&
|
|
103
|
+
m.d === 1 &&
|
|
104
|
+
Number.isInteger(m.e) &&
|
|
105
|
+
Number.isInteger(m.f)
|
|
99
106
|
);
|
|
100
107
|
}
|
|
101
108
|
|