react-x11 2.2.1 → 2.3.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.
@@ -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
+ }