react-x11 2.44.0 → 2.45.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/cocoa/app.js +6 -0
- package/src/cocoa/panehost.js +38 -22
- package/src/cocoa/panewindow.js +59 -7
- package/src/cocoa/window.js +6 -0
- package/src/frame/index.js +16 -0
- package/src/index.d.ts +3 -2
- package/src/node.d.ts +14 -0
- package/src/nodes/invalidate.js +30 -0
- package/src/nodes/scrollable.js +49 -0
- package/src/nodes/window/floors.js +20 -5
- package/src/nodes/window/size.js +32 -7
- package/src/wayland/context2d.js +233 -59
- package/src/wayland/glyphatlas.js +43 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.45.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"yoga-layout": "^3.2.1"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
|
-
"@windowkit/appkit": "^0.
|
|
112
|
+
"@windowkit/appkit": "^0.27.0",
|
|
113
113
|
"@windowkit/wayland": "^3.1.1",
|
|
114
114
|
"@windowkit/win32": "^0.0.9",
|
|
115
115
|
"dbus-native": "^0.16.0",
|
package/src/cocoa/app.js
CHANGED
|
@@ -1014,8 +1014,11 @@ export class CocoaApp {
|
|
|
1014
1014
|
wnd?._shown?.(msg.seq);
|
|
1015
1015
|
} else if (msg?.type === 'pane-rect') {
|
|
1016
1016
|
pane.setPaneSize(msg.width, msg.height, msg.scale, msg.screen);
|
|
1017
|
+
if (typeof msg.live === 'boolean') pane.setLive(msg.live);
|
|
1017
1018
|
this._flushPaneOutbox();
|
|
1018
1019
|
this._afterInput();
|
|
1020
|
+
} else if (msg?.type === 'pane-live') {
|
|
1021
|
+
pane.setLive(msg.live === true);
|
|
1019
1022
|
} else if (msg?.type === 'pane-menu-answer') {
|
|
1020
1023
|
const answer = this._paneMenus?.get(msg.menu);
|
|
1021
1024
|
this._paneMenus?.delete(msg.menu);
|
|
@@ -1687,6 +1690,9 @@ export class CocoaApp {
|
|
|
1687
1690
|
const wnd = this._window(ev);
|
|
1688
1691
|
if (!wnd || wnd.destroyed) return;
|
|
1689
1692
|
wnd.liveResizing = ev.phase === 'begin';
|
|
1693
|
+
// and what lives in the window without being it hears: a `<Frame>`'s
|
|
1694
|
+
// pane, which has no window of its own to be resized live
|
|
1695
|
+
wnd.emit('liveresize', { live: wnd.liveResizing });
|
|
1690
1696
|
}
|
|
1691
1697
|
|
|
1692
1698
|
/**
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -77,21 +77,30 @@ class PaneLayer {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Whether a pane's last frame is shown at its size
|
|
81
|
-
* frame of the layer's new size lands, or stretched
|
|
82
|
-
* the layer its new size the moment the window lays out, and the
|
|
83
|
-
* frame of that size comes a pane frame later: Core Animation
|
|
84
|
-
* the frame it has to the new bounds meanwhile, and a page's
|
|
85
|
-
* was drawn scaled a little at every step of a window dragged
|
|
86
|
-
* then at its size again. A pane that keeps up has its frame
|
|
87
|
-
* size instead, its edge carried over what it does not cover
|
|
88
|
-
* (`_edgeProps`); one that falls behind the budget is stretched
|
|
89
|
-
* the better picture for the long wait — a pane
|
|
90
|
-
* frames a second shows a squeezed page, not a page
|
|
91
|
-
* The budget is the window's live-resize
|
|
92
|
-
* (`createRoot({ cocoa: { resizeWait } })`), how long a frame
|
|
93
|
-
* size may take before the old one is shown in its place: 0
|
|
94
|
-
* always, as before.
|
|
80
|
+
* Whether a pane's last frame is shown at its size across its layer until
|
|
81
|
+
* a frame of the layer's new size lands, or stretched across it. The host
|
|
82
|
+
* gives the layer its new size the moment the window lays out, and the
|
|
83
|
+
* pane's frame of that size comes a pane frame later: Core Animation
|
|
84
|
+
* stretches the frame it has to the new bounds meanwhile, and a page's
|
|
85
|
+
* left column was drawn scaled a little at every step of a window dragged
|
|
86
|
+
* wider, and then at its size again. A pane that keeps up has its frame
|
|
87
|
+
* shown at its size instead, its edge carried over what it does not cover
|
|
88
|
+
* yet (`_edgeProps`); one that falls behind the budget is stretched
|
|
89
|
+
* across, which is the better picture for the long wait — a pane
|
|
90
|
+
* repainting at three frames a second shows a squeezed page, not a page
|
|
91
|
+
* and a smeared strip. The budget is the window's live-resize
|
|
92
|
+
* handshake's (`createRoot({ cocoa: { resizeWait } })`), how long a frame
|
|
93
|
+
* of a new size may take before the old one is shown in its place: 0
|
|
94
|
+
* stretches always, both ways, as before.
|
|
95
|
+
*
|
|
96
|
+
* Down the layer the frame is at its size whatever this says. A width
|
|
97
|
+
* moves where a page's lines break, and what comes is a page laid out
|
|
98
|
+
* again, which a squeezed one is nearer to than one cut off with a strip
|
|
99
|
+
* beside it. A height moves nothing a page lays out — its lines are as
|
|
100
|
+
* wide as they were, its blocks where they were — so what comes is the
|
|
101
|
+
* frame as it is with more of the page under it, and a frame stretched
|
|
102
|
+
* down was the page drawn too tall at every refresh of the wait, where
|
|
103
|
+
* at its size only the strip under it is not yet the page's.
|
|
95
104
|
*
|
|
96
105
|
* Behind is a pane whose frames come later than the budget, not one with
|
|
97
106
|
* a size waiting that long. `<Frame>` sends a pane the newest size once
|
|
@@ -125,8 +134,11 @@ class PaneLayer {
|
|
|
125
134
|
* carried over the rest (`contentsCenter`, the part of the contents that
|
|
126
135
|
* stretches while everything else keeps its size) — the last column
|
|
127
136
|
* across a window dragged wider, the last row down one dragged taller,
|
|
128
|
-
* and the corner pixel into the corner.
|
|
129
|
-
* the frame is stretched, and
|
|
137
|
+
* and the corner pixel into the corner. Across a pane that fell behind
|
|
138
|
+
* the frame is stretched, and down it is still at its size
|
|
139
|
+
* (`_anchoredNow`): the whole of each row stretched to the new width,
|
|
140
|
+
* cropped or with its last row carried down. Both rects are the whole
|
|
141
|
+
* when the frame is the layer's size, and at a budget of 0.
|
|
130
142
|
*
|
|
131
143
|
* Anchored by gravity alone, the strip the frame did not cover showed
|
|
132
144
|
* whatever was under the pane, which is the `<Frame>`'s background: in
|
|
@@ -147,15 +159,19 @@ class PaneLayer {
|
|
|
147
159
|
const rect = this._rect;
|
|
148
160
|
let crop = null;
|
|
149
161
|
let edge = null;
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
|
|
163
|
+
if (frame && rect && budget > 0) {
|
|
164
|
+
// across at its size while the pane keeps up, and down always; an
|
|
165
|
+
// axis stretched is shown whole, the frame's every column across
|
|
166
|
+
const across = this._anchoredNow();
|
|
167
|
+
const w = across ? Math.min(frame.width, rect.width) : frame.width;
|
|
152
168
|
const h = Math.min(frame.height, rect.height);
|
|
153
169
|
if (w < frame.width || h < frame.height) {
|
|
154
170
|
crop = [0, 0, w / frame.width, h / frame.height];
|
|
155
171
|
}
|
|
156
|
-
// in the shown part's own unit square; an axis that did not grow
|
|
157
|
-
//
|
|
158
|
-
const wider = rect.width > w;
|
|
172
|
+
// in the shown part's own unit square; an axis that did not grow, or
|
|
173
|
+
// is stretched, is all centre
|
|
174
|
+
const wider = across && rect.width > w;
|
|
159
175
|
const taller = rect.height > h;
|
|
160
176
|
if (wider || taller) {
|
|
161
177
|
edge = [
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -30,6 +30,12 @@ import { BackendContext2D } from '../backend/context2d.js';
|
|
|
30
30
|
// the pane drawing.
|
|
31
31
|
const ANSWER_TIMEOUT_MS = 250;
|
|
32
32
|
|
|
33
|
+
// Where a ring's slot with no buffer made yet ranks among the buffers a
|
|
34
|
+
// frame may take (`_takeBack`): after every one done with and let go of,
|
|
35
|
+
// ranked by their present's sequence number, and before one the
|
|
36
|
+
// WindowServer still reads, ranked from 2 ** 32 up.
|
|
37
|
+
const EMPTY_SLOT = 2 ** 32 - 1;
|
|
38
|
+
|
|
33
39
|
let nextPaneId = 1;
|
|
34
40
|
|
|
35
41
|
export class CocoaPaneWindow {
|
|
@@ -44,6 +50,8 @@ export class CocoaPaneWindow {
|
|
|
44
50
|
// polls it through the pane's ref, so both spellings answer
|
|
45
51
|
this.id = this.windowId;
|
|
46
52
|
this.windowNumber = this.windowId;
|
|
53
|
+
// the host's word, which the pane has no AppKit to hear (`setLive`)
|
|
54
|
+
this.liveResizing = false;
|
|
47
55
|
this.scale = app.scale ?? 2;
|
|
48
56
|
// Device pixels, as every window's attributes are (`windowAttributes`
|
|
49
57
|
// multiplies the props through once): the size the pane is born at,
|
|
@@ -106,6 +114,20 @@ export class CocoaPaneWindow {
|
|
|
106
114
|
* a host that does not say. A move alone is a `resize` that `moved`, the
|
|
107
115
|
* event a window's own move is, so what is anchored here follows it.
|
|
108
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* Whether the window the pane is in is being resized live, as the host
|
|
119
|
+
* says (`pane-rect`, `pane-live`, src/frame/index.js): what a window's
|
|
120
|
+
* `liveResizing` is in a process of its own, where AppKit brackets the
|
|
121
|
+
* drag of its edge. Core defers a content floor's measurement while it
|
|
122
|
+
* is (src/nodes/window/size.js), and catches it up the frame after it
|
|
123
|
+
* ends; and it is announced as a window announces its own.
|
|
124
|
+
*/
|
|
125
|
+
setLive(live) {
|
|
126
|
+
if (this.destroyed || this.liveResizing === live) return;
|
|
127
|
+
this.liveResizing = live;
|
|
128
|
+
this.emit('liveresize', { live });
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
setPaneSize(width, height, scale, screen) {
|
|
110
132
|
if (this.destroyed) return;
|
|
111
133
|
if (scale) this.scale = scale;
|
|
@@ -261,6 +283,8 @@ export class CocoaPaneWindow {
|
|
|
261
283
|
let done = false;
|
|
262
284
|
for (let i = 0; i < this._ring.length; i += 1) {
|
|
263
285
|
const s = this._ring[i];
|
|
286
|
+
// a slot with no buffer yet gets one, which nothing names
|
|
287
|
+
if (!s) return false;
|
|
264
288
|
if (i === this._shownIndex || !this._done(s)) continue;
|
|
265
289
|
if (!this._held(s)) return false;
|
|
266
290
|
done = true;
|
|
@@ -354,6 +378,7 @@ export class CocoaPaneWindow {
|
|
|
354
378
|
const release = this._native.releaseSurface;
|
|
355
379
|
if (typeof release !== 'function') return;
|
|
356
380
|
for (const s of ring) {
|
|
381
|
+
if (!s) continue;
|
|
357
382
|
if (s.presentedSeq > this._shownSeq) this._retired.push(s);
|
|
358
383
|
else release.call(this._native, s.handle);
|
|
359
384
|
}
|
|
@@ -407,6 +432,20 @@ export class CocoaPaneWindow {
|
|
|
407
432
|
for (const s of retired) release.call(this._native, s.handle);
|
|
408
433
|
}
|
|
409
434
|
|
|
435
|
+
/**
|
|
436
|
+
* A buffer of the ring, `w` by `h` device px, made the first time a frame
|
|
437
|
+
* takes its slot rather than with the ring. A drag resizes the pane a
|
|
438
|
+
* tick at a time and gives each size one frame, so a ring made whole was
|
|
439
|
+
* three buffers made and cleared for the one drawn into: Zen Garden 101
|
|
440
|
+
* in the browser example spent 4.4 ms of every 12 ms frame of a drag in
|
|
441
|
+
* `_ensureSurface`. Cleared where nothing is copied over it.
|
|
442
|
+
*/
|
|
443
|
+
_newBuffer(w, h, clear) {
|
|
444
|
+
const s = this._native.createSurfaceIOSurface(w, h, this.scale, true);
|
|
445
|
+
if (clear) this._native.ctxClearRect(s.handle, 0, 0, w, h);
|
|
446
|
+
return s;
|
|
447
|
+
}
|
|
448
|
+
|
|
410
449
|
_ensureSurface() {
|
|
411
450
|
const w = this.width;
|
|
412
451
|
const h = this.height;
|
|
@@ -417,12 +456,10 @@ export class CocoaPaneWindow {
|
|
|
417
456
|
) {
|
|
418
457
|
const hadSurface = Boolean(this._ring);
|
|
419
458
|
this._releaseRing();
|
|
420
|
-
this
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
this._ring.push(s);
|
|
425
|
-
}
|
|
459
|
+
// the buffer this frame draws into, and the rest of the ring as a
|
|
460
|
+
// frame first takes it (`_takeBack`)
|
|
461
|
+
this._ring = new Array(CocoaPaneWindow.RING).fill(null);
|
|
462
|
+
this._ring[0] = this._newBuffer(w, h, true);
|
|
426
463
|
this._drawIndex = 0;
|
|
427
464
|
this._shownIndex = -1;
|
|
428
465
|
this._native.surfaceLock(this._ring[0].handle);
|
|
@@ -478,6 +515,15 @@ export class CocoaPaneWindow {
|
|
|
478
515
|
for (let i = 0; i < ring.length; i += 1) {
|
|
479
516
|
const s = ring[i];
|
|
480
517
|
if (s === last) continue;
|
|
518
|
+
// a slot no buffer has been made for yet: after one let go of, which
|
|
519
|
+
// costs nothing to take, and before any the WindowServer still reads
|
|
520
|
+
if (!s) {
|
|
521
|
+
if (EMPTY_SLOT < rank) {
|
|
522
|
+
rank = EMPTY_SLOT;
|
|
523
|
+
pick = i;
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
481
527
|
// done and let go of, then done, then on its way, then on the layer:
|
|
482
528
|
// the oldest of the best — but of the done, the one the screen showed
|
|
483
529
|
// before the host's latest answers last
|
|
@@ -494,7 +540,13 @@ export class CocoaPaneWindow {
|
|
|
494
540
|
pick = i;
|
|
495
541
|
}
|
|
496
542
|
}
|
|
497
|
-
|
|
543
|
+
// what the frame just presented is copied over whole, below, and only a
|
|
544
|
+
// slot with none to copy is cleared
|
|
545
|
+
const back = (ring[pick] ??= this._newBuffer(
|
|
546
|
+
this._surfaceSize.width,
|
|
547
|
+
this._surfaceSize.height,
|
|
548
|
+
!last,
|
|
549
|
+
));
|
|
498
550
|
this._drawIndex = pick;
|
|
499
551
|
this._surface = back.handle;
|
|
500
552
|
// a different native surface owns the graphics state now — the context
|
package/src/cocoa/window.js
CHANGED
|
@@ -278,6 +278,12 @@ export class CocoaWindow {
|
|
|
278
278
|
let list = this._handlers.get(name);
|
|
279
279
|
if (!list) this._handlers.set(name, (list = []));
|
|
280
280
|
list.push(fn);
|
|
281
|
+
// and the way to stop listening, for a listener that goes before the
|
|
282
|
+
// window does (`<Frame>`'s, for `liveresize`)
|
|
283
|
+
return () => {
|
|
284
|
+
const at = list.indexOf(fn);
|
|
285
|
+
if (at >= 0) list.splice(at, 1);
|
|
286
|
+
};
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
emit(name, ev) {
|
package/src/frame/index.js
CHANGED
|
@@ -574,6 +574,20 @@ function PaneHostView({
|
|
|
574
574
|
let acks = false;
|
|
575
575
|
let inFlight = false;
|
|
576
576
|
let waiting = null;
|
|
577
|
+
// And whether the window is being resized live — AppKit's drag of its
|
|
578
|
+
// edge, from its begin to its end (`liveResizing`) — which the pane
|
|
579
|
+
// has no window to hear. Core defers measuring a content floor for a
|
|
580
|
+
// drag's length and catches it up after it (src/nodes/window/size.js),
|
|
581
|
+
// and an element puts off what it would rather do once the drag ends:
|
|
582
|
+
// a pane measured its floors at every step, a document a pixel wide
|
|
583
|
+
// among them, and laid a document out whole at every rest of the drag.
|
|
584
|
+
// With every rect, and on its own where only that changed — the end
|
|
585
|
+
// of a drag comes after its last size.
|
|
586
|
+
const hostWindow = node.root?.window;
|
|
587
|
+
const liveNow = () => hostWindow?.liveResizing === true;
|
|
588
|
+
const offLive = hostWindow?.on?.('liveresize', (ev) =>
|
|
589
|
+
s.trySend?.({ type: 'pane-live', live: ev?.live === true }),
|
|
590
|
+
);
|
|
577
591
|
const transmit = (rect) => {
|
|
578
592
|
sent = rect;
|
|
579
593
|
s.trySend?.({
|
|
@@ -581,6 +595,7 @@ function PaneHostView({
|
|
|
581
595
|
width: rect.width,
|
|
582
596
|
height: rect.height,
|
|
583
597
|
scale: rect.scale,
|
|
598
|
+
live: liveNow(),
|
|
584
599
|
...(rect.screen && { screen: rect.screen }),
|
|
585
600
|
});
|
|
586
601
|
if (!acks) return;
|
|
@@ -685,6 +700,7 @@ function PaneHostView({
|
|
|
685
700
|
waiting = null;
|
|
686
701
|
offAnchor?.();
|
|
687
702
|
offFocus?.();
|
|
703
|
+
if (typeof offLive === 'function') offLive();
|
|
688
704
|
node.defaultCursor = undefined;
|
|
689
705
|
node.root?.events?.refreshCursor();
|
|
690
706
|
node.absolutize = origAbsolutize;
|
package/src/index.d.ts
CHANGED
|
@@ -292,8 +292,9 @@ export interface RootOptions {
|
|
|
292
292
|
* the frame is painted on another thread (50 by default; 0 lets the edge
|
|
293
293
|
* move without waiting). A `<Frame>` pane's frames of a new size are held
|
|
294
294
|
* to it as well: within it, the pane's last frame stays at its size while
|
|
295
|
-
* the next comes, and past it, it is stretched to the pane's new
|
|
296
|
-
* every one is
|
|
295
|
+
* the next comes, and past it, it is stretched across to the pane's new
|
|
296
|
+
* width and stays at its size down it; at 0 every one is stretched both
|
|
297
|
+
* ways.
|
|
297
298
|
* `screenPoll` is how often the screen layout is re-read while a
|
|
298
299
|
* {@link useScreens} subscriber is mounted, in ms — 500 by default, 0 for
|
|
299
300
|
* never. macOS has no event for a display plugged in, unplugged or
|
package/src/node.d.ts
CHANGED
|
@@ -750,6 +750,20 @@ export declare class Node {
|
|
|
750
750
|
* each scroll the pane blits, after layout.
|
|
751
751
|
*/
|
|
752
752
|
viewportFixedRects(): Rect[] | null;
|
|
753
|
+
/**
|
|
754
|
+
* What this element draws under the viewport of the scroll pane it is in
|
|
755
|
+
* moved by (dx, dy) device pixels, and what the viewport shows is to stay
|
|
756
|
+
* where it is on screen: the pane's offsets move by as much, in the
|
|
757
|
+
* layout pass running now, after it measures what it holds and before it
|
|
758
|
+
* places it (CSS Scroll Anchoring 1) — a document laid out again at
|
|
759
|
+
* another width, whose text above the fold broke into more lines or
|
|
760
|
+
* fewer. Asked of the nearest scroll pane; shifts in one pass add up.
|
|
761
|
+
* Ask from the layout pass (`measureContent`); asked outside one, the
|
|
762
|
+
* shift waits for the next and asks for it. A `scrollTo` made since the
|
|
763
|
+
* last pass drops it, a `scrollIntoView` lands after it, and the result
|
|
764
|
+
* is clamped as any scroll. Returns whether a pane took it.
|
|
765
|
+
*/
|
|
766
|
+
anchorScrollBy(dx: number, dy: number): boolean;
|
|
753
767
|
/**
|
|
754
768
|
* How far this element's own drawing reaches past its box, in device
|
|
755
769
|
* pixels on every side — 0, the default, for an element that stays inside
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -137,6 +137,36 @@ export class NodeInvalidate {
|
|
|
137
137
|
return null;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* What this element draws under the viewport of the scroll pane it is
|
|
142
|
+
* in moved by (dx, dy) device pixels, and what the viewport shows is to
|
|
143
|
+
* stay where it is on screen: the pane's offsets move by as much, in the
|
|
144
|
+
* layout pass running now, after it measures what it holds and before it
|
|
145
|
+
* places it (CSS Scroll Anchoring 1). A document laid out again at
|
|
146
|
+
* another width is the case: its text above the fold breaks into more
|
|
147
|
+
* lines or fewer, and the paragraph at the top of the viewport moves
|
|
148
|
+
* down or up the document by the difference — which, unanswered, scrolls
|
|
149
|
+
* the reader a page away from where they were.
|
|
150
|
+
*
|
|
151
|
+
* Asked of the nearest scroll pane, the one whose viewport the element
|
|
152
|
+
* is seen through; shifts asked of it in one pass add up. Ask from the
|
|
153
|
+
* layout pass — `measureContent`, or what it calls — where the pane
|
|
154
|
+
* places its content after; asked outside one, the shift waits for the
|
|
155
|
+
* next pass, and asks for it. A `scrollTo` the application made since
|
|
156
|
+
* the last pass drops it, since the shift was measured against the
|
|
157
|
+
* offsets before that scroll; a `scrollIntoView` lands after it; and the
|
|
158
|
+
* pane clamps the result to what it holds, as any scroll. Returns
|
|
159
|
+
* whether a pane took it: false where nothing scrolls the element.
|
|
160
|
+
*/
|
|
161
|
+
anchorScrollBy(dx, dy) {
|
|
162
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
163
|
+
if (typeof n._anchorScrollBy === 'function' && n.isScroller()) {
|
|
164
|
+
return n._anchorScrollBy(dx, dy);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
/**
|
|
141
171
|
* How far this element's own drawing reaches past its box — device
|
|
142
172
|
* pixels, on every side — or 0, the default, for one that stays inside
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -330,6 +330,10 @@ export const Scrollable = (Base) =>
|
|
|
330
330
|
// the viewport grew, under it. A browser fires `scroll` for each, so
|
|
331
331
|
// `onScroll` hears of them too, once the pass is over.
|
|
332
332
|
const from = { x: this.scrollX, y: this.scrollY };
|
|
333
|
+
// What the content asked to keep in place lands before them all
|
|
334
|
+
// (`anchorScrollBy`): it moved under the viewport in this pass, and
|
|
335
|
+
// a scroll anyone asks for is from where it shows what it did
|
|
336
|
+
this._applyAnchorShift();
|
|
333
337
|
// A scrollTo held for this pass lands first, so a node asked into view
|
|
334
338
|
// in the same frame is brought in from where that scroll put the pane:
|
|
335
339
|
// the order the two have on a laid-out pane, where scrollTo applies at
|
|
@@ -607,6 +611,10 @@ export const Scrollable = (Base) =>
|
|
|
607
611
|
}
|
|
608
612
|
this.scrollX = next.x;
|
|
609
613
|
this.scrollY = next.y;
|
|
614
|
+
// a scroll asked for since the last pass is where the pane goes: a
|
|
615
|
+
// shift the content asks for in the next was measured against the
|
|
616
|
+
// offsets before it (`_applyAnchorShift`)
|
|
617
|
+
this._scrolledSincePass = true;
|
|
610
618
|
if (moved) this.props.onScroll?.(this._scrollEvent());
|
|
611
619
|
// A scroll reflows this viewport's contents and nothing else, and the
|
|
612
620
|
// viewport clips them, so the damage is this node's own rect. It is a
|
|
@@ -707,6 +715,47 @@ export const Scrollable = (Base) =>
|
|
|
707
715
|
return true;
|
|
708
716
|
}
|
|
709
717
|
|
|
718
|
+
/**
|
|
719
|
+
* The pane's half of `Node.anchorScrollBy`: a shift of the offsets the
|
|
720
|
+
* content asked for, added up until the pass applies it
|
|
721
|
+
* (`_applyAnchorShift`). The pass measures the content again, since
|
|
722
|
+
* what moved under the viewport moved by being laid out; one asked
|
|
723
|
+
* outside a pass asks for one.
|
|
724
|
+
*/
|
|
725
|
+
_anchorScrollBy(dx, dy) {
|
|
726
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return false;
|
|
727
|
+
if (dx === 0 && dy === 0) return true;
|
|
728
|
+
const shift = (this._anchorShift ??= { x: 0, y: 0 });
|
|
729
|
+
shift.x += dx;
|
|
730
|
+
shift.y += dy;
|
|
731
|
+
this._scrollMeasureDirty = true;
|
|
732
|
+
const root = this.root;
|
|
733
|
+
if (root && !root._inFlush) root.invalidate(true, this, 'scroll');
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Apply the shift the content asked for (`_anchorScrollBy`), before
|
|
739
|
+
* anything else the pass moves the offsets by: on each axis it scrolls,
|
|
740
|
+
* the clamp after takes it into the extent just measured. Its
|
|
741
|
+
* `onScroll` and its claim of the viewport are the pass's, as for a
|
|
742
|
+
* held `scrollTo`. A scroll the application asked for since the last
|
|
743
|
+
* pass drops it: the shift was measured against the offsets before
|
|
744
|
+
* that scroll, which is where the application wants the pane now.
|
|
745
|
+
*/
|
|
746
|
+
_applyAnchorShift() {
|
|
747
|
+
const shift = this._anchorShift;
|
|
748
|
+
const scrolled = this._scrolledSincePass === true;
|
|
749
|
+
this._scrolledSincePass = false;
|
|
750
|
+
if (!shift) return;
|
|
751
|
+
this._anchorShift = null;
|
|
752
|
+
if (scrolled) return;
|
|
753
|
+
// `scrollX` is a distance from the start in either direction, and
|
|
754
|
+
// content that moved towards the end is followed the same way
|
|
755
|
+
this.scrollX += this.direction === 'rtl' ? -shift.x : shift.x;
|
|
756
|
+
this.scrollY += shift.y;
|
|
757
|
+
}
|
|
758
|
+
|
|
710
759
|
/**
|
|
711
760
|
* Answer a held request (`_holdScrollTo`) against the extent this pass
|
|
712
761
|
* has just measured: each held axis's base, clamped, then each step
|
|
@@ -533,10 +533,12 @@ export function setMeasuringShrink(node, axis, out) {
|
|
|
533
533
|
* It is still laid out, though, and a leaf's answer could reach something
|
|
534
534
|
* that is read through its size. So it is marked only where it cannot:
|
|
535
535
|
*
|
|
536
|
-
* - every box from
|
|
537
|
-
* its content — the
|
|
538
|
-
*
|
|
539
|
-
*
|
|
536
|
+
* - every box from `node` down to it is as wide as something other than
|
|
537
|
+
* its content — `node` the width the pass gives it (no room, for the
|
|
538
|
+
* window; its column's, for a spine root; its own, for a box that sizes
|
|
539
|
+
* itself), and below it a width it names or a column stretching it — so
|
|
540
|
+
* the width the leaf answers is one yoga replaces, and nothing above it
|
|
541
|
+
* is sized from it;
|
|
540
542
|
* - no box is handed a share of space the leaf's height changed, which is
|
|
541
543
|
* how a height could become somebody's width — an aspect ratio, a column
|
|
542
544
|
* that wraps, an image sized to the height it is given. Every column from
|
|
@@ -656,15 +658,28 @@ function stretchedAlong(node, axis) {
|
|
|
656
658
|
* names are numbers — a height, a basis (a height, down a column), a floor or
|
|
657
659
|
* a ceiling; and `'unknown'` for a share of something (a percentage), which
|
|
658
660
|
* a pass with no height on offer resolves as it likes.
|
|
661
|
+
*
|
|
662
|
+
* A floor of nothing bounds nothing: a box as tall as its content is at
|
|
663
|
+
* least that tall already, so its height is its content's. It is how a box
|
|
664
|
+
* says it may shrink, and a browser's page has one on every box from the
|
|
665
|
+
* window down to its scroller, so that a page taller than the window
|
|
666
|
+
* scrolls. Read as a height of the box's own, it stopped the walk at the
|
|
667
|
+
* first of them, where the scroller gives way: the whole document in the
|
|
668
|
+
* scroller was set a word to a line each time the tree was measured, 100
|
|
669
|
+
* to 250 ms of a frame for a long Wikipedia article, and nothing read any
|
|
670
|
+
* of it. And it took the box pinned to both edges of such a column for one
|
|
671
|
+
* whose height nothing in flow decides, which it is.
|
|
659
672
|
*/
|
|
660
673
|
function heightOf(yoga) {
|
|
674
|
+
const floor = yoga.getMinHeight();
|
|
661
675
|
let own = false;
|
|
662
676
|
for (const value of [
|
|
663
677
|
yoga.getHeight(),
|
|
664
678
|
yoga.getFlexBasis(),
|
|
665
|
-
|
|
679
|
+
floor.value === 0 ? null : floor,
|
|
666
680
|
yoga.getMaxHeight(),
|
|
667
681
|
]) {
|
|
682
|
+
if (value === null) continue;
|
|
668
683
|
if (value.unit === Yoga.UNIT_PERCENT) return 'unknown';
|
|
669
684
|
if (value.unit === Yoga.UNIT_POINT) own = true;
|
|
670
685
|
}
|
package/src/nodes/window/size.js
CHANGED
|
@@ -354,12 +354,28 @@ export class WindowSize {
|
|
|
354
354
|
if (axis === 'width') {
|
|
355
355
|
const shrunk = [];
|
|
356
356
|
setMeasuringShrink(root, axis, shrunk);
|
|
357
|
+
// Nothing reads a root's own width — only heights travel up a spine
|
|
358
|
+
// (spine.js) — so what it holds is read only where a floor is written
|
|
359
|
+
// from it, as down the window's columns, and a leaf that is the root
|
|
360
|
+
// is laid out at the width its column offers, which is no answer of
|
|
361
|
+
// its own: a document in a scroller is a root when it changes.
|
|
362
|
+
const read = NO_UNREAD_WIDTHS || this._readEveryWidth;
|
|
363
|
+
const unread = [];
|
|
364
|
+
if (!read && root._measureFn) {
|
|
365
|
+
root._widthUnread = true;
|
|
366
|
+
unread.push(root);
|
|
367
|
+
} else if (!read) {
|
|
368
|
+
markUnreadLeaves(root, unread);
|
|
369
|
+
}
|
|
357
370
|
this._scopedFloorPasses += 1;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
371
|
+
try {
|
|
372
|
+
yoga.calculateLayout(widths.widthPass, undefined, dir);
|
|
373
|
+
} finally {
|
|
374
|
+
for (const leaf of unread) leaf._widthUnread = false;
|
|
375
|
+
}
|
|
376
|
+
const span = contentSpan(root, axis, null, this, read);
|
|
377
|
+
const own = declaresOwnMinimum(root, axis);
|
|
378
|
+
if (own || read) root._floorW = own ? yoga.getComputedWidth() : span;
|
|
363
379
|
restoreShrink(shrunk);
|
|
364
380
|
return;
|
|
365
381
|
}
|
|
@@ -443,9 +459,18 @@ export class WindowSize {
|
|
|
443
459
|
if (axis === 'width') {
|
|
444
460
|
const shrunk = [];
|
|
445
461
|
setMeasuringShrink(boundary, axis, shrunk);
|
|
462
|
+
// its own extent is its style's, so what it holds is read only where
|
|
463
|
+
// a floor is written from it, as down the window's columns
|
|
464
|
+
const read = NO_UNREAD_WIDTHS || this._readEveryWidth;
|
|
465
|
+
const unread = [];
|
|
466
|
+
if (!read) markUnreadLeaves(boundary, unread);
|
|
446
467
|
this._scopedFloorPasses += 1;
|
|
447
|
-
|
|
448
|
-
|
|
468
|
+
try {
|
|
469
|
+
yoga.calculateLayout(undefined, undefined, dir);
|
|
470
|
+
} finally {
|
|
471
|
+
for (const leaf of unread) leaf._widthUnread = false;
|
|
472
|
+
}
|
|
473
|
+
contentSpan(boundary, axis, null, this, read);
|
|
449
474
|
restoreShrink(shrunk);
|
|
450
475
|
return;
|
|
451
476
|
}
|
package/src/wayland/context2d.js
CHANGED
|
@@ -92,6 +92,9 @@ const SWIZZLE_STRAIGHT = 2; // straight RGBA (ImageData)
|
|
|
92
92
|
/** floats per vertex: pos(2) uv(2) color(4) params(4) */
|
|
93
93
|
const STRIDE = 12;
|
|
94
94
|
const MAX_VERTS = 6 * 4096;
|
|
95
|
+
/** What a context's vertex array starts at: a surface that draws a handful
|
|
96
|
+
* of quads should not pay for the window's batch. */
|
|
97
|
+
const FIRST_VERTS = 6 * 64;
|
|
95
98
|
const TRI = [0, 1, 2, 0, 2, 3];
|
|
96
99
|
|
|
97
100
|
const VERT = `
|
|
@@ -265,6 +268,63 @@ function parseColorUncached(value) {
|
|
|
265
268
|
* use. Interpolated premultiplied, which is what keeps a fade to transparent
|
|
266
269
|
* from passing through grey.
|
|
267
270
|
*/
|
|
271
|
+
/**
|
|
272
|
+
* The textures made for a gradient or an image are found by the object, in
|
|
273
|
+
* a WeakMap — and a WeakMap cannot say when its key goes, so its entry went
|
|
274
|
+
* with the key and the texture stayed in the driver. A page that makes its
|
|
275
|
+
* gradients as it paints made a texture a paint, and every surface context
|
|
276
|
+
* left behind what it had uploaded. Each is registered here and deleted
|
|
277
|
+
* when its key is collected, unless the context that made it was destroyed
|
|
278
|
+
* first and deleted it then (`made` is that context's set, which is all the
|
|
279
|
+
* registration holds of it).
|
|
280
|
+
*/
|
|
281
|
+
const textureReaper = new FinalizationRegistry(({ gl, tex, made }) => {
|
|
282
|
+
if (!made.delete(tex)) return;
|
|
283
|
+
try {
|
|
284
|
+
gl.deleteTexture(tex);
|
|
285
|
+
} catch {
|
|
286
|
+
// the device went first
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* What the contexts on one device share (device.js): the program and its
|
|
292
|
+
* locations, the GL buffer a batch goes up through, and the glyph atlas.
|
|
293
|
+
* Each was a context's own, so every offscreen surface compiled and linked
|
|
294
|
+
* the program, allocated a buffer as large as the window's batch and
|
|
295
|
+
* rasterised every glyph it drew into an empty atlas — a few milliseconds a
|
|
296
|
+
* surface, which <Html> made at every width of a resize. Counted by the
|
|
297
|
+
* contexts that hold it; the last one destroyed deletes it.
|
|
298
|
+
*
|
|
299
|
+
* Not the vertex array: the device's owner is the only context with quads
|
|
300
|
+
* buffered, but `releaseDevice` lets a context keep a batch across foreign
|
|
301
|
+
* GL with no owner at all, and another context's quads written into a
|
|
302
|
+
* shared array would land on top of it. Each context keeps its own, grown
|
|
303
|
+
* as it draws. The GL buffer is filled from it immediately before each draw,
|
|
304
|
+
* so sharing that is safe.
|
|
305
|
+
*/
|
|
306
|
+
function sharedOf(device, gl) {
|
|
307
|
+
if (device.shared) return device.shared;
|
|
308
|
+
const atlas = new GlyphAtlas(gl);
|
|
309
|
+
const shared = {
|
|
310
|
+
refs: 0,
|
|
311
|
+
program: null,
|
|
312
|
+
loc: null,
|
|
313
|
+
buffer: null,
|
|
314
|
+
atlas,
|
|
315
|
+
/** the contexts with glyph quads buffered against the atlas */
|
|
316
|
+
glyphHolders: new Set(),
|
|
317
|
+
};
|
|
318
|
+
// Quads already buffered against the atlas must be drawn before it lays
|
|
319
|
+
// itself out again under them — every context's, owner or not: drawing
|
|
320
|
+
// one claims the device and puts its own state back first.
|
|
321
|
+
atlas.onBeforeGrow = () => {
|
|
322
|
+
for (const ctx of [...shared.glyphHolders]) ctx._flush();
|
|
323
|
+
};
|
|
324
|
+
device.shared = shared;
|
|
325
|
+
return shared;
|
|
326
|
+
}
|
|
327
|
+
|
|
268
328
|
class Gradient {
|
|
269
329
|
constructor(kind, a) {
|
|
270
330
|
this.kind = kind; // 'linear' | 'radial'
|
|
@@ -378,18 +438,16 @@ export class WaylandContext2D {
|
|
|
378
438
|
this.fontManager = fontManager;
|
|
379
439
|
/** @see ./device.js — the state this context shares with its siblings */
|
|
380
440
|
this._device = glDevice(gl);
|
|
441
|
+
this._shared = sharedOf(this._device, gl);
|
|
442
|
+
this._shared.refs++;
|
|
381
443
|
this._makeCurrent = makeCurrent ?? null;
|
|
382
444
|
// `TextLayout.draw` opens with `ctx.window.app.display.Render` and reads
|
|
383
445
|
// one constant from it. Satisfy the shape rather than fork the layout.
|
|
384
446
|
this.window = { app: { display: { Render: RENDER } } };
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
this.
|
|
389
|
-
// Path fills that go through the CPU rasteriser land here as A8 masks,
|
|
390
|
-
// drawn like glyphs; emptied every frame, since a mask is one draw.
|
|
391
|
-
this.masks = new GlyphAtlas(gl, { size: 256 });
|
|
392
|
-
this.masks.onBeforeGrow = () => this._flush();
|
|
447
|
+
/** the device's glyph atlas (`sharedOf`) */
|
|
448
|
+
this.atlas = this._shared.atlas;
|
|
449
|
+
/** this context's mask atlas, made the first time a fill needs one */
|
|
450
|
+
this._masks = null;
|
|
393
451
|
this._maskSeq = 0;
|
|
394
452
|
/** the non-rectangular clips in force, device space, outermost first */
|
|
395
453
|
this._clipPath = null;
|
|
@@ -426,7 +484,7 @@ export class WaylandContext2D {
|
|
|
426
484
|
this._width = 0;
|
|
427
485
|
this._height = 0;
|
|
428
486
|
|
|
429
|
-
this._verts = new Float32Array(
|
|
487
|
+
this._verts = new Float32Array(FIRST_VERTS * STRIDE);
|
|
430
488
|
this._n = 0;
|
|
431
489
|
this._mode = MODE_SOLID;
|
|
432
490
|
this._texture = null;
|
|
@@ -439,26 +497,69 @@ export class WaylandContext2D {
|
|
|
439
497
|
/** image object -> { tex, w, h } */
|
|
440
498
|
this._textures = new WeakMap();
|
|
441
499
|
this._gradTex = new WeakMap();
|
|
500
|
+
/** the textures the two maps above hold, for `destroy()` */
|
|
501
|
+
this._madeTextures = new Set();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Path fills that go through the CPU rasteriser land here as A8 masks,
|
|
506
|
+
* drawn like glyphs; emptied every frame, since a mask is one draw. A
|
|
507
|
+
* mask's key is this context's own count (`_maskSeq`), so the atlas is
|
|
508
|
+
* this context's — and only made once a fill needs it.
|
|
509
|
+
*/
|
|
510
|
+
get masks() {
|
|
511
|
+
if (!this._masks) {
|
|
512
|
+
this._masks = new GlyphAtlas(this.gl, { size: 256 });
|
|
513
|
+
this._masks.onBeforeGrow = () => this._flush();
|
|
514
|
+
}
|
|
515
|
+
return this._masks;
|
|
442
516
|
}
|
|
443
517
|
|
|
444
518
|
// ---- lifecycle --------------------------------------------------------
|
|
445
519
|
|
|
446
|
-
/**
|
|
520
|
+
/** Take the device's program and buffer, compiling and allocating them
|
|
521
|
+
* the first time. Context must be current. */
|
|
447
522
|
init() {
|
|
448
|
-
const gl = this.gl;
|
|
449
523
|
if (this._program) return;
|
|
524
|
+
const shared = this._shared;
|
|
525
|
+
if (!shared.program) this._build(shared);
|
|
526
|
+
this._program = shared.program;
|
|
527
|
+
this._loc = shared.loc;
|
|
528
|
+
this._buffer = shared.buffer;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Compile the program and allocate the vertex buffer the device shares. */
|
|
532
|
+
_build(shared) {
|
|
533
|
+
const gl = this.gl;
|
|
450
534
|
const vs = compile(gl, gl.VERTEX_SHADER, VERT, 'vertex');
|
|
451
|
-
|
|
535
|
+
let fs;
|
|
536
|
+
try {
|
|
537
|
+
fs = compile(gl, gl.FRAGMENT_SHADER, FRAG, 'fragment');
|
|
538
|
+
} catch (err) {
|
|
539
|
+
gl.deleteShader(vs);
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
452
542
|
const p = gl.createProgram();
|
|
453
543
|
gl.attachShader(p, vs);
|
|
454
544
|
gl.attachShader(p, fs);
|
|
455
545
|
gl.linkProgram(p);
|
|
456
|
-
|
|
457
|
-
|
|
546
|
+
const linked = gl.getProgramParameter(p, gl.LINK_STATUS);
|
|
547
|
+
const log = linked ? '' : gl.getProgramInfoLog(p);
|
|
548
|
+
// The shader objects are ours to delete once the program is linked.
|
|
549
|
+
// Deleting a program detaches its shaders and leaves them (GLES 3.0,
|
|
550
|
+
// 7.3), so every context — a window's, and each offscreen surface's —
|
|
551
|
+
// left two behind, and a resize that makes a surface a frame left two a
|
|
552
|
+
// frame. Deleted while attached, they are flagged and go with the
|
|
553
|
+
// program.
|
|
554
|
+
gl.deleteShader(vs);
|
|
555
|
+
gl.deleteShader(fs);
|
|
556
|
+
if (!linked) {
|
|
557
|
+
gl.deleteProgram(p);
|
|
558
|
+
throw new Error(`2d program failed to link: ${log}`);
|
|
458
559
|
}
|
|
459
|
-
|
|
560
|
+
shared.program = p;
|
|
460
561
|
const u = (n) => gl.getUniformLocation(p, n);
|
|
461
|
-
|
|
562
|
+
shared.loc = {
|
|
462
563
|
aPos: gl.getAttribLocation(p, 'aPos'),
|
|
463
564
|
aUV: gl.getAttribLocation(p, 'aUV'),
|
|
464
565
|
aColor: gl.getAttribLocation(p, 'aColor'),
|
|
@@ -471,11 +572,16 @@ export class WaylandContext2D {
|
|
|
471
572
|
uGradA: u('uGradA'),
|
|
472
573
|
uGradB: u('uGradB'),
|
|
473
574
|
};
|
|
474
|
-
|
|
475
|
-
gl.bindBuffer(gl.ARRAY_BUFFER,
|
|
575
|
+
shared.buffer = gl.createBuffer();
|
|
576
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, shared.buffer);
|
|
476
577
|
// The addon's bufferData takes a TypedArray, not the byte-length
|
|
477
|
-
// overload WebGL also accepts. Allocated once
|
|
478
|
-
|
|
578
|
+
// overload WebGL also accepts. Allocated once a device, as large as a
|
|
579
|
+
// batch can be; frames use bufferSubData.
|
|
580
|
+
gl.bufferData(
|
|
581
|
+
gl.ARRAY_BUFFER,
|
|
582
|
+
new Float32Array(MAX_VERTS * STRIDE),
|
|
583
|
+
gl.DYNAMIC_DRAW,
|
|
584
|
+
);
|
|
479
585
|
}
|
|
480
586
|
|
|
481
587
|
/** Where this context draws. A window's backing, or a surface's target. */
|
|
@@ -521,7 +627,7 @@ export class WaylandContext2D {
|
|
|
521
627
|
this._clipPath = null;
|
|
522
628
|
this._stencilling = false;
|
|
523
629
|
this._maskSeq = 0;
|
|
524
|
-
this.
|
|
630
|
+
this._masks?.reset();
|
|
525
631
|
this._applyState();
|
|
526
632
|
// scratch for paths and clips: a frame starts with it clean
|
|
527
633
|
const gl = this.gl;
|
|
@@ -1059,6 +1165,7 @@ export class WaylandContext2D {
|
|
|
1059
1165
|
const ramp = grad.ramp();
|
|
1060
1166
|
if (!tex) {
|
|
1061
1167
|
tex = gl.createTexture();
|
|
1168
|
+
this._keepTexture(grad, tex);
|
|
1062
1169
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1063
1170
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1064
1171
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
@@ -1631,8 +1738,9 @@ export class WaylandContext2D {
|
|
|
1631
1738
|
? data
|
|
1632
1739
|
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1633
1740
|
if (!entry || entry.w !== w || entry.h !== h) {
|
|
1634
|
-
if (entry)
|
|
1741
|
+
if (entry) this._dropTexture(entry.tex);
|
|
1635
1742
|
const tex = gl.createTexture();
|
|
1743
|
+
this._keepTexture(img, tex);
|
|
1636
1744
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
1637
1745
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
1638
1746
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
@@ -1792,13 +1900,12 @@ export class WaylandContext2D {
|
|
|
1792
1900
|
const size = run.size ?? font.size ?? 16;
|
|
1793
1901
|
const color = this._color(run.color ?? this.fillStyle);
|
|
1794
1902
|
if (color[3] === 0) continue;
|
|
1795
|
-
const
|
|
1903
|
+
const table = this.atlas.table(`${fontKey(font)}|${size}`);
|
|
1796
1904
|
const tex = this.atlas.bind();
|
|
1797
1905
|
this._setMode(MODE_GLYPH, tex, SWIZZLE_RGBA);
|
|
1906
|
+
this._shared.glyphHolders.add(this);
|
|
1798
1907
|
for (const g of run.glyphs) {
|
|
1799
|
-
const entry = this.atlas.
|
|
1800
|
-
font.rasterize(g.id, size),
|
|
1801
|
-
);
|
|
1908
|
+
const entry = this.atlas.glyph(table, font, g.id, size);
|
|
1802
1909
|
if (entry.empty) continue;
|
|
1803
1910
|
// `left`/`top` are the bitmap's offset from the glyph origin in the
|
|
1804
1911
|
// rasteriser's **y-down** pixel space: a cap-height glyph has a
|
|
@@ -1807,31 +1914,20 @@ export class WaylandContext2D {
|
|
|
1807
1914
|
const oy = Math.round(applyY(m, g.x, g.y)) + entry.top;
|
|
1808
1915
|
// the atlas may have grown mid-run; rebind so the batch names the
|
|
1809
1916
|
// new texture (the old quads were flushed by onBeforeGrow)
|
|
1810
|
-
if (this.atlas.texture !== this._texture)
|
|
1917
|
+
if (this.atlas.texture !== this._texture) {
|
|
1811
1918
|
this._setMode(MODE_GLYPH, this.atlas.bind(), SWIZZLE_RGBA);
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
[
|
|
1824
|
-
entry.u0,
|
|
1825
|
-
entry.v0,
|
|
1826
|
-
entry.u1,
|
|
1827
|
-
entry.v0,
|
|
1828
|
-
entry.u1,
|
|
1829
|
-
entry.v1,
|
|
1830
|
-
entry.u0,
|
|
1831
|
-
entry.v1,
|
|
1832
|
-
],
|
|
1919
|
+
this._shared.glyphHolders.add(this);
|
|
1920
|
+
}
|
|
1921
|
+
this._quadRect(
|
|
1922
|
+
ox,
|
|
1923
|
+
oy,
|
|
1924
|
+
ox + entry.w,
|
|
1925
|
+
oy + entry.h,
|
|
1926
|
+
entry.u0,
|
|
1927
|
+
entry.v0,
|
|
1928
|
+
entry.u1,
|
|
1929
|
+
entry.v1,
|
|
1833
1930
|
color,
|
|
1834
|
-
ZERO4,
|
|
1835
1931
|
);
|
|
1836
1932
|
this.shapeStats.glyphs++;
|
|
1837
1933
|
}
|
|
@@ -1992,7 +2088,7 @@ export class WaylandContext2D {
|
|
|
1992
2088
|
// `_take` is the only thing that can get it issued before someone reads
|
|
1993
2089
|
// the pixels it makes. A property compare in the common case.
|
|
1994
2090
|
this._claim();
|
|
1995
|
-
|
|
2091
|
+
this._room(6);
|
|
1996
2092
|
const v = this._verts;
|
|
1997
2093
|
const each = params.length === 16 ? 4 : 0;
|
|
1998
2094
|
let o = this._n * STRIDE;
|
|
@@ -2019,7 +2115,7 @@ export class WaylandContext2D {
|
|
|
2019
2115
|
/** One triangle, for the stencil pass. Colour and params are irrelevant. */
|
|
2020
2116
|
_tri(x0, y0, x1, y1, x2, y2) {
|
|
2021
2117
|
this._claim();
|
|
2022
|
-
|
|
2118
|
+
this._room(3);
|
|
2023
2119
|
const v = this._verts;
|
|
2024
2120
|
let o = this._n * STRIDE;
|
|
2025
2121
|
const pts = [x0, y0, x1, y1, x2, y2];
|
|
@@ -2031,11 +2127,61 @@ export class WaylandContext2D {
|
|
|
2031
2127
|
this._n += 3;
|
|
2032
2128
|
}
|
|
2033
2129
|
|
|
2130
|
+
/**
|
|
2131
|
+
* An axis-aligned quad, TL-TR-BR-BL, with no params: `_quad` for a glyph,
|
|
2132
|
+
* written straight into the batch rather than through two arrays of
|
|
2133
|
+
* corners made for it — a glyph's worth of garbage a glyph.
|
|
2134
|
+
*/
|
|
2135
|
+
_quadRect(x0, y0, x1, y1, u0, v0, u1, v1, color) {
|
|
2136
|
+
this._claim();
|
|
2137
|
+
this._room(6);
|
|
2138
|
+
const v = this._verts;
|
|
2139
|
+
let o = this._n * STRIDE;
|
|
2140
|
+
const r = color[0];
|
|
2141
|
+
const g = color[1];
|
|
2142
|
+
const b = color[2];
|
|
2143
|
+
const a = color[3];
|
|
2144
|
+
for (let k = 0; k < 6; k++) {
|
|
2145
|
+
const i = TRI[k];
|
|
2146
|
+
const right = i === 1 || i === 2;
|
|
2147
|
+
const bottom = i >= 2;
|
|
2148
|
+
v[o++] = right ? x1 : x0;
|
|
2149
|
+
v[o++] = bottom ? y1 : y0;
|
|
2150
|
+
v[o++] = right ? u1 : u0;
|
|
2151
|
+
v[o++] = bottom ? v1 : v0;
|
|
2152
|
+
v[o++] = r;
|
|
2153
|
+
v[o++] = g;
|
|
2154
|
+
v[o++] = b;
|
|
2155
|
+
v[o++] = a;
|
|
2156
|
+
v[o++] = 0;
|
|
2157
|
+
v[o++] = 0;
|
|
2158
|
+
v[o++] = 0;
|
|
2159
|
+
v[o++] = 0;
|
|
2160
|
+
}
|
|
2161
|
+
this._n += 6;
|
|
2162
|
+
this.shapeStats.quads++;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/** Room for `count` more vertices: the batch drawn where it is full, and
|
|
2166
|
+
* the array grown where it is smaller than a batch can be. */
|
|
2167
|
+
_room(count) {
|
|
2168
|
+
if (this._n + count > MAX_VERTS) this._flush();
|
|
2169
|
+
const need = (this._n + count) * STRIDE;
|
|
2170
|
+
if (need <= this._verts.length) return;
|
|
2171
|
+
const grown = new Float32Array(
|
|
2172
|
+
Math.min(MAX_VERTS * STRIDE, Math.max(need, this._verts.length * 2)),
|
|
2173
|
+
);
|
|
2174
|
+
grown.set(this._verts.subarray(0, this._n * STRIDE));
|
|
2175
|
+
this._verts = grown;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2034
2178
|
_flush() {
|
|
2035
2179
|
// Before the early return, not after: `clearRect` and `putImageData`
|
|
2036
2180
|
// flush, change the blend function and flush again, and the second
|
|
2037
2181
|
// flush must not be the one that re-takes the device and puts the
|
|
2038
2182
|
// blending back.
|
|
2183
|
+
// drawn or empty, nothing of this context's is left against the atlas
|
|
2184
|
+
this._shared.glyphHolders.delete(this);
|
|
2039
2185
|
this._claim();
|
|
2040
2186
|
if (this._n === 0) return;
|
|
2041
2187
|
const gl = this.gl;
|
|
@@ -2072,7 +2218,8 @@ export class WaylandContext2D {
|
|
|
2072
2218
|
// then it samples texels those glyphs never reached: garbled text,
|
|
2073
2219
|
// kept until that region is next repainted.
|
|
2074
2220
|
if (this._texture === this.atlas.texture) this.atlas.bind();
|
|
2075
|
-
else if (this._texture === this.
|
|
2221
|
+
else if (this._masks && this._texture === this._masks.texture)
|
|
2222
|
+
this._masks.bind();
|
|
2076
2223
|
gl.activeTexture(gl.TEXTURE0);
|
|
2077
2224
|
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
|
2078
2225
|
gl.uniform1i(this._loc.uTex, 0);
|
|
@@ -2127,14 +2274,41 @@ export class WaylandContext2D {
|
|
|
2127
2274
|
};
|
|
2128
2275
|
}
|
|
2129
2276
|
|
|
2277
|
+
/** A texture made for `key`: deleted when `key` is collected, or when
|
|
2278
|
+
* this context is destroyed, whichever comes first. */
|
|
2279
|
+
_keepTexture(key, tex) {
|
|
2280
|
+
this._madeTextures.add(tex);
|
|
2281
|
+
textureReaper.register(key, {
|
|
2282
|
+
gl: this.gl,
|
|
2283
|
+
tex,
|
|
2284
|
+
made: this._madeTextures,
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
/** A texture made for a key that is being given another. */
|
|
2289
|
+
_dropTexture(tex) {
|
|
2290
|
+
if (this._madeTextures.delete(tex)) this.gl.deleteTexture(tex);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2130
2293
|
destroy() {
|
|
2131
2294
|
// Nothing of this context's is installed any more, and the device
|
|
2132
2295
|
// record should not be the one thing still holding on to it.
|
|
2296
|
+
if (this._destroyed) return;
|
|
2297
|
+
this._destroyed = true;
|
|
2133
2298
|
this._release();
|
|
2134
|
-
this.
|
|
2135
|
-
this.
|
|
2136
|
-
|
|
2137
|
-
|
|
2299
|
+
for (const tex of this._madeTextures) this.gl.deleteTexture(tex);
|
|
2300
|
+
this._madeTextures.clear();
|
|
2301
|
+
this._masks?.destroy();
|
|
2302
|
+
this._masks = null;
|
|
2303
|
+
const shared = this._shared;
|
|
2304
|
+
shared.glyphHolders.delete(this);
|
|
2305
|
+
if (--shared.refs === 0) {
|
|
2306
|
+
// the last context on the device: what it shared goes with it
|
|
2307
|
+
shared.atlas.destroy();
|
|
2308
|
+
if (shared.buffer) this.gl.deleteBuffer(shared.buffer);
|
|
2309
|
+
if (shared.program) this.gl.deleteProgram(shared.program);
|
|
2310
|
+
if (this._device.shared === shared) this._device.shared = null;
|
|
2311
|
+
}
|
|
2138
2312
|
this._buffer = null;
|
|
2139
2313
|
this._program = null;
|
|
2140
2314
|
}
|
|
@@ -2328,9 +2502,9 @@ function compile(gl, type, src, what) {
|
|
|
2328
2502
|
gl.shaderSource(sh, src);
|
|
2329
2503
|
gl.compileShader(sh);
|
|
2330
2504
|
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
);
|
|
2505
|
+
const log = gl.getShaderInfoLog(sh);
|
|
2506
|
+
gl.deleteShader(sh);
|
|
2507
|
+
throw new Error(`${what} shader failed to compile: ${log}`);
|
|
2334
2508
|
}
|
|
2335
2509
|
return sh;
|
|
2336
2510
|
}
|
|
@@ -34,6 +34,8 @@ export class GlyphAtlas {
|
|
|
34
34
|
this.size = size;
|
|
35
35
|
/** key -> { x, y, w, h, left, top, u0, v0, u1, v1 } */
|
|
36
36
|
this.entries = new Map();
|
|
37
|
+
/** a face at a size -> (glyph id -> entry), for `glyph()` */
|
|
38
|
+
this._tables = new Map();
|
|
37
39
|
this._shelfY = PAD;
|
|
38
40
|
this._shelfH = 0;
|
|
39
41
|
this._shelfX = PAD;
|
|
@@ -58,17 +60,47 @@ export class GlyphAtlas {
|
|
|
58
60
|
get(key, raster) {
|
|
59
61
|
const hit = this.entries.get(key);
|
|
60
62
|
if (hit) return hit;
|
|
63
|
+
const entry = this._insert(raster());
|
|
64
|
+
if (entry.kept) this.entries.set(key, entry);
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The entries of one face at one size, by glyph id. A run of text looks
|
|
70
|
+
* its table up once, by the string that names the face and the size, and
|
|
71
|
+
* each glyph in it by number (`glyph()`): a string key built and hashed a
|
|
72
|
+
* glyph was most of what drawing text cost once the glyphs were in.
|
|
73
|
+
*
|
|
74
|
+
* A table is emptied in place when the atlas grows or resets, so one held
|
|
75
|
+
* across a `glyph()` that grew the atlas finds its glyphs missing rather
|
|
76
|
+
* than placed where they no longer are.
|
|
77
|
+
*/
|
|
78
|
+
table(key) {
|
|
79
|
+
let table = this._tables.get(key);
|
|
80
|
+
if (!table) this._tables.set(key, (table = new Map()));
|
|
81
|
+
return table;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One glyph of a `table()`, rasterised by `font` on a miss. */
|
|
85
|
+
glyph(table, font, id, size) {
|
|
86
|
+
const hit = table.get(id);
|
|
87
|
+
if (hit) return hit;
|
|
88
|
+
const entry = this._insert(font.rasterize(id, size));
|
|
89
|
+
if (entry.kept) table.set(id, entry);
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
61
92
|
|
|
62
|
-
|
|
93
|
+
/** Place a rasterised glyph, or say it has no ink. `kept` is whether the
|
|
94
|
+
* answer is worth remembering: a glyph the atlas had no room for is not,
|
|
95
|
+
* so the next frame asks again. */
|
|
96
|
+
_insert(g) {
|
|
63
97
|
// A space has no ink. Cache the absence so the miss is paid once.
|
|
64
98
|
if (!g || g.width <= 0 || g.height <= 0) {
|
|
65
|
-
|
|
66
|
-
this.entries.set(key, empty);
|
|
67
|
-
return empty;
|
|
99
|
+
return { empty: true, kept: true, left: g?.left ?? 0, top: g?.top ?? 0 };
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
const spot = this._place(g.width, g.height);
|
|
71
|
-
if (!spot) return { empty: true, left: g.left, top: g.top };
|
|
103
|
+
if (!spot) return { empty: true, kept: false, left: g.left, top: g.top };
|
|
72
104
|
|
|
73
105
|
for (let row = 0; row < g.height; row++) {
|
|
74
106
|
const src = row * g.width;
|
|
@@ -78,8 +110,9 @@ export class GlyphAtlas {
|
|
|
78
110
|
this._markDirty(spot.x, spot.y, g.width, g.height);
|
|
79
111
|
|
|
80
112
|
const inv = 1 / this.size;
|
|
81
|
-
|
|
113
|
+
return {
|
|
82
114
|
empty: false,
|
|
115
|
+
kept: true,
|
|
83
116
|
x: spot.x,
|
|
84
117
|
y: spot.y,
|
|
85
118
|
w: g.width,
|
|
@@ -91,8 +124,6 @@ export class GlyphAtlas {
|
|
|
91
124
|
u1: (spot.x + g.width) * inv,
|
|
92
125
|
v1: (spot.y + g.height) * inv,
|
|
93
126
|
};
|
|
94
|
-
this.entries.set(key, entry);
|
|
95
|
-
return entry;
|
|
96
127
|
}
|
|
97
128
|
|
|
98
129
|
_markDirty(x, y, w, h) {
|
|
@@ -139,6 +170,7 @@ export class GlyphAtlas {
|
|
|
139
170
|
this.size *= 2;
|
|
140
171
|
this._pixels = new Uint8Array(this.size * this.size);
|
|
141
172
|
this.entries.clear();
|
|
173
|
+
for (const table of this._tables.values()) table.clear();
|
|
142
174
|
this._shelfX = PAD;
|
|
143
175
|
this._shelfY = PAD;
|
|
144
176
|
this._shelfH = 0;
|
|
@@ -158,8 +190,9 @@ export class GlyphAtlas {
|
|
|
158
190
|
* the CPU copy until new entries overwrite them; nothing names them.
|
|
159
191
|
*/
|
|
160
192
|
reset() {
|
|
161
|
-
if (this.entries.size === 0) return;
|
|
193
|
+
if (this.entries.size === 0 && this._tables.size === 0) return;
|
|
162
194
|
this.entries.clear();
|
|
195
|
+
for (const table of this._tables.values()) table.clear();
|
|
163
196
|
this._shelfX = PAD;
|
|
164
197
|
this._shelfY = PAD;
|
|
165
198
|
this._shelfH = 0;
|
|
@@ -233,5 +266,6 @@ export class GlyphAtlas {
|
|
|
233
266
|
if (this.texture) this.gl.deleteTexture(this.texture);
|
|
234
267
|
this.texture = null;
|
|
235
268
|
this.entries.clear();
|
|
269
|
+
for (const table of this._tables.values()) table.clear();
|
|
236
270
|
}
|
|
237
271
|
}
|