react-x11 2.13.0 → 2.15.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 +11 -8
- package/src/Reconciler.js +34 -21
- package/src/components/Select.js +8 -2
- package/src/events.js +8 -2
- package/src/glnodes.js +15 -3
- package/src/index.d.ts +5 -0
- package/src/nodes/boxpaint.js +9 -0
- package/src/nodes/preedit.js +64 -14
- package/src/ntk.js +10 -1
- package/src/scale.js +52 -22
- package/src/screencolor.js +104 -17
- package/src/types/system.d.ts +14 -1
- package/src/wayland/app.js +574 -0
- package/src/wayland/backendwindow.js +1199 -0
- package/src/wayland/clipboard.js +326 -0
- package/src/wayland/connection.js +485 -0
- package/src/wayland/context2d.js +2290 -0
- package/src/wayland/decorations.js +476 -0
- package/src/wayland/device.js +55 -0
- package/src/wayland/dmabuf.js +89 -0
- package/src/wayland/dnd.js +581 -0
- package/src/wayland/fdutil.js +108 -0
- package/src/wayland/framestyle.js +257 -0
- package/src/wayland/framewatch.js +190 -0
- package/src/wayland/glarea.js +372 -0
- package/src/wayland/glcontext.js +415 -0
- package/src/wayland/glyphatlas.js +237 -0
- package/src/wayland/input.js +417 -0
- package/src/wayland/keysymnames.js +35 -0
- package/src/wayland/layershell.js +363 -0
- package/src/wayland/outputs.js +601 -0
- package/src/wayland/protocols/cursor-shape-v1.json +1 -0
- package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
- package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
- package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
- package/src/wayland/protocols/fractional-scale-v1.json +1 -0
- package/src/wayland/protocols/index.json +131 -0
- package/src/wayland/protocols/kde-server-decoration.json +1 -0
- package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
- package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
- package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
- package/src/wayland/protocols/presentation-time.json +1 -0
- package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
- package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
- package/src/wayland/protocols/tablet-v2.json +1 -0
- package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
- package/src/wayland/protocols/viewporter.json +1 -0
- package/src/wayland/protocols/wayland.json +1 -0
- package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
- package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-activation-v1.json +1 -0
- package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
- package/src/wayland/protocols/xdg-shell.json +1 -0
- package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
- package/src/wayland/readback.js +99 -0
- package/src/wayland/screencopy.js +584 -0
- package/src/wayland/seat.js +584 -0
- package/src/wayland/shm.js +226 -0
- package/src/wayland/ssd.js +234 -0
- package/src/wayland/surface.js +141 -0
- package/src/wayland/swapchain.js +411 -0
- package/src/wayland/tablet.js +522 -0
- package/src/wayland/target.js +269 -0
- package/src/wayland/text.js +113 -0
- package/src/wayland/textinput.js +671 -0
- package/src/wayland/touch.js +284 -0
- package/src/wayland/window.js +854 -0
- package/src/wayland/xkb.js +425 -0
- package/src/windowstate.js +41 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"examples:appearance": "tsx examples/appearance.jsx",
|
|
21
21
|
"examples:animation": "tsx examples/animation.jsx",
|
|
22
22
|
"examples:simple": "tsx examples/simple.jsx",
|
|
23
|
+
"examples:wayland-gl": "node examples/wayland-gl.mjs",
|
|
24
|
+
"examples:wayland-ui": "node examples/wayland-ui.mjs",
|
|
23
25
|
"examples:simple-nojsx": "node examples/simple-nojsx.js",
|
|
24
26
|
"examples:xeyes": "tsx examples/xeyes.jsx",
|
|
25
27
|
"examples:dashboard": "tsx examples/dashboard.jsx",
|
|
@@ -100,20 +102,21 @@
|
|
|
100
102
|
"ical.js": "^2.2.1",
|
|
101
103
|
"linebreak": "^1.1.0",
|
|
102
104
|
"ntk": "^8.7.0",
|
|
103
|
-
"react-reconciler": "^0.
|
|
105
|
+
"react-reconciler": "^0.34.0",
|
|
104
106
|
"yoga-layout": "^3.2.1"
|
|
105
107
|
},
|
|
106
108
|
"optionalDependencies": {
|
|
107
109
|
"@windowkit/appkit": "^0.10.0",
|
|
110
|
+
"@windowkit/wayland": "^3.1.1",
|
|
108
111
|
"dbus-native": "^0.15.1",
|
|
109
|
-
"x11-dri": "^0.
|
|
112
|
+
"x11-dri": "^0.9.0"
|
|
110
113
|
},
|
|
111
114
|
"peerDependencies": {
|
|
112
115
|
"@babel/core": "^8.0.0",
|
|
113
116
|
"@babel/plugin-transform-react-jsx": "^8.0.0",
|
|
114
117
|
"hot-module-replacement": "^4.0.1",
|
|
115
|
-
"react": "^19.
|
|
116
|
-
"react-refresh": "^0.
|
|
118
|
+
"react": "^19.3.0",
|
|
119
|
+
"react-refresh": "^0.19.0"
|
|
117
120
|
},
|
|
118
121
|
"peerDependenciesMeta": {
|
|
119
122
|
"@babel/core": {
|
|
@@ -146,9 +149,9 @@
|
|
|
146
149
|
"hot-module-replacement": "^4.0.1",
|
|
147
150
|
"katex": "^0.18.1",
|
|
148
151
|
"prettier": "^3.6.0",
|
|
149
|
-
"react": "^19.
|
|
150
|
-
"react-devtools-core": "^
|
|
151
|
-
"react-refresh": "^0.
|
|
152
|
+
"react": "^19.3.0",
|
|
153
|
+
"react-devtools-core": "^8.0.0",
|
|
154
|
+
"react-refresh": "^0.19.0",
|
|
152
155
|
"tsx": "^4.23.1",
|
|
153
156
|
"typescript": "^7.0.2",
|
|
154
157
|
"ws": "^8.21.1"
|
package/src/Reconciler.js
CHANGED
|
@@ -453,6 +453,10 @@ const HostConfig = {
|
|
|
453
453
|
},
|
|
454
454
|
startSuspendingCommit() {},
|
|
455
455
|
suspendInstance() {},
|
|
456
|
+
// react-reconciler 0.34 calls this on every commit whose lanes are all
|
|
457
|
+
// transition, retry or idle lanes, whether or not the tree has a
|
|
458
|
+
// <ViewTransition>. There is no view transition here to wait on.
|
|
459
|
+
suspendOnActiveViewTransition() {},
|
|
456
460
|
waitForCommitToBeReady() {
|
|
457
461
|
return null;
|
|
458
462
|
},
|
|
@@ -574,11 +578,11 @@ const isNtkApp = (v) =>
|
|
|
574
578
|
*/
|
|
575
579
|
function resolveBackend(options) {
|
|
576
580
|
const asked = options.backend ?? process.env.REACT_X11_BACKEND ?? 'auto';
|
|
577
|
-
if (asked === 'x11' || asked === 'cocoa') return asked;
|
|
581
|
+
if (asked === 'x11' || asked === 'cocoa' || asked === 'wayland') return asked;
|
|
578
582
|
if (asked !== 'auto') {
|
|
579
583
|
throw new Error(
|
|
580
584
|
`react-x11: unknown backend ${JSON.stringify(asked)} — expected ` +
|
|
581
|
-
"'x11', 'cocoa' or 'auto'.",
|
|
585
|
+
"'x11', 'cocoa', 'wayland' or 'auto'.",
|
|
582
586
|
);
|
|
583
587
|
}
|
|
584
588
|
// Naming an X endpoint is choosing X11: a `display` or a `stream` (the
|
|
@@ -715,25 +719,34 @@ export async function createRoot(options = {}) {
|
|
|
715
719
|
rest.backend === 'cocoa' || process.env.REACT_X11_BACKEND === 'cocoa';
|
|
716
720
|
const connecting = !owned
|
|
717
721
|
? Promise.resolve(borrowed)
|
|
718
|
-
: backend === '
|
|
719
|
-
?
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
)
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
722
|
+
: backend === 'wayland'
|
|
723
|
+
? // Opt-in only, never through 'auto': X11 stays the default on Linux
|
|
724
|
+
// because the remote case (docs/remote.md) is the flagship reason
|
|
725
|
+
// this project exists and Wayland has no network transparency. The
|
|
726
|
+
// backend is opt-in; docs/wayland-backend.md says what it does
|
|
727
|
+
// and does not do.
|
|
728
|
+
import('./wayland/app.js').then(({ createWaylandApp }) =>
|
|
729
|
+
createWaylandApp(rest),
|
|
730
|
+
)
|
|
731
|
+
: backend === 'cocoa'
|
|
732
|
+
? import('./cocoa/app.js')
|
|
733
|
+
.then(({ createCocoaApp }) => createCocoaApp(rest))
|
|
734
|
+
.catch((err) => {
|
|
735
|
+
// Asked for by name, the bridge is required and its absence is
|
|
736
|
+
// the error (it says how to install). Reached by 'auto', a mac
|
|
737
|
+
// without it falls back to X11 so an XQuartz setup keeps
|
|
738
|
+
// working — said once, because a silent fallback would look
|
|
739
|
+
// like the native backend being broken rather than absent.
|
|
740
|
+
if (cocoaAsked) throw err;
|
|
741
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
742
|
+
console.warn(
|
|
743
|
+
'react-x11: no @windowkit/appkit bridge — falling back to the ' +
|
|
744
|
+
`X11 backend. (${err.message.split('\n')[0]})`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return connectX11();
|
|
748
|
+
})
|
|
749
|
+
: connectX11();
|
|
737
750
|
const layout = loadLayout();
|
|
738
751
|
const integrations = loadIntegrations(); // null when there is nothing to install
|
|
739
752
|
const [app] = await Promise.all([connecting, layout, integrations]);
|
package/src/components/Select.js
CHANGED
|
@@ -354,11 +354,17 @@ export function Select({
|
|
|
354
354
|
);
|
|
355
355
|
const chosenRowTop = MENU_BORDER + metrics.pad + chosen * metrics.row;
|
|
356
356
|
const top = triggerCapsBottom - chosenRowTop - rowCapsBottom;
|
|
357
|
+
// `top` is where the menu starts against the trigger's own top, so it
|
|
358
|
+
// goes in as an `offset` back past the trigger's bottom — not as an `at`.
|
|
359
|
+
// Tracking asks whether `at` is still in view, and a menu opened over a
|
|
360
|
+
// trigger near the window's top edge starts above that edge: the popup
|
|
361
|
+
// is a real window and may, but the rect asked about was then "scrolled
|
|
362
|
+
// out", and the menu closed on the next unrelated layout pass (#552).
|
|
363
|
+
// What has to stay visible is the trigger.
|
|
357
364
|
return {
|
|
358
365
|
placement: 'bottom',
|
|
359
|
-
offset:
|
|
366
|
+
offset: top - triggerHeight,
|
|
360
367
|
flip: false,
|
|
361
|
-
at: { x: 0, y: top, width: node ? node.abs.width / scale : 0, height: 0 },
|
|
362
368
|
alignOffset: -metrics.hang.left,
|
|
363
369
|
height,
|
|
364
370
|
width,
|
package/src/events.js
CHANGED
|
@@ -1345,8 +1345,14 @@ export class EventManager {
|
|
|
1345
1345
|
* then the element's own `defaultComposition` unless one of them called
|
|
1346
1346
|
* `preventDefault()`. Same seam, same order as `defaultKeyDown`.
|
|
1347
1347
|
*/
|
|
1348
|
-
_composition(phase, target, data, native) {
|
|
1349
|
-
|
|
1348
|
+
_composition(phase, target, data, native, extra) {
|
|
1349
|
+
// `extra` is what an input method knows beyond the text — the cursor
|
|
1350
|
+
// inside its preedit (`cursorBegin`/`cursorEnd`, src/wayland/textinput.js);
|
|
1351
|
+
// the client-side composer has none, and the event is the same shape
|
|
1352
|
+
const ev = this.dispatch('Composition' + phase, target, native, {
|
|
1353
|
+
data,
|
|
1354
|
+
...extra,
|
|
1355
|
+
});
|
|
1350
1356
|
if (!ev.defaultPrevented) target.defaultComposition?.(ev);
|
|
1351
1357
|
return ev;
|
|
1352
1358
|
}
|
package/src/glnodes.js
CHANGED
|
@@ -272,9 +272,18 @@ export class GlAreaNode extends Node {
|
|
|
272
272
|
// (`EventManager._surfaceAt`).
|
|
273
273
|
this._joinSurfaces();
|
|
274
274
|
this.gl = wnd.getContext('opengl', config);
|
|
275
|
-
//
|
|
275
|
+
// A buffer freed by the display is a frame that can be drawn again — for
|
|
276
|
+
// a frame that was waiting for one. The Cocoa surface and the CGL
|
|
277
|
+
// context reopen their gate one display period after *every* swap, not
|
|
278
|
+
// only after a refused one, and a request per reopening turned
|
|
279
|
+
// frameLoop 'demand' into a loop at display rate: a still scene drew
|
|
280
|
+
// every frame, forever.
|
|
276
281
|
if (typeof this.gl?.onFrameAvailable !== 'undefined') {
|
|
277
|
-
this.gl.onFrameAvailable = () =>
|
|
282
|
+
this.gl.onFrameAvailable = () => {
|
|
283
|
+
if (!this._frameRefused) return;
|
|
284
|
+
this._frameRefused = false;
|
|
285
|
+
this.requestFrame();
|
|
286
|
+
};
|
|
278
287
|
}
|
|
279
288
|
// The context is only usable once MakeCurrent has answered, and that is
|
|
280
289
|
// where a server refusing indirect GLX says so (ntk gives the rejection
|
|
@@ -384,7 +393,10 @@ export class GlAreaNode extends Node {
|
|
|
384
393
|
// On the direct backend every buffer may still be held by the display,
|
|
385
394
|
// and drawing into one before it comes back would paint what is on
|
|
386
395
|
// screen. `onFrameAvailable` asks for this frame again when one frees.
|
|
387
|
-
if (direct && gl.canRender && !gl.canRender())
|
|
396
|
+
if (direct && gl.canRender && !gl.canRender()) {
|
|
397
|
+
this._frameRefused = true;
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
388
400
|
// binds this surface — the GPU context is shared between every <glarea>
|
|
389
401
|
// on the connection — and picks up a resize
|
|
390
402
|
gl.makeCurrent?.();
|
package/src/index.d.ts
CHANGED
|
@@ -300,6 +300,11 @@ export interface RootOptions {
|
|
|
300
300
|
| 'off'
|
|
301
301
|
| {
|
|
302
302
|
mode?: 'auto' | 'direct' | 'indirect' | 'off';
|
|
303
|
+
/**
|
|
304
|
+
* Wayland: the DRM node to render on. Defaults to
|
|
305
|
+
* `REACT_X11_GL_DEVICE`, then the first render node, then, on a
|
|
306
|
+
* machine with no render node, each card node in turn.
|
|
307
|
+
*/
|
|
303
308
|
devicePath?: string | null;
|
|
304
309
|
maxInFlight?: number;
|
|
305
310
|
linearFallback?: boolean;
|
package/src/nodes/boxpaint.js
CHANGED
|
@@ -254,6 +254,15 @@ export class NodeBoxPaint {
|
|
|
254
254
|
ctx.fill();
|
|
255
255
|
continue;
|
|
256
256
|
}
|
|
257
|
+
// A context that can feather a rounded rectangle itself — the GPU
|
|
258
|
+
// backend's distance-field shadow — skips the coverage surface, the
|
|
259
|
+
// CPU blur and the paint cache entirely. Feature-detected, like
|
|
260
|
+
// `presentFrame`: an ntk context has no such method and takes the
|
|
261
|
+
// path below.
|
|
262
|
+
if (typeof ctx.fillShadow === 'function') {
|
|
263
|
+
ctx.fillShadow(rect, r, shadow.blur, color);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
257
266
|
this._paintBlurredShadow(ctx, rect, r, shadow.blur, color);
|
|
258
267
|
}
|
|
259
268
|
}
|
package/src/nodes/preedit.js
CHANGED
|
@@ -43,12 +43,18 @@ export class TextInputPreedit {
|
|
|
43
43
|
|
|
44
44
|
/** A value index in the displayed string. The caret is at the composition
|
|
45
45
|
* while it is open, so it maps to the *end* of the preedit — which is
|
|
46
|
-
* where the next keystroke of the sequence appears
|
|
46
|
+
* where the next keystroke of the sequence appears — unless the input
|
|
47
|
+
* method said where inside its preedit the cursor is (`_preeditCursor`,
|
|
48
|
+
* a compositor IME converting one segment of a longer phrase). */
|
|
47
49
|
_displayIndex(index) {
|
|
48
50
|
if (!this._preedit) return index;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
const start = this._preeditStart();
|
|
52
|
+
if (index < start) return index;
|
|
53
|
+
const length = Array.from(this._preedit).length;
|
|
54
|
+
if (index === start && this._preeditCursor) {
|
|
55
|
+
return start + Math.min(length, this._preeditCursor.begin);
|
|
56
|
+
}
|
|
57
|
+
return index + length;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/** The inverse, for indices that come back out of a layout. A hit inside
|
|
@@ -61,10 +67,20 @@ export class TextInputPreedit {
|
|
|
61
67
|
return Math.max(start, index - Array.from(this._preedit).length);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
/**
|
|
71
|
+
* `cursor` is `{ begin, end }` in code points into `text` — where an input
|
|
72
|
+
* method wants the caret inside its preedit, or a segment of it
|
|
73
|
+
* highlighted when the two differ — and null for the end, which is where
|
|
74
|
+
* the client-side composer's caret always is.
|
|
75
|
+
*/
|
|
76
|
+
_setPreedit(text, cursor = null) {
|
|
77
|
+
const range = text && cursor ? cursor : null;
|
|
78
|
+
if (text === this._preedit && sameRange(range, this._preeditCursor)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
66
81
|
if (!this._preedit) this._preeditAt = this._selection()[0];
|
|
67
82
|
this._preedit = text;
|
|
83
|
+
this._preeditCursor = range;
|
|
68
84
|
this._repaint();
|
|
69
85
|
}
|
|
70
86
|
|
|
@@ -79,7 +95,18 @@ export class TextInputPreedit {
|
|
|
79
95
|
*/
|
|
80
96
|
defaultComposition(ev) {
|
|
81
97
|
if (ev.type !== 'compositionEnd') {
|
|
82
|
-
|
|
98
|
+
// `cursorBegin`/`cursorEnd` ride the event only when an input method
|
|
99
|
+
// put them there (src/wayland/textinput.js); a dead key carries none
|
|
100
|
+
const begin = ev.cursorBegin;
|
|
101
|
+
const cursor = Number.isInteger(begin)
|
|
102
|
+
? {
|
|
103
|
+
begin,
|
|
104
|
+
end: Number.isInteger(ev.cursorEnd)
|
|
105
|
+
? Math.max(begin, ev.cursorEnd)
|
|
106
|
+
: begin,
|
|
107
|
+
}
|
|
108
|
+
: null;
|
|
109
|
+
this._setPreedit(ev.data, cursor);
|
|
83
110
|
return;
|
|
84
111
|
}
|
|
85
112
|
this._setPreedit('');
|
|
@@ -114,14 +141,37 @@ export class TextInputPreedit {
|
|
|
114
141
|
// this and a composition at a wrap point is two spans — drawn as one
|
|
115
142
|
// batch, so the underline costs one request however it wraps
|
|
116
143
|
const rects = [];
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
const underline = (from, to, thickness) => {
|
|
145
|
+
for (let li = from.line; li <= to.line; li++) {
|
|
146
|
+
const line = layout.lines[li];
|
|
147
|
+
if (!line) break;
|
|
148
|
+
const x0 = li === from.line ? from.x : line.x;
|
|
149
|
+
const x1 = li === to.line ? to.x : line.x + line.width;
|
|
150
|
+
if (x1 <= x0) continue;
|
|
151
|
+
rects.push(
|
|
152
|
+
originX + x0,
|
|
153
|
+
originY + line.y + line.ascent + 1,
|
|
154
|
+
x1 - x0,
|
|
155
|
+
thickness,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
underline(from, to, 1);
|
|
160
|
+
// The segment an input method is converting, when it named one: the
|
|
161
|
+
// convention (GTK, Qt, the browsers) is a heavier line under the part of
|
|
162
|
+
// the phrase the candidates are for, and a plain one under the rest.
|
|
163
|
+
const cursor = this._preeditCursor;
|
|
164
|
+
if (cursor && cursor.end > cursor.begin) {
|
|
165
|
+
underline(
|
|
166
|
+
layout.caretPosition(start + cursor.begin),
|
|
167
|
+
layout.caretPosition(start + cursor.end),
|
|
168
|
+
2,
|
|
169
|
+
);
|
|
124
170
|
}
|
|
125
171
|
if (rects.length) ctx.fillRects(rects);
|
|
126
172
|
}
|
|
127
173
|
}
|
|
174
|
+
|
|
175
|
+
function sameRange(a, b) {
|
|
176
|
+
return a === b || (!!a && !!b && a.begin === b.begin && a.end === b.end);
|
|
177
|
+
}
|
package/src/ntk.js
CHANGED
|
@@ -37,13 +37,22 @@ export { default } from 'ntk';
|
|
|
37
37
|
* ntk's offscreen `Surface`, on whichever backend `app` is.
|
|
38
38
|
*
|
|
39
39
|
* An app that makes its own surfaces answers `createSurface(options)` —
|
|
40
|
-
* the Cocoa app does, over a CG bitmap (src/cocoa/surface.js)
|
|
40
|
+
* the Cocoa app does, over a CG bitmap (src/cocoa/surface.js), the Wayland
|
|
41
|
+
* app over a GL render target (src/wayland/surface.js) — and an ntk
|
|
41
42
|
* connection has no such method and gets ntk's pixmap. The result is
|
|
42
43
|
* whichever implementation answered, not an instance of this class: the
|
|
43
44
|
* contract is the shape — `width`/`height`, `getContext('2d')`, `render`,
|
|
44
45
|
* `clear`, `copyWithin`, `destroy`, and `ctx.drawImage(surface, …)` —
|
|
45
46
|
* (docs/extending.md "Scrolling the pixels, not just the offset"), and
|
|
46
47
|
* nothing needs `instanceof`.
|
|
48
|
+
*
|
|
49
|
+
* Part of that shape is that a context may be **held**: ntk tells a caller
|
|
50
|
+
* doing many draws to take `getContext('2d')` once rather than one per
|
|
51
|
+
* frame, and that holds on every backend here, including the ones where a
|
|
52
|
+
* surface is a GPU target sharing a device with the window (#566). Draws
|
|
53
|
+
* through a held context land in the surface whenever they are made, and
|
|
54
|
+
* leave nothing for the caller to restore; `render(fn)` is the scoped form,
|
|
55
|
+
* for a caller that wants a frame's clean transform and clip.
|
|
47
56
|
*/
|
|
48
57
|
export class Surface {
|
|
49
58
|
constructor(app, options) {
|
package/src/scale.js
CHANGED
|
@@ -166,6 +166,18 @@ const VIRTUAL_EDID_VENDORS = new Set([
|
|
|
166
166
|
]);
|
|
167
167
|
const VIRTUAL_MODEL = /qemu|virtual|vbox|vmware|parallels|bochs|bhyve/i;
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Whether an EDID's vendor and model name software — or a compositor's
|
|
171
|
+
* `wl_output` make and model, which are the same two strings read off the
|
|
172
|
+
* same EDID (mutter says 'RHT' / 'QEMU Monitor' for a QEMU guest's screen).
|
|
173
|
+
*/
|
|
174
|
+
export function isVirtualDisplay(vendor, model) {
|
|
175
|
+
return (
|
|
176
|
+
(vendor != null && VIRTUAL_EDID_VENDORS.has(vendor)) ||
|
|
177
|
+
(model != null && VIRTUAL_MODEL.test(model))
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
169
181
|
/**
|
|
170
182
|
* The 128-byte EDID base block → what the scale ladder wants from it:
|
|
171
183
|
* vendor, model name, the physical size, and the one derived judgement —
|
|
@@ -202,9 +214,7 @@ export function parseEdid(buffer) {
|
|
|
202
214
|
break;
|
|
203
215
|
}
|
|
204
216
|
}
|
|
205
|
-
const virtual =
|
|
206
|
-
(vendor !== null && VIRTUAL_EDID_VENDORS.has(vendor)) ||
|
|
207
|
-
(model !== null && VIRTUAL_MODEL.test(model));
|
|
217
|
+
const virtual = isVirtualDisplay(vendor, model);
|
|
208
218
|
return { vendor, model, mmWidth, mmHeight, virtual };
|
|
209
219
|
}
|
|
210
220
|
|
|
@@ -352,13 +362,17 @@ export function monitorScaleFromMetadata(monitor, { perPanel = true } = {}) {
|
|
|
352
362
|
}
|
|
353
363
|
|
|
354
364
|
// No physical truth to reason from. The pixel grid alone still separates
|
|
355
|
-
// "unmistakably a retina panel" from everything else
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
365
|
+
// "unmistakably a retina panel" from everything else. 2560x1600, a 30"
|
|
366
|
+
// desk monitor, is the tallest grid a 1x panel ships with, and nothing is
|
|
367
|
+
// sold as 1x with a short side between that and 1800 — so a short side
|
|
368
|
+
// past 1600 is a 2x grid: the 2560x1664 and 2880x1800 laptop lids, and a
|
|
369
|
+
// VM *window* over a retina panel, which is all a guest sees of its host
|
|
370
|
+
// (2488x1668 is a UTM window on a 14" MacBook). A long side of 3000 or
|
|
371
|
+
// more catches the wide ones. 2560x1440 and 2560x1600 stay at 1 on
|
|
372
|
+
// purpose — the commonest *1x* desk monitors there are, and only
|
|
373
|
+
// millimetres could tell them from a 13" retina lid, which is exactly the
|
|
374
|
+
// data this branch does not have.
|
|
375
|
+
if (perPanel && (Math.min(pxW, pxH) > 1600 || Math.max(pxW, pxH) >= 3000)) {
|
|
362
376
|
return {
|
|
363
377
|
scale: 2,
|
|
364
378
|
source: 'resolution',
|
|
@@ -379,6 +393,26 @@ export function monitorScaleFromMetadata(monitor, { perPanel = true } = {}) {
|
|
|
379
393
|
// The desktop-configuration rungs
|
|
380
394
|
// --------------------------------------------------------------------------
|
|
381
395
|
|
|
396
|
+
/**
|
|
397
|
+
* The factor a person or the app pinned, or null for `'auto'`:
|
|
398
|
+
* `REACT_X11_SCALE` (0.5–8) first, then a numeric `createRoot({ scale })`
|
|
399
|
+
* clamped to [0.5, 8]. The ladder's first word, and the only one a backend
|
|
400
|
+
* with a scale of its own (a Wayland compositor's) lets outrank that scale.
|
|
401
|
+
*/
|
|
402
|
+
export function pinnedScale(option) {
|
|
403
|
+
const own = Number(process.env.REACT_X11_SCALE);
|
|
404
|
+
if (Number.isFinite(own) && own >= 0.5 && own <= 8) {
|
|
405
|
+
return { scale: own, source: 'REACT_X11_SCALE' };
|
|
406
|
+
}
|
|
407
|
+
if (typeof option === 'number' && Number.isFinite(option)) {
|
|
408
|
+
return { scale: Math.min(8, Math.max(0.5, option)), source: 'option' };
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** `REACT_X11_DEBUG_SCALE`'s line, for the backends that decide their own. */
|
|
414
|
+
export const traceScale = trace;
|
|
415
|
+
|
|
382
416
|
function envScale() {
|
|
383
417
|
const own = Number(process.env.REACT_X11_SCALE);
|
|
384
418
|
// wider bounds than `snapScale` on purpose: an explicit override is a
|
|
@@ -609,18 +643,14 @@ export async function beginScale(app, option) {
|
|
|
609
643
|
session = new ScaleSession();
|
|
610
644
|
sessions.set(app, session);
|
|
611
645
|
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
614
|
-
session.scale =
|
|
615
|
-
session.source =
|
|
616
|
-
trace(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (typeof option === 'number' && Number.isFinite(option)) {
|
|
621
|
-
session.scale = Math.min(8, Math.max(0.5, option));
|
|
622
|
-
session.source = 'option';
|
|
623
|
-
trace(`${session.scale}x from createRoot({ scale })`);
|
|
646
|
+
const pinned = pinnedScale(option);
|
|
647
|
+
if (pinned) {
|
|
648
|
+
session.scale = pinned.scale;
|
|
649
|
+
session.source = pinned.source;
|
|
650
|
+
trace(
|
|
651
|
+
`${pinned.scale}x from ` +
|
|
652
|
+
(pinned.source === 'option' ? 'createRoot({ scale })' : pinned.source),
|
|
653
|
+
);
|
|
624
654
|
return session;
|
|
625
655
|
}
|
|
626
656
|
|