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 CHANGED
@@ -79,12 +79,14 @@ three:
79
79
  - **three platforms.** X11 only. macOS means XQuartz — a separate install, a
80
80
  non-native look and no menu bar integration. Windows is out; if you need
81
81
  Windows, use Electron or Tauri.
82
- - **native Wayland.** There is no Wayland backend and there is not going to
83
- be one; that would be a different renderer, not a flag. Ordinary
82
+ - **native Wayland — today.** There is no Wayland backend yet. Ordinary
84
83
  application windows work fine on a Wayland desktop through Xwayland, which
85
84
  is not going away — but the desktop-shell half of X11 (panel struts,
86
85
  global key grabs, screen capture, and the window-manager example below)
87
- needs a real X session.
86
+ needs a real X session. A native backend is researched and planned as a
87
+ **second target beside X11, not a migration**:
88
+ [docs/wayland.md](docs/wayland.md) is the RFC, from the fd transport up
89
+ to what the rendering would ride on.
88
90
  - **reusing web components.** There is no DOM. Your MUI, your Tailwind and
89
91
  your `recharts` do not come with you; the state, data-fetching, validation
90
92
  and math libraries mostly do. [docs/ecosystem.md](docs/ecosystem.md) says
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -34,6 +34,7 @@
34
34
  "xi2:probe": "node scripts/xi2-probe.mjs",
35
35
  "globalmenu:host": "node scripts/globalmenu-host.mjs",
36
36
  "devtools:proxy": "node scripts/devtools-proxy.mjs",
37
+ "examples:configurator": "tsx examples/configurator/index.jsx",
37
38
  "examples:fonts": "tsx examples/fonts.jsx",
38
39
  "examples:form": "tsx examples/form.jsx",
39
40
  "examples:rules": "tsx examples/rules.jsx",
@@ -59,12 +60,13 @@
59
60
  "stress:check": "tsx scripts/check-stress.jsx",
60
61
  "screenshots": "tsx scripts/screenshots.jsx",
61
62
  "screenshots:framed": "tsx scripts/screenshots-framed.jsx",
62
- "bench": "tsx scripts/bench/protocol.js",
63
+ "bench": "node --expose-gc --import tsx scripts/bench/protocol.js",
63
64
  "bench:pixels": "tsx scripts/bench/pixels.js",
64
65
  "bench:frames": "tsx scripts/bench/frames.js",
65
66
  "docs:dev": "npm --prefix website start",
66
67
  "docs:build": "npm --prefix website run build",
67
- "docs:test": "npm --prefix website test"
68
+ "docs:test": "npm --prefix website test",
69
+ "bench:presenters": "node --import tsx scripts/bench/presenters.js"
68
70
  },
69
71
  "repository": {
70
72
  "type": "git",
@@ -83,12 +85,14 @@
83
85
  "node": ">=20.19"
84
86
  },
85
87
  "dependencies": {
86
- "ntk": "^8.5.0",
88
+ "ntk": "^8.7.0",
87
89
  "react-reconciler": "^0.33.0",
88
90
  "yoga-layout": "^3.2.1"
89
91
  },
90
92
  "optionalDependencies": {
91
- "dbus-native": "^0.15.1"
93
+ "@windowkit/appkit": "^0.1.0",
94
+ "dbus-native": "^0.15.1",
95
+ "x11-dri": "^0.7.0"
92
96
  },
93
97
  "peerDependencies": {
94
98
  "@babel/core": "^8.0.0",
@@ -118,6 +122,9 @@
118
122
  "@babel/core": "^8.0.1",
119
123
  "@babel/plugin-transform-react-jsx": "^8.0.1",
120
124
  "@eslint/js": "^9.32.0",
125
+ "@fontsource/instrument-serif": "^5.3.0",
126
+ "@fontsource/inter": "^5.3.0",
127
+ "@fontsource/jetbrains-mono": "^5.3.0",
121
128
  "@types/react": "^19.2.17",
122
129
  "eslint": "^9.32.0",
123
130
  "eslint-plugin-react": "^7.37.5",
package/src/Reconciler.js CHANGED
@@ -571,6 +571,36 @@ async function connect(options) {
571
571
  const isNtkApp = (v) =>
572
572
  Boolean(v) && typeof v.createWindow === 'function' && typeof v.X === 'object';
573
573
 
574
+ /**
575
+ * Which display system a root that opens its own connection talks to.
576
+ *
577
+ * `'auto'` — the default — is one rule: macOS gets the native Core
578
+ * Animation backend, everything else gets X11 via `$DISPLAY`. (More rungs —
579
+ * Wayland — join this ladder, not a new one; docs/wayland.md.) On a mac
580
+ * without the `@windowkit/appkit` bridge installed, auto falls back to X11 so
581
+ * an XQuartz setup keeps working; asking for `'cocoa'` by name means the
582
+ * bridge is required, and its absence is an error that says how to fix it.
583
+ * `REACT_X11_BACKEND` overrides for A/B runs without touching code.
584
+ */
585
+ function resolveBackend(options) {
586
+ const asked = options.backend ?? process.env.REACT_X11_BACKEND ?? 'auto';
587
+ if (asked === 'x11' || asked === 'cocoa') return asked;
588
+ if (asked !== 'auto') {
589
+ throw new Error(
590
+ `react-x11: unknown backend ${JSON.stringify(asked)} — expected ` +
591
+ "'x11', 'cocoa' or 'auto'.",
592
+ );
593
+ }
594
+ // Naming an X endpoint is choosing X11: a `display` or a `stream` (the
595
+ // tests' in-process server, a tunnel) would be silently ignored by any
596
+ // other backend, and an ignored connection option is worse than either
597
+ // answer.
598
+ if (options.display !== undefined || options.stream !== undefined) {
599
+ return 'x11';
600
+ }
601
+ return process.platform === 'darwin' ? 'cocoa' : 'x11';
602
+ }
603
+
574
604
  // What a root that opens its own connection forwards to ntk. `stream` is
575
605
  // how you reach a server that is not on the other end of $DISPLAY — an
576
606
  // in-process one, a tunnel — and is what the tests connect through.
@@ -652,11 +682,24 @@ export async function createRoot(options = {}) {
652
682
  }
653
683
  const { app: borrowed, onDisconnect, ...rest } = options;
654
684
  const owned = borrowed === undefined;
685
+ // Which display system this root speaks — resolved before anything runs,
686
+ // because the desktop-integration default below depends on it. A borrowed
687
+ // app already IS a backend, so the option only steers owned connections.
688
+ const backend = owned ? resolveBackend(rest) : null;
655
689
  // Before anything starts, for two reasons: a bad `desktop` shape must throw
656
690
  // with nothing in flight, like the check above it — and `startA11y()` below
657
691
  // reads this policy, so it has to be settled before the first await, not
658
692
  // after (src/desktopintegration.js).
659
- setDesktopIntegration(rest.desktop);
693
+ //
694
+ // The cocoa backend's desktop is not freedesktop's: the AT-SPI bridge and
695
+ // the D-Bus global menu have nothing to register against there, so they
696
+ // default off (their macOS replacements are their own workstream —
697
+ // docs/macos.md). The appearance ladder stays on: its macOS rung is the
698
+ // one that answers. An explicit `desktop` option still wins.
699
+ setDesktopIntegration(
700
+ rest.desktop ??
701
+ (backend === 'cocoa' ? { a11y: false, globalMenu: false } : undefined),
702
+ );
660
703
  // The connection is started first, and the order is the point rather than a
661
704
  // detail. `loadLayout()` is only nominally asynchronous: instantiating the
662
705
  // engine blocks the event loop for 15-50 ms before it returns its promise
@@ -669,16 +712,38 @@ export async function createRoot(options = {}) {
669
712
  // synchronous block, and only where the handshake takes longer than it. It
670
713
  // is not measurable on a Unix socket, and it is lost in the noise on a link
671
714
  // slow enough to matter. This is the right order, not a fast one.
672
- const connecting = owned
673
- ? connect(
674
- Object.fromEntries(
675
- CONNECT_OPTIONS.filter((k) => rest[k] !== undefined).map((k) => [
676
- k,
677
- rest[k],
678
- ]),
679
- ),
680
- )
681
- : Promise.resolve(borrowed);
715
+ const connectX11 = () =>
716
+ connect(
717
+ Object.fromEntries(
718
+ CONNECT_OPTIONS.filter((k) => rest[k] !== undefined).map((k) => [
719
+ k,
720
+ rest[k],
721
+ ]),
722
+ ),
723
+ );
724
+ const cocoaAsked =
725
+ rest.backend === 'cocoa' || process.env.REACT_X11_BACKEND === 'cocoa';
726
+ const connecting = !owned
727
+ ? Promise.resolve(borrowed)
728
+ : backend === 'cocoa'
729
+ ? import('./cocoa/app.js')
730
+ .then(({ createCocoaApp }) => createCocoaApp(rest))
731
+ .catch((err) => {
732
+ // Asked for by name, the bridge is required and its absence is
733
+ // the error (it says how to install). Reached by 'auto', a mac
734
+ // without it falls back to X11 so an XQuartz setup keeps
735
+ // working — said once, because a silent fallback would look
736
+ // like the native backend being broken rather than absent.
737
+ if (cocoaAsked) throw err;
738
+ if (process.env.NODE_ENV !== 'production') {
739
+ console.warn(
740
+ 'react-x11: no @windowkit/appkit bridge — falling back to the ' +
741
+ `X11 backend. (${err.message.split('\n')[0]})`,
742
+ );
743
+ }
744
+ return connectX11();
745
+ })
746
+ : connectX11();
682
747
  const layout = loadLayout();
683
748
  const integrations = loadIntegrations(); // null when there is nothing to install
684
749
  const [app] = await Promise.all([connecting, layout, integrations]);
package/src/anchor.js CHANGED
@@ -28,11 +28,20 @@ import { availableArea } from './screens.js';
28
28
  export function screenRect(node) {
29
29
  if (!node?.abs) return null;
30
30
  const origin = windowOrigin(node);
31
+ // Logical pixels, like every rect application code touches: this is a
32
+ // public export, and the rects it is compared against — `anchorRect`'s
33
+ // result, `anchorArea` — answer in logical too. The device-pixel sum
34
+ // (origin + abs) divides once, here; a caller that needs the device rect
35
+ // is placement math and multiplies back by `node.scale` itself. On a 1x
36
+ // display the two units coincide, which is how a mix survived until the
37
+ // first 2x backend ran the tooltip arrow into the wrong half of the
38
+ // bubble.
39
+ const s = node.scale ?? 1;
31
40
  return {
32
- x: origin.x + node.abs.x,
33
- y: origin.y + node.abs.y,
34
- width: node.abs.width,
35
- height: node.abs.height,
41
+ x: (origin.x + node.abs.x) / s,
42
+ y: (origin.y + node.abs.y) / s,
43
+ width: node.abs.width / s,
44
+ height: node.abs.height / s,
36
45
  };
37
46
  }
38
47
 
@@ -136,7 +145,8 @@ export function deviceAnchorArea(node) {
136
145
  const app = node?.app;
137
146
  if (!app) return null;
138
147
  const at = screenRect(node);
139
- return availableArea(app, at ? { x: at.x, y: at.y } : null);
148
+ const s = node.scale ?? 1;
149
+ return availableArea(app, at ? { x: at.x * s, y: at.y * s } : null);
140
150
  }
141
151
 
142
152
  /**
package/src/appcontext.js CHANGED
@@ -57,7 +57,16 @@ export function useApp() {
57
57
  return app;
58
58
  }
59
59
 
60
- const SUPPORTS_FEATURES = new Set(['transparency', 'shaders']);
60
+ const SUPPORTS_FEATURES = new Set([
61
+ 'transparency',
62
+ 'shaders',
63
+ 'nativeControls',
64
+ ]);
65
+
66
+ // 'nativeControls' is a property of the backend, decided before the first
67
+ // render and never changing after — so its subscription has nothing to
68
+ // deliver and its snapshot is a property test.
69
+ const NEVER_CHANGES = () => () => {};
61
70
 
62
71
  /**
63
72
  * Can this **display** do something, as a value a component can branch on?
@@ -105,6 +114,13 @@ const SUPPORTS_FEATURES = new Set(['transparency', 'shaders']);
105
114
  * disagree exactly when the policy did not ask. The capabilities also say
106
115
  * *why* this is false; see docs/gl.md.
107
116
  *
117
+ * `'nativeControls'` is true when this backend renders the platform's own
118
+ * control bezels — today the Cocoa backend, never X11. The widget set
119
+ * already branches on it through the theme's `controls: 'auto'` policy, so
120
+ * this hook is for application code composing its own controls that wants
121
+ * to sit beside native ones. It is a property of the backend and never
122
+ * changes over the app's life.
123
+ *
108
124
  * Unlike `'transparency'`, which comes and goes with the compositor, this
109
125
  * settles once and then holds still: under a policy that could pick direct,
110
126
  * `createRoot()` waits for ntk's probe before handing the app back, so the
@@ -127,18 +143,22 @@ export function useSupports(feature) {
127
143
  // side of it from disagreeing (see watchDirectGL).
128
144
  const subscribe = useCallback(
129
145
  (onChange) =>
130
- feature === 'shaders'
131
- ? watchDirectGL(app, onChange)
132
- : watchCompositing(app, onChange),
146
+ feature === 'nativeControls'
147
+ ? NEVER_CHANGES()
148
+ : feature === 'shaders'
149
+ ? watchDirectGL(app, onChange)
150
+ : watchCompositing(app, onChange),
133
151
  [app, feature],
134
152
  );
135
153
  // a boolean, so the snapshot is stable for a given state — returning the
136
154
  // visual object here would tear on every render
137
155
  const snapshot = useCallback(
138
156
  () =>
139
- feature === 'shaders'
140
- ? hasDirectGL(app)
141
- : compositingActive(app) && Boolean(argbVisual(app)),
157
+ feature === 'nativeControls'
158
+ ? Boolean(app.nativeBezels)
159
+ : feature === 'shaders'
160
+ ? hasDirectGL(app)
161
+ : compositingActive(app) && Boolean(argbVisual(app)),
142
162
  [app, feature],
143
163
  );
144
164
  return useSyncExternalStore(subscribe, snapshot, snapshot);