react-x11 2.18.1 → 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/app.js +25 -10
- package/src/cocoa/glarea.js +2 -1
- package/src/cocoa/overlay.js +40 -3
- package/src/cocoa/panewindow.js +5 -0
- package/src/cocoa/window.js +9 -0
- 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/app.js
CHANGED
|
@@ -85,6 +85,17 @@ const SCREEN_POLL_MS = 500;
|
|
|
85
85
|
// have finished moving the window (`_recheckScreens`).
|
|
86
86
|
const SCREEN_RECHECK_MS = 1000;
|
|
87
87
|
|
|
88
|
+
/** One frame callback; a throw is reported, not allowed to end the tick. */
|
|
89
|
+
function runFrame(entry, now) {
|
|
90
|
+
try {
|
|
91
|
+
entry.cb(now);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
queueMicrotask(() => {
|
|
94
|
+
throw err;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
export class CocoaApp {
|
|
89
100
|
constructor(native, options = {}) {
|
|
90
101
|
this._native = native;
|
|
@@ -92,7 +103,7 @@ export class CocoaApp {
|
|
|
92
103
|
// CocoaWindow._key -> CocoaWindow: the number, or on a worker the handle
|
|
93
104
|
this._windows = new Map();
|
|
94
105
|
this._grabWindow = null;
|
|
95
|
-
this._rafQueue = []; // [{ cb, wnd }]
|
|
106
|
+
this._rafQueue = []; // [{ cb, wnd, surface }]
|
|
96
107
|
// the app's own frame clock, for a frame no window owns (a pane's)
|
|
97
108
|
this._rafLast = 0;
|
|
98
109
|
// an explicit interval applies to every window; null means each
|
|
@@ -843,8 +854,8 @@ export class CocoaApp {
|
|
|
843
854
|
* paced claim (src/pacing.js) land at its wait rather than at its wait
|
|
844
855
|
* rounded up to the pump.
|
|
845
856
|
*/
|
|
846
|
-
_requestFrame(cb, wnd = null) {
|
|
847
|
-
this._rafQueue.push({ cb, wnd });
|
|
857
|
+
_requestFrame(cb, wnd = null, surface = false) {
|
|
858
|
+
this._rafQueue.push({ cb, wnd, surface });
|
|
848
859
|
if ((this._pump || this._threaded) && this._rafQueue.length === 1) {
|
|
849
860
|
const now = performance.now();
|
|
850
861
|
this._armFrameTimer(Math.max(1, this._frameWait(wnd ?? this, now)), now);
|
|
@@ -947,6 +958,14 @@ export class CocoaApp {
|
|
|
947
958
|
const queue = this._rafQueue;
|
|
948
959
|
this._rafQueue = [];
|
|
949
960
|
let soonest = Infinity;
|
|
961
|
+
// A `<glarea>`'s frames, due in this tick, run after every window's
|
|
962
|
+
// own: its children are laid out, painted and presented in the window's
|
|
963
|
+
// flush, so a GL frame drawn ahead of that flush reached the screen
|
|
964
|
+
// under the overlay of the frame before — for as long as the flush took,
|
|
965
|
+
// which is more than a display frame when the change costs layout
|
|
966
|
+
// (issue #641). The Wayland backend orders them the same way
|
|
967
|
+
// (`requestSurfaceFrame`).
|
|
968
|
+
const surfaces = [];
|
|
950
969
|
for (const entry of queue) {
|
|
951
970
|
// A window whose last flip has not reached its layer yet (threaded
|
|
952
971
|
// mode's fence, `CocoaWindow.frameInFlight`) waits before
|
|
@@ -974,14 +993,10 @@ export class CocoaApp {
|
|
|
974
993
|
this._rafQueue.push(entry);
|
|
975
994
|
continue;
|
|
976
995
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
} catch (err) {
|
|
980
|
-
queueMicrotask(() => {
|
|
981
|
-
throw err;
|
|
982
|
-
});
|
|
983
|
-
}
|
|
996
|
+
if (entry.surface) surfaces.push(entry);
|
|
997
|
+
else runFrame(entry, now);
|
|
984
998
|
}
|
|
999
|
+
for (const entry of surfaces) runFrame(entry, now);
|
|
985
1000
|
this._armFrameTimer(soonest, now);
|
|
986
1001
|
}
|
|
987
1002
|
|
package/src/cocoa/glarea.js
CHANGED
|
@@ -232,8 +232,9 @@ export class CocoaGLArea {
|
|
|
232
232
|
for (const fn of this._listeners.get(name) ?? []) fn(ev);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// on the owning window's clock, after its flush in the same tick (#641)
|
|
235
236
|
requestAnimationFrame(cb) {
|
|
236
|
-
return this.parent.
|
|
237
|
+
return this.parent.requestSurfaceFrame(cb);
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
/**
|
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/cocoa/panewindow.js
CHANGED
|
@@ -99,6 +99,11 @@ export class CocoaPaneWindow {
|
|
|
99
99
|
return this.app._requestFrame(cb);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** A `<glarea>`'s frame, after this pane's own (`CocoaWindow`'s). */
|
|
103
|
+
requestSurfaceFrame(cb) {
|
|
104
|
+
return this.app._requestFrame(cb, null, true);
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
/**
|
|
103
108
|
* Whether the host may still be showing the frame before the last one —
|
|
104
109
|
* the gate `flushPendingFrames` (src/frames.js) is written around: a
|
package/src/cocoa/window.js
CHANGED
|
@@ -915,6 +915,15 @@ export class CocoaWindow {
|
|
|
915
915
|
return this.app._requestFrame(cb, this);
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
+
/**
|
|
919
|
+
* A `<glarea>`'s frame: on this window's clock, and after this window's
|
|
920
|
+
* own frame in the tick that runs both, so the GL frame goes out after
|
|
921
|
+
* the overlay its children were painted on (src/cocoa/glarea.js).
|
|
922
|
+
*/
|
|
923
|
+
requestSurfaceFrame(cb) {
|
|
924
|
+
return this.app._requestFrame(cb, this, true);
|
|
925
|
+
}
|
|
926
|
+
|
|
918
927
|
/**
|
|
919
928
|
* The earliest moment this window's clock will hand out another frame,
|
|
920
929
|
* for a gate of `interval` ms — the display's period by default, which
|
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);
|