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,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
|
+
}
|