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,326 @@
1
+ // The clipboard and the primary selection, over `wl_data_device` and
2
+ // `zwp_primary_selection_device_v1`.
3
+ //
4
+ // The model is X's selections with the round trips taken out. An owner
5
+ // advertises MIME types (`wl_data_source.offer`); the compositor tells
6
+ // everyone else there is a new offer (`wl_data_device.selection`) carrying a
7
+ // `wl_data_offer` that lists those types; a reader hands the offer a pipe's
8
+ // write end (`receive`) and reads the content from the other end; the
9
+ // compositor asks the owner to `send` into it. There is no `INCR`, no
10
+ // property round trip, no `TARGETS` request — the types came with the offer.
11
+ //
12
+ // What is the same as X: ownership needs a recent input serial, because a
13
+ // client that was not just interacted with may not take the selection; and
14
+ // the two selections are two different objects. `CLIPBOARD` is
15
+ // `wl_data_device`, `PRIMARY` is the primary-selection protocol, and a
16
+ // compositor may have only the first.
17
+ //
18
+ // Type names: react-x11's `clipboard.js` speaks X's — `UTF8_STRING`,
19
+ // `STRING`, `TEXT` — as well as MIME. Both are offered when writing text, and
20
+ // `targets()` answers both spellings for a text offer, so a caller written
21
+ // against either finds what it expects.
22
+
23
+ import { makePipe, readAll, writeAll, closeFd } from './fdutil.js';
24
+
25
+ const TEXT_MIMES = [
26
+ 'text/plain;charset=utf-8',
27
+ 'text/plain',
28
+ 'UTF8_STRING',
29
+ 'STRING',
30
+ 'TEXT',
31
+ ];
32
+ const X_TEXT = new Set(['UTF8_STRING', 'STRING', 'TEXT', 'COMPOUND_TEXT']);
33
+
34
+ function isText(mime) {
35
+ return (
36
+ X_TEXT.has(mime) || mime === 'text/plain' || mime.startsWith('text/plain;')
37
+ );
38
+ }
39
+
40
+ /** The MIME to actually ask an offer for, given an X-style or MIME name. */
41
+ function pickMime(offered, want) {
42
+ if (offered.includes(want)) return want;
43
+ if (isText(want)) return offered.find((m) => isText(m)) ?? null;
44
+ return null;
45
+ }
46
+
47
+ class Selection {
48
+ /**
49
+ * @param {object} opts
50
+ * @param {string} opts.name 'CLIPBOARD' | 'PRIMARY'
51
+ * @param {object} opts.device a `wl_data_device` or `zwp_primary_selection_device_v1`
52
+ * @param {object} opts.manager the manager that creates sources
53
+ * @param {string} opts.createSource the manager's request for a source:
54
+ * `create_data_source` on wl_data_device_manager, `create_source` on the
55
+ * primary-selection manager
56
+ * @param {() => number} opts.serial the latest input serial
57
+ */
58
+ constructor({ name, device, manager, createSource, serial }) {
59
+ this.name = name;
60
+ this.device = device;
61
+ this.manager = manager;
62
+ this.createSource = createSource;
63
+ this.serial = serial;
64
+ /** the current offer from someone else, with its advertised types */
65
+ this.offer = null;
66
+ this.offerTypes = [];
67
+ /** our own source while we own the selection */
68
+ this.source = null;
69
+ this.ownData = null;
70
+ this._pendingOffers = new Map(); // offer id -> types
71
+ /** `watch()` handlers, told when the selection changes hands */
72
+ this.watchers = new Set();
73
+
74
+ device.on('data_offer', (offer) => {
75
+ const types = [];
76
+ this._pendingOffers.set(offer.id, types);
77
+ offer.on('offer', (mime) => types.push(mime));
78
+ });
79
+ device.on('selection', (offer) => {
80
+ const id = offer?.id ?? offer;
81
+ if (this.offer && this.offer.id !== id) {
82
+ try {
83
+ this.offer.$.destroy();
84
+ } catch {
85
+ /* gone */
86
+ }
87
+ }
88
+ if (!offer || offer === 0) {
89
+ this.offer = null;
90
+ this.offerTypes = [];
91
+ this._notify('closed');
92
+ return;
93
+ }
94
+ this.offer = offer;
95
+ this.offerTypes = this._pendingOffers.get(id) ?? [];
96
+ this._pendingOffers.delete(id);
97
+ // an offer from our own source means we still own it; anyone else's
98
+ // means we lost it
99
+ if (this.source && !this._isOurs(offer)) this._dropSource();
100
+ this._notify('new-owner');
101
+ });
102
+ }
103
+
104
+ /**
105
+ * The event ntk's XFixes watch delivers, `{ selection, owner, timestamp,
106
+ * selectionTimestamp, reason }`, from the compositor's `selection` event —
107
+ * which is the same subscription with nothing to register: every client
108
+ * with a data device is told. There is no owner *window* to name here, so
109
+ * `owner` is the one thing an edit menu reads it for: nonzero while
110
+ * something is on the selection (our own source included, as X reports
111
+ * our own window), 0 once it is empty.
112
+ */
113
+ _notify(reason) {
114
+ if (this.watchers.size === 0) return;
115
+ const now = Date.now();
116
+ const ev = {
117
+ selection: this.name,
118
+ owner: this.offer ? 1 : 0,
119
+ timestamp: now,
120
+ selectionTimestamp: now,
121
+ reason,
122
+ };
123
+ for (const fn of [...this.watchers]) {
124
+ try {
125
+ fn(ev);
126
+ } catch (err) {
127
+ queueMicrotask(() => {
128
+ throw err;
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ /** @returns {() => void} stops watching */
135
+ watch(handler) {
136
+ this.watchers.add(handler);
137
+ let stopped = false;
138
+ return () => {
139
+ if (stopped) return;
140
+ stopped = true;
141
+ this.watchers.delete(handler);
142
+ };
143
+ }
144
+
145
+ _isOurs() {
146
+ // Compositors hand back our own offer when we own the selection; the
147
+ // types match exactly what we offered, which is the only test there is.
148
+ if (!this.ownData) return false;
149
+ const ours = Object.keys(this.ownData);
150
+ return (
151
+ ours.length === this.offerTypes.length &&
152
+ ours.every((t) => this.offerTypes.includes(t))
153
+ );
154
+ }
155
+
156
+ _dropSource() {
157
+ if (this.source) {
158
+ try {
159
+ this.source.$.destroy();
160
+ } catch {
161
+ /* gone */
162
+ }
163
+ }
164
+ this.source = null;
165
+ this.ownData = null;
166
+ }
167
+
168
+ /** Offer `data` — a string, or `{ [type]: string|Buffer }`. */
169
+ async write(data) {
170
+ const map = {};
171
+ if (typeof data === 'string' || Buffer.isBuffer(data)) {
172
+ for (const t of TEXT_MIMES) map[t] = data;
173
+ } else if (data && typeof data === 'object') {
174
+ for (const [t, v] of Object.entries(data)) {
175
+ if (v == null) continue;
176
+ map[t] = v;
177
+ if (isText(t)) for (const alias of TEXT_MIMES) map[alias] ??= v;
178
+ }
179
+ }
180
+ if (Object.keys(map).length === 0) return;
181
+ this._dropSource();
182
+ // The two managers name it differently; calling the primary selection's
183
+ // name on wl_data_device_manager threw, so copying to the clipboard
184
+ // never worked here.
185
+ const source = this.manager.$[this.createSource]();
186
+ for (const t of Object.keys(map)) source.$.offer(t);
187
+ source.on('send', (mime, fd) => {
188
+ const v =
189
+ map[mime] ??
190
+ map[Object.keys(map).find((k) => isText(k) && isText(mime))];
191
+ const bytes = Buffer.isBuffer(v)
192
+ ? v
193
+ : Buffer.from(String(v ?? ''), 'utf8');
194
+ writeAll(fd, bytes).catch(() => closeFd(fd));
195
+ });
196
+ source.on('cancelled', () => {
197
+ if (this.source === source) {
198
+ this.source = null;
199
+ this.ownData = null;
200
+ }
201
+ try {
202
+ source.$.destroy();
203
+ } catch {
204
+ /* gone */
205
+ }
206
+ });
207
+ source.on('target', () => {});
208
+ source.on('action', () => {});
209
+ source.on('dnd_drop_performed', () => {});
210
+ source.on('dnd_finished', () => {});
211
+ this.source = source;
212
+ this.ownData = map;
213
+ this.device.$.set_selection(source.id, this.serial());
214
+ }
215
+
216
+ async clear() {
217
+ this._dropSource();
218
+ // a null source clears it
219
+ this.device.$.set_selection(null, this.serial());
220
+ }
221
+
222
+ async targets() {
223
+ if (this.ownData) return Object.keys(this.ownData);
224
+ const types = [...this.offerTypes];
225
+ if (types.some(isText))
226
+ for (const t of TEXT_MIMES) if (!types.includes(t)) types.push(t);
227
+ return types;
228
+ }
229
+
230
+ async read(target = 'UTF8_STRING') {
231
+ if (this.ownData) {
232
+ const v =
233
+ this.ownData[target] ??
234
+ (isText(target)
235
+ ? Object.entries(this.ownData).find(([k]) => isText(k))?.[1]
236
+ : undefined);
237
+ if (v === undefined) return null;
238
+ return isText(target)
239
+ ? String(v)
240
+ : Buffer.isBuffer(v)
241
+ ? v
242
+ : Buffer.from(String(v));
243
+ }
244
+ if (!this.offer) return null;
245
+ const mime = pickMime(this.offerTypes, target);
246
+ if (!mime) return null;
247
+ const { read, write } = makePipe();
248
+ this.offer.$.receive(mime, write);
249
+ // The write end is the compositor's now; ours is closed once sent (the
250
+ // transport consumes it). Flushing the request is what starts the copy.
251
+ const bytes = await readAll(read);
252
+ return isText(target) ? bytes.toString('utf8') : bytes;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * The `app.clipboard` object react-x11's `createClipboard(app)` wraps.
258
+ *
259
+ * @param {object} opts
260
+ * @param {import('./connection.js').WaylandConnection} opts.conn
261
+ * @param {object} opts.seat the `wl_seat` proxy
262
+ * @param {() => number} opts.serial the latest input serial
263
+ */
264
+ export async function createWaylandClipboard({ conn, seat, serial }) {
265
+ const selections = new Map();
266
+ const ddm = await conn.bind('wl_data_device_manager');
267
+ if (ddm) {
268
+ const device = ddm.$.get_data_device(seat.id);
269
+ selections.set(
270
+ 'CLIPBOARD',
271
+ new Selection({
272
+ name: 'CLIPBOARD',
273
+ device,
274
+ manager: ddm,
275
+ createSource: 'create_data_source',
276
+ serial,
277
+ }),
278
+ );
279
+ }
280
+ const psm = await conn.bind('zwp_primary_selection_device_manager_v1');
281
+ if (psm) {
282
+ const device = psm.$.get_device(seat.id);
283
+ selections.set(
284
+ 'PRIMARY',
285
+ new Selection({
286
+ name: 'PRIMARY',
287
+ device,
288
+ manager: psm,
289
+ createSource: 'create_source',
290
+ serial,
291
+ }),
292
+ );
293
+ }
294
+
295
+ const sel = (name = 'CLIPBOARD') => {
296
+ const s = selections.get(name);
297
+ if (!s) {
298
+ throw new Error(
299
+ name === 'PRIMARY'
300
+ ? 'clipboard: this compositor has no primary selection (zwp_primary_selection_device_manager_v1)'
301
+ : `clipboard: unknown selection ${name}`,
302
+ );
303
+ }
304
+ return s;
305
+ };
306
+
307
+ return {
308
+ backend: 'wayland',
309
+ selections: [...selections.keys()],
310
+ write: (data, { selection } = {}) => sel(selection).write(data),
311
+ clear: (selection) => sel(selection).clear(),
312
+ targets: ({ selection } = {}) => sel(selection).targets(),
313
+ read: ({ selection, target } = {}) => sel(selection).read(target),
314
+ // async like ntk's, whose registration is a round trip; a caller awaits
315
+ // the unwatch function either way
316
+ watch: async (selection, handler) => {
317
+ if (typeof handler !== 'function') {
318
+ throw new TypeError('clipboard: watch needs a handler function');
319
+ }
320
+ return sel(selection).watch(handler);
321
+ },
322
+ /** the data device, for drag-and-drop to build on */
323
+ dataDevice: selections.get('CLIPBOARD')?.device ?? null,
324
+ manager: ddm,
325
+ };
326
+ }