react-x11 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +4 -3
  2. package/src/scale.js +136 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -49,6 +49,7 @@
49
49
  "examples:foreign": "tsx examples/foreign.jsx",
50
50
  "examples:frame": "tsx examples/frame.jsx",
51
51
  "labs:text-baseline": "tsx examples/labs/text-baseline.jsx",
52
+ "labs:direct-gl": "tsx examples/labs/direct-gl.jsx",
52
53
  "examples:viewer3d": "tsx examples/viewer3d.jsx",
53
54
  "examples:transparent": "tsx examples/transparent.jsx",
54
55
  "examples:windows": "tsx examples/windows.jsx",
@@ -82,7 +83,7 @@
82
83
  "node": ">=20.19"
83
84
  },
84
85
  "dependencies": {
85
- "ntk": "^8.3.1",
86
+ "ntk": "^8.4.0",
86
87
  "react-reconciler": "^0.33.0",
87
88
  "yoga-layout": "^3.2.1"
88
89
  },
@@ -90,10 +91,10 @@
90
91
  "dbus-native": "^0.15.1"
91
92
  },
92
93
  "peerDependencies": {
93
- "react": "^19.0.0",
94
94
  "@babel/core": "^8.0.0",
95
95
  "@babel/plugin-transform-react-jsx": "^8.0.0",
96
96
  "hot-module-replacement": "^4.0.1",
97
+ "react": "^19.0.0",
97
98
  "react-refresh": "^0.18.0"
98
99
  },
99
100
  "peerDependenciesMeta": {
package/src/scale.js CHANGED
@@ -54,6 +54,23 @@
54
54
  // subtly wrong size everywhere.
55
55
  // 6. **1**, the answer X11 shipped with in 1987.
56
56
  //
57
+ // Rungs 4 and 5 read hardware, so they are skipped where the connection is
58
+ // not describing hardware, and both cases are servers people really run:
59
+ //
60
+ // * **XQuartz** composes in macOS *points* and hands X the point space —
61
+ // the retina lid arrives as 1728x1080, already density-normalised, and
62
+ // the window server scales it onto the panel afterwards. Inferring a
63
+ // factor here doubles a size macOS is about to double again. Detected
64
+ // by the `Apple-WM` extension, exempted exactly like `XWAYLAND` in
65
+ // `VIRTUAL_OUTPUT_NAME`: the compositor owns scaling, so we do not.
66
+ // * **A single synthetic output** covering every monitor. XQuartz, Xvfb,
67
+ // VNC servers and Xephyr answer the RandR walk with one output named
68
+ // `default` whose CRTC is the *union* of the desktop; two 2560x1440
69
+ // monitors union to 5120x1440, which rung 5 would read as a retina
70
+ // panel. Xinerama reports those heads separately, so more heads than
71
+ // outputs retires rung 5 — see `isUnionOutput`. Rung 4 survives it:
72
+ // millimetres describe real glass however the pixels were split.
73
+ //
57
74
  // A machine can defeat every rung above the last two — the one this was
58
75
  // written against does: UTM in retina mode hands the guest the MacBook's
59
76
  // full 3456x2168 grid, QEMU's EDID invents millimetres that read as 100dpi,
@@ -269,13 +286,48 @@ export function snapScale(value) {
269
286
  return Math.min(3, Math.max(1, Math.round(value * 4) / 4));
270
287
  }
271
288
 
289
+ /**
290
+ * Is this RandR output list describing panels, or one synthetic output
291
+ * covering the whole desktop?
292
+ *
293
+ * Servers that never grew a real output model — XQuartz, Xvfb, x11vnc and
294
+ * TigerVNC, Xephyr, old drivers under `xorg.conf` — answer the RandR walk
295
+ * with a single output (usually named `default`, always without
296
+ * millimetres) whose CRTC is the *union* of every monitor attached. On a
297
+ * desktop with two monitors side by side that union is twice as wide as any
298
+ * panel in it, and rung 5 reads pixel counts: a pair of 2560x1440 monitors
299
+ * unions to 5120x1440 and is judged retina-class, which is how a 1x desk
300
+ * ends up drawing everything at double size.
301
+ *
302
+ * Xinerama is the cross-check, and it is free of the same blind spot: the
303
+ * same servers report the heads individually there, because that is the
304
+ * only geometry protocol they implement. More heads than outputs means the
305
+ * output list is not per-panel data, whatever it claims.
306
+ */
307
+ export function isUnionOutput(outputs, heads) {
308
+ return (
309
+ Array.isArray(outputs) &&
310
+ outputs.length > 0 &&
311
+ Number.isInteger(heads) &&
312
+ outputs.length < heads
313
+ );
314
+ }
315
+
272
316
  /**
273
317
  * One monitor's metadata → `{ scale, source, reason }`, using only what the
274
318
  * connection reported: pixel geometry, claimed millimetres, EDID. This is
275
319
  * rungs 4 and 5 of the ladder for one output; the caller stacks the
276
320
  * desktop-configuration rungs above it.
321
+ *
322
+ * `perPanel: false` says this record's geometry spans more than one monitor
323
+ * (`isUnionOutput` above), which retires rung 5 alone: the resolution class
324
+ * is a statement about *a panel* and means nothing about a union of them.
325
+ * Rung 4 still answers when it can, because credible millimetres describe
326
+ * real glass however the pixels were divided up afterwards — a `--setmonitor`
327
+ * split of one ultrawide is the case that reaches this branch on a server
328
+ * whose RandR is otherwise perfectly honest.
277
329
  */
278
- export function monitorScaleFromMetadata(monitor) {
330
+ export function monitorScaleFromMetadata(monitor, { perPanel = true } = {}) {
279
331
  const mm = classifyMm(monitor, monitor);
280
332
  const pxW = monitor.width ?? 0;
281
333
  const pxH = monitor.height ?? 0;
@@ -306,7 +358,7 @@ export function monitorScaleFromMetadata(monitor) {
306
358
  // grids stay at 1 on purpose — 2560x1440 is the commonest *1x* desk
307
359
  // monitor there is, and only millimetres could tell it from a 13" retina
308
360
  // lid, which is exactly the data this branch does not have.
309
- if (Math.min(pxW, pxH) >= 1800 || Math.max(pxW, pxH) >= 3000) {
361
+ if (perPanel && (Math.min(pxW, pxH) >= 1800 || Math.max(pxW, pxH) >= 3000)) {
310
362
  return {
311
363
  scale: 2,
312
364
  source: 'resolution',
@@ -316,7 +368,10 @@ export function monitorScaleFromMetadata(monitor) {
316
368
  return {
317
369
  scale: 1,
318
370
  source: 'default',
319
- reason: `no credible density data (mm ${mm}, ${pxW}x${pxH})`,
371
+ reason: perPanel
372
+ ? `no credible density data (mm ${mm}, ${pxW}x${pxH})`
373
+ : `${pxW}x${pxH} spans several monitors, so its pixel count says ` +
374
+ `nothing about any panel (mm ${mm})`,
320
375
  };
321
376
  }
322
377
 
@@ -468,6 +523,57 @@ async function readOutputs(app) {
468
523
  return connected.length ? connected : null;
469
524
  }
470
525
 
526
+ /**
527
+ * How many monitors Xinerama says are attached, or null where the server
528
+ * does not answer. Read only to sanity-check the RandR walk against
529
+ * (`isUnionOutput`) — the geometry itself is `screens.js`'s business.
530
+ *
531
+ * A server with the extension present but inactive replies with one screen
532
+ * covering everything, which is the same answer as "one monitor" and is
533
+ * treated as such: the cross-check needs a *disagreement* to fire.
534
+ */
535
+ async function countHeads(app) {
536
+ const xinerama = await requireExtension(app, 'xinerama');
537
+ if (!xinerama?.QueryScreens) return null;
538
+ const screens = await call(xinerama.QueryScreens.bind(xinerama));
539
+ return Array.isArray(screens) && screens.length ? screens.length : null;
540
+ }
541
+
542
+ /**
543
+ * Is this XQuartz?
544
+ *
545
+ * It matters because XQuartz is not handing us a framebuffer: macOS composes
546
+ * the desktop in *points* and hands X the point space, already normalised
547
+ * for density. A 16" retina lid arrives as 1728x1080 and a 1x desk monitor
548
+ * beside it as 2560x1403 — the same 14pt text is the same physical size on
549
+ * both, and the window server scales what it is given onto whichever panel
550
+ * the window lands on. Every hardware rung below is therefore answering a
551
+ * question that was already answered: inferring 2x from the pixels would
552
+ * double a size macOS is about to double again.
553
+ *
554
+ * This is the `XWAYLAND` exemption in `VIRTUAL_OUTPUT_NAME` for the same
555
+ * reason and by the same rule — the compositor owns scaling, so we do not —
556
+ * and it sits below the configured rungs, not above them, because a person
557
+ * who typed `REACT_X11_SCALE` or `GDK_SCALE` outranks the platform.
558
+ *
559
+ * `Apple-WM` is the extension quartz-wm drives; it is present on every
560
+ * XQuartz server and on no other X server, including when the client is
561
+ * remote and only the display is a Mac (which is exactly when the point
562
+ * space is still the truth).
563
+ */
564
+ function isQuartzServer(X) {
565
+ if (typeof X?.QueryExtension !== 'function') return Promise.resolve(false);
566
+ return new Promise((resolve) => {
567
+ try {
568
+ X.QueryExtension('Apple-WM', (err, reply) =>
569
+ resolve(!err && !!reply?.present),
570
+ );
571
+ } catch {
572
+ resolve(false);
573
+ }
574
+ });
575
+ }
576
+
471
577
  // --------------------------------------------------------------------------
472
578
  // The session
473
579
  // --------------------------------------------------------------------------
@@ -555,11 +661,35 @@ export async function beginScale(app, option) {
555
661
  return session;
556
662
  }
557
663
 
558
- // Rungs 4-5: the hardware, one verdict per output.
559
- const outputs = await readOutputs(app);
664
+ // Rungs 4-5: the hardware, one verdict per output — but only where the
665
+ // connection is describing hardware. The three reads go out together
666
+ // because none of them depends on another and this is the chain the first
667
+ // window waits behind.
668
+ const [quartz, outputs, heads] = await Promise.all([
669
+ isQuartzServer(X),
670
+ readOutputs(app),
671
+ countHeads(app),
672
+ ]);
673
+
674
+ if (quartz) {
675
+ // 1, and not because nothing answered: macOS already did the scaling.
676
+ session.source = 'xquartz';
677
+ trace(
678
+ '1x from Apple-WM — XQuartz hands X macOS point space, already scaled',
679
+ );
680
+ return session;
681
+ }
682
+
683
+ const perPanel = !isUnionOutput(outputs, heads);
560
684
  if (outputs) {
685
+ if (!perPanel) {
686
+ trace(
687
+ `${outputs.length} RandR output(s) against ${heads} Xinerama heads: ` +
688
+ 'the output list is not per-panel, so the resolution class is out',
689
+ );
690
+ }
561
691
  for (const monitor of outputs) {
562
- const verdict = monitorScaleFromMetadata(monitor);
692
+ const verdict = monitorScaleFromMetadata(monitor, { perPanel });
563
693
  session.monitors.set(monitor.name, {
564
694
  scale: verdict.scale,
565
695
  source: verdict.source,