react-x11 2.2.1 → 2.3.1
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 +5 -3
- package/package.json +12 -5
- package/src/Reconciler.js +76 -11
- package/src/anchor.js +15 -5
- package/src/appcontext.js +27 -7
- package/src/cocoa/app.js +557 -0
- package/src/cocoa/bezels.js +161 -0
- package/src/cocoa/context2d.js +679 -0
- package/src/cocoa/fonts.js +541 -0
- package/src/cocoa/glarea.js +321 -0
- package/src/cocoa/globalmenu.js +149 -0
- package/src/cocoa/keymap.js +86 -0
- package/src/cocoa/native.js +51 -0
- package/src/cocoa/panehost.js +50 -0
- package/src/cocoa/panewindow.js +236 -0
- package/src/cocoa/presenter.js +837 -0
- package/src/cocoa/window.js +383 -0
- package/src/components/Button.js +68 -0
- package/src/components/Checkbox.js +43 -0
- package/src/components/Radio.js +37 -1
- package/src/components/Select.js +73 -28
- package/src/components/Slider.js +59 -0
- package/src/components/Switch.js +52 -0
- package/src/components/native.js +142 -0
- package/src/decorations.js +21 -24
- package/src/editmenu.js +23 -14
- package/src/events.js +11 -2
- package/src/frame/childmain.js +9 -0
- package/src/frame/index.js +162 -2
- package/src/glnodes.js +43 -0
- package/src/globalmenu.js +11 -3
- package/src/index.d.ts +10 -0
- package/src/node.d.ts +19 -6
- package/src/nodes.js +107 -33
- package/src/paintcache.js +15 -4
- package/src/palette.js +14 -0
- package/src/styles.js +5 -0
- package/src/testing/index.d.ts +8 -0
- package/src/types/elements.d.ts +10 -0
- package/src/types/events.d.ts +5 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// <glarea> on the Cocoa backend: GL into a CALayer, no X server anywhere.
|
|
2
|
+
//
|
|
3
|
+
// The shape mirrors the X11 direct path (ntk's renderingcontext_cgl) with
|
|
4
|
+
// the XQuartz-specific half swapped out: the same x11-dri CGL context and
|
|
5
|
+
// WebGL-shaped `gl` table, but instead of attaching to a window surface the
|
|
6
|
+
// X server exported, frames render into IOSurface-backed framebuffers
|
|
7
|
+
// (x11-dri `createTarget`) and present by handing the IOSurface's process-
|
|
8
|
+
// global id to the area's own sublayer (`setLayerContentsIOSurface`). Two
|
|
9
|
+
// targets alternate, like any swapchain; the WindowServer composites.
|
|
10
|
+
//
|
|
11
|
+
// What GlAreaNode needs from us is the child-"window" contract it already
|
|
12
|
+
// speaks (src/glnodes.js): `createWindow({ parent, … })` answering an
|
|
13
|
+
// object with `getContext('opengl', config)`, `setState(rect)`, `map()`,
|
|
14
|
+
// `destroy()`, `requestAnimationFrame`. On X11 that child is a real X
|
|
15
|
+
// window stacked above the parent's drawing; here it is a sublayer of the
|
|
16
|
+
// window's root layer with a high zPosition — the same "GL sits above the
|
|
17
|
+
// 2D" semantics, by the same mechanism the platform gives us.
|
|
18
|
+
//
|
|
19
|
+
// ## The API ladder
|
|
20
|
+
//
|
|
21
|
+
// `chooseGLConfig(spec)` honours `spec.api` (the `<glarea glx>` prop):
|
|
22
|
+
// `'auto'` and `'gl'` are this file — CGL, OpenGL 4.1 core on Metal, the
|
|
23
|
+
// zero-dependency rung that exists on every Mac. `'gles'` (ANGLE on Metal)
|
|
24
|
+
// and `'webgpu'` are named rungs that answer with what they would take,
|
|
25
|
+
// so the ladder is visible before it is built. Policy decides, the machine
|
|
26
|
+
// answers — the same rule the GL policy follows everywhere else.
|
|
27
|
+
|
|
28
|
+
const RUNGS = ['auto', 'gl'];
|
|
29
|
+
const KNOWN_RUNGS = ['auto', 'gl', 'gles', 'webgpu'];
|
|
30
|
+
|
|
31
|
+
let driPromise = null;
|
|
32
|
+
function loadDri() {
|
|
33
|
+
if (!driPromise) {
|
|
34
|
+
driPromise = import('x11-dri').then((m) => m.default ?? m);
|
|
35
|
+
}
|
|
36
|
+
return driPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The app-wide GL runtime: one CGL context shared by every `<glarea>`, the
|
|
41
|
+
* way the X11 direct backend shares one GPU context per connection. Created
|
|
42
|
+
* lazily by the first `chooseGLConfig` and kept on the app.
|
|
43
|
+
*/
|
|
44
|
+
export class CocoaGLRuntime {
|
|
45
|
+
constructor(dri) {
|
|
46
|
+
this.dri = dri;
|
|
47
|
+
this.gl = dri.gl;
|
|
48
|
+
this.ctx = new dri.apple.Context({
|
|
49
|
+
alphaSize: 8,
|
|
50
|
+
depthSize: 24,
|
|
51
|
+
stencilSize: 8,
|
|
52
|
+
doubleBuffer: false,
|
|
53
|
+
profile: 'core',
|
|
54
|
+
});
|
|
55
|
+
this.ctx.makeCurrent();
|
|
56
|
+
this.glVersion = this.ctx.glVersion;
|
|
57
|
+
// frame pacing: how long a swap closes the canRender gate for. macOS
|
|
58
|
+
// answers directly — XQuartz's RandR never could (x11-dri >= 0.6).
|
|
59
|
+
const hz = dri.apple.refreshRate?.();
|
|
60
|
+
this.frameInterval = hz ? 1000 / hz : 1000 / 60;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
destroy() {
|
|
64
|
+
this.ctx.destroy();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve the app's runtime, throwing the reason when there is none. */
|
|
69
|
+
export async function cocoaGLConfig(app, spec) {
|
|
70
|
+
const mode = app.glPolicy?.mode ?? 'auto';
|
|
71
|
+
if (mode === 'off') {
|
|
72
|
+
const err = new Error(
|
|
73
|
+
"glPolicy is 'off' on this connection, so no GL context is created at all",
|
|
74
|
+
);
|
|
75
|
+
err.code = 'GL_DISABLED';
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
if (mode === 'indirect') {
|
|
79
|
+
const err = new Error(
|
|
80
|
+
"glPolicy is 'indirect', which is GLX — the Cocoa backend has no X " +
|
|
81
|
+
"server to speak it to. Use 'auto' (the default here) or 'direct'.",
|
|
82
|
+
);
|
|
83
|
+
err.code = 'GL_POLICY_INDIRECT';
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
const api = spec?.api ?? 'auto';
|
|
87
|
+
if (!RUNGS.includes(api)) {
|
|
88
|
+
const err = new Error(
|
|
89
|
+
KNOWN_RUNGS.includes(api)
|
|
90
|
+
? `<glarea> api '${api}' is a named rung this backend has not ` +
|
|
91
|
+
`built yet — today's rungs are ${RUNGS.join(', ')} (CGL, OpenGL ` +
|
|
92
|
+
`4.1 core on Metal). 'gles' arrives with vendored ANGLE, ` +
|
|
93
|
+
`'webgpu' with a wgpu bridge.`
|
|
94
|
+
: `<glarea> api '${api}' is not a rung — expected one of ` +
|
|
95
|
+
`${KNOWN_RUNGS.join(', ')}.`,
|
|
96
|
+
);
|
|
97
|
+
err.code = 'GL_API_UNAVAILABLE';
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
await resolveCocoaGLRuntime(app);
|
|
101
|
+
return { backend: 'direct', api: 'gl' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One runtime per app, one probe per app: chooseGLConfig and
|
|
106
|
+
* `app.glCapabilities()` share this promise, so two `<glarea>`s mounting in
|
|
107
|
+
* the same commit cannot race two contexts into existence, and the settled
|
|
108
|
+
* answer lands in `_glCapsResolved` either way — the property
|
|
109
|
+
* `useSupports('shaders')` reads (src/glbackend.js).
|
|
110
|
+
*/
|
|
111
|
+
export function resolveCocoaGLRuntime(app) {
|
|
112
|
+
if (!app._cocoaGLPromise) {
|
|
113
|
+
app._cocoaGLPromise = loadDri()
|
|
114
|
+
.then((dri) => {
|
|
115
|
+
app._cocoaGL = new CocoaGLRuntime(dri);
|
|
116
|
+
app._glCapsResolved = { direct: true };
|
|
117
|
+
return app._cocoaGL;
|
|
118
|
+
})
|
|
119
|
+
.catch((cause) => {
|
|
120
|
+
const err = new Error(
|
|
121
|
+
'<glarea> on the Cocoa backend needs the x11-dri addon (the ' +
|
|
122
|
+
'GPU/GL half); it did not load: ' +
|
|
123
|
+
cause.message,
|
|
124
|
+
);
|
|
125
|
+
err.code = 'GL_NO_ADDON';
|
|
126
|
+
app._glCapsResolved = { direct: false, reason: err };
|
|
127
|
+
throw err;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return app._cocoaGLPromise;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The child-"window" a GlAreaNode owns here: one sublayer of the owning
|
|
135
|
+
* window's root layer, above everything the 2D presenters put there.
|
|
136
|
+
*/
|
|
137
|
+
export class CocoaGLArea {
|
|
138
|
+
constructor(app, options) {
|
|
139
|
+
this.app = app;
|
|
140
|
+
this.parent = options.parent;
|
|
141
|
+
this._native = app._native;
|
|
142
|
+
this.scale = this.parent.scale ?? app.scale ?? 1;
|
|
143
|
+
this.destroyed = false;
|
|
144
|
+
this._reactX11Node = null;
|
|
145
|
+
this.onWheel = options.onWheel ?? null;
|
|
146
|
+
this.layer = this._native.createLayer();
|
|
147
|
+
this._native.addSublayer(this.parent._layer, this.layer);
|
|
148
|
+
this.rect = null;
|
|
149
|
+
this.setState({
|
|
150
|
+
x: options.x ?? 0,
|
|
151
|
+
y: options.y ?? 0,
|
|
152
|
+
width: options.width ?? 1,
|
|
153
|
+
height: options.height ?? 1,
|
|
154
|
+
});
|
|
155
|
+
this._context = null;
|
|
156
|
+
this._listeners = new Map();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Geometry in device px, the unit GlAreaNode's rects are in. */
|
|
160
|
+
setState(rect) {
|
|
161
|
+
if (this.destroyed) return;
|
|
162
|
+
this.rect = rect;
|
|
163
|
+
const s = this.scale;
|
|
164
|
+
this._native.setLayerProps(this.layer, {
|
|
165
|
+
frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
|
|
166
|
+
// above both presenters' content: the surface presenter's contents
|
|
167
|
+
// live on the root layer itself, the layers presenter's visuals top
|
|
168
|
+
// out at paintOrder zPositions — this clears either.
|
|
169
|
+
zPosition: 1e7,
|
|
170
|
+
// GL renders bottom-up and an IOSurface displays row 0 at the top;
|
|
171
|
+
// mirroring the layer is the flip, applied where the compositor is
|
|
172
|
+
// already transforming instead of in anyone's shader.
|
|
173
|
+
transform: { scaleY: -1 },
|
|
174
|
+
hidden: false,
|
|
175
|
+
});
|
|
176
|
+
this._context?._resized(rect.width, rect.height);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
get width() {
|
|
180
|
+
return this.rect?.width ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get height() {
|
|
184
|
+
return this.rect?.height ?? 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
move(x, y) {
|
|
188
|
+
this.setState({ ...this.rect, x, y });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
resize(width, height) {
|
|
192
|
+
this.setState({ ...this.rect, width, height });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
map() {
|
|
196
|
+
if (!this.destroyed) {
|
|
197
|
+
this._native.setLayerProps(this.layer, { hidden: false });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
on(name, fn) {
|
|
202
|
+
let set = this._listeners.get(name);
|
|
203
|
+
if (!set) this._listeners.set(name, (set = new Set()));
|
|
204
|
+
set.add(fn);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
emit(name, ev) {
|
|
208
|
+
for (const fn of this._listeners.get(name) ?? []) fn(ev);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
requestAnimationFrame(cb) {
|
|
212
|
+
return this.parent.requestAnimationFrame(cb);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getContext(kind, config) {
|
|
216
|
+
if (kind !== 'opengl' || this.destroyed) return null;
|
|
217
|
+
if (!this._context) {
|
|
218
|
+
this._context = createGLAreaContext(this.app._cocoaGL, this, config);
|
|
219
|
+
}
|
|
220
|
+
return this._context;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
destroy() {
|
|
224
|
+
if (this.destroyed) return;
|
|
225
|
+
this.destroyed = true;
|
|
226
|
+
this._context?._destroy();
|
|
227
|
+
this._context = null;
|
|
228
|
+
this._native.removeFromSuperlayer(this.layer);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The `gl` a `<glarea>`'s onDraw receives: the shared WebGL-shaped table
|
|
234
|
+
* with this area's swapchain behind it. Prototype-delegates to the table so
|
|
235
|
+
* every entry point is present without copying; the own properties are the
|
|
236
|
+
* per-area contract GlAreaNode drives (`backend`, `ready`, `makeCurrent`,
|
|
237
|
+
* `SwapBuffers`, `canRender`, `onFrameAvailable`).
|
|
238
|
+
*/
|
|
239
|
+
function createGLAreaContext(runtime, area, config) {
|
|
240
|
+
const native = area._native;
|
|
241
|
+
const ctx = Object.create(runtime.gl);
|
|
242
|
+
let front = null;
|
|
243
|
+
let back = null;
|
|
244
|
+
let width = 0;
|
|
245
|
+
let height = 0;
|
|
246
|
+
let gateClosed = false;
|
|
247
|
+
let gateTimer = null;
|
|
248
|
+
let destroyed = false;
|
|
249
|
+
|
|
250
|
+
const ensureTargets = () => {
|
|
251
|
+
const w = Math.max(1, area.rect?.width ?? 1);
|
|
252
|
+
const h = Math.max(1, area.rect?.height ?? 1);
|
|
253
|
+
if (front && width === w && height === h) return;
|
|
254
|
+
front?.destroy();
|
|
255
|
+
back?.destroy();
|
|
256
|
+
front = runtime.ctx.createTarget(w, h);
|
|
257
|
+
back = runtime.ctx.createTarget(w, h);
|
|
258
|
+
width = w;
|
|
259
|
+
height = h;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
ctx.backend = 'direct';
|
|
263
|
+
ctx.config = config;
|
|
264
|
+
ctx.glVersion = runtime.glVersion;
|
|
265
|
+
ctx.ready = Promise.resolve();
|
|
266
|
+
ctx.onFrameAvailable = null;
|
|
267
|
+
|
|
268
|
+
// On the direct backend every buffer may still be held by the display;
|
|
269
|
+
// here the WindowServer reads the front IOSurface while we draw the back
|
|
270
|
+
// one, so the honest gate is one display period per swap — the same
|
|
271
|
+
// timer-reopened gate the XQuartz CGL flavor uses (no backpressure
|
|
272
|
+
// exists on either).
|
|
273
|
+
ctx.canRender = () => !destroyed && !gateClosed;
|
|
274
|
+
|
|
275
|
+
ctx.makeCurrent = () => {
|
|
276
|
+
if (destroyed) return;
|
|
277
|
+
ensureTargets();
|
|
278
|
+
runtime.ctx.bindTarget(back);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// WebGL's "null means the default framebuffer" — and this surface's
|
|
282
|
+
// default is the back target's FBO, not GL's framebuffer zero, which on
|
|
283
|
+
// a swapchain like this is nothing at all. A scene that renders through
|
|
284
|
+
// its own FBO and unbinds at the end (the SSAA pattern) lands back here.
|
|
285
|
+
ctx.bindFramebuffer = (target, fb) => {
|
|
286
|
+
runtime.gl.bindFramebuffer(target, fb == null ? (back?.fbo ?? 0) : fb);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
ctx.SwapBuffers = () => {
|
|
290
|
+
if (destroyed || !back) return;
|
|
291
|
+
runtime.gl.flush();
|
|
292
|
+
native.setLayerContentsIOSurface(area.layer, back.iosurfaceId);
|
|
293
|
+
const shown = back;
|
|
294
|
+
back = front;
|
|
295
|
+
front = shown;
|
|
296
|
+
gateClosed = true;
|
|
297
|
+
gateTimer = setTimeout(() => {
|
|
298
|
+
gateTimer = null;
|
|
299
|
+
gateClosed = false;
|
|
300
|
+
ctx.onFrameAvailable?.();
|
|
301
|
+
}, runtime.frameInterval);
|
|
302
|
+
// the timer must not hold the process open for an idle scene
|
|
303
|
+
gateTimer.unref?.();
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
ctx._resized = () => {
|
|
307
|
+
// targets are rebuilt lazily on the next makeCurrent, so a resize
|
|
308
|
+
// storm costs two allocations per drawn frame, not per notify
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
ctx._destroy = () => {
|
|
312
|
+
if (destroyed) return;
|
|
313
|
+
destroyed = true;
|
|
314
|
+
if (gateTimer) clearTimeout(gateTimer);
|
|
315
|
+
front?.destroy();
|
|
316
|
+
back?.destroy();
|
|
317
|
+
front = back = null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return ctx;
|
|
321
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The macOS menu bar as a global-menu export (docs/macos.md §Menus).
|
|
2
|
+
//
|
|
3
|
+
// The Linux global menu was built for exactly this moment: the item
|
|
4
|
+
// vocabulary is data, and the pure snapshot/IdAllocator machinery in
|
|
5
|
+
// dbusmenu.js produces stable ids with no D-Bus in sight. This adapter
|
|
6
|
+
// consumes precisely that — same owner shape as GlobalMenuExport
|
|
7
|
+
// (start/stop/update/onChange), so `useGlobalMenu` swaps the transport and
|
|
8
|
+
// nothing above it can tell. Where the D-Bus export must wait for a
|
|
9
|
+
// registrar to answer, the menu bar is a platform constant: `onChange(true)`
|
|
10
|
+
// fires as soon as the menu is installed, and `MenuBar`'s drawn fallback
|
|
11
|
+
// never renders here.
|
|
12
|
+
//
|
|
13
|
+
// Activation comes back as a backend event carrying the item's id —
|
|
14
|
+
// AppKit's menu tracking is a modal loop, and those already deliver into JS
|
|
15
|
+
// on this backend (live resize does) — routed by the app to the active
|
|
16
|
+
// export, which runs the item's own onSelect: the same contract the
|
|
17
|
+
// registrar path has.
|
|
18
|
+
import { IdAllocator, ROOT_ID, snapshot } from '../dbusmenu.js';
|
|
19
|
+
|
|
20
|
+
// The synthesized app menu's Quit item. Negative on purpose: IdAllocator
|
|
21
|
+
// only counts up from ROOT_ID, so no real item can collide with it.
|
|
22
|
+
const QUIT_ID = -2;
|
|
23
|
+
|
|
24
|
+
// NSEventModifierFlags. No Control here on purpose: a cross-platform
|
|
25
|
+
// shortcut's primary modifier maps to Command, not the Mac's own ⌃.
|
|
26
|
+
const FLAG_SHIFT = 1 << 17;
|
|
27
|
+
const FLAG_OPTION = 1 << 19;
|
|
28
|
+
const FLAG_COMMAND = 1 << 20;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One dbusmenu chord — `[['Control', 'z']]` — as NSMenu key equivalents.
|
|
32
|
+
* `Control` maps to Command deliberately: a cross-platform app writes its
|
|
33
|
+
* shortcuts in the primary modifier, and on macOS the primary modifier is
|
|
34
|
+
* ⌘ — a literal mapping would put every accelerator on a modifier no Mac
|
|
35
|
+
* user presses. Multi-chord sequences and non-character keys have no
|
|
36
|
+
* NSMenu spelling and are dropped (the shortcut still works — the app's
|
|
37
|
+
* own key handling is untouched; only the menu's hint goes unshown).
|
|
38
|
+
*/
|
|
39
|
+
function keyEquivalent(shortcut) {
|
|
40
|
+
const chord = Array.isArray(shortcut) ? shortcut[0] : null;
|
|
41
|
+
if (!Array.isArray(chord) || shortcut.length !== 1) return null;
|
|
42
|
+
let modifiers = 0;
|
|
43
|
+
let key = null;
|
|
44
|
+
for (const part of chord) {
|
|
45
|
+
if (part === 'Control' || part === 'Super') modifiers |= FLAG_COMMAND;
|
|
46
|
+
else if (part === 'Shift') modifiers |= FLAG_SHIFT;
|
|
47
|
+
else if (part === 'Alt') modifiers |= FLAG_OPTION;
|
|
48
|
+
else key = part;
|
|
49
|
+
}
|
|
50
|
+
if (typeof key !== 'string' || [...key].length !== 1) return null;
|
|
51
|
+
return { key: key.toLowerCase(), modifiers: modifiers || FLAG_COMMAND };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class CocoaGlobalMenuExport {
|
|
55
|
+
constructor(app, { getMenus, onSelect, onAboutToShow, target, onChange }) {
|
|
56
|
+
this.app = app;
|
|
57
|
+
this.getMenus = getMenus;
|
|
58
|
+
this.onSelect = onSelect;
|
|
59
|
+
this.onAboutToShow = onAboutToShow;
|
|
60
|
+
this.target = target;
|
|
61
|
+
this.onChange = onChange;
|
|
62
|
+
this.alloc = new IdAllocator();
|
|
63
|
+
this.nodes = null;
|
|
64
|
+
this.exported = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async start() {
|
|
68
|
+
this.app._registerGlobalMenu(this);
|
|
69
|
+
this.update(this.getMenus?.() ?? []);
|
|
70
|
+
this.exported = true;
|
|
71
|
+
this.onChange?.(true);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async stop() {
|
|
75
|
+
this.exported = false;
|
|
76
|
+
this.app._unregisterGlobalMenu(this);
|
|
77
|
+
this.onChange?.(false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
update(menus) {
|
|
81
|
+
this.nodes = snapshot(menus ?? [], this.alloc);
|
|
82
|
+
if (this.app._activeGlobalMenu === this) this._install();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_install() {
|
|
86
|
+
this.app._native.setMainMenu(this._spec());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The snapshot as the bridge's menu spec. The app menu is synthesized in
|
|
91
|
+
* front — macOS requires the first menu and shows the process name on it
|
|
92
|
+
* regardless of title — carrying Quit, which routes back through
|
|
93
|
+
* `activate` like every real item.
|
|
94
|
+
*/
|
|
95
|
+
_spec() {
|
|
96
|
+
const itemsOf = (ids) =>
|
|
97
|
+
ids.map((id) => {
|
|
98
|
+
const node = this.nodes.get(id);
|
|
99
|
+
const props = node.props;
|
|
100
|
+
const out = { id };
|
|
101
|
+
if (props.type === 'separator') {
|
|
102
|
+
out.separator = true;
|
|
103
|
+
if (props.visible === false) out.hidden = true;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
out.title = String(props.label ?? '');
|
|
107
|
+
if (props.enabled === false) out.enabled = false;
|
|
108
|
+
if (props.visible === false) out.hidden = true;
|
|
109
|
+
if (props['toggle-state'] === 1) out.checked = true;
|
|
110
|
+
// the serialisable icon pair (menuitem.js): the name is read in
|
|
111
|
+
// the platform's icon theme — SF Symbols here, freedesktop on a
|
|
112
|
+
// Linux panel — and the bytes are the literal-pixel fallback
|
|
113
|
+
if (props['icon-name']) out.iconName = props['icon-name'];
|
|
114
|
+
if (props['icon-data']) out.iconData = props['icon-data'];
|
|
115
|
+
const key = keyEquivalent(props.shortcut);
|
|
116
|
+
if (key) {
|
|
117
|
+
out.key = key.key;
|
|
118
|
+
out.modifiers = key.modifiers;
|
|
119
|
+
}
|
|
120
|
+
if (node.childIds.length) out.items = itemsOf(node.childIds);
|
|
121
|
+
return out;
|
|
122
|
+
});
|
|
123
|
+
const root = this.nodes.get(ROOT_ID);
|
|
124
|
+
const menus = itemsOf(root.childIds).map((menu) => ({
|
|
125
|
+
title: menu.title ?? '',
|
|
126
|
+
items: menu.items ?? [],
|
|
127
|
+
}));
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
title: 'App',
|
|
131
|
+
items: [{ id: QUIT_ID, title: 'Quit', key: 'q' }],
|
|
132
|
+
},
|
|
133
|
+
...menus,
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A backend menu-activate event landed on this export. */
|
|
138
|
+
activate(id) {
|
|
139
|
+
if (id === QUIT_ID) {
|
|
140
|
+
// the same route the close button takes: the app decides what
|
|
141
|
+
// closing means, exactly as it would for the red light
|
|
142
|
+
const wnd = this.target?.window ?? [...this.app._windows.values()][0];
|
|
143
|
+
wnd?.emit('close', { preventDefault() {} });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const item = this.nodes?.get(id)?.item;
|
|
147
|
+
if (item) this.onSelect?.(item);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// macOS key events -> the renderer's keysym vocabulary (src/keysyms.js).
|
|
2
|
+
//
|
|
3
|
+
// The rule mirrors X's own: Latin-1 keysyms are their code points, everything
|
|
4
|
+
// else printable is 0x01000000 + code point, and the editing/navigation keys
|
|
5
|
+
// have fixed keysyms looked up here by kVK_* virtual key code. NSEvent's
|
|
6
|
+
// three character views map onto the three keysym roles events.js reads:
|
|
7
|
+
//
|
|
8
|
+
// charsBase (modifiers stripped) -> baseKeysym — what chords match
|
|
9
|
+
// charsShifted (only Shift applied) -> keysym / codepoint — what was typed
|
|
10
|
+
//
|
|
11
|
+
// Function and editing keys type Unicode private-use characters (U+F700…),
|
|
12
|
+
// which must never leak out as code points; the kVK table wins over the
|
|
13
|
+
// character rule for exactly those.
|
|
14
|
+
import { keysymOf, MOD } from '../keysyms.js';
|
|
15
|
+
|
|
16
|
+
// kVK_* virtual key codes -> X keysyms, for the keys whose characters are
|
|
17
|
+
// private-use or nothing at all.
|
|
18
|
+
const VK_KEYSYMS = new Map([
|
|
19
|
+
[36, 0xff0d], // Return
|
|
20
|
+
[48, 0xff09], // Tab
|
|
21
|
+
// NOT space (kVK 49): space TYPES a character, so it goes through the
|
|
22
|
+
// character rule below and keeps its code point — a table entry here made
|
|
23
|
+
// `codepoint` undefined and a pressed spacebar inserted nothing.
|
|
24
|
+
[51, 0xff08], // Delete (backspace)
|
|
25
|
+
[53, 0xff1b], // Escape
|
|
26
|
+
[76, 0xff8d], // KP_Enter
|
|
27
|
+
[115, 0xff50], // Home
|
|
28
|
+
[116, 0xff55], // Page Up
|
|
29
|
+
[117, 0xffff], // Forward Delete
|
|
30
|
+
[119, 0xff57], // End
|
|
31
|
+
[121, 0xff56], // Page Down
|
|
32
|
+
[123, 0xff51], // Left
|
|
33
|
+
[124, 0xff53], // Right
|
|
34
|
+
[125, 0xff54], // Down
|
|
35
|
+
[126, 0xff52], // Up
|
|
36
|
+
[114, 0xff63], // Help -> Insert
|
|
37
|
+
[122, 0xffbe], // F1
|
|
38
|
+
[120, 0xffbf], // F2
|
|
39
|
+
[99, 0xffc0], // F3
|
|
40
|
+
[118, 0xffc1], // F4
|
|
41
|
+
[96, 0xffc2], // F5
|
|
42
|
+
[97, 0xffc3], // F6
|
|
43
|
+
[98, 0xffc4], // F7
|
|
44
|
+
[100, 0xffc5], // F8
|
|
45
|
+
[101, 0xffc6], // F9
|
|
46
|
+
[109, 0xffc7], // F10
|
|
47
|
+
[103, 0xffc8], // F11
|
|
48
|
+
[111, 0xffc9], // F12
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const PRIVATE_USE = (cp) => cp >= 0xf700 && cp <= 0xf8ff;
|
|
52
|
+
|
|
53
|
+
function keysymFromChars(chars) {
|
|
54
|
+
if (!chars) return 0;
|
|
55
|
+
const cp = chars.codePointAt(0);
|
|
56
|
+
if (cp == null || cp < 0x20 || PRIVATE_USE(cp)) return 0;
|
|
57
|
+
return keysymOf(String.fromCodePoint(cp));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The three key facts events.js reads off a native key event, from the raw
|
|
62
|
+
* @windowkit/appkit payload.
|
|
63
|
+
*/
|
|
64
|
+
export function decodeKey(ev) {
|
|
65
|
+
const fixed = VK_KEYSYMS.get(ev.keyCode);
|
|
66
|
+
if (fixed) {
|
|
67
|
+
return { keysym: fixed, baseKeysym: fixed, codepoint: undefined };
|
|
68
|
+
}
|
|
69
|
+
const keysym = keysymFromChars(ev.charsShifted);
|
|
70
|
+
const baseKeysym = keysymFromChars(ev.charsBase) || keysym;
|
|
71
|
+
const cp = ev.charsShifted?.codePointAt(0);
|
|
72
|
+
const codepoint =
|
|
73
|
+
cp != null && cp >= 0x20 && !PRIVATE_USE(cp) ? cp : undefined;
|
|
74
|
+
return { keysym: keysym || baseKeysym, baseKeysym, codepoint };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** AppKit modifier booleans -> the X-style state mask events carry. */
|
|
78
|
+
export function modifierMask(ev) {
|
|
79
|
+
let mask = 0;
|
|
80
|
+
if (ev.shift) mask |= MOD.Shift;
|
|
81
|
+
if (ev.capsLock) mask |= MOD.Lock;
|
|
82
|
+
if (ev.control) mask |= MOD.Control;
|
|
83
|
+
if (ev.option) mask |= MOD.Alt; // ⌥ is Alt (Mod1), the DOM's own mapping
|
|
84
|
+
if (ev.command) mask |= MOD.Super; // ⌘ is Super (Mod4) -> ev.metaKey
|
|
85
|
+
return mask;
|
|
86
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The bridge to @windowkit/appkit — the macOS Cocoa/Core Animation
|
|
2
|
+
// backend's native half. Resolved lazily so that importing react-x11 on
|
|
3
|
+
// Linux (or on a mac without the addon installed) costs nothing;
|
|
4
|
+
// `createRoot` only walks in here once the backend decision has landed on
|
|
5
|
+
// 'cocoa'. That laziness is what lets the package be an *optional*
|
|
6
|
+
// dependency: on Linux npm skips it (its `os` field is darwin-only) and
|
|
7
|
+
// nothing here ever asks for it.
|
|
8
|
+
//
|
|
9
|
+
// Resolution order: `REACT_X11_CALAYERS_PATH` (a checkout, for
|
|
10
|
+
// development), then the installed `@windowkit/appkit` package. The addon
|
|
11
|
+
// is CommonJS + a .node binary, so `createRequire` is the honest loader.
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
const PACKAGE = '@windowkit/appkit';
|
|
17
|
+
|
|
18
|
+
let cached = null;
|
|
19
|
+
|
|
20
|
+
export function loadNative() {
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
if (process.platform !== 'darwin') {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"react-x11: the 'cocoa' backend is macOS-only. On this platform use " +
|
|
25
|
+
'the X11 backend (the default when DISPLAY is set), or pass ' +
|
|
26
|
+
"createRoot({ backend: 'x11' }).",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const tried = [];
|
|
30
|
+
const path = process.env.REACT_X11_CALAYERS_PATH;
|
|
31
|
+
for (const spec of [path, PACKAGE].filter(Boolean)) {
|
|
32
|
+
try {
|
|
33
|
+
const mod = require(spec);
|
|
34
|
+
// the package's index.js exports the raw addon as `native`; a direct
|
|
35
|
+
// path to a built checkout may be the addon itself
|
|
36
|
+
cached = mod.native ?? mod;
|
|
37
|
+
return cached;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
tried.push(` ${spec}: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error(
|
|
43
|
+
`react-x11: the cocoa backend needs the ${PACKAGE} native bridge ` +
|
|
44
|
+
'and none could be loaded:\n' +
|
|
45
|
+
tried.join('\n') +
|
|
46
|
+
`\nInstall it with \`npm install ${PACKAGE}\` (macOS, needs the ` +
|
|
47
|
+
'Xcode command-line tools), or point REACT_X11_CALAYERS_PATH at a ' +
|
|
48
|
+
'built checkout. To use X11 instead, set DISPLAY and pass ' +
|
|
49
|
+
"createRoot({ backend: 'x11' }).",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The host's half of a Cocoa frame pane: one sublayer over the window
|
|
2
|
+
// content (the same stacking a glarea and an X11 foreign window get), whose
|
|
3
|
+
// contents are whatever IOSurface the pane process last presented. The pane
|
|
4
|
+
// owns its buffers and its drawing; this side owns the layer, the layout
|
|
5
|
+
// and the input — CPU offloading, not isolation (docs/frame.md).
|
|
6
|
+
export class CocoaPaneHost {
|
|
7
|
+
constructor(app, wnd) {
|
|
8
|
+
this.app = app;
|
|
9
|
+
this.wnd = wnd;
|
|
10
|
+
this._native = app._native;
|
|
11
|
+
this.layer = this._native.createLayer();
|
|
12
|
+
this._native.addSublayer(wnd._layer, this.layer);
|
|
13
|
+
this.destroyed = false;
|
|
14
|
+
this._rect = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Geometry in device px, the node's abs — points at the layer. */
|
|
18
|
+
setRect(rect) {
|
|
19
|
+
if (this.destroyed) return;
|
|
20
|
+
const s = this.wnd.scale;
|
|
21
|
+
const prev = this._rect;
|
|
22
|
+
if (
|
|
23
|
+
prev &&
|
|
24
|
+
prev.x === rect.x &&
|
|
25
|
+
prev.y === rect.y &&
|
|
26
|
+
prev.width === rect.width &&
|
|
27
|
+
prev.height === rect.height
|
|
28
|
+
) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this._rect = { ...rect };
|
|
32
|
+
this._native.setLayerProps(this.layer, {
|
|
33
|
+
frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
|
|
34
|
+
zPosition: 1e7,
|
|
35
|
+
hidden: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A pane-present landed: scan out of the named shared surface. */
|
|
40
|
+
present(iosurfaceId) {
|
|
41
|
+
if (this.destroyed) return;
|
|
42
|
+
this._native.setLayerContentsIOSurface(this.layer, iosurfaceId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
destroy() {
|
|
46
|
+
if (this.destroyed) return;
|
|
47
|
+
this.destroyed = true;
|
|
48
|
+
this._native.removeFromSuperlayer(this.layer);
|
|
49
|
+
}
|
|
50
|
+
}
|