react-x11 2.13.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.
Files changed (70) hide show
  1. package/package.json +11 -8
  2. package/src/Reconciler.js +34 -21
  3. package/src/components/Select.js +8 -2
  4. package/src/events.js +8 -2
  5. package/src/glnodes.js +15 -3
  6. package/src/index.d.ts +5 -0
  7. package/src/nodes/boxpaint.js +9 -0
  8. package/src/nodes/preedit.js +64 -14
  9. package/src/ntk.js +10 -1
  10. package/src/scale.js +52 -22
  11. package/src/screencolor.js +104 -17
  12. package/src/types/system.d.ts +14 -1
  13. package/src/wayland/app.js +574 -0
  14. package/src/wayland/backendwindow.js +1199 -0
  15. package/src/wayland/clipboard.js +326 -0
  16. package/src/wayland/connection.js +485 -0
  17. package/src/wayland/context2d.js +2290 -0
  18. package/src/wayland/decorations.js +476 -0
  19. package/src/wayland/device.js +55 -0
  20. package/src/wayland/dmabuf.js +89 -0
  21. package/src/wayland/dnd.js +581 -0
  22. package/src/wayland/fdutil.js +108 -0
  23. package/src/wayland/framestyle.js +257 -0
  24. package/src/wayland/framewatch.js +190 -0
  25. package/src/wayland/glarea.js +372 -0
  26. package/src/wayland/glcontext.js +415 -0
  27. package/src/wayland/glyphatlas.js +237 -0
  28. package/src/wayland/input.js +417 -0
  29. package/src/wayland/keysymnames.js +35 -0
  30. package/src/wayland/layershell.js +363 -0
  31. package/src/wayland/outputs.js +601 -0
  32. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  33. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  34. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  35. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  36. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  37. package/src/wayland/protocols/index.json +131 -0
  38. package/src/wayland/protocols/kde-server-decoration.json +1 -0
  39. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  40. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  41. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  42. package/src/wayland/protocols/presentation-time.json +1 -0
  43. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  44. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  45. package/src/wayland/protocols/tablet-v2.json +1 -0
  46. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  47. package/src/wayland/protocols/viewporter.json +1 -0
  48. package/src/wayland/protocols/wayland.json +1 -0
  49. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  50. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  51. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  52. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  53. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  54. package/src/wayland/protocols/xdg-shell.json +1 -0
  55. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  56. package/src/wayland/readback.js +99 -0
  57. package/src/wayland/screencopy.js +584 -0
  58. package/src/wayland/seat.js +584 -0
  59. package/src/wayland/shm.js +226 -0
  60. package/src/wayland/ssd.js +234 -0
  61. package/src/wayland/surface.js +141 -0
  62. package/src/wayland/swapchain.js +411 -0
  63. package/src/wayland/tablet.js +522 -0
  64. package/src/wayland/target.js +269 -0
  65. package/src/wayland/text.js +113 -0
  66. package/src/wayland/textinput.js +671 -0
  67. package/src/wayland/touch.js +284 -0
  68. package/src/wayland/window.js +854 -0
  69. package/src/wayland/xkb.js +425 -0
  70. package/src/windowstate.js +41 -1
@@ -0,0 +1,226 @@
1
+ // Shared-memory buffers: the CPU path to a `wl_buffer`, beside the dma-buf
2
+ // one the swapchain takes.
3
+ //
4
+ // Not for frames — every window frame here is a dma-buf and no pixel crosses
5
+ // the socket — but a screen capture has to land somewhere the client can
6
+ // read, and a compositor fills a `wl_shm` buffer, which is memory both sides
7
+ // map. On this side "map" is the problem: neither Node nor Bun has mmap. What
8
+ // both have is pread/pwrite on a descriptor, and a pool's memory *is* a file
9
+ // — a memfd (x11-dri's `memfdCreate`), or without the addon an unlinked file
10
+ // under /dev/shm — so the pixels are read back with `fs.readSync` from offset
11
+ // 0 once the compositor says the frame is ready. That is one copy of one
12
+ // screen on a path nobody runs per frame; the mmap it stands in for would
13
+ // have saved it and cost a native binding.
14
+ //
15
+ // Two descriptors per pool, on purpose. The transport closes a descriptor it
16
+ // has sent — `wl_shm.create_pool` hands ownership to the compositor — so the
17
+ // one that goes on the wire is a dup and the original stays ours.
18
+ //
19
+ // Pixel layouts are the DRM fourcc ones `wl_shm.format` names: a packed
20
+ // little-endian 32-bit word, so `XRGB8888` is the bytes B, G, R, X in memory
21
+ // and `XBGR8888` is R, G, B, X. This machine is little-endian, as is every
22
+ // machine a Wayland compositor has shipped on.
23
+
24
+ import { EventEmitter } from 'node:events';
25
+ import fs from 'node:fs';
26
+ import { createRequire } from 'node:module';
27
+ import { closeFd } from './fdutil.js';
28
+
29
+ const require = createRequire(import.meta.url);
30
+
31
+ /** The `wl_shm.format` values this file reads and writes. */
32
+ export const SHM_FORMAT = {
33
+ ARGB8888: 0,
34
+ XRGB8888: 1,
35
+ XBGR8888: 0x34324258,
36
+ ABGR8888: 0x34324241,
37
+ };
38
+
39
+ const BGRX = new Set([SHM_FORMAT.ARGB8888, SHM_FORMAT.XRGB8888]);
40
+ const RGBX = new Set([SHM_FORMAT.XBGR8888, SHM_FORMAT.ABGR8888]);
41
+ const WITH_ALPHA = new Set([SHM_FORMAT.ARGB8888, SHM_FORMAT.ABGR8888]);
42
+
43
+ export function isSupportedShmFormat(format) {
44
+ return BGRX.has(format) || RGBX.has(format);
45
+ }
46
+
47
+ /**
48
+ * Shared memory of `size` bytes, as two descriptors on the same pages: `fd`
49
+ * to keep, `wire` to give to the compositor.
50
+ *
51
+ * @returns {{ fd: number, wire: number, size: number }}
52
+ */
53
+ export function openSharedMemory(size, name = 'react-x11') {
54
+ try {
55
+ const dri = require('x11-dri');
56
+ if (
57
+ typeof dri.memfdCreate === 'function' &&
58
+ typeof dri.dup === 'function'
59
+ ) {
60
+ const fd = dri.memfdCreate(size, name);
61
+ return { fd, wire: dri.dup(fd), size };
62
+ }
63
+ } catch {
64
+ /* the addon is optional; the file below works without it */
65
+ }
66
+ // A file both ends open, gone from the namespace the moment both have.
67
+ const path = `/dev/shm/${name}-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
68
+ const { O_RDWR, O_CREAT, O_EXCL } = fs.constants;
69
+ const fd = fs.openSync(path, O_RDWR | O_CREAT | O_EXCL, 0o600);
70
+ let wire = -1;
71
+ try {
72
+ fs.ftruncateSync(fd, size);
73
+ wire = fs.openSync(path, O_RDWR);
74
+ return { fd, wire, size };
75
+ } catch (err) {
76
+ closeFd(fd);
77
+ throw err;
78
+ } finally {
79
+ try {
80
+ fs.unlinkSync(path);
81
+ } catch {
82
+ /* already gone */
83
+ }
84
+ }
85
+ }
86
+
87
+ export class ShmBuffer extends EventEmitter {
88
+ /**
89
+ * A pool holding exactly one buffer.
90
+ *
91
+ * @param {object} opts
92
+ * @param {object} opts.shm the `wl_shm` proxy
93
+ * @param {number} opts.width in pixels
94
+ * @param {number} opts.height
95
+ * @param {number} [opts.stride=width*4] bytes per row
96
+ * @param {number} [opts.format=SHM_FORMAT.XRGB8888]
97
+ * @param {string} [opts.name] what the memfd is called in /proc
98
+ */
99
+ constructor({
100
+ shm,
101
+ width,
102
+ height,
103
+ stride = width * 4,
104
+ format = SHM_FORMAT.XRGB8888,
105
+ name = 'react-x11-shm',
106
+ }) {
107
+ super();
108
+ this.width = width;
109
+ this.height = height;
110
+ this.stride = stride;
111
+ this.format = format;
112
+ this.size = stride * height;
113
+ const mem = openSharedMemory(this.size, name);
114
+ this.fd = mem.fd;
115
+ this.pool = shm.$.create_pool(mem.wire, this.size);
116
+ /** the `wl_buffer` proxy */
117
+ this.buffer = this.pool.$.create_buffer(0, width, height, stride, format);
118
+ this.destroyed = false;
119
+ this.buffer.on('release', () => this.emit('release'));
120
+ }
121
+
122
+ /** Everything in the pool, read back. */
123
+ read() {
124
+ const out = Buffer.allocUnsafe(this.size);
125
+ let off = 0;
126
+ while (off < this.size) {
127
+ const n = fs.readSync(this.fd, out, off, this.size - off, off);
128
+ if (n <= 0) break;
129
+ off += n;
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /** Overwrite the pool from offset 0. */
135
+ write(bytes) {
136
+ let off = 0;
137
+ while (off < bytes.length) {
138
+ off += fs.writeSync(this.fd, bytes, off, bytes.length - off, off);
139
+ }
140
+ }
141
+
142
+ destroy() {
143
+ if (this.destroyed) return;
144
+ this.destroyed = true;
145
+ try {
146
+ this.buffer.$.destroy();
147
+ this.pool.$.destroy();
148
+ } catch {
149
+ /* connection gone */
150
+ }
151
+ closeFd(this.fd);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * One pixel of a pool, as 0–255 channels.
157
+ *
158
+ * @param {Buffer|Uint8Array} bytes the pool's contents
159
+ * @param {{ stride:number, format:number }} layout
160
+ */
161
+ export function shmPixel(bytes, { stride, format }, x, y) {
162
+ const o = y * stride + x * 4;
163
+ if (BGRX.has(format)) {
164
+ return { r: bytes[o + 2], g: bytes[o + 1], b: bytes[o] };
165
+ }
166
+ if (RGBX.has(format)) {
167
+ return { r: bytes[o], g: bytes[o + 1], b: bytes[o + 2] };
168
+ }
169
+ throw new Error(`unsupported wl_shm format 0x${format.toString(16)}`);
170
+ }
171
+
172
+ /**
173
+ * A pool's contents as straight RGBA, top row first — the shape
174
+ * `readback.js`'s `encodePNG` takes.
175
+ *
176
+ * @param {object} frame
177
+ * @param {Buffer|Uint8Array} frame.bytes
178
+ * @param {boolean} [frame.yInvert] the rows are bottom-up (screencopy's flag)
179
+ */
180
+ export function shmToRGBA({
181
+ bytes,
182
+ width,
183
+ height,
184
+ stride,
185
+ format,
186
+ yInvert = false,
187
+ }) {
188
+ if (!isSupportedShmFormat(format)) {
189
+ throw new Error(`unsupported wl_shm format 0x${format.toString(16)}`);
190
+ }
191
+ const swap = BGRX.has(format);
192
+ const alpha = WITH_ALPHA.has(format);
193
+ const data = new Uint8Array(width * height * 4);
194
+ for (let y = 0; y < height; y++) {
195
+ const src = (yInvert ? height - 1 - y : y) * stride;
196
+ const dst = y * width * 4;
197
+ for (let x = 0; x < width; x++) {
198
+ const s = src + x * 4;
199
+ const d = dst + x * 4;
200
+ if (swap) {
201
+ data[d] = bytes[s + 2];
202
+ data[d + 1] = bytes[s + 1];
203
+ data[d + 2] = bytes[s];
204
+ } else {
205
+ data[d] = bytes[s];
206
+ data[d + 1] = bytes[s + 1];
207
+ data[d + 2] = bytes[s + 2];
208
+ }
209
+ data[d + 3] = alpha ? bytes[s + 3] : 255;
210
+ }
211
+ }
212
+ return { width, height, data };
213
+ }
214
+
215
+ /** Reverse the row order in place: a y-inverted frame the right way up. */
216
+ export function flipRows(bytes, stride, height) {
217
+ const tmp = Buffer.allocUnsafe(stride);
218
+ for (let y = 0; y < height >> 1; y++) {
219
+ const a = y * stride;
220
+ const b = (height - 1 - y) * stride;
221
+ bytes.copy(tmp, 0, a, a + stride);
222
+ bytes.copy(bytes, a, b, b + stride);
223
+ tmp.copy(bytes, b, 0, stride);
224
+ }
225
+ return bytes;
226
+ }
@@ -0,0 +1,234 @@
1
+ // Server-side decorations: asking the compositor to draw the frame, and
2
+ // falling back to drawing it ourselves only when nothing will.
3
+ //
4
+ // The compositor may draw the frame — wlroots compositors and KDE do, when
5
+ // asked, and GNOME does not advertise the protocol at all — so "who draws the
6
+ // titlebar" is negotiated per toplevel. The client creates a decoration
7
+ // object *before the toplevel's first commit* and says which mode it would
8
+ // prefer; the compositor answers with `configure(mode)`, which is part of
9
+ // the surface's configure sequence and takes effect on the same ack as the
10
+ // size and states it arrived with. A client prefers, it never insists: sway
11
+ // grants server_side, and a compositor that will not draw frames answers
12
+ // client_side — which is the state the backend was already in, so
13
+ // `decorations.js` stays and is switched off rather than removed.
14
+ //
15
+ // ## Two protocols say the same thing
16
+ //
17
+ // `xdg-decoration-unstable-v1` is the one to ask with, and where it is
18
+ // offered it is the only one used. But it arrived late (2018) and KDE had
19
+ // shipped `org_kde_kwin_server_decoration` years before, so a compositor may
20
+ // advertise only the older one — KWin still carries both, and a handful of
21
+ // smaller compositors never picked up the xdg protocol. Asking with whatever
22
+ // is there is the difference between a real titlebar and an imitation, so
23
+ // `createServerDecoration` tries them in that order and only gives up on
24
+ // server-side when neither global exists.
25
+ //
26
+ // The KDE protocol is the same conversation in different words: it hangs off
27
+ // the `wl_surface` rather than the `xdg_toplevel`, its enum has a third value
28
+ // (`none`, no frame at all), its destructor is `release`, and — the one
29
+ // difference that matters — its `mode` event is not part of a configure
30
+ // sequence. Nothing promises a configure will follow it, so that driver
31
+ // flushes itself (`flushesOutsideConfigure`); the xdg one keeps the stricter
32
+ // discipline below.
33
+ //
34
+ // The mode is adopted, not applied on arrival, for the reason the configure
35
+ // ack is deferred (window.js): a mode change is a size change — the surface
36
+ // loses or gains its own titlebar — and the frame that adopts it has to be
37
+ // painted at the new insets. `adopt()` runs from the xdg_surface.configure
38
+ // handler, so a window hears 'decorationmode' before it hears 'configure'.
39
+
40
+ import { EventEmitter } from 'node:events';
41
+
42
+ /** `zxdg_toplevel_decoration_v1.mode`. */
43
+ export const DECORATION_MODE = { CLIENT_SIDE: 1, SERVER_SIDE: 2 };
44
+
45
+ /**
46
+ * `org_kde_kwin_server_decoration.mode`. The two that overlap with
47
+ * xdg-decoration share its numbering; `NONE` — no frame from either side —
48
+ * has no xdg equivalent and is only ever received, never asked for. A window
49
+ * that wants no frame asks for `CLIENT` and then draws nothing, which is the
50
+ * same picture and one less thing for `decorationPolicy` to express.
51
+ */
52
+ export const KDE_DECORATION_MODE = { NONE: 0, CLIENT: 1, SERVER: 2 };
53
+
54
+ /**
55
+ * What `decorations` means, at the app (`createRoot({ decorations })`) and
56
+ * per window (`<window decorations={false}>`, the one value the tree passes):
57
+ *
58
+ * undefined | true | 'server' a frame — the compositor's where it offers
59
+ * one, this backend's own otherwise
60
+ * 'client' a frame, always this backend's own
61
+ * false no frame at all
62
+ *
63
+ * The per-window `false` wins over whatever the app said. A window with no
64
+ * frame still gets a decoration object when the compositor has the protocol,
65
+ * to *decline* server-side: sway frames every toplevel that has not said
66
+ * client_side, and a frameless window would come up with a title bar.
67
+ *
68
+ * @returns {{ draw: boolean, prefer: 'server'|'client' }} whether the window
69
+ * wants a frame at all, and which side should draw it if so
70
+ */
71
+ export function decorationPolicy(appOption, windowOption) {
72
+ const draw = appOption !== false && windowOption !== false;
73
+ const prefer = draw && appOption !== 'client' ? 'server' : 'client';
74
+ return { draw, prefer };
75
+ }
76
+
77
+ /**
78
+ * What both protocols have in common: a mode that arrives from the
79
+ * compositor, is held until the frame that can paint it adopts it, and an
80
+ * object that has to be destroyed before the thing it decorates.
81
+ */
82
+ class Decoration extends EventEmitter {
83
+ constructor() {
84
+ super();
85
+ /** the mode in effect: 'server' | 'client', null before the first answer */
86
+ this.mode = null;
87
+ /** a mode the compositor sent that the next configure adopts */
88
+ this.pending = null;
89
+ this.destroyed = false;
90
+ }
91
+
92
+ /**
93
+ * Whether an answer can arrive with no configure behind it, in which case
94
+ * whoever owns this has to flush it rather than wait (window.js). False
95
+ * for xdg-decoration, whose mode is part of the configure sequence.
96
+ */
97
+ get flushesOutsideConfigure() {
98
+ return false;
99
+ }
100
+
101
+ /** The compositor's answer, held until `adopt()`. */
102
+ _answer(mode) {
103
+ this.pending = mode;
104
+ if (this.flushesOutsideConfigure) this.emit('pending');
105
+ }
106
+
107
+ /**
108
+ * Adopt the pending mode, if there is one.
109
+ *
110
+ * @returns {boolean} whether the mode in effect changed
111
+ */
112
+ adopt() {
113
+ if (this.pending == null) return false;
114
+ const changed = this.pending !== this.mode;
115
+ this.mode = this.pending;
116
+ this.pending = null;
117
+ if (changed) this.emit('mode', this.mode);
118
+ return changed;
119
+ }
120
+
121
+ /** Before the toplevel: destroying them the other way round is a protocol error. */
122
+ destroy() {
123
+ if (this.destroyed) return;
124
+ this.destroyed = true;
125
+ try {
126
+ this._release();
127
+ } catch {
128
+ /* the connection may already be gone */
129
+ }
130
+ }
131
+ }
132
+
133
+ export class ServerDecoration extends Decoration {
134
+ /**
135
+ * Create the decoration object and state a preference. Must run before
136
+ * the toplevel's first commit; `WaylandWindow.createSync` calls it there.
137
+ *
138
+ * @param {object} opts
139
+ * @param {object} opts.manager the `zxdg_decoration_manager_v1` proxy
140
+ * @param {object} opts.toplevel the `xdg_toplevel` proxy
141
+ * @param {'server'|'client'} [opts.prefer='server']
142
+ */
143
+ constructor({ manager, toplevel, prefer = 'server' }) {
144
+ super();
145
+ this.proxy = manager.$.get_toplevel_decoration(toplevel.id);
146
+ this.proxy.on('configure', (mode) => {
147
+ this._answer(mode === DECORATION_MODE.SERVER_SIDE ? 'server' : 'client');
148
+ });
149
+ this.prefer(prefer);
150
+ }
151
+
152
+ /** State (or restate) the preference; the compositor answers with a configure. */
153
+ prefer(prefer) {
154
+ if (this.destroyed) return;
155
+ this.proxy.$.set_mode(
156
+ prefer === 'client'
157
+ ? DECORATION_MODE.CLIENT_SIDE
158
+ : DECORATION_MODE.SERVER_SIDE,
159
+ );
160
+ }
161
+
162
+ _release() {
163
+ this.proxy.$.destroy();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * The same conversation over `org_kde_kwin_server_decoration`, for the
169
+ * compositors that have only that one. It decorates the `wl_surface`, so it
170
+ * is created from the surface rather than the toplevel — but the ordering
171
+ * rule is the toplevel's all the same, since that is what carries the frame.
172
+ */
173
+ export class KdeServerDecoration extends Decoration {
174
+ /**
175
+ * @param {object} opts
176
+ * @param {object} opts.manager the `org_kde_kwin_server_decoration_manager`
177
+ * @param {object} opts.surface the `wl_surface` proxy
178
+ * @param {'server'|'client'} [opts.prefer='server']
179
+ */
180
+ constructor({ manager, surface, prefer = 'server' }) {
181
+ super();
182
+ this.proxy = manager.$.create(surface.id);
183
+ this.proxy.on('mode', (mode) => {
184
+ // `none` is not server-side, and this side draws nothing for it: the
185
+ // window that asked for no frame is already drawing nothing.
186
+ this._answer(mode === KDE_DECORATION_MODE.SERVER ? 'server' : 'client');
187
+ });
188
+ this.prefer(prefer);
189
+ }
190
+
191
+ /** Nothing sequences this protocol's `mode` event; window.js flushes it. */
192
+ get flushesOutsideConfigure() {
193
+ return true;
194
+ }
195
+
196
+ prefer(prefer) {
197
+ if (this.destroyed) return;
198
+ this.proxy.$.request_mode(
199
+ prefer === 'client'
200
+ ? KDE_DECORATION_MODE.CLIENT
201
+ : KDE_DECORATION_MODE.SERVER,
202
+ );
203
+ }
204
+
205
+ _release() {
206
+ this.proxy.$.release();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Ask whichever protocol the compositor has, preferring the standard one.
212
+ * Returns null only when there is nobody to ask — the case where the frame
213
+ * has to be this backend's own (GNOME).
214
+ *
215
+ * @param {object} opts
216
+ * @param {object} [opts.manager] `zxdg_decoration_manager_v1`, if bound
217
+ * @param {object} [opts.kdeManager] `org_kde_kwin_server_decoration_manager`
218
+ * @param {object} opts.toplevel the `xdg_toplevel` proxy
219
+ * @param {object} opts.surface the `wl_surface` proxy
220
+ * @param {'server'|'client'} [opts.prefer='server']
221
+ * @returns {Decoration|null}
222
+ */
223
+ export function createServerDecoration({
224
+ manager,
225
+ kdeManager,
226
+ toplevel,
227
+ surface,
228
+ prefer = 'server',
229
+ }) {
230
+ if (manager) return new ServerDecoration({ manager, toplevel, prefer });
231
+ if (kdeManager)
232
+ return new KdeServerDecoration({ manager: kdeManager, surface, prefer });
233
+ return null;
234
+ }
@@ -0,0 +1,141 @@
1
+ // An offscreen surface: pixels that persist, drawn into with a 2d context and
2
+ // drawn from with `drawImage`.
3
+ //
4
+ // ntk's `Surface` is an X pixmap and a Picture; the Cocoa backend's is a CG
5
+ // bitmap. Here it is a render target (target.js) — a texture with a
6
+ // framebuffer in front of it — which is also what a window's backing store
7
+ // is, so the paint cache, scroll blits and the window all go through one
8
+ // piece of code. The contract is the one `src/ntk.js` documents: `width`/
9
+ // `height`, `getContext('2d')`, `render(fn)`, `clear()`, `copyWithin(src,
10
+ // dx, dy)`, `destroy()`, and being a valid `drawImage` source.
11
+ //
12
+ // One thing to know about GL: a context has to be *current* to draw, and
13
+ // only one surface is current at a time. Every target here shares the app's
14
+ // one EGL context, so `render()` asks the app to make it current before
15
+ // binding the target — which normally costs nothing, because it already is.
16
+ //
17
+ // Sharing one device is also why `getContext('2d')` used to be a trap. ntk
18
+ // tells callers who draw often to hold a context rather than take one per
19
+ // frame, and a held context here drew wherever the device was last pointed
20
+ // — which, outside `render()`, was the window: the draws went nowhere
21
+ // visible and nothing said so (#566). The context now takes the device back
22
+ // whenever it finds it in someone else's hands (device.js), so holding one
23
+ // works as ntk documents, and a node that painted into a surface in the
24
+ // middle of the window's frame no longer has to put the window's GL state
25
+ // back by hand either.
26
+
27
+ import { WaylandContext2D } from './context2d.js';
28
+ import { GLTarget } from './target.js';
29
+
30
+ export class WaylandSurface {
31
+ /**
32
+ * @param {import('./app.js').WaylandApp} app
33
+ * @param {object} opts
34
+ * @param {number} opts.width
35
+ * @param {number} opts.height
36
+ * @param {'argb32'|'a8'} [opts.format='argb32']
37
+ */
38
+ constructor(app, { width, height, format = 'argb32' } = {}) {
39
+ if (format !== 'argb32' && format !== 'a8') {
40
+ throw new Error(
41
+ `Surface: unknown format ${JSON.stringify(format)} (argb32 or a8)`,
42
+ );
43
+ }
44
+ this.app = app;
45
+ this.width = Math.max(1, Math.round(width));
46
+ this.height = Math.max(1, Math.round(height));
47
+ this.format = format;
48
+ this._destroyed = false;
49
+ app.makeCurrent();
50
+ this.target = new GLTarget(app.gl, {
51
+ width: this.width,
52
+ height: this.height,
53
+ stencil: true,
54
+ format,
55
+ });
56
+ this._ctx = null;
57
+ }
58
+
59
+ getContext(name = '2d') {
60
+ if (this._destroyed) throw new Error('Surface: destroyed');
61
+ if (name !== '2d') {
62
+ throw new Error(
63
+ `Surface: getContext(${JSON.stringify(name)}) — a surface has a '2d' context and nothing else.`,
64
+ );
65
+ }
66
+ if (!this._ctx) {
67
+ this._ctx = new WaylandContext2D(this.app.gl, {
68
+ fontManager: this.app.fonts,
69
+ target: this.target,
70
+ // A surface may be drawn into between frames, when the app's notion
71
+ // of a current GL surface is whatever it last was; the context asks
72
+ // for one before it re-takes the device, as `render()` does.
73
+ makeCurrent: () => this.app.makeCurrent(),
74
+ });
75
+ this.app.makeCurrent();
76
+ this._ctx.init();
77
+ }
78
+ return this._ctx;
79
+ }
80
+
81
+ /**
82
+ * Draw into the surface through a context that starts clean — identity
83
+ * transform, no clip — and is flushed when the callback returns.
84
+ *
85
+ * Drawing through `getContext('2d')` directly works too, and keeps the
86
+ * transform and clip it was left with; this is the scoped form, for a
87
+ * caller that wants a frame's worth of state rather than a canvas's.
88
+ */
89
+ render(fn) {
90
+ if (this._destroyed) return this;
91
+ const ctx = this.getContext('2d');
92
+ this.app.makeCurrent();
93
+ ctx.begin(this.width, this.height);
94
+ try {
95
+ fn(ctx);
96
+ } finally {
97
+ ctx.end();
98
+ this.app.rebindWindowTarget();
99
+ }
100
+ return this;
101
+ }
102
+
103
+ /** Reset every pixel to fully transparent. */
104
+ clear() {
105
+ if (this._destroyed) return this;
106
+ this.render((ctx) => ctx.clearRect(0, 0, this.width, this.height));
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Scroll the pixels of `src` by (dx, dy) in place. True when a copy was
112
+ * made; false means nothing survives the shift and the caller repaints.
113
+ */
114
+ copyWithin(src, dx, dy) {
115
+ if (this._destroyed) return false;
116
+ this.app.makeCurrent();
117
+ const ok = this.target.copyWithin(src, dx, dy);
118
+ this.app.rebindWindowTarget();
119
+ return ok;
120
+ }
121
+
122
+ /** What `blurCoverage` and friends would want; not available on the GPU. */
123
+ get bytes() {
124
+ throw new Error(
125
+ 'Surface.bytes: pixels live on the GPU; use getContext("2d").getImageData()',
126
+ );
127
+ }
128
+
129
+ destroy() {
130
+ if (this._destroyed) return;
131
+ this._destroyed = true;
132
+ this.app.makeCurrent();
133
+ this._ctx?.destroy();
134
+ this._ctx = null;
135
+ this.target.destroy();
136
+ }
137
+
138
+ [Symbol.dispose]() {
139
+ this.destroy();
140
+ }
141
+ }