react-x11 2.14.0 → 2.15.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 +1 -1
- package/src/glnodes.js +15 -3
- package/src/ntk.js +10 -1
- package/src/types/system.d.ts +14 -1
- package/src/wayland/app.js +14 -0
- package/src/wayland/backendwindow.js +86 -10
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +176 -19
- package/src/wayland/device.js +55 -0
- package/src/wayland/framewatch.js +190 -0
- package/src/wayland/glarea.js +1 -0
- package/src/wayland/protocols/index.json +4 -0
- package/src/wayland/protocols/kde-server-decoration.json +1 -0
- package/src/wayland/ssd.js +154 -26
- package/src/wayland/surface.js +18 -0
- package/src/wayland/target.js +6 -0
- package/src/wayland/window.js +36 -9
- package/src/windowstate.js +41 -1
package/package.json
CHANGED
package/src/glnodes.js
CHANGED
|
@@ -272,9 +272,18 @@ export class GlAreaNode extends Node {
|
|
|
272
272
|
// (`EventManager._surfaceAt`).
|
|
273
273
|
this._joinSurfaces();
|
|
274
274
|
this.gl = wnd.getContext('opengl', config);
|
|
275
|
-
//
|
|
275
|
+
// A buffer freed by the display is a frame that can be drawn again — for
|
|
276
|
+
// a frame that was waiting for one. The Cocoa surface and the CGL
|
|
277
|
+
// context reopen their gate one display period after *every* swap, not
|
|
278
|
+
// only after a refused one, and a request per reopening turned
|
|
279
|
+
// frameLoop 'demand' into a loop at display rate: a still scene drew
|
|
280
|
+
// every frame, forever.
|
|
276
281
|
if (typeof this.gl?.onFrameAvailable !== 'undefined') {
|
|
277
|
-
this.gl.onFrameAvailable = () =>
|
|
282
|
+
this.gl.onFrameAvailable = () => {
|
|
283
|
+
if (!this._frameRefused) return;
|
|
284
|
+
this._frameRefused = false;
|
|
285
|
+
this.requestFrame();
|
|
286
|
+
};
|
|
278
287
|
}
|
|
279
288
|
// The context is only usable once MakeCurrent has answered, and that is
|
|
280
289
|
// where a server refusing indirect GLX says so (ntk gives the rejection
|
|
@@ -384,7 +393,10 @@ export class GlAreaNode extends Node {
|
|
|
384
393
|
// On the direct backend every buffer may still be held by the display,
|
|
385
394
|
// and drawing into one before it comes back would paint what is on
|
|
386
395
|
// screen. `onFrameAvailable` asks for this frame again when one frees.
|
|
387
|
-
if (direct && gl.canRender && !gl.canRender())
|
|
396
|
+
if (direct && gl.canRender && !gl.canRender()) {
|
|
397
|
+
this._frameRefused = true;
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
388
400
|
// binds this surface — the GPU context is shared between every <glarea>
|
|
389
401
|
// on the connection — and picks up a resize
|
|
390
402
|
gl.makeCurrent?.();
|
package/src/ntk.js
CHANGED
|
@@ -37,13 +37,22 @@ export { default } from 'ntk';
|
|
|
37
37
|
* ntk's offscreen `Surface`, on whichever backend `app` is.
|
|
38
38
|
*
|
|
39
39
|
* An app that makes its own surfaces answers `createSurface(options)` —
|
|
40
|
-
* the Cocoa app does, over a CG bitmap (src/cocoa/surface.js)
|
|
40
|
+
* the Cocoa app does, over a CG bitmap (src/cocoa/surface.js), the Wayland
|
|
41
|
+
* app over a GL render target (src/wayland/surface.js) — and an ntk
|
|
41
42
|
* connection has no such method and gets ntk's pixmap. The result is
|
|
42
43
|
* whichever implementation answered, not an instance of this class: the
|
|
43
44
|
* contract is the shape — `width`/`height`, `getContext('2d')`, `render`,
|
|
44
45
|
* `clear`, `copyWithin`, `destroy`, and `ctx.drawImage(surface, …)` —
|
|
45
46
|
* (docs/extending.md "Scrolling the pixels, not just the offset"), and
|
|
46
47
|
* nothing needs `instanceof`.
|
|
48
|
+
*
|
|
49
|
+
* Part of that shape is that a context may be **held**: ntk tells a caller
|
|
50
|
+
* doing many draws to take `getContext('2d')` once rather than one per
|
|
51
|
+
* frame, and that holds on every backend here, including the ones where a
|
|
52
|
+
* surface is a GPU target sharing a device with the window (#566). Draws
|
|
53
|
+
* through a held context land in the surface whenever they are made, and
|
|
54
|
+
* leave nothing for the caller to restore; `render(fn)` is the scoped form,
|
|
55
|
+
* for a caller that wants a frame's clean transform and clip.
|
|
47
56
|
*/
|
|
48
57
|
export class Surface {
|
|
49
58
|
constructor(app, options) {
|
package/src/types/system.d.ts
CHANGED
|
@@ -100,7 +100,8 @@ export function useScale(): number;
|
|
|
100
100
|
export interface WindowState {
|
|
101
101
|
/** This window has the keyboard. */
|
|
102
102
|
readonly focused: boolean;
|
|
103
|
-
/** **The one to branch on**: not minimized,
|
|
103
|
+
/** **The one to branch on**: not minimized, not fully covered, and still
|
|
104
|
+
* being given frames to draw. */
|
|
104
105
|
readonly visible: boolean;
|
|
105
106
|
/**
|
|
106
107
|
* Fully covered by other windows — **always false when a compositing
|
|
@@ -109,6 +110,18 @@ export interface WindowState {
|
|
|
109
110
|
* entirely visible. Prefer `visible`.
|
|
110
111
|
*/
|
|
111
112
|
readonly obscured: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* The display is still scheduling frames for this window.
|
|
115
|
+
*
|
|
116
|
+
* False on the **Wayland** backend once the compositor has stopped sending
|
|
117
|
+
* frame callbacks — a surface it is not showing gets none, so nothing the
|
|
118
|
+
* app draws reaches the screen until it starts again. It is how that
|
|
119
|
+
* backend answers the question `obscured` answers on a bare X server, it
|
|
120
|
+
* folds into `visible` the same way, and it takes a couple of seconds of
|
|
121
|
+
* silence to be sure. Always true where the backend has no such signal:
|
|
122
|
+
* X11, Cocoa, and the headless mock.
|
|
123
|
+
*/
|
|
124
|
+
readonly presenting: boolean;
|
|
112
125
|
/** `_NET_WM_STATE_HIDDEN`: iconified, or shaded away. */
|
|
113
126
|
readonly minimized: boolean;
|
|
114
127
|
/** Both axes. One axis alone shows up in `states`. */
|
package/src/wayland/app.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { EventEmitter } from 'node:events';
|
|
27
27
|
import { createRequire } from 'node:module';
|
|
28
28
|
import { WaylandConnection } from './connection.js';
|
|
29
|
+
import { releaseDevice } from './device.js';
|
|
29
30
|
import { WaylandSeat } from './seat.js';
|
|
30
31
|
import { WaylandBackendWindow } from './backendwindow.js';
|
|
31
32
|
import { WaylandSurface } from './surface.js';
|
|
@@ -107,6 +108,8 @@ export class WaylandApp extends EventEmitter {
|
|
|
107
108
|
this.outputs = null;
|
|
108
109
|
/** xdg-decoration, layer-shell, shm: null where the compositor has none */
|
|
109
110
|
this.decorationManager = null;
|
|
111
|
+
/** the older KDE decoration protocol; only bound if the above is missing */
|
|
112
|
+
this.kdeDecorationManager = null;
|
|
110
113
|
this.layerShell = null;
|
|
111
114
|
this.shm = null;
|
|
112
115
|
/** screen capture and the eyedropper (screencopy.js); null on GNOME */
|
|
@@ -159,6 +162,13 @@ export class WaylandApp extends EventEmitter {
|
|
|
159
162
|
// defers to it: the pin is read here instead, from the same options.
|
|
160
163
|
await app._beginOutputs(conn, options);
|
|
161
164
|
app.decorationManager = await conn.bind('zxdg_decoration_manager_v1');
|
|
165
|
+
// The older KDE protocol says the same thing, and some compositors have
|
|
166
|
+
// only that one. Bound where the standard one is missing, which is the
|
|
167
|
+
// only time it would be used — a frame the compositor draws beats one
|
|
168
|
+
// this backend imitates (ssd.js).
|
|
169
|
+
app.kdeDecorationManager = app.decorationManager
|
|
170
|
+
? null
|
|
171
|
+
: await conn.bind('org_kde_kwin_server_decoration_manager');
|
|
162
172
|
app.layerShell = await conn.bind('zwlr_layer_shell_v1');
|
|
163
173
|
app.shm = await conn.bind('wl_shm');
|
|
164
174
|
app.seat = await WaylandSeat.bind(conn);
|
|
@@ -255,6 +265,10 @@ export class WaylandApp extends EventEmitter {
|
|
|
255
265
|
|
|
256
266
|
/** After an offscreen render, put the window's backing target back. */
|
|
257
267
|
rebindWindowTarget() {
|
|
268
|
+
// The framebuffer changes here without any 2d context being told, so
|
|
269
|
+
// none of them owns the device's state any more and the next one to
|
|
270
|
+
// draw re-establishes its own (device.js).
|
|
271
|
+
if (this.gl) releaseDevice(this.gl);
|
|
258
272
|
for (const w of this.windows.values()) {
|
|
259
273
|
if (w._frameSize) {
|
|
260
274
|
w.glctx.bindBacking();
|
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
// `wl_surface.frame` callback (or paints straight away for the first frame,
|
|
19
19
|
// which nothing can pace), the callback runs the renderer's paint into the
|
|
20
20
|
// backing target, and the result is copied to a swapchain buffer and
|
|
21
|
-
// committed with the next frame request in the same commit.
|
|
21
|
+
// committed with the next frame request in the same commit. A compositor
|
|
22
|
+
// stops answering those for a surface it is not showing, and the loop then
|
|
23
|
+
// parks with nothing to say for itself — so it is watched (framewatch.js),
|
|
24
|
+
// which is where `presenting` comes from.
|
|
22
25
|
//
|
|
23
|
-
// The frame is drawn here only
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
26
|
+
// The frame is drawn here only as a last resort: where either decoration
|
|
27
|
+
// protocol is offered the window asks for server-side decorations (ssd.js)
|
|
28
|
+
// and, granted them, switches its own off, which the tree sees as the
|
|
29
|
+
// content growing. And a window whose `windowType` is a
|
|
27
30
|
// dock, a wallpaper, a notification or a splash is not a toplevel at all
|
|
28
31
|
// where the compositor has layer-shell (layershell.js) — same surface, same
|
|
29
32
|
// loop, no frame.
|
|
@@ -33,6 +36,7 @@ import { writeSync, writeFileSync } from 'node:fs';
|
|
|
33
36
|
import { snapshotPNG } from './readback.js';
|
|
34
37
|
import { WaylandWindow, TOPLEVEL_STATE } from './window.js';
|
|
35
38
|
import { WaylandGLContext } from './glcontext.js';
|
|
39
|
+
import { FrameWatch } from './framewatch.js';
|
|
36
40
|
import { WaylandContext2D } from './context2d.js';
|
|
37
41
|
import { Decorations } from './decorations.js';
|
|
38
42
|
import { appearanceSnapshot } from '../appearance.js';
|
|
@@ -122,6 +126,17 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
122
126
|
this._surfaceRaf = [];
|
|
123
127
|
/** rects (content px) a surface or pane changed since the last present */
|
|
124
128
|
this._surfaceDamage = [];
|
|
129
|
+
/**
|
|
130
|
+
* The frame loop's liveness (framewatch.js). A compositor is entitled to
|
|
131
|
+
* stop sending frame callbacks to a surface it is not showing, and that
|
|
132
|
+
* silence is the only thing that says so — it is not an error, and
|
|
133
|
+
* nothing above here can see it (#567).
|
|
134
|
+
*/
|
|
135
|
+
this._frameWatch = new FrameWatch({
|
|
136
|
+
id: this.id,
|
|
137
|
+
pending: () => this._hasFrameWork(),
|
|
138
|
+
onChange: (presenting) => this.emit('presenting', presenting),
|
|
139
|
+
});
|
|
125
140
|
|
|
126
141
|
// Frame and content sizes. `attributes.width/height` are what the tree
|
|
127
142
|
// measured, in device pixels of content; the surface adds the frame.
|
|
@@ -206,9 +221,14 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
206
221
|
width: surfaceW,
|
|
207
222
|
height: surfaceH,
|
|
208
223
|
parent: app.parentFor(attributes, { toplevelOnly: true })?.wl ?? null,
|
|
209
|
-
decorations:
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
decorations:
|
|
225
|
+
app.decorationManager || app.kdeDecorationManager
|
|
226
|
+
? {
|
|
227
|
+
manager: app.decorationManager,
|
|
228
|
+
kdeManager: app.kdeDecorationManager,
|
|
229
|
+
prefer: this._decorPolicy.prefer,
|
|
230
|
+
}
|
|
231
|
+
: null,
|
|
212
232
|
});
|
|
213
233
|
}
|
|
214
234
|
this.wl.scale = scale;
|
|
@@ -481,6 +501,15 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
481
501
|
this._armFrame();
|
|
482
502
|
}
|
|
483
503
|
|
|
504
|
+
/** Is there a repaint waiting on the next frame? The three ways there can
|
|
505
|
+
* be one: the renderer's callbacks, a `<glarea>`'s, and the frame the
|
|
506
|
+
* decorations ask for. */
|
|
507
|
+
_hasFrameWork() {
|
|
508
|
+
return (
|
|
509
|
+
this._raf.length > 0 || this._surfaceRaf.length > 0 || this._frameDirty
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
484
513
|
/**
|
|
485
514
|
* Get a frame in flight.
|
|
486
515
|
*
|
|
@@ -508,7 +537,14 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
508
537
|
if (this.isDragPreview) return;
|
|
509
538
|
// A closed connection arms nothing: a frame request on it rejects at
|
|
510
539
|
// once, the rejection re-arms, and that loop never yields.
|
|
511
|
-
if (this.
|
|
540
|
+
if (this._destroyed || this.app.conn?.destroyed) return;
|
|
541
|
+
if (this._frameArmed) {
|
|
542
|
+
// The repaint rides the frame already in flight — unless that flight is
|
|
543
|
+
// the compositor's silence, in which case this is the moment the app
|
|
544
|
+
// started drawing for nobody (framewatch.js).
|
|
545
|
+
this._frameWatch.workArrived();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
512
548
|
this._frameArmed = true;
|
|
513
549
|
this.wl.whenConfigured
|
|
514
550
|
.then(() => {
|
|
@@ -522,8 +558,12 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
522
558
|
}
|
|
523
559
|
const vsync = this.wl.scheduleFrame();
|
|
524
560
|
this.wl.surface.$.commit();
|
|
561
|
+
this._frameWatch.waiting();
|
|
525
562
|
vsync.then(
|
|
526
|
-
(t) =>
|
|
563
|
+
(t) => {
|
|
564
|
+
this._frameWatch.arrived();
|
|
565
|
+
this._fireRaf(t);
|
|
566
|
+
},
|
|
527
567
|
() => this._frameIdle(),
|
|
528
568
|
);
|
|
529
569
|
})
|
|
@@ -533,11 +573,34 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
533
573
|
/** The frame is over with nothing presented; run again only if asked. */
|
|
534
574
|
_frameIdle() {
|
|
535
575
|
this._frameArmed = false;
|
|
576
|
+
// Nothing is outstanding now — which is not the same as being shown, so
|
|
577
|
+
// the watch stops counting rather than declaring anything.
|
|
578
|
+
this._frameWatch.idle();
|
|
536
579
|
if (!this._destroyed && (this._raf.length || this._frameDirty)) {
|
|
537
580
|
this._armFrame();
|
|
538
581
|
}
|
|
539
582
|
}
|
|
540
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Is the compositor still scheduling frames for this surface?
|
|
586
|
+
*
|
|
587
|
+
* False once a frame callback has gone unanswered for long enough to mean
|
|
588
|
+
* the surface is not being shown (framewatch.js) — the app's own animation
|
|
589
|
+
* and simulation are drawing for nobody until this is true again. The tree
|
|
590
|
+
* reads it through `useWindowState().presenting`, which also folds it into
|
|
591
|
+
* `visible`; this is the window-object half of that.
|
|
592
|
+
*/
|
|
593
|
+
get presenting() {
|
|
594
|
+
return this._frameWatch.presenting;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Subscribe to it. The X11 and Cocoa windows have no such signal, and
|
|
598
|
+
* `windowstate.js` takes the absence of this method as "always true". */
|
|
599
|
+
onPresentingChange(fn) {
|
|
600
|
+
this.on('presenting', fn);
|
|
601
|
+
return () => this.off('presenting', fn);
|
|
602
|
+
}
|
|
603
|
+
|
|
541
604
|
/** Translate into the content area and clip to it. */
|
|
542
605
|
_enterContent(ctx) {
|
|
543
606
|
const o = this.contentOrigin;
|
|
@@ -630,6 +693,13 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
630
693
|
* time — a background fill reaching under a static scene — the surface's
|
|
631
694
|
* pixels are gone with the repaint, so its node is asked for the frame
|
|
632
695
|
* now, before the present, rather than showing the fill for a frame.
|
|
696
|
+
*
|
|
697
|
+
* `restoreGLState()` is here for the foreign GL specifically: a
|
|
698
|
+
* `<glarea>` draws with entry points nobody is tracking, so the state it
|
|
699
|
+
* left has to be assumed lost. An *offscreen* surface painted in the
|
|
700
|
+
* middle of the frame needs nothing said about it — its context and this
|
|
701
|
+
* one hand the device back and forth on their own (device.js), so a node
|
|
702
|
+
* that drew into one does not have to know the window has GL state.
|
|
633
703
|
*/
|
|
634
704
|
_runSurfaces(ctx, time, wholeFrame) {
|
|
635
705
|
ctx?.flush();
|
|
@@ -733,9 +803,14 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
733
803
|
`${damage === 'all' ? 'full' : damage.length + ' rect(s)'}\n`,
|
|
734
804
|
);
|
|
735
805
|
}
|
|
806
|
+
// The frame request `endFrame` put in this commit is the loop's only
|
|
807
|
+
// liveness from here on: the compositor answers it when it next shows
|
|
808
|
+
// this surface, and never if it stops (framewatch.js).
|
|
809
|
+
this._frameWatch.waiting();
|
|
736
810
|
Promise.resolve(vsync).then(
|
|
737
811
|
(t) => {
|
|
738
812
|
this._presentInFlight = false;
|
|
813
|
+
this._frameWatch.arrived();
|
|
739
814
|
if (!this._destroyed) this._fireRaf(t);
|
|
740
815
|
},
|
|
741
816
|
() => {
|
|
@@ -1111,6 +1186,7 @@ export class WaylandBackendWindow extends EventEmitter {
|
|
|
1111
1186
|
if (this._destroyed) return;
|
|
1112
1187
|
this._destroyed = true;
|
|
1113
1188
|
this._raf.length = 0;
|
|
1189
|
+
this._frameWatch.stop();
|
|
1114
1190
|
this.app.dnd?.detach(this);
|
|
1115
1191
|
this.app.makeCurrent();
|
|
1116
1192
|
for (const ctx of this._contexts.values()) ctx.destroy?.();
|
|
@@ -66,6 +66,9 @@ export const PROTOCOLS = [
|
|
|
66
66
|
'tablet-v2',
|
|
67
67
|
'xdg-activation-v1',
|
|
68
68
|
'xdg-decoration-unstable-v1',
|
|
69
|
+
// the older KDE protocol that does the same job, for the compositors that
|
|
70
|
+
// have only that one (ssd.js picks between them)
|
|
71
|
+
'kde-server-decoration',
|
|
69
72
|
'primary-selection-unstable-v1',
|
|
70
73
|
'text-input-unstable-v3',
|
|
71
74
|
'pointer-constraints-unstable-v1',
|
package/src/wayland/context2d.js
CHANGED
|
@@ -43,6 +43,7 @@ import { createRequire } from 'node:module';
|
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
import { pathToFileURL } from 'node:url';
|
|
45
45
|
import { Path2D, Image as NtkImage } from 'ntk';
|
|
46
|
+
import { glDevice } from './device.js';
|
|
46
47
|
import { GlyphAtlas } from './glyphatlas.js';
|
|
47
48
|
import { GLTarget } from './target.js';
|
|
48
49
|
|
|
@@ -203,6 +204,17 @@ function scaleOf(m) {
|
|
|
203
204
|
return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])) || 1;
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The Render ops a `drawGlyphs` call can name, numbered as XRender numbers
|
|
209
|
+
* them — the same table the Cocoa backend answers (`src/cocoa/context2d.js`),
|
|
210
|
+
* so `ctx.Render.PictOp.Over` reads the same on every backend. The op is
|
|
211
|
+
* ignored here: text composites Over, and for the opaque inks text uses Src
|
|
212
|
+
* and Over agree.
|
|
213
|
+
*/
|
|
214
|
+
const RENDER = Object.freeze({ PictOp: Object.freeze({ Src: 1, Over: 3 }) });
|
|
215
|
+
|
|
216
|
+
const clamp01 = (v) => Math.min(1, Math.max(0, Number(v) || 0));
|
|
217
|
+
|
|
206
218
|
const colorCache = new Map();
|
|
207
219
|
|
|
208
220
|
/**
|
|
@@ -390,13 +402,20 @@ export class WaylandContext2D {
|
|
|
390
402
|
* @param {object} [opts]
|
|
391
403
|
* @param {object} [opts.fontManager] an ntk FontManager, for `fillText`
|
|
392
404
|
* @param {import('./target.js').GLTarget} [opts.target] where to draw
|
|
405
|
+
* @param {() => void} [opts.makeCurrent] called before this context takes
|
|
406
|
+
* the device back, for an owner that has to make a GL surface current
|
|
407
|
+
* first — an offscreen surface, whose context may be drawn through at
|
|
408
|
+
* any time, not only inside a window's frame
|
|
393
409
|
*/
|
|
394
|
-
constructor(gl, { fontManager = null, target = null } = {}) {
|
|
410
|
+
constructor(gl, { fontManager = null, target = null, makeCurrent } = {}) {
|
|
395
411
|
this.gl = gl;
|
|
396
412
|
this.fontManager = fontManager;
|
|
413
|
+
/** @see ./device.js — the state this context shares with its siblings */
|
|
414
|
+
this._device = glDevice(gl);
|
|
415
|
+
this._makeCurrent = makeCurrent ?? null;
|
|
397
416
|
// `TextLayout.draw` opens with `ctx.window.app.display.Render` and reads
|
|
398
417
|
// one constant from it. Satisfy the shape rather than fork the layout.
|
|
399
|
-
this.window = { app: { display: { Render:
|
|
418
|
+
this.window = { app: { display: { Render: RENDER } } };
|
|
400
419
|
this.atlas = new GlyphAtlas(gl);
|
|
401
420
|
// Quads already buffered against the atlas must be drawn before the
|
|
402
421
|
// atlas re-lays itself out under them.
|
|
@@ -495,8 +514,12 @@ export class WaylandContext2D {
|
|
|
495
514
|
|
|
496
515
|
/** Where this context draws. A window's backing, or a surface's target. */
|
|
497
516
|
attach(target) {
|
|
498
|
-
if (this._target
|
|
517
|
+
if (this._target === target) return;
|
|
518
|
+
this._flush();
|
|
499
519
|
this._target = target;
|
|
520
|
+
// The projection and the bound framebuffer both belonged to the old
|
|
521
|
+
// target; the next draw re-establishes them against this one.
|
|
522
|
+
this._release();
|
|
500
523
|
}
|
|
501
524
|
|
|
502
525
|
get target() {
|
|
@@ -509,6 +532,7 @@ export class WaylandContext2D {
|
|
|
509
532
|
*/
|
|
510
533
|
begin(width, height, timestamp = 0) {
|
|
511
534
|
this.init();
|
|
535
|
+
this._device.owner = this;
|
|
512
536
|
const t = this._target;
|
|
513
537
|
if (t) {
|
|
514
538
|
t.bind();
|
|
@@ -540,9 +564,11 @@ export class WaylandContext2D {
|
|
|
540
564
|
}
|
|
541
565
|
|
|
542
566
|
/**
|
|
543
|
-
* The GL state every draw here assumes
|
|
544
|
-
*
|
|
545
|
-
* `
|
|
567
|
+
* The GL state every draw here assumes: the target bound, the projection
|
|
568
|
+
* sized to it, and the rest of the device saying what the shader was
|
|
569
|
+
* written against. `begin()` sets it; `_claim()` sets it again whenever
|
|
570
|
+
* something else has been drawing — another context, or a `<glarea>`'s
|
|
571
|
+
* foreign GL — since this one last did.
|
|
546
572
|
*/
|
|
547
573
|
_applyState() {
|
|
548
574
|
const gl = this.gl;
|
|
@@ -558,14 +584,8 @@ export class WaylandContext2D {
|
|
|
558
584
|
// Premultiplied alpha throughout — colours are premultiplied on the way
|
|
559
585
|
// in, and the glyph and texture paths both produce premultiplied output.
|
|
560
586
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
/** After foreign GL drew — a `<glarea>`'s frame — before drawing again. */
|
|
564
|
-
restoreGLState() {
|
|
565
|
-
const gl = this.gl;
|
|
566
|
-
this._flush();
|
|
567
|
-
this._applyState();
|
|
568
|
-
// …and what `begin()` never had to say, because nothing here changes it
|
|
587
|
+
// …and what a frame of this context's own never has to say, because
|
|
588
|
+
// nothing here changes it. Foreign GL does.
|
|
569
589
|
if (typeof gl.bindVertexArray === 'function') gl.bindVertexArray(null);
|
|
570
590
|
gl.blendEquation(gl.FUNC_ADD);
|
|
571
591
|
gl.stencilMask(0xff);
|
|
@@ -574,6 +594,51 @@ export class WaylandContext2D {
|
|
|
574
594
|
gl.activeTexture(gl.TEXTURE0);
|
|
575
595
|
}
|
|
576
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Take the device before touching GL (device.js).
|
|
599
|
+
*
|
|
600
|
+
* Contexts on this backend share one GLES context, so the framebuffer and
|
|
601
|
+
* the state a draw needs are whatever the last one to draw left behind.
|
|
602
|
+
* This is the one place that notices and puts them back — which is what
|
|
603
|
+
* makes a held offscreen-surface context work the way ntk documents it,
|
|
604
|
+
* rather than only inside `Surface#render` (#566), and what spares a node
|
|
605
|
+
* that painted into a surface mid-frame from having to restore the
|
|
606
|
+
* window's context by hand afterwards.
|
|
607
|
+
*
|
|
608
|
+
* Nothing happens in the common case: a property compare against a
|
|
609
|
+
* context that is already the owner.
|
|
610
|
+
*/
|
|
611
|
+
_claim() {
|
|
612
|
+
// Mid stencil-then-cover the state is set up for the pass, not for a
|
|
613
|
+
// plain batch, and the device cannot have changed hands under it.
|
|
614
|
+
if (this._device.owner === this || this._stencilling) return;
|
|
615
|
+
this._device.owner = this;
|
|
616
|
+
this._makeCurrent?.();
|
|
617
|
+
this.init();
|
|
618
|
+
// As `begin()` has it: a target's size *is* the projection.
|
|
619
|
+
const t = this._target;
|
|
620
|
+
if (t) {
|
|
621
|
+
this._width = t.width;
|
|
622
|
+
this._height = t.height;
|
|
623
|
+
}
|
|
624
|
+
this._applyState();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Give the device up, so the next draw through this context re-takes it. */
|
|
628
|
+
_release() {
|
|
629
|
+
if (this._device.owner === this) this._device.owner = null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** After foreign GL drew — a `<glarea>`'s frame — before drawing again. */
|
|
633
|
+
restoreGLState() {
|
|
634
|
+
// Foreign GL leaves no trace in the bookkeeping, so say so and re-take
|
|
635
|
+
// the device. State first, then the flush: anything still buffered is
|
|
636
|
+
// drawn under this context's state rather than under what was left.
|
|
637
|
+
this._release();
|
|
638
|
+
this._claim();
|
|
639
|
+
this._flush();
|
|
640
|
+
}
|
|
641
|
+
|
|
577
642
|
/** Flush what is buffered without ending the frame. */
|
|
578
643
|
flush() {
|
|
579
644
|
this._flush();
|
|
@@ -866,8 +931,29 @@ export class WaylandContext2D {
|
|
|
866
931
|
this._rect(x, y, w, h, 0, this.strokeStyle, Math.max(0.5, this.lineWidth));
|
|
867
932
|
}
|
|
868
933
|
|
|
869
|
-
/**
|
|
934
|
+
/**
|
|
935
|
+
* The batched form react-x11 uses for backgrounds and selection bands.
|
|
936
|
+
*
|
|
937
|
+
* Three shapes, because all three are in use: `[[x,y,w,h],…]`,
|
|
938
|
+
* `[{x,y,width,height},…]`, and the flat `[x,y,w,h,x,y,w,h,…]` that
|
|
939
|
+
* `Render.FillRectangles` takes on the wire. ntk and the Cocoa context
|
|
940
|
+
* both accept the flat list, so it is what a caller written against
|
|
941
|
+
* either of them hands us; reading it as one rectangle per element makes
|
|
942
|
+
* every field `undefined` and draws nothing at all.
|
|
943
|
+
*/
|
|
870
944
|
fillRects(rects) {
|
|
945
|
+
if (!rects?.length) return;
|
|
946
|
+
if (typeof rects[0] === 'number') {
|
|
947
|
+
// a trailing partial rectangle is not one, so stop four short
|
|
948
|
+
for (let i = 0; i + 3 < rects.length; i += 4) {
|
|
949
|
+
const x = rects[i];
|
|
950
|
+
const y = rects[i + 1];
|
|
951
|
+
const w = rects[i + 2];
|
|
952
|
+
const h = rects[i + 3];
|
|
953
|
+
this._rect(x, y, w, h, 0, this.fillStyle, 0);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
871
957
|
for (const r of rects) {
|
|
872
958
|
const x = r.x ?? r[0];
|
|
873
959
|
const y = r.y ?? r[1];
|
|
@@ -1093,6 +1179,10 @@ export class WaylandContext2D {
|
|
|
1093
1179
|
_fillPolys(polys, rule, style) {
|
|
1094
1180
|
const bounds = polysBounds(polys);
|
|
1095
1181
|
if (!bounds || bounds.w <= 0 || bounds.h <= 0) return;
|
|
1182
|
+
// Before `_fillCoverage`, which cuts its raster to `_width`/`_height`:
|
|
1183
|
+
// on a context that has never opened a frame those are the target's
|
|
1184
|
+
// only once the device has been claimed.
|
|
1185
|
+
this._claim();
|
|
1096
1186
|
let color = null;
|
|
1097
1187
|
if (!(style instanceof Gradient)) {
|
|
1098
1188
|
color = this._color(style);
|
|
@@ -1334,6 +1424,7 @@ export class WaylandContext2D {
|
|
|
1334
1424
|
* `(img, sx, sy, sw, sh, dx, dy, dw, dh)`, as canvas.
|
|
1335
1425
|
*/
|
|
1336
1426
|
drawImage(img, ...args) {
|
|
1427
|
+
this._claim();
|
|
1337
1428
|
const src = this._textureFor(img);
|
|
1338
1429
|
if (!src) return;
|
|
1339
1430
|
let sx = 0;
|
|
@@ -1421,6 +1512,9 @@ export class WaylandContext2D {
|
|
|
1421
1512
|
getImageData(x, y, w, h) {
|
|
1422
1513
|
this._flush();
|
|
1423
1514
|
const gl = this.gl;
|
|
1515
|
+
// `readPixels` reads the bound framebuffer, and a blit or another
|
|
1516
|
+
// context may have left it pointing elsewhere since the last draw.
|
|
1517
|
+
this._target?.bind();
|
|
1424
1518
|
const W = Math.max(0, w | 0);
|
|
1425
1519
|
const H = Math.max(0, h | 0);
|
|
1426
1520
|
const raw = new Uint8Array(W * H * 4);
|
|
@@ -1543,6 +1637,44 @@ export class WaylandContext2D {
|
|
|
1543
1637
|
|
|
1544
1638
|
// ---- text -------------------------------------------------------------
|
|
1545
1639
|
|
|
1640
|
+
/** ntk's Render extension object, as much of it as glyph runs need. */
|
|
1641
|
+
get Render() {
|
|
1642
|
+
return RENDER;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* A solid ink for `drawGlyphs`. ntk answers an XRender `Picture` here and
|
|
1647
|
+
* the Cocoa backend the colour itself; this answers the same stand-in
|
|
1648
|
+
* `_stylePicture` builds, so the way ntk documents the call
|
|
1649
|
+
*
|
|
1650
|
+
* ctx.drawGlyphs(ctx.Render.PictOp.Over,
|
|
1651
|
+
* ctx.createSolidPicture(r, g, b, a), runs)
|
|
1652
|
+
*
|
|
1653
|
+
* draws on every backend unchanged. Half the pair — `drawGlyphs` without
|
|
1654
|
+
* this — left every caller that positions its own glyphs (a terminal
|
|
1655
|
+
* grid, a tabular column) feature-detecting, and a feature test that came
|
|
1656
|
+
* back false drew a frame with no text in it rather than throwing (issue
|
|
1657
|
+
* #565).
|
|
1658
|
+
*
|
|
1659
|
+
* The channels are **premultiplied** 0..1, as XRender solids are, which
|
|
1660
|
+
* is the form this context carries colours in — so they pass through
|
|
1661
|
+
* `parseColor` untouched. A channel brighter than the alpha is not a
|
|
1662
|
+
* premultiplied colour; it is clamped down to it, which is the same
|
|
1663
|
+
* colour the Cocoa backend lands on for that input (it un-premultiplies
|
|
1664
|
+
* and clamps to 1).
|
|
1665
|
+
*/
|
|
1666
|
+
createSolidPicture(r, g, b, a) {
|
|
1667
|
+
const alpha = clamp01(a);
|
|
1668
|
+
return {
|
|
1669
|
+
color: Object.freeze([
|
|
1670
|
+
Math.min(clamp01(r), alpha),
|
|
1671
|
+
Math.min(clamp01(g), alpha),
|
|
1672
|
+
Math.min(clamp01(b), alpha),
|
|
1673
|
+
alpha,
|
|
1674
|
+
]),
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1546
1678
|
/**
|
|
1547
1679
|
* ntk's glyph-drawing contract, which is how `TextLayout.draw` reaches a
|
|
1548
1680
|
* context — and therefore how every `<text>`, `<textinput>` and
|
|
@@ -1551,16 +1683,17 @@ export class WaylandContext2D {
|
|
|
1551
1683
|
* The signature is XRender's: a `PictOp`, a source `Picture`, and glyph
|
|
1552
1684
|
* runs already placed at absolute baselines. The op is always `Over` for
|
|
1553
1685
|
* text and the source is always a solid colour, so what this does is take
|
|
1554
|
-
* the colour back out of the
|
|
1555
|
-
*
|
|
1686
|
+
* the colour back out of the ink handed over (`_inkOf`) and draw the runs
|
|
1687
|
+
* through the atlas.
|
|
1556
1688
|
*
|
|
1557
1689
|
* @param {number} op ignored — text is always drawn `Over`
|
|
1558
|
-
* @param {
|
|
1690
|
+
* @param {object|string|number[]|null} src a `createSolidPicture` ink, a
|
|
1691
|
+
* `_stylePicture` stand-in, a colour, or null for the fill style
|
|
1559
1692
|
* @param {Array<{run:object, x:number, y:number}>} positioned
|
|
1560
1693
|
*/
|
|
1561
1694
|
drawGlyphs(op, src, positioned) {
|
|
1562
1695
|
if (!Array.isArray(positioned)) return;
|
|
1563
|
-
const color =
|
|
1696
|
+
const color = this._inkOf(src);
|
|
1564
1697
|
const runs = [];
|
|
1565
1698
|
for (const p of positioned) {
|
|
1566
1699
|
const run = p.run;
|
|
@@ -1583,6 +1716,22 @@ export class WaylandContext2D {
|
|
|
1583
1716
|
return { color };
|
|
1584
1717
|
}
|
|
1585
1718
|
|
|
1719
|
+
/**
|
|
1720
|
+
* The colour a `drawGlyphs` source paints with: a `createSolidPicture`
|
|
1721
|
+
* ink or a `_stylePicture` stand-in (both carry `.color`), a CSS colour
|
|
1722
|
+
* string, a premultiplied `[r, g, b, a]` — or, for anything this context
|
|
1723
|
+
* cannot ink glyphs with (a gradient), the fill style in force. The same
|
|
1724
|
+
* set the Cocoa backend takes, so a caller that skips the picture and
|
|
1725
|
+
* names a colour draws here too.
|
|
1726
|
+
*/
|
|
1727
|
+
_inkOf(src) {
|
|
1728
|
+
if (typeof src === 'string' || Array.isArray(src)) return src;
|
|
1729
|
+
const color = src?.color;
|
|
1730
|
+
return typeof color === 'string' || Array.isArray(color)
|
|
1731
|
+
? color
|
|
1732
|
+
: this.fillStyle;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1586
1735
|
get _backgroundPicture() {
|
|
1587
1736
|
return { color: this.fillStyle };
|
|
1588
1737
|
}
|
|
@@ -1838,6 +1987,11 @@ export class WaylandContext2D {
|
|
|
1838
1987
|
}
|
|
1839
1988
|
|
|
1840
1989
|
_flush() {
|
|
1990
|
+
// Before the early return, not after: `clearRect` and `putImageData`
|
|
1991
|
+
// flush, change the blend function and flush again, and the second
|
|
1992
|
+
// flush must not be the one that re-takes the device and puts the
|
|
1993
|
+
// blending back.
|
|
1994
|
+
this._claim();
|
|
1841
1995
|
if (this._n === 0) return;
|
|
1842
1996
|
const gl = this.gl;
|
|
1843
1997
|
const { aPos, aUV, aColor, aParams } = this._loc;
|
|
@@ -1929,6 +2083,9 @@ export class WaylandContext2D {
|
|
|
1929
2083
|
}
|
|
1930
2084
|
|
|
1931
2085
|
destroy() {
|
|
2086
|
+
// Nothing of this context's is installed any more, and the device
|
|
2087
|
+
// record should not be the one thing still holding on to it.
|
|
2088
|
+
this._release();
|
|
1932
2089
|
this.atlas.destroy();
|
|
1933
2090
|
this.masks.destroy();
|
|
1934
2091
|
if (this._buffer) this.gl.deleteBuffer(this._buffer);
|