react-x11 2.51.1 → 2.53.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/README.md +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
package/src/wayland/seat.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { EventEmitter } from 'node:events';
|
|
28
28
|
import fs from 'node:fs';
|
|
29
|
-
import { XkbKeymap } from './xkb.js';
|
|
29
|
+
import { XkbKeymap, REAL_MODS, layoutsFromKeymap } from './xkb.js';
|
|
30
30
|
|
|
31
31
|
/** `wl_seat.capability` bits. */
|
|
32
32
|
export const SEAT_CAP = { POINTER: 1, KEYBOARD: 2, TOUCH: 4 };
|
|
@@ -185,7 +185,10 @@ export class WaylandSeat extends EventEmitter {
|
|
|
185
185
|
this.xkb = null;
|
|
186
186
|
/** X-style modifier mask: depressed | latched | locked */
|
|
187
187
|
this.mods = 0;
|
|
188
|
+
/** the locked half alone — Caps Lock and Num Lock (keyboardState) */
|
|
189
|
+
this.locked = 0;
|
|
188
190
|
this.group = 0;
|
|
191
|
+
this._layouts = null;
|
|
189
192
|
/** pointer buttons currently held, as an X state mask */
|
|
190
193
|
this.buttonMask = 0;
|
|
191
194
|
this.repeat = { rate: 25, delay: 600 };
|
|
@@ -437,6 +440,7 @@ export class WaylandSeat extends EventEmitter {
|
|
|
437
440
|
: null;
|
|
438
441
|
this.keymap = { format, size, text };
|
|
439
442
|
this.xkb = text ? XkbKeymap.parse(text) : null;
|
|
443
|
+
this._layouts = null;
|
|
440
444
|
this.emit('keymap', this.keymap, this.xkb);
|
|
441
445
|
} catch (err) {
|
|
442
446
|
this.emit(
|
|
@@ -473,6 +477,7 @@ export class WaylandSeat extends EventEmitter {
|
|
|
473
477
|
k.on('modifiers', (serial, depressed, latched, locked, group) => {
|
|
474
478
|
this.lastSerial = serial;
|
|
475
479
|
this.mods = (depressed | latched | locked) & 0xff;
|
|
480
|
+
this.locked = locked;
|
|
476
481
|
this.group = group;
|
|
477
482
|
this.emit('modifiers', {
|
|
478
483
|
mods: this.mods,
|
|
@@ -501,6 +506,28 @@ export class WaylandSeat extends EventEmitter {
|
|
|
501
506
|
});
|
|
502
507
|
}
|
|
503
508
|
|
|
509
|
+
/**
|
|
510
|
+
* The locks and the layout, in `useKeyboardState()`'s shape
|
|
511
|
+
* (src/keyboardstate.js). Caps Lock is Lock, which no keymap can move;
|
|
512
|
+
* Num Lock is wherever this keymap binds the virtual `NumLock`, with the
|
|
513
|
+
* X convention's Mod2 where it does not say.
|
|
514
|
+
*
|
|
515
|
+
* Only as fresh as the last `wl_keyboard.modifiers`, and a compositor sends
|
|
516
|
+
* those to the client with keyboard focus alone: a lock toggled while
|
|
517
|
+
* another app has the keyboard arrives when focus comes back. That is the
|
|
518
|
+
* protocol, not a shortcut — X11's StateNotify reaches every client.
|
|
519
|
+
*/
|
|
520
|
+
keyboardState() {
|
|
521
|
+
const numLock = this.xkb?.maskOf('NumLock') || REAL_MODS.Mod2;
|
|
522
|
+
this._layouts ??= Object.freeze(layoutsFromKeymap(this.keymap?.text));
|
|
523
|
+
return {
|
|
524
|
+
capsLock: Boolean(this.locked & REAL_MODS.Lock),
|
|
525
|
+
numLock: Boolean(this.locked & numLock),
|
|
526
|
+
group: this.group,
|
|
527
|
+
layouts: this._layouts,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
504
531
|
/** A key event in the shape events.js reads, decoded against the keymap. */
|
|
505
532
|
_keyEvent(type, keycode, time, serial, repeat = false) {
|
|
506
533
|
const state = XkbKeymap.stateOf(this.mods, this.group);
|
package/src/wayland/swapchain.js
CHANGED
|
@@ -191,6 +191,17 @@ export class WaylandSwapchain {
|
|
|
191
191
|
|
|
192
192
|
/** Give the chain buffers of this size, keeping its GBM surface. */
|
|
193
193
|
_resize(w, h) {
|
|
194
|
+
// The size they already are keeps them: x11-dri makes nothing again and
|
|
195
|
+
// moves no generation, so the wl_buffers stay too. Dropped, the next
|
|
196
|
+
// swap answered a buffer the compositor already had — `isNew: false`, no
|
|
197
|
+
// descriptor — that this chain no longer knew, and imported it with
|
|
198
|
+
// none. A small surface asks for this on every change of size: `slackFor`
|
|
199
|
+
// rounds both sides up to the same 128. What each buffer holds is of the
|
|
200
|
+
// old size, so all of it is owed.
|
|
201
|
+
if (w === this.allocWidth && h === this.allocHeight) {
|
|
202
|
+
for (const entry of this.buffers.values()) entry.damage = 'all';
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
194
205
|
// In place: the handle stays valid, the context keeps its GL objects and
|
|
195
206
|
// stays current. Buffers from the old generation are gone, so their
|
|
196
207
|
// `wl_buffer`s go with them — the compositor may still be showing one, so
|
|
@@ -299,6 +310,18 @@ export class WaylandSwapchain {
|
|
|
299
310
|
return true;
|
|
300
311
|
}
|
|
301
312
|
this._swapsSinceNew = 0;
|
|
313
|
+
if (!out.isNew) {
|
|
314
|
+
// GBM exported this one already, so it carries no descriptor, and the
|
|
315
|
+
// chain has no wl_buffer for it: the two have lost track of each
|
|
316
|
+
// other, which no compositor did.
|
|
317
|
+
this._fail(
|
|
318
|
+
new Error(
|
|
319
|
+
`GPU buffer ${id} came round again with no wl_buffer for it ` +
|
|
320
|
+
'(react-x11 bug: the swapchain dropped a buffer GBM still holds)',
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
302
325
|
return await this._import(id, out, generation, damage);
|
|
303
326
|
}
|
|
304
327
|
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// `<video frames>` on Wayland: a frame's planes go up as textures as the
|
|
2
|
+
// decoder made them, and the colour conversion is a fragment shader drawing
|
|
3
|
+
// into the `<video>`'s own surface (design/architecture/video.md §5).
|
|
4
|
+
//
|
|
5
|
+
// The backend's 2D context is already a GLES rasterizer, so this is the GLX
|
|
6
|
+
// transport of §2.3 made native: no conversion in JavaScript for any format,
|
|
7
|
+
// and NV12 and I420 cross into the GPU at 1.5 bytes a pixel. Y is an R8
|
|
8
|
+
// texture; NV12's chroma one RG8 texture at half size, I420's two R8 ones;
|
|
9
|
+
// BGRA one RGBA8 texture read back to front. The shader applies the sink's
|
|
10
|
+
// matrix and range exactly as `frameToRGBA` does — no transfer function,
|
|
11
|
+
// because nothing else on this backend is colour-managed either — so a frame
|
|
12
|
+
// drawn here and one converted in JavaScript are the same picture, chroma
|
|
13
|
+
// apart: sampled bilinearly here, as a player does, and by nearest there.
|
|
14
|
+
//
|
|
15
|
+
// What it writes into is the surface the drawn presentation already keeps
|
|
16
|
+
// (src/nodes/video.js): a render target, which the window's context then
|
|
17
|
+
// composites at the picture's rect like any `drawImage`. Nothing lifts on
|
|
18
|
+
// Wayland — no subsurfaces, by the backend's own design — so this is the
|
|
19
|
+
// whole presentation.
|
|
20
|
+
//
|
|
21
|
+
// The GL is shared with every 2D context on the app (device.js): whatever
|
|
22
|
+
// the device's owner still has buffered is drawn before the target is
|
|
23
|
+
// written, the shader's vertex state lives in a vertex array of its own,
|
|
24
|
+
// and the device is released afterwards so the next context to draw puts
|
|
25
|
+
// its own state back. `UNPACK_ROW_LENGTH`, which a decoder's padded rows
|
|
26
|
+
// need, is set back to 0 for the uploads that assume it.
|
|
27
|
+
|
|
28
|
+
import { flushDevice, releaseDevice } from './device.js';
|
|
29
|
+
|
|
30
|
+
const VERT = `
|
|
31
|
+
attribute vec2 aPos;
|
|
32
|
+
attribute vec2 aUV;
|
|
33
|
+
varying vec2 vUV;
|
|
34
|
+
void main() {
|
|
35
|
+
vUV = aUV;
|
|
36
|
+
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
37
|
+
}`;
|
|
38
|
+
|
|
39
|
+
const FRAG = `
|
|
40
|
+
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
41
|
+
precision highp float;
|
|
42
|
+
#else
|
|
43
|
+
precision mediump float;
|
|
44
|
+
#endif
|
|
45
|
+
varying vec2 vUV;
|
|
46
|
+
uniform int uLayout;
|
|
47
|
+
uniform sampler2D uY;
|
|
48
|
+
uniform sampler2D uC;
|
|
49
|
+
uniform sampler2D uV;
|
|
50
|
+
uniform vec2 uChroma;
|
|
51
|
+
uniform mat3 uMatrix;
|
|
52
|
+
uniform vec3 uOffset;
|
|
53
|
+
void main() {
|
|
54
|
+
if (uLayout == 0) {
|
|
55
|
+
// BGRA bytes uploaded as RGBA: blue is in r. Opaque, whatever the
|
|
56
|
+
// fourth byte held.
|
|
57
|
+
gl_FragColor = vec4(texture2D(uY, vUV).bgr, 1.0);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
float y = texture2D(uY, vUV).r;
|
|
61
|
+
vec2 cuv = vUV * uChroma;
|
|
62
|
+
vec2 c = uLayout == 1
|
|
63
|
+
? texture2D(uC, cuv).rg
|
|
64
|
+
: vec2(texture2D(uC, cuv).r, texture2D(uV, cuv).r);
|
|
65
|
+
vec3 rgb = uMatrix * (vec3(y, c) - uOffset);
|
|
66
|
+
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
|
|
67
|
+
}`;
|
|
68
|
+
|
|
69
|
+
const LAYOUT = { BGRA: 0, NV12: 1, I420: 2 };
|
|
70
|
+
|
|
71
|
+
// Kr and Kb, as src/videoframes.js has them
|
|
72
|
+
const MATRIX = {
|
|
73
|
+
bt709: [0.2126, 0.0722],
|
|
74
|
+
bt601: [0.299, 0.114],
|
|
75
|
+
bt2020: [0.2627, 0.0593],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const matrices = new Map();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The column-major mat3 and the offset that take normalized Y'CbCr to
|
|
82
|
+
* R'G'B' for a matrix and a range — the same numbers `frameToRGBA`'s tables
|
|
83
|
+
* are made of, in units of 1 rather than of 255.
|
|
84
|
+
*/
|
|
85
|
+
export function videoMatrix(colorSpace, range) {
|
|
86
|
+
const key = `${colorSpace}/${range}`;
|
|
87
|
+
let m = matrices.get(key);
|
|
88
|
+
if (m) return m;
|
|
89
|
+
const [kr, kb] = MATRIX[colorSpace];
|
|
90
|
+
const kg = 1 - kr - kb;
|
|
91
|
+
const full = range === 'full';
|
|
92
|
+
const ys = full ? 1 : 255 / 219;
|
|
93
|
+
const cs = full ? 1 : 255 / 224;
|
|
94
|
+
m = {
|
|
95
|
+
matrix: Float32Array.of(
|
|
96
|
+
// the column Y' multiplies
|
|
97
|
+
ys,
|
|
98
|
+
ys,
|
|
99
|
+
ys,
|
|
100
|
+
// Cb
|
|
101
|
+
0,
|
|
102
|
+
-cs * ((2 * kb * (1 - kb)) / kg),
|
|
103
|
+
cs * 2 * (1 - kb),
|
|
104
|
+
// Cr
|
|
105
|
+
cs * 2 * (1 - kr),
|
|
106
|
+
-cs * ((2 * kr * (1 - kr)) / kg),
|
|
107
|
+
0,
|
|
108
|
+
),
|
|
109
|
+
offset: Float32Array.of(full ? 0 : 16 / 255, 128 / 255, 128 / 255),
|
|
110
|
+
};
|
|
111
|
+
matrices.set(key, m);
|
|
112
|
+
return m;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The planes of a frame as textures: format, size and the bytes per texel
|
|
116
|
+
* `UNPACK_ROW_LENGTH` counts a decoder's stride in. */
|
|
117
|
+
function planesOf(gl, format, width, height) {
|
|
118
|
+
const cw = Math.ceil(width / 2);
|
|
119
|
+
const ch = Math.ceil(height / 2);
|
|
120
|
+
const r8 = { internal: gl.R8, format: gl.RED, texel: 1 };
|
|
121
|
+
switch (format) {
|
|
122
|
+
case 'NV12':
|
|
123
|
+
return [
|
|
124
|
+
{ ...r8, width, height },
|
|
125
|
+
{ internal: gl.RG8, format: gl.RG, texel: 2, width: cw, height: ch },
|
|
126
|
+
];
|
|
127
|
+
case 'I420':
|
|
128
|
+
return [
|
|
129
|
+
{ ...r8, width, height },
|
|
130
|
+
{ ...r8, width: cw, height: ch },
|
|
131
|
+
{ ...r8, width: cw, height: ch },
|
|
132
|
+
];
|
|
133
|
+
case 'BGRA':
|
|
134
|
+
return [{ internal: gl.RGBA, format: gl.RGBA, texel: 4, width, height }];
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One per GL device: the program, and the quad it draws. */
|
|
140
|
+
const programs = new WeakMap();
|
|
141
|
+
|
|
142
|
+
function programFor(gl) {
|
|
143
|
+
let p = programs.get(gl);
|
|
144
|
+
if (p) return p;
|
|
145
|
+
const vs = compile(gl, gl.VERTEX_SHADER, VERT, 'video vertex');
|
|
146
|
+
let fs;
|
|
147
|
+
try {
|
|
148
|
+
fs = compile(gl, gl.FRAGMENT_SHADER, FRAG, 'video fragment');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
gl.deleteShader(vs);
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
const program = gl.createProgram();
|
|
154
|
+
gl.attachShader(program, vs);
|
|
155
|
+
gl.attachShader(program, fs);
|
|
156
|
+
gl.linkProgram(program);
|
|
157
|
+
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
158
|
+
const log = linked ? '' : gl.getProgramInfoLog(program);
|
|
159
|
+
gl.deleteShader(vs);
|
|
160
|
+
gl.deleteShader(fs);
|
|
161
|
+
if (!linked) {
|
|
162
|
+
gl.deleteProgram(program);
|
|
163
|
+
throw new Error(`video program failed to link: ${log}`);
|
|
164
|
+
}
|
|
165
|
+
const u = (name) => gl.getUniformLocation(program, name);
|
|
166
|
+
const aPos = gl.getAttribLocation(program, 'aPos');
|
|
167
|
+
const aUV = gl.getAttribLocation(program, 'aUV');
|
|
168
|
+
// A strip over the whole target. The 2D context draws device row 0 at the
|
|
169
|
+
// top of clip space, so the frame's first row — texture row 0 — goes
|
|
170
|
+
// there: v = 0 at clip y = +1.
|
|
171
|
+
const buffer = gl.createBuffer();
|
|
172
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
173
|
+
gl.bufferData(
|
|
174
|
+
gl.ARRAY_BUFFER,
|
|
175
|
+
Float32Array.of(-1, 1, 0, 0, -1, -1, 0, 1, 1, 1, 1, 0, 1, -1, 1, 1),
|
|
176
|
+
gl.STATIC_DRAW,
|
|
177
|
+
);
|
|
178
|
+
// its own vertex array, so the 2D contexts' attribute state — on the
|
|
179
|
+
// default one, re-pointed every batch — is never this program's
|
|
180
|
+
const vao = gl.createVertexArray();
|
|
181
|
+
gl.bindVertexArray(vao);
|
|
182
|
+
gl.enableVertexAttribArray(aPos);
|
|
183
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
|
|
184
|
+
gl.enableVertexAttribArray(aUV);
|
|
185
|
+
gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8);
|
|
186
|
+
gl.bindVertexArray(null);
|
|
187
|
+
p = {
|
|
188
|
+
program,
|
|
189
|
+
buffer,
|
|
190
|
+
vao,
|
|
191
|
+
maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
|
|
192
|
+
loc: {
|
|
193
|
+
uLayout: u('uLayout'),
|
|
194
|
+
uY: u('uY'),
|
|
195
|
+
uC: u('uC'),
|
|
196
|
+
uV: u('uV'),
|
|
197
|
+
uChroma: u('uChroma'),
|
|
198
|
+
uMatrix: u('uMatrix'),
|
|
199
|
+
uOffset: u('uOffset'),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
programs.set(gl, p);
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Each surface's plane textures, kept across frames of one size. */
|
|
207
|
+
const uploads = new WeakMap();
|
|
208
|
+
|
|
209
|
+
function texturesFor(gl, surface, format, width, height) {
|
|
210
|
+
let kept = uploads.get(surface);
|
|
211
|
+
if (
|
|
212
|
+
kept &&
|
|
213
|
+
kept.format === format &&
|
|
214
|
+
kept.width === width &&
|
|
215
|
+
kept.height === height
|
|
216
|
+
) {
|
|
217
|
+
return kept;
|
|
218
|
+
}
|
|
219
|
+
if (kept) deleteTextures(gl, kept);
|
|
220
|
+
const planes = planesOf(gl, format, width, height);
|
|
221
|
+
kept = {
|
|
222
|
+
format,
|
|
223
|
+
width,
|
|
224
|
+
height,
|
|
225
|
+
planes,
|
|
226
|
+
textures: planes.map(() => gl.createTexture()),
|
|
227
|
+
fresh: true,
|
|
228
|
+
};
|
|
229
|
+
uploads.set(surface, kept);
|
|
230
|
+
return kept;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function deleteTextures(gl, kept) {
|
|
234
|
+
for (const tex of kept.textures) gl.deleteTexture(tex);
|
|
235
|
+
kept.textures = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A plane's rows at a stride `UNPACK_ROW_LENGTH` cannot say — one that is
|
|
240
|
+
* not a whole number of texels — packed tight. Every decoder pads to whole
|
|
241
|
+
* pixels, so this is the case that should never come.
|
|
242
|
+
*/
|
|
243
|
+
function tight(data, stride, rowBytes, rows) {
|
|
244
|
+
const out = new Uint8Array(rowBytes * rows);
|
|
245
|
+
for (let r = 0; r < rows; r++) {
|
|
246
|
+
out.set(data.subarray(r * stride, r * stride + rowBytes), r * rowBytes);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* `frame` from `sink` into `surface` (a WaylandSurface at the frame's size).
|
|
253
|
+
* False — and the frame is converted in JavaScript instead — where the
|
|
254
|
+
* surface is not one of this backend's, the app has no GL, or the frame is
|
|
255
|
+
* larger than a texture can be.
|
|
256
|
+
*/
|
|
257
|
+
export function writeVideoFrame(app, surface, sink, frame) {
|
|
258
|
+
const gl = app?.gl;
|
|
259
|
+
const target = surface?.target;
|
|
260
|
+
if (!gl || !target?.fbo || target.destroyed) return false;
|
|
261
|
+
if (!(sink.format in LAYOUT)) return false;
|
|
262
|
+
app.makeCurrent();
|
|
263
|
+
const p = programFor(gl);
|
|
264
|
+
if (sink.width > p.maxSize || sink.height > p.maxSize) return false;
|
|
265
|
+
// Anything a context still has buffered is drawn first: one of its draws
|
|
266
|
+
// may read this target, and must read the frame it was given (device.js).
|
|
267
|
+
flushDevice(gl);
|
|
268
|
+
const kept = texturesFor(gl, surface, sink.format, sink.width, sink.height);
|
|
269
|
+
try {
|
|
270
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
271
|
+
kept.planes.forEach((plane, i) => {
|
|
272
|
+
const rowBytes = plane.width * plane.texel;
|
|
273
|
+
const stride = frame.strides[i];
|
|
274
|
+
let data = frame.planes[i];
|
|
275
|
+
let rowLength = 0;
|
|
276
|
+
if (stride !== rowBytes) {
|
|
277
|
+
if (stride % plane.texel === 0) rowLength = stride / plane.texel;
|
|
278
|
+
else data = tight(data, stride, rowBytes, plane.height);
|
|
279
|
+
}
|
|
280
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, rowLength);
|
|
281
|
+
gl.activeTexture(gl.TEXTURE0 + i);
|
|
282
|
+
gl.bindTexture(gl.TEXTURE_2D, kept.textures[i]);
|
|
283
|
+
if (kept.fresh) {
|
|
284
|
+
// Y and BGRA meet the target texel for texel; chroma is half size,
|
|
285
|
+
// and read between its samples as a player reads it
|
|
286
|
+
const filter = i === 0 ? gl.NEAREST : gl.LINEAR;
|
|
287
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
288
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
289
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
290
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
291
|
+
gl.texImage2D(
|
|
292
|
+
gl.TEXTURE_2D,
|
|
293
|
+
0,
|
|
294
|
+
plane.internal,
|
|
295
|
+
plane.width,
|
|
296
|
+
plane.height,
|
|
297
|
+
0,
|
|
298
|
+
plane.format,
|
|
299
|
+
gl.UNSIGNED_BYTE,
|
|
300
|
+
data,
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
gl.texSubImage2D(
|
|
304
|
+
gl.TEXTURE_2D,
|
|
305
|
+
0,
|
|
306
|
+
0,
|
|
307
|
+
0,
|
|
308
|
+
plane.width,
|
|
309
|
+
plane.height,
|
|
310
|
+
plane.format,
|
|
311
|
+
gl.UNSIGNED_BYTE,
|
|
312
|
+
data,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
kept.fresh = false;
|
|
317
|
+
|
|
318
|
+
target.bind();
|
|
319
|
+
gl.disable(gl.BLEND);
|
|
320
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
321
|
+
gl.disable(gl.STENCIL_TEST);
|
|
322
|
+
gl.disable(gl.DEPTH_TEST);
|
|
323
|
+
gl.disable(gl.CULL_FACE);
|
|
324
|
+
gl.colorMask(true, true, true, true);
|
|
325
|
+
gl.useProgram(p.program);
|
|
326
|
+
const { loc } = p;
|
|
327
|
+
gl.uniform1i(loc.uLayout, LAYOUT[sink.format]);
|
|
328
|
+
gl.uniform1i(loc.uY, 0);
|
|
329
|
+
gl.uniform1i(loc.uC, 1);
|
|
330
|
+
gl.uniform1i(loc.uV, 2);
|
|
331
|
+
// full-size texture coordinates onto a chroma plane rounded up: an odd
|
|
332
|
+
// frame's last chroma column covers one picture column, not two
|
|
333
|
+
gl.uniform2f(
|
|
334
|
+
loc.uChroma,
|
|
335
|
+
sink.width / (2 * Math.ceil(sink.width / 2)),
|
|
336
|
+
sink.height / (2 * Math.ceil(sink.height / 2)),
|
|
337
|
+
);
|
|
338
|
+
const { matrix, offset } = videoMatrix(sink.colorSpace, sink.range);
|
|
339
|
+
gl.uniformMatrix3fv(loc.uMatrix, false, matrix);
|
|
340
|
+
gl.uniform3fv(loc.uOffset, offset);
|
|
341
|
+
gl.bindVertexArray(p.vao);
|
|
342
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
343
|
+
gl.bindVertexArray(null);
|
|
344
|
+
} finally {
|
|
345
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
346
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
347
|
+
// the program, the blend and the framebuffer were the 2D contexts' to
|
|
348
|
+
// say: the next one to draw says them again
|
|
349
|
+
releaseDevice(gl);
|
|
350
|
+
app.rebindWindowTarget?.();
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** `surface` is let go: its plane textures with it. */
|
|
356
|
+
export function releaseVideoFrame(app, surface) {
|
|
357
|
+
const kept = uploads.get(surface);
|
|
358
|
+
if (!kept) return;
|
|
359
|
+
uploads.delete(surface);
|
|
360
|
+
const gl = app?.gl;
|
|
361
|
+
if (!gl) return;
|
|
362
|
+
app.makeCurrent();
|
|
363
|
+
deleteTextures(gl, kept);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function compile(gl, type, src, what) {
|
|
367
|
+
const sh = gl.createShader(type);
|
|
368
|
+
gl.shaderSource(sh, src);
|
|
369
|
+
gl.compileShader(sh);
|
|
370
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
371
|
+
const log = gl.getShaderInfoLog(sh);
|
|
372
|
+
gl.deleteShader(sh);
|
|
373
|
+
throw new Error(`${what} shader failed to compile: ${log}`);
|
|
374
|
+
}
|
|
375
|
+
return sh;
|
|
376
|
+
}
|
package/src/wayland/xkb.js
CHANGED
|
@@ -331,6 +331,14 @@ export class XkbKeymap {
|
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* A modifier by name, as the real mask the compositor reports it under —
|
|
336
|
+
* `'NumLock'` is whichever ModN this keymap binds it to, `'Lock'` is Lock.
|
|
337
|
+
*/
|
|
338
|
+
maskOf(name) {
|
|
339
|
+
return this._modMask(name);
|
|
340
|
+
}
|
|
341
|
+
|
|
334
342
|
/** A modifier name (real or virtual) as a real mask. */
|
|
335
343
|
_modMask(name) {
|
|
336
344
|
if (name in REAL_MODS) return REAL_MODS[name];
|
|
@@ -461,6 +469,135 @@ export class XkbKeymap {
|
|
|
461
469
|
}
|
|
462
470
|
}
|
|
463
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Files the rules add to `xkb_symbols` that are not a layout: the model's
|
|
474
|
+
* extras (`inet(evdev)`) and every option's (`group(alt_shift_toggle)`,
|
|
475
|
+
* `compose(rctrl)`). Only consulted where a keymap has one group and so no
|
|
476
|
+
* group suffix to anchor on.
|
|
477
|
+
*/
|
|
478
|
+
const NOT_LAYOUTS = new Set([
|
|
479
|
+
'altwin',
|
|
480
|
+
'capslock',
|
|
481
|
+
'compose',
|
|
482
|
+
'ctrl',
|
|
483
|
+
'eurosign',
|
|
484
|
+
'fkeys',
|
|
485
|
+
'grab',
|
|
486
|
+
'group',
|
|
487
|
+
'hyper',
|
|
488
|
+
'inet',
|
|
489
|
+
'keypad',
|
|
490
|
+
'kpdl',
|
|
491
|
+
'level2',
|
|
492
|
+
'level3',
|
|
493
|
+
'level5',
|
|
494
|
+
'nbsp',
|
|
495
|
+
'parens',
|
|
496
|
+
'rupeesign',
|
|
497
|
+
'scrolllock',
|
|
498
|
+
'shift',
|
|
499
|
+
'srvr_ctrl',
|
|
500
|
+
'srvrkeys',
|
|
501
|
+
'terminate',
|
|
502
|
+
'typo',
|
|
503
|
+
]);
|
|
504
|
+
|
|
505
|
+
/** `macintosh_vndr/us(alt)` → `us`: the file, without directory or variant. */
|
|
506
|
+
const fileOf = (token) => token.replace(/\(.*$/, '').replace(/^.*\//, '');
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The layout codes a keymap was compiled from — `['us', 'ru']`, what X11
|
|
510
|
+
* reads out of `_XKB_RULES_NAMES` and what a Wayland client has nothing
|
|
511
|
+
* like. Pure, and exported for the test.
|
|
512
|
+
*
|
|
513
|
+
* The one place a compositor's keymap still says is the **name** of its
|
|
514
|
+
* `xkb_symbols` section. libxkbcommon names it after the include string the
|
|
515
|
+
* rules built — `pc+us+ru:2+inet(evdev)+group(alt_shift_toggle)` — with both
|
|
516
|
+
* `+` and `:` written as `_`:
|
|
517
|
+
*
|
|
518
|
+
* xkb_symbols "pc_us_ru_2_inet(evdev)_group(alt_shift_toggle)" {
|
|
519
|
+
* name[1]="English (US)";
|
|
520
|
+
* name[2]="Russian";
|
|
521
|
+
*
|
|
522
|
+
* The rules put the model's file first, then the layouts in order — each one
|
|
523
|
+
* after the first followed by its group number — then the model's extras and
|
|
524
|
+
* the options. So group N is the file right before the bare `N`, group 1 is
|
|
525
|
+
* the file right before group 2, and with a single group it is the file
|
|
526
|
+
* before the first extra. `name[N]` is how many groups there are, and an
|
|
527
|
+
* answer that cannot account for every one of them is no answer: the list
|
|
528
|
+
* comes back empty, which is what "not known" means in src/keyboardstate.js.
|
|
529
|
+
*
|
|
530
|
+
* The `name[N]` strings are not the answer themselves: they are
|
|
531
|
+
* descriptions, `"Russian"`, where an indicator wants `ru` — the same reason
|
|
532
|
+
* the X11 path reads the rules rather than XKB's group names.
|
|
533
|
+
*/
|
|
534
|
+
export function layoutsFromKeymap(text) {
|
|
535
|
+
if (typeof text !== 'string') return [];
|
|
536
|
+
const at = text.search(/xkb_symbols\s+"/);
|
|
537
|
+
if (at < 0) return [];
|
|
538
|
+
const section = text.slice(at);
|
|
539
|
+
const name = /^xkb_symbols\s+"([^"]*)"/.exec(section)?.[1];
|
|
540
|
+
let groups = 0;
|
|
541
|
+
for (const m of section.matchAll(/\bname\[(?:group)?(\d+)\]\s*=/gi)) {
|
|
542
|
+
groups = Math.max(groups, Number(m[1]));
|
|
543
|
+
}
|
|
544
|
+
if (!name || !groups || groups > 4) return [];
|
|
545
|
+
|
|
546
|
+
const tokens = splitIncludes(name);
|
|
547
|
+
const layouts = [];
|
|
548
|
+
if (groups === 1) {
|
|
549
|
+
let i = tokens.findIndex((t, n) => n > 0 && NOT_LAYOUTS.has(fileOf(t)));
|
|
550
|
+
if (i < 0) i = tokens.length;
|
|
551
|
+
if (i < 2) return [];
|
|
552
|
+
layouts.push(tokens[i - 1]);
|
|
553
|
+
} else {
|
|
554
|
+
let prev = -1;
|
|
555
|
+
for (let g = 2; g <= groups; g++) {
|
|
556
|
+
const i = tokens.indexOf(String(g), prev + 1);
|
|
557
|
+
if (i < 2) return [];
|
|
558
|
+
if (g === 2) layouts.push(tokens[i - 2]);
|
|
559
|
+
layouts.push(tokens[i - 1]);
|
|
560
|
+
prev = i;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const codes = layouts.map(fileOf);
|
|
564
|
+
if (codes.some((c) => !/^[a-z][a-z0-9-]*$/i.test(c) || NOT_LAYOUTS.has(c))) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
// `empty` is what a keymap with no layout to name is built from; reporting
|
|
568
|
+
// it would put "EMPTY" in an indicator (the X11 path drops it too)
|
|
569
|
+
return codes.every((c) => c === 'empty') ? [] : codes;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* `pc_us(dvorak)_ru(phonetic_winkeys)_2` → `['pc', 'us(dvorak)',
|
|
574
|
+
* 'ru(phonetic_winkeys)', '2']`. Split where a `_`, `+` or `:` stands outside
|
|
575
|
+
* parentheses — the raw include string spells them that way, and a
|
|
576
|
+
* variant's own underscores are inside them. Two file names have an
|
|
577
|
+
* underscore of their own, the `*_vndr/` directories and `srvr_ctrl`, and are
|
|
578
|
+
* kept whole.
|
|
579
|
+
*/
|
|
580
|
+
function splitIncludes(name) {
|
|
581
|
+
const guarded = name
|
|
582
|
+
.replace(/([a-z0-9]+)_vndr\//gi, '$1\u0001vndr/')
|
|
583
|
+
.replace(/srvr_ctrl/g, 'srvr\u0001ctrl');
|
|
584
|
+
const out = [];
|
|
585
|
+
let depth = 0;
|
|
586
|
+
let cur = '';
|
|
587
|
+
for (const ch of guarded) {
|
|
588
|
+
if (ch === '(') depth++;
|
|
589
|
+
else if (ch === ')') depth = Math.max(0, depth - 1);
|
|
590
|
+
if (depth === 0 && (ch === '_' || ch === '+' || ch === ':')) {
|
|
591
|
+
if (cur) out.push(cur);
|
|
592
|
+
cur = '';
|
|
593
|
+
} else {
|
|
594
|
+
cur += ch;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (cur) out.push(cur);
|
|
598
|
+
return out.map((t) => t.replaceAll('\u0001', '_'));
|
|
599
|
+
}
|
|
600
|
+
|
|
464
601
|
/**
|
|
465
602
|
* `XkbKSIsKeypad`: the one run of keysyms the keypad produces, `KP_Space`
|
|
466
603
|
* through `KP_Equal`. Every keypad key is in it on both of its levels — the
|
package/src/win32/a11y.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// property reads — so answering from JS would pace the screen reader by the
|
|
8
8
|
// application's busiest moment. The bridge therefore *pushes*, and
|
|
9
9
|
// `windows/src/uia.cc` answers from its copy. The reasoning is
|
|
10
|
-
//
|
|
10
|
+
// design/windows.md §"Accessibility: UI Automation"; what matters here is the
|
|
11
11
|
// consequence: **this file's job is a diff, not an answer.**
|
|
12
12
|
//
|
|
13
13
|
// Everything it pushes comes from `src/a11y.js`, which is pure functions over
|
package/src/win32/app.js
CHANGED
|
@@ -362,7 +362,7 @@ export class Win32App {
|
|
|
362
362
|
// `embeddable` is "created, not shown; somebody else will place me", and
|
|
363
363
|
// on this backend that cannot be a window: a composition target stops
|
|
364
364
|
// presenting the moment its window becomes a child, so an HWND is not
|
|
365
|
-
// something another process can take (
|
|
365
|
+
// something another process can take (design/windows-embedding.md). What a
|
|
366
366
|
// host can take is the **buffer**, so an embeddable window here is the
|
|
367
367
|
// same shared surface a `<Frame>` pane draws into — whether this process
|
|
368
368
|
// was forked as a pane or is a guest somebody else started.
|
|
@@ -825,7 +825,7 @@ export class Win32App {
|
|
|
825
825
|
break;
|
|
826
826
|
}
|
|
827
827
|
case 'dpichanged':
|
|
828
|
-
// The renderer cannot re-scale a live window yet (
|
|
828
|
+
// The renderer cannot re-scale a live window yet (design/windows.md
|
|
829
829
|
// §Layout, open question 8), so this is recorded and not acted on.
|
|
830
830
|
wnd.emit('statechange', { scale: event.a / 96 });
|
|
831
831
|
break;
|
package/src/win32/fonts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The Windows text engine: DirectWrite behind the same `app.fonts` contract
|
|
2
2
|
// ntk's FontManager answers on X11 and CoreText answers on macOS. The renderer
|
|
3
|
-
// touches exactly this surface (
|
|
3
|
+
// touches exactly this surface (design/windows.md §"Text"):
|
|
4
4
|
//
|
|
5
5
|
// fonts.layout(spans, base, { maxWidth, align, lineHeight, maxLines,
|
|
6
6
|
// overflow, direction })
|