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,269 @@
1
+ // A render target: a framebuffer object with a colour texture behind it, and
2
+ // a depth/stencil renderbuffer when asked for one.
3
+ //
4
+ // This is what makes the Wayland backend's painting model the same as the
5
+ // X11 one's. An XRender context draws at a pixmap that persists — a partial
6
+ // repaint changes some pixels and leaves the rest — and react-x11's whole
7
+ // dirty-rectangle architecture assumes exactly that. A GBM swapchain does
8
+ // not offer it: buffers rotate, and the one a frame draws into last showed
9
+ // the frame before last, so a partial repaint into it leaves stale pixels
10
+ // everywhere it did not touch. Drawing into a persistent target instead, and
11
+ // copying the changed rectangles into whichever buffer comes round, is what
12
+ // lets the renderer's partial repaints be correct without it knowing.
13
+ //
14
+ // The same object is an offscreen surface — `app.createSurface()`, the paint
15
+ // cache, scroll blits — because that is all an offscreen surface is: pixels
16
+ // that persist and can be drawn into and sampled from. One class, no special
17
+ // cases.
18
+ //
19
+ // The stencil attachment is what arbitrary paths fill through
20
+ // (stencil-then-cover in context2d.js). Putting it on the target rather than
21
+ // asking EGL for it means the window's own config no longer has to carry
22
+ // stencil bits at all; the request in glcontext.js stays as belt and braces.
23
+
24
+ import { releaseDevice } from './device.js';
25
+
26
+ export class GLTarget {
27
+ /**
28
+ * @param {object} gl the GLES entry points
29
+ * @param {object} opts
30
+ * @param {number} opts.width
31
+ * @param {number} opts.height
32
+ * @param {boolean} [opts.stencil=true] attach a DEPTH24_STENCIL8 renderbuffer
33
+ * @param {'argb32'|'a8'} [opts.format='argb32']
34
+ */
35
+ constructor(gl, { width, height, stencil = true, format = 'argb32' } = {}) {
36
+ this.gl = gl;
37
+ this.width = 0;
38
+ this.height = 0;
39
+ this.format = format;
40
+ this.wantStencil = stencil;
41
+ this.fbo = null;
42
+ this.texture = null;
43
+ this.depthStencil = null;
44
+ this.destroyed = false;
45
+ this.resize(width, height);
46
+ }
47
+
48
+ /**
49
+ * Reallocate at a new size. Contents are lost: the caller that needs the
50
+ * old pixels (a window being resized) copies them across first, and the
51
+ * ones that do not (the paint cache) simply repaint.
52
+ */
53
+ resize(width, height) {
54
+ const gl = this.gl;
55
+ const w = Math.max(1, Math.round(width));
56
+ const h = Math.max(1, Math.round(height));
57
+ if (w === this.width && h === this.height && this.fbo) return false;
58
+ this.width = w;
59
+ this.height = h;
60
+
61
+ if (!this.texture) this.texture = gl.createTexture();
62
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
63
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
64
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
65
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
66
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
67
+ // Always RGBA8. An 'a8' surface is a coverage mask and could be R8, but
68
+ // the 2d context has one glyph/mask path, over R8 atlases; the memory
69
+ // difference is not worth a second shader mode.
70
+ gl.texImage2D(
71
+ gl.TEXTURE_2D,
72
+ 0,
73
+ gl.RGBA,
74
+ w,
75
+ h,
76
+ 0,
77
+ gl.RGBA,
78
+ gl.UNSIGNED_BYTE,
79
+ null,
80
+ );
81
+
82
+ if (!this.fbo) this.fbo = gl.createFramebuffer();
83
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
84
+ gl.framebufferTexture2D(
85
+ gl.FRAMEBUFFER,
86
+ gl.COLOR_ATTACHMENT0,
87
+ gl.TEXTURE_2D,
88
+ this.texture,
89
+ 0,
90
+ );
91
+ if (this.wantStencil) {
92
+ if (!this.depthStencil) this.depthStencil = gl.createRenderbuffer();
93
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthStencil);
94
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, w, h);
95
+ gl.framebufferRenderbuffer(
96
+ gl.FRAMEBUFFER,
97
+ gl.DEPTH_STENCIL_ATTACHMENT,
98
+ gl.RENDERBUFFER,
99
+ this.depthStencil,
100
+ );
101
+ }
102
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
103
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
104
+ throw new Error(
105
+ `could not build a ${w}x${h} render target: framebuffer status 0x${status.toString(16)}`,
106
+ );
107
+ }
108
+ // Fresh storage has undefined contents; a surface starts transparent.
109
+ gl.disable(gl.SCISSOR_TEST);
110
+ gl.clearColor(0, 0, 0, 0);
111
+ gl.clear(
112
+ gl.COLOR_BUFFER_BIT | (this.wantStencil ? gl.STENCIL_BUFFER_BIT : 0),
113
+ );
114
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
115
+ return true;
116
+ }
117
+
118
+ /** Route draws here and size the viewport to match. */
119
+ bind() {
120
+ const gl = this.gl;
121
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
122
+ gl.viewport(0, 0, this.width, this.height);
123
+ }
124
+
125
+ /**
126
+ * Copy rectangles of this target into the framebuffer currently bound for
127
+ * drawing (the swapchain buffer, or another target's fbo).
128
+ *
129
+ * Both are y-down as far as everyone above this line is concerned; GL is
130
+ * y-up, and since source and destination are flipped the same way, a
131
+ * same-orientation blit is a straight copy of the same rows. `rects` null
132
+ * means everything.
133
+ */
134
+ blitTo(drawFbo, rects, dstWidth = this.width, dstHeight = this.height) {
135
+ const gl = this.gl;
136
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fbo);
137
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawFbo);
138
+ gl.disable(gl.SCISSOR_TEST);
139
+ const list = rects ?? [
140
+ { x: 0, y: 0, width: this.width, height: this.height },
141
+ ];
142
+ for (const r of list) {
143
+ const x0 = Math.max(0, Math.floor(r.x));
144
+ const y0 = Math.max(0, Math.floor(r.y));
145
+ const x1 = Math.min(this.width, dstWidth, Math.ceil(r.x + r.width));
146
+ const y1 = Math.min(this.height, dstHeight, Math.ceil(r.y + r.height));
147
+ if (x1 <= x0 || y1 <= y0) continue;
148
+ // Texture row 0 is GL's bottom; our row 0 is the top. The target was
149
+ // drawn with the same convention as the swapchain buffer will be read
150
+ // with, so the GL-space rectangle is the same on both sides.
151
+ const gy0 = this.height - y1;
152
+ const gy1 = this.height - y0;
153
+ const dgy0 = dstHeight - y1;
154
+ const dgy1 = dstHeight - y0;
155
+ gl.blitFramebuffer(
156
+ x0,
157
+ gy0,
158
+ x1,
159
+ gy1,
160
+ x0,
161
+ dgy0,
162
+ x1,
163
+ dgy1,
164
+ gl.COLOR_BUFFER_BIT,
165
+ gl.NEAREST,
166
+ );
167
+ }
168
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
169
+ // The framebuffer and the scissor were the 2d contexts' to say; they are
170
+ // not any more, so the next one to draw re-establishes them (device.js).
171
+ releaseDevice(gl);
172
+ }
173
+
174
+ /**
175
+ * Shift a band of pixels in place — the scroll blit. Answers whether a
176
+ * copy was made; false means nothing survives the shift and the caller
177
+ * repaints as it would have anyway.
178
+ *
179
+ * Same contract as ntk's `Surface.copyWithin` (ntk#252): integer deltas
180
+ * only, the band is `src ∩ (src + delta)`, nothing outside `src` written.
181
+ * `glBlitFramebuffer` with overlapping source and destination on the same
182
+ * framebuffer is undefined, so this goes through a scratch texture: copy
183
+ * the surviving band out, then blit it back shifted.
184
+ */
185
+ copyWithin(src, dx, dy) {
186
+ if (this.destroyed) return false;
187
+ if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
188
+ if (dx === 0 && dy === 0) return false;
189
+ const x0 = Math.max(0, Math.floor(src.x));
190
+ const y0 = Math.max(0, Math.floor(src.y));
191
+ const x1 = Math.min(this.width, Math.ceil(src.x + src.width));
192
+ const y1 = Math.min(this.height, Math.ceil(src.y + src.height));
193
+ const dstX0 = Math.max(x0, x0 + dx);
194
+ const dstY0 = Math.max(y0, y0 + dy);
195
+ const dstX1 = Math.min(x1, x1 + dx);
196
+ const dstY1 = Math.min(y1, y1 + dy);
197
+ if (!(dstX1 > dstX0 && dstY1 > dstY0)) return false;
198
+ const bw = dstX1 - dstX0;
199
+ const bh = dstY1 - dstY0;
200
+ const sx = dstX0 - dx;
201
+ const sy = dstY0 - dy;
202
+
203
+ const gl = this.gl;
204
+ const scratch = scratchFor(gl, bw, bh);
205
+ // out: this[sx,sy,bw,bh] -> scratch[0,0]
206
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fbo);
207
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, scratch.fbo);
208
+ gl.disable(gl.SCISSOR_TEST);
209
+ gl.blitFramebuffer(
210
+ sx,
211
+ this.height - (sy + bh),
212
+ sx + bw,
213
+ this.height - sy,
214
+ 0,
215
+ scratch.height - bh,
216
+ bw,
217
+ scratch.height,
218
+ gl.COLOR_BUFFER_BIT,
219
+ gl.NEAREST,
220
+ );
221
+ // back: scratch[0,0,bw,bh] -> this[dstX0,dstY0]
222
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, scratch.fbo);
223
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.fbo);
224
+ gl.blitFramebuffer(
225
+ 0,
226
+ scratch.height - bh,
227
+ bw,
228
+ scratch.height,
229
+ dstX0,
230
+ this.height - (dstY0 + bh),
231
+ dstX0 + bw,
232
+ this.height - dstY0,
233
+ gl.COLOR_BUFFER_BIT,
234
+ gl.NEAREST,
235
+ );
236
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
237
+ releaseDevice(gl);
238
+ return true;
239
+ }
240
+
241
+ destroy() {
242
+ if (this.destroyed) return;
243
+ this.destroyed = true;
244
+ const gl = this.gl;
245
+ if (this.fbo) gl.deleteFramebuffer(this.fbo);
246
+ if (this.texture) gl.deleteTexture(this.texture);
247
+ if (this.depthStencil) gl.deleteRenderbuffer(this.depthStencil);
248
+ this.fbo = this.texture = this.depthStencil = null;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * One shared scratch target per GL context for `copyWithin`, grown to the
254
+ * largest band asked for. A scroll happens every frame of a fling; allocating
255
+ * per call would be the wrong trade.
256
+ */
257
+ const scratches = new WeakMap();
258
+
259
+ function scratchFor(gl, w, h) {
260
+ let s = scratches.get(gl);
261
+ if (!s || s.width < w || s.height < h) {
262
+ const nw = Math.max(w, s?.width ?? 0);
263
+ const nh = Math.max(h, s?.height ?? 0);
264
+ s?.destroy();
265
+ s = new GLTarget(gl, { width: nw, height: nh, stencil: false });
266
+ scratches.set(gl, s);
267
+ }
268
+ return s;
269
+ }
@@ -0,0 +1,113 @@
1
+ // Shaped text, positioned for the GPU context.
2
+ //
3
+ // Nothing here shapes anything: ntk's `FontManager.shape` does the work, and
4
+ // it does it with no display connection at all — fontconfig matching, fontkit
5
+ // faces, harfbuzz-equivalent shaping, bidi levels. That is the RFC's claim
6
+ // about the portable half of the text stack, and it holds exactly: the same
7
+ // call, on the same fonts, produces the same runs the X11 backend draws.
8
+ //
9
+ // What this adds is the last inch the X11 backend gets from XRender for free.
10
+ // ntk's shaped output gives each glyph an advance (`ax`) and an offset
11
+ // (`dx`/`dy`) rather than a position, because `CompositeGlyphs` walks the run
12
+ // and accumulates advances server-side. There is no server here, so the walk
13
+ // happens on this side and comes out as absolute pen positions the atlas can
14
+ // place quads at.
15
+
16
+ /**
17
+ * Turn ntk's shaped runs into positioned glyph runs.
18
+ *
19
+ * Positions are relative to the text origin: x from the start of the run, y
20
+ * on the baseline. The caller translates to put it somewhere.
21
+ */
22
+ export function positionRuns(shaped, { color } = {}) {
23
+ const out = [];
24
+ let penX = 0;
25
+ for (const run of shaped.runs) {
26
+ const glyphs = [];
27
+ for (const g of run.glyphs) {
28
+ glyphs.push({ id: g.id, x: penX + (g.dx ?? 0), y: g.dy ?? 0 });
29
+ penX += g.ax ?? 0;
30
+ }
31
+ out.push({
32
+ font: run.font,
33
+ size: run.size,
34
+ // A run's identity for atlas keys: the same face at the same size is
35
+ // the same pixels, whatever string it came from.
36
+ fontKey: run.font?.postscriptName ?? run.font?.familyName ?? 'font',
37
+ glyphs,
38
+ color,
39
+ });
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /**
45
+ * A small shaping cache in front of ntk's.
46
+ *
47
+ * ntk memoises at word level internally; this memoises the whole positioned
48
+ * result, because a UI redraws the same labels every frame and the win is
49
+ * skipping the walk and the object churn, not the shaping.
50
+ */
51
+ export class TextShaper {
52
+ constructor(fontManager, { limit = 512 } = {}) {
53
+ this.fm = fontManager;
54
+ this.limit = limit;
55
+ this._cache = new Map();
56
+ }
57
+
58
+ /**
59
+ * @returns {Promise<{width:number, runs:Array, ascent:number, descent:number, lineHeight:number}>}
60
+ */
61
+ /**
62
+ * The same, synchronously — ntk's matching and shaping are synchronous once
63
+ * the fontconfig cache is warm, which it is after the first call. For the
64
+ * window frame's title, which is drawn inside a frame and cannot await.
65
+ */
66
+ measureSync(text, spec) {
67
+ const key = `${spec.family}|${spec.size}|${spec.weight ?? ''}|${spec.italic ? 'i' : ''}|${text}`;
68
+ const hit = this._cache.get(key);
69
+ if (hit) return hit;
70
+ const shaped = this.fm.shape(text, spec);
71
+ if (!shaped || typeof shaped.then === 'function') return null;
72
+ const font = shaped.runs[0]?.font ?? this.fm.match(spec);
73
+ if (!font || typeof font.then === 'function') return null;
74
+ const m = font.metrics(spec.size);
75
+ const entry = {
76
+ width: shaped.width,
77
+ runs: positionRuns(shaped),
78
+ ascent: m.ascent,
79
+ descent: m.descent,
80
+ lineHeight: m.lineHeight,
81
+ };
82
+ this._cache.set(key, entry);
83
+ if (this._cache.size > this.limit)
84
+ this._cache.delete(this._cache.keys().next().value);
85
+ return entry;
86
+ }
87
+
88
+ async measure(text, spec) {
89
+ const key = `${spec.family}|${spec.size}|${spec.weight ?? ''}|${spec.italic ? 'i' : ''}|${text}`;
90
+ const hit = this._cache.get(key);
91
+ if (hit) {
92
+ // refresh LRU position
93
+ this._cache.delete(key);
94
+ this._cache.set(key, hit);
95
+ return hit;
96
+ }
97
+ const shaped = await this.fm.shape(text, spec);
98
+ const font = shaped.runs[0]?.font ?? (await this.fm.match(spec));
99
+ const m = font.metrics(spec.size);
100
+ const entry = {
101
+ width: shaped.width,
102
+ runs: positionRuns(shaped),
103
+ ascent: m.ascent,
104
+ descent: m.descent,
105
+ lineHeight: m.lineHeight,
106
+ };
107
+ this._cache.set(key, entry);
108
+ if (this._cache.size > this.limit) {
109
+ this._cache.delete(this._cache.keys().next().value);
110
+ }
111
+ return entry;
112
+ }
113
+ }