react-x11 2.13.0 → 2.14.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 (63) 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/index.d.ts +5 -0
  6. package/src/nodes/boxpaint.js +9 -0
  7. package/src/nodes/preedit.js +64 -14
  8. package/src/scale.js +52 -22
  9. package/src/screencolor.js +104 -17
  10. package/src/wayland/app.js +560 -0
  11. package/src/wayland/backendwindow.js +1123 -0
  12. package/src/wayland/clipboard.js +326 -0
  13. package/src/wayland/connection.js +482 -0
  14. package/src/wayland/context2d.js +2133 -0
  15. package/src/wayland/decorations.js +476 -0
  16. package/src/wayland/dmabuf.js +89 -0
  17. package/src/wayland/dnd.js +581 -0
  18. package/src/wayland/fdutil.js +108 -0
  19. package/src/wayland/framestyle.js +257 -0
  20. package/src/wayland/glarea.js +371 -0
  21. package/src/wayland/glcontext.js +415 -0
  22. package/src/wayland/glyphatlas.js +237 -0
  23. package/src/wayland/input.js +417 -0
  24. package/src/wayland/keysymnames.js +35 -0
  25. package/src/wayland/layershell.js +363 -0
  26. package/src/wayland/outputs.js +601 -0
  27. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  28. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  29. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  30. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  31. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  32. package/src/wayland/protocols/index.json +127 -0
  33. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  34. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  35. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  36. package/src/wayland/protocols/presentation-time.json +1 -0
  37. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  38. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  39. package/src/wayland/protocols/tablet-v2.json +1 -0
  40. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  41. package/src/wayland/protocols/viewporter.json +1 -0
  42. package/src/wayland/protocols/wayland.json +1 -0
  43. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  44. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  45. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  46. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  47. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  48. package/src/wayland/protocols/xdg-shell.json +1 -0
  49. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  50. package/src/wayland/readback.js +99 -0
  51. package/src/wayland/screencopy.js +584 -0
  52. package/src/wayland/seat.js +584 -0
  53. package/src/wayland/shm.js +226 -0
  54. package/src/wayland/ssd.js +106 -0
  55. package/src/wayland/surface.js +123 -0
  56. package/src/wayland/swapchain.js +411 -0
  57. package/src/wayland/tablet.js +522 -0
  58. package/src/wayland/target.js +263 -0
  59. package/src/wayland/text.js +113 -0
  60. package/src/wayland/textinput.js +671 -0
  61. package/src/wayland/touch.js +284 -0
  62. package/src/wayland/window.js +827 -0
  63. package/src/wayland/xkb.js +425 -0
@@ -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,106 @@
1
+ // Server-side decorations, through xdg-decoration.
2
+ //
3
+ // The compositor may draw the frame — wlroots compositors and KDE do, when
4
+ // asked, and GNOME does not advertise the protocol at all — so "who draws the
5
+ // titlebar" is negotiated per toplevel. The client creates a decoration
6
+ // object *before the toplevel's first commit* and says which mode it would
7
+ // prefer; the compositor answers with `configure(mode)`, which is part of
8
+ // the surface's configure sequence and takes effect on the same ack as the
9
+ // size and states it arrived with. A client prefers, it never insists: sway
10
+ // grants server_side, and a compositor that will not draw frames answers
11
+ // client_side — which is the state the backend was already in, so
12
+ // `decorations.js` stays and is switched off rather than removed.
13
+ //
14
+ // The mode is adopted, not applied on arrival, for the reason the configure
15
+ // ack is deferred (window.js): a mode change is a size change — the surface
16
+ // loses or gains its own titlebar — and the frame that adopts it has to be
17
+ // painted at the new insets. `adopt()` runs from the xdg_surface.configure
18
+ // handler, so a window hears 'decorationmode' before it hears 'configure'.
19
+
20
+ import { EventEmitter } from 'node:events';
21
+
22
+ /** `zxdg_toplevel_decoration_v1.mode`. */
23
+ export const DECORATION_MODE = { CLIENT_SIDE: 1, SERVER_SIDE: 2 };
24
+
25
+ /**
26
+ * What `decorations` means, at the app (`createRoot({ decorations })`) and
27
+ * per window (`<window decorations={false}>`, the one value the tree passes):
28
+ *
29
+ * undefined | true | 'server' a frame — the compositor's where it offers
30
+ * one, this backend's own otherwise
31
+ * 'client' a frame, always this backend's own
32
+ * false no frame at all
33
+ *
34
+ * The per-window `false` wins over whatever the app said. A window with no
35
+ * frame still gets a decoration object when the compositor has the protocol,
36
+ * to *decline* server-side: sway frames every toplevel that has not said
37
+ * client_side, and a frameless window would come up with a title bar.
38
+ *
39
+ * @returns {{ draw: boolean, prefer: 'server'|'client' }} whether the window
40
+ * wants a frame at all, and which side should draw it if so
41
+ */
42
+ export function decorationPolicy(appOption, windowOption) {
43
+ const draw = appOption !== false && windowOption !== false;
44
+ const prefer = draw && appOption !== 'client' ? 'server' : 'client';
45
+ return { draw, prefer };
46
+ }
47
+
48
+ export class ServerDecoration extends EventEmitter {
49
+ /**
50
+ * Create the decoration object and state a preference. Must run before
51
+ * the toplevel's first commit; `WaylandWindow.createSync` calls it there.
52
+ *
53
+ * @param {object} opts
54
+ * @param {object} opts.manager the `zxdg_decoration_manager_v1` proxy
55
+ * @param {object} opts.toplevel the `xdg_toplevel` proxy
56
+ * @param {'server'|'client'} [opts.prefer='server']
57
+ */
58
+ constructor({ manager, toplevel, prefer = 'server' }) {
59
+ super();
60
+ this.proxy = manager.$.get_toplevel_decoration(toplevel.id);
61
+ /** the mode in effect: 'server' | 'client', null before the first configure */
62
+ this.mode = null;
63
+ /** a mode the compositor sent that the next xdg_surface.configure adopts */
64
+ this.pending = null;
65
+ this.destroyed = false;
66
+ this.proxy.on('configure', (mode) => {
67
+ this.pending = mode === DECORATION_MODE.SERVER_SIDE ? 'server' : 'client';
68
+ });
69
+ this.prefer(prefer);
70
+ }
71
+
72
+ /** State (or restate) the preference; the compositor answers with a configure. */
73
+ prefer(prefer) {
74
+ if (this.destroyed) return;
75
+ this.proxy.$.set_mode(
76
+ prefer === 'client'
77
+ ? DECORATION_MODE.CLIENT_SIDE
78
+ : DECORATION_MODE.SERVER_SIDE,
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Adopt the pending mode, if there is one.
84
+ *
85
+ * @returns {boolean} whether the mode in effect changed
86
+ */
87
+ adopt() {
88
+ if (this.pending == null) return false;
89
+ const changed = this.pending !== this.mode;
90
+ this.mode = this.pending;
91
+ this.pending = null;
92
+ if (changed) this.emit('mode', this.mode);
93
+ return changed;
94
+ }
95
+
96
+ /** Before the toplevel: destroying them the other way round is a protocol error. */
97
+ destroy() {
98
+ if (this.destroyed) return;
99
+ this.destroyed = true;
100
+ try {
101
+ this.proxy.$.destroy();
102
+ } catch {
103
+ /* the connection may already be gone */
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,123 @@
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
+ import { WaylandContext2D } from './context2d.js';
18
+ import { GLTarget } from './target.js';
19
+
20
+ export class WaylandSurface {
21
+ /**
22
+ * @param {import('./app.js').WaylandApp} app
23
+ * @param {object} opts
24
+ * @param {number} opts.width
25
+ * @param {number} opts.height
26
+ * @param {'argb32'|'a8'} [opts.format='argb32']
27
+ */
28
+ constructor(app, { width, height, format = 'argb32' } = {}) {
29
+ if (format !== 'argb32' && format !== 'a8') {
30
+ throw new Error(
31
+ `Surface: unknown format ${JSON.stringify(format)} (argb32 or a8)`,
32
+ );
33
+ }
34
+ this.app = app;
35
+ this.width = Math.max(1, Math.round(width));
36
+ this.height = Math.max(1, Math.round(height));
37
+ this.format = format;
38
+ this._destroyed = false;
39
+ app.makeCurrent();
40
+ this.target = new GLTarget(app.gl, {
41
+ width: this.width,
42
+ height: this.height,
43
+ stencil: true,
44
+ format,
45
+ });
46
+ this._ctx = null;
47
+ }
48
+
49
+ getContext(name = '2d') {
50
+ if (this._destroyed) throw new Error('Surface: destroyed');
51
+ if (name !== '2d') {
52
+ throw new Error(
53
+ `Surface: getContext(${JSON.stringify(name)}) — a surface has a '2d' context and nothing else.`,
54
+ );
55
+ }
56
+ if (!this._ctx) {
57
+ this._ctx = new WaylandContext2D(this.app.gl, {
58
+ fontManager: this.app.fonts,
59
+ target: this.target,
60
+ });
61
+ this.app.makeCurrent();
62
+ this._ctx.init();
63
+ }
64
+ return this._ctx;
65
+ }
66
+
67
+ /**
68
+ * Draw into the surface through a context that starts clean — identity
69
+ * transform, no clip — and is flushed when the callback returns.
70
+ */
71
+ render(fn) {
72
+ if (this._destroyed) return this;
73
+ const ctx = this.getContext('2d');
74
+ this.app.makeCurrent();
75
+ ctx.begin(this.width, this.height);
76
+ try {
77
+ fn(ctx);
78
+ } finally {
79
+ ctx.end();
80
+ this.app.rebindWindowTarget();
81
+ }
82
+ return this;
83
+ }
84
+
85
+ /** Reset every pixel to fully transparent. */
86
+ clear() {
87
+ if (this._destroyed) return this;
88
+ this.render((ctx) => ctx.clearRect(0, 0, this.width, this.height));
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Scroll the pixels of `src` by (dx, dy) in place. True when a copy was
94
+ * made; false means nothing survives the shift and the caller repaints.
95
+ */
96
+ copyWithin(src, dx, dy) {
97
+ if (this._destroyed) return false;
98
+ this.app.makeCurrent();
99
+ const ok = this.target.copyWithin(src, dx, dy);
100
+ this.app.rebindWindowTarget();
101
+ return ok;
102
+ }
103
+
104
+ /** What `blurCoverage` and friends would want; not available on the GPU. */
105
+ get bytes() {
106
+ throw new Error(
107
+ 'Surface.bytes: pixels live on the GPU; use getContext("2d").getImageData()',
108
+ );
109
+ }
110
+
111
+ destroy() {
112
+ if (this._destroyed) return;
113
+ this._destroyed = true;
114
+ this.app.makeCurrent();
115
+ this._ctx?.destroy();
116
+ this._ctx = null;
117
+ this.target.destroy();
118
+ }
119
+
120
+ [Symbol.dispose]() {
121
+ this.destroy();
122
+ }
123
+ }