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.
- package/package.json +11 -8
- package/src/Reconciler.js +34 -21
- package/src/components/Select.js +8 -2
- package/src/events.js +8 -2
- package/src/glnodes.js +15 -3
- package/src/index.d.ts +5 -0
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/ntk.js +10 -1
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/types/system.d.ts +14 -1
- package/src/wayland/app.js +574 -0
- package/src/wayland/backendwindow.js +1199 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +485 -0
- package/src/wayland/context2d.js +2290 -0
- package/src/wayland/decorations.js +476 -0
- package/src/wayland/device.js +55 -0
- package/src/wayland/dmabuf.js +89 -0
- package/src/wayland/dnd.js +581 -0
- package/src/wayland/fdutil.js +108 -0
- package/src/wayland/framestyle.js +257 -0
- package/src/wayland/framewatch.js +190 -0
- package/src/wayland/glarea.js +372 -0
- package/src/wayland/glcontext.js +415 -0
- package/src/wayland/glyphatlas.js +237 -0
- package/src/wayland/input.js +417 -0
- package/src/wayland/keysymnames.js +35 -0
- package/src/wayland/layershell.js +363 -0
- package/src/wayland/outputs.js +601 -0
- package/src/wayland/protocols/cursor-shape-v1.json +1 -0
- package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
- package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
- package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
- package/src/wayland/protocols/fractional-scale-v1.json +1 -0
- package/src/wayland/protocols/index.json +131 -0
- package/src/wayland/protocols/kde-server-decoration.json +1 -0
- package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
- package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
- package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
- package/src/wayland/protocols/presentation-time.json +1 -0
- package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
- package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
- package/src/wayland/protocols/tablet-v2.json +1 -0
- package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
- package/src/wayland/protocols/viewporter.json +1 -0
- package/src/wayland/protocols/wayland.json +1 -0
- package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
- package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-activation-v1.json +1 -0
- package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-shell.json +1 -0
- package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
- package/src/wayland/readback.js +99 -0
- package/src/wayland/screencopy.js +584 -0
- package/src/wayland/seat.js +584 -0
- package/src/wayland/shm.js +226 -0
- package/src/wayland/ssd.js +234 -0
- package/src/wayland/surface.js +141 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +269 -0
- package/src/wayland/text.js +113 -0
- package/src/wayland/textinput.js +671 -0
- package/src/wayland/touch.js +284 -0
- package/src/wayland/window.js +854 -0
- package/src/wayland/xkb.js +425 -0
- 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
|
+
}
|