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.
- package/package.json +4 -3
- 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
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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,
|