react-x11 2.50.0 → 2.51.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 +5 -1
- package/package.json +2 -2
- package/src/DevToolsIntegration.js +126 -42
- package/src/Reconciler.js +26 -10
- package/src/cli/dev.js +68 -0
- package/src/cli/index.js +4 -0
- package/src/cocoa/app.js +22 -0
- package/src/cocoa/relaunch.js +4 -2
- package/src/cocoa/window.js +98 -15
- package/src/nodes/scrollable.js +4 -1
- package/src/types/elements.d.ts +5 -2
- package/src/types/system.d.ts +4 -1
package/README.md
CHANGED
|
@@ -464,6 +464,10 @@ npx react-devtools # 1. start the standalone UI
|
|
|
464
464
|
REACT_X11_DEVTOOLS=1 npm run examples:dashboard # 2. run any example with the bridge on
|
|
465
465
|
```
|
|
466
466
|
|
|
467
|
+
Under `npx react-x11 dev` the bridge is on by default (with
|
|
468
|
+
`react-devtools-core` installed): run `npx react-devtools`, before or after,
|
|
469
|
+
and the app shows up.
|
|
470
|
+
|
|
467
471
|
The component tree, props and hook state show up live in the DevTools
|
|
468
472
|
window, and editing any of them re-renders the app. Hovering an element in
|
|
469
473
|
the tree tints its rect in the app's window; the toolbar's crosshair picks
|
|
@@ -498,7 +502,7 @@ Alt+Click any rendered element (Option+Click on macOS, either backend) to
|
|
|
498
502
|
open the JSX line that created it in your editor. `REACT_X11_EDITOR` picks
|
|
499
503
|
the CLI and enables the feature in one go; `REACT_X11_CLICK_TO_COMPONENT=1`
|
|
500
504
|
does the same with the default editor
|
|
501
|
-
(`cursor`). See [docs/click-to-component.md](docs/click-to-component.md).
|
|
505
|
+
(`cursor`), and `npx react-x11 dev` turns it on by default. See [docs/click-to-component.md](docs/click-to-component.md).
|
|
502
506
|
|
|
503
507
|
## TypeScript
|
|
504
508
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.51.1",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"yoga-layout": "^3.2.1"
|
|
113
113
|
},
|
|
114
114
|
"optionalDependencies": {
|
|
115
|
-
"@windowkit/appkit": "^0.
|
|
115
|
+
"@windowkit/appkit": "^0.30.1",
|
|
116
116
|
"@windowkit/wayland": "^3.1.1",
|
|
117
117
|
"@windowkit/win32": "^0.0.9",
|
|
118
118
|
"dbus-native": "^0.16.0",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Opt-in React DevTools bridge. Enabled by setting REACT_X11_DEVTOOLS=1 in
|
|
2
|
-
// the environment
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// the environment, and on by default under `react-x11 dev`; requires the
|
|
3
|
+
// `react-devtools-core` dev dependency. Run the standalone UI
|
|
4
|
+
// (`npx react-devtools`) and the app, in either order:
|
|
5
|
+
// REACT_X11_DEVTOOLS=1 npm run examples:dashboard
|
|
5
6
|
//
|
|
6
7
|
// Most of what DevTools can do costs this file nothing: the component tree,
|
|
7
8
|
// props/state/hooks, and every edit the frontend makes go through the
|
|
@@ -12,12 +13,14 @@
|
|
|
12
13
|
// Each of those is an agent listener or an option below.
|
|
13
14
|
import { spawn } from 'node:child_process';
|
|
14
15
|
import fs from 'node:fs';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
15
17
|
import os from 'node:os';
|
|
16
18
|
import path from 'node:path';
|
|
17
19
|
import { setInspectHandler } from './events.js';
|
|
18
20
|
import { flattenStyle, isStyleProp, STYLE_PROP_NAMES } from './styles.js';
|
|
19
21
|
|
|
20
22
|
let api = null;
|
|
23
|
+
let prepared = null;
|
|
21
24
|
let connected = false;
|
|
22
25
|
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
@@ -107,9 +110,16 @@ export function createStorage(persist) {
|
|
|
107
110
|
/**
|
|
108
111
|
* Install the DevTools global hook. Must run before the first React commit
|
|
109
112
|
* so mounted roots are observed — Reconciler.js awaits this at module load
|
|
110
|
-
* when REACT_X11_DEVTOOLS is set
|
|
113
|
+
* when REACT_X11_DEVTOOLS is set — and before React Refresh patches the
|
|
114
|
+
* hook, which `react-x11 dev` calls it early for: the backend installs no
|
|
115
|
+
* hook where one exists, and Refresh's is a stub. Once per process.
|
|
111
116
|
*/
|
|
112
|
-
export
|
|
117
|
+
export function prepare() {
|
|
118
|
+
prepared ??= install();
|
|
119
|
+
return prepared;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function install() {
|
|
113
123
|
try {
|
|
114
124
|
// Each hook's source location is read off a stack captured at the hook
|
|
115
125
|
// dispatch, and V8 keeps ten frames — fewer than a loader leaves
|
|
@@ -122,9 +132,6 @@ export async function prepare() {
|
|
|
122
132
|
// react-devtools-core's backend bundle expects browser-ish globals
|
|
123
133
|
global.self ??= global;
|
|
124
134
|
global.window ??= global;
|
|
125
|
-
if (!global.WebSocket) {
|
|
126
|
-
global.WebSocket = (await import('ws')).default;
|
|
127
|
-
}
|
|
128
135
|
// The update-tracing overlay schedules its redraws on animation
|
|
129
136
|
// frames, which Node does not have; without these the first traced
|
|
130
137
|
// commit throws inside the backend. Unref'd, because an overlay must
|
|
@@ -162,6 +169,7 @@ export async function prepare() {
|
|
|
162
169
|
const mod = await import('react-devtools-core');
|
|
163
170
|
// v7 exposes the API on the default export under node ESM interop
|
|
164
171
|
api = mod.default?.initialize ? mod.default : mod;
|
|
172
|
+
HostSocket = hostSocketClass();
|
|
165
173
|
// The hook settings the user last chose in the DevTools UI — console
|
|
166
174
|
// patching and friends. `initialize()` takes them, `onSettingsUpdated`
|
|
167
175
|
// (below) records them, so a setting survives a restart.
|
|
@@ -170,7 +178,7 @@ export async function prepare() {
|
|
|
170
178
|
api = null;
|
|
171
179
|
console.warn(
|
|
172
180
|
'react-x11: REACT_X11_DEVTOOLS is set but devtools could not be loaded. ' +
|
|
173
|
-
'Install react-devtools-core
|
|
181
|
+
'Install react-devtools-core. Original error: ' +
|
|
174
182
|
err.message,
|
|
175
183
|
);
|
|
176
184
|
}
|
|
@@ -254,6 +262,80 @@ export function reloadAndProfile(recordChangeDescriptions, recordTimeline) {
|
|
|
254
262
|
// Connect
|
|
255
263
|
// ---------------------------------------------------------------------------
|
|
256
264
|
|
|
265
|
+
// The backend keeps trying to connect for as long as the process lives — a
|
|
266
|
+
// timer every two seconds while no UI is listening, and an open socket while
|
|
267
|
+
// one is — and either is enough to keep a Node process up after the app's
|
|
268
|
+
// last window closed. Under `react-x11 dev`, where it is on by default, that
|
|
269
|
+
// was every session hanging on close. Neither is reachable from the options:
|
|
270
|
+
// so the backend's own code runs in a scope where the timers it starts are
|
|
271
|
+
// unref'd and the sockets it opens are, too. The scope is entered where the
|
|
272
|
+
// backend is — our connect call, its socket's close and error handlers that
|
|
273
|
+
// schedule the retry, and the timers it set in the scope — and swaps the
|
|
274
|
+
// globals only for that synchronous stretch, so the app's own `setTimeout`
|
|
275
|
+
// and `WebSocket` are never the ones changed.
|
|
276
|
+
let HostSocket = null;
|
|
277
|
+
|
|
278
|
+
function inBackend(fn) {
|
|
279
|
+
const { setTimeout: appSetTimeout, WebSocket: appWebSocket } = globalThis;
|
|
280
|
+
globalThis.setTimeout = (callback, ...rest) => {
|
|
281
|
+
const timer = appSetTimeout(
|
|
282
|
+
typeof callback === 'function'
|
|
283
|
+
? (...args) => inBackend(() => callback(...args))
|
|
284
|
+
: callback,
|
|
285
|
+
...rest,
|
|
286
|
+
);
|
|
287
|
+
timer?.unref?.();
|
|
288
|
+
return timer;
|
|
289
|
+
};
|
|
290
|
+
if (HostSocket) globalThis.WebSocket = HostSocket;
|
|
291
|
+
try {
|
|
292
|
+
return fn();
|
|
293
|
+
} finally {
|
|
294
|
+
globalThis.setTimeout = appSetTimeout;
|
|
295
|
+
globalThis.WebSocket = appWebSocket;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The backend's WebSocket: `ws`, from react-devtools-core's own
|
|
301
|
+
* dependencies, with its socket unref'd once open and its close and error
|
|
302
|
+
* handlers run in the backend's scope. Falls back to the platform's
|
|
303
|
+
* WebSocket, which works and holds the process open.
|
|
304
|
+
*/
|
|
305
|
+
function hostSocketClass() {
|
|
306
|
+
let WS;
|
|
307
|
+
try {
|
|
308
|
+
WS = createRequire(import.meta.resolve('react-devtools-core'))('ws');
|
|
309
|
+
} catch {
|
|
310
|
+
return globalThis.WebSocket ?? null;
|
|
311
|
+
}
|
|
312
|
+
class Socket extends WS {
|
|
313
|
+
constructor(...args) {
|
|
314
|
+
super(...args);
|
|
315
|
+
this.on('open', () => this._socket?.unref?.());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const name of ['onclose', 'onerror']) {
|
|
319
|
+
const base = Object.getOwnPropertyDescriptor(WS.prototype, name);
|
|
320
|
+
if (!base?.set) continue;
|
|
321
|
+
Object.defineProperty(Socket.prototype, name, {
|
|
322
|
+
configurable: true,
|
|
323
|
+
get() {
|
|
324
|
+
return base.get.call(this);
|
|
325
|
+
},
|
|
326
|
+
set(handler) {
|
|
327
|
+
base.set.call(
|
|
328
|
+
this,
|
|
329
|
+
typeof handler === 'function'
|
|
330
|
+
? (...args) => inBackend(() => handler(...args))
|
|
331
|
+
: handler,
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return Socket;
|
|
337
|
+
}
|
|
338
|
+
|
|
257
339
|
/** Connect the backend to the standalone DevTools app and register the
|
|
258
340
|
* renderer. Called by Reconciler.js right after the renderer is created. */
|
|
259
341
|
export function connect(renderer) {
|
|
@@ -261,39 +343,41 @@ export function connect(renderer) {
|
|
|
261
343
|
connected = true;
|
|
262
344
|
const profiling = readProfilingOnStart();
|
|
263
345
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
346
|
+
inBackend(() =>
|
|
347
|
+
api.connectToDevTools({
|
|
348
|
+
isAppActive: () => true,
|
|
349
|
+
host: process.env.REACT_X11_DEVTOOLS_HOST || 'localhost',
|
|
350
|
+
port: Number(process.env.REACT_X11_DEVTOOLS_PORT) || 8097,
|
|
351
|
+
// The style editor: `style` is a react-x11 style prop (an object, or a
|
|
352
|
+
// nested array of them), which is the shape DevTools' own resolver
|
|
353
|
+
// expects to be handed flat. `validAttributes` is what it offers to
|
|
354
|
+
// add to an element.
|
|
355
|
+
resolveRNStyle: resolveStyle,
|
|
356
|
+
nativeStyleEditorValidAttributes: STYLE_PROP_NAMES,
|
|
357
|
+
// Hook settings (console patching and so on) are the user's, not the
|
|
358
|
+
// run's: keep them where the next run will find them.
|
|
359
|
+
onSettingsUpdated: (settings) => {
|
|
360
|
+
try {
|
|
361
|
+
global.localStorage?.setItem(
|
|
362
|
+
HOOK_SETTINGS_KEY,
|
|
363
|
+
JSON.stringify(settings),
|
|
364
|
+
);
|
|
365
|
+
} catch {
|
|
366
|
+
// an unwritable store is not worth a warning per keystroke
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
// The backend's own check for this asks whether synchronous XHR works,
|
|
370
|
+
// which is a browser question — the honest answer for a node process
|
|
371
|
+
// that can re-exec itself is yes.
|
|
372
|
+
isReloadAndProfileSupported: true,
|
|
373
|
+
isProfiling: profiling != null,
|
|
374
|
+
onReloadAndProfile: reloadAndProfile,
|
|
375
|
+
onReloadAndProfileFlagsReset: () => {
|
|
376
|
+
delete process.env.REACT_X11_DEVTOOLS_PROFILING;
|
|
377
|
+
delete process.env.REACT_X11_DEVTOOLS_SESSION;
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
297
381
|
|
|
298
382
|
// No argument: react-reconciler 0.33 takes none, and the object this used
|
|
299
383
|
// to pass was discarded in silence. The renderer's name and version now
|
package/src/Reconciler.js
CHANGED
|
@@ -592,16 +592,15 @@ let integrationsSettled = false;
|
|
|
592
592
|
function loadIntegrations() {
|
|
593
593
|
if (integrationsSettled) return null;
|
|
594
594
|
if (
|
|
595
|
-
!
|
|
596
|
-
!
|
|
597
|
-
!process.env.REACT_X11_EDITOR &&
|
|
595
|
+
!envFlag('REACT_X11_DEVTOOLS') &&
|
|
596
|
+
!clickToComponentWanted() &&
|
|
598
597
|
!process.env.REACT_X11_TRACE
|
|
599
598
|
) {
|
|
600
599
|
return null;
|
|
601
600
|
}
|
|
602
601
|
if (integrations) return integrations;
|
|
603
602
|
integrations = (async () => {
|
|
604
|
-
if (
|
|
603
|
+
if (envFlag('REACT_X11_DEVTOOLS')) {
|
|
605
604
|
// Install the DevTools hook before any React commit, and register the
|
|
606
605
|
// renderer with the standalone DevTools app.
|
|
607
606
|
const devtools = await import('./DevToolsIntegration.js');
|
|
@@ -609,12 +608,7 @@ function loadIntegrations() {
|
|
|
609
608
|
devtools.connect(Renderer);
|
|
610
609
|
}
|
|
611
610
|
|
|
612
|
-
if (
|
|
613
|
-
process.env.REACT_X11_CLICK_TO_COMPONENT ||
|
|
614
|
-
process.env.REACT_X11_EDITOR
|
|
615
|
-
) {
|
|
616
|
-
// Naming an editor already means you want the feature on — no need to
|
|
617
|
-
// also set REACT_X11_CLICK_TO_COMPONENT=1 just to pick one.
|
|
611
|
+
if (clickToComponentWanted()) {
|
|
618
612
|
const clickToComponent = await import('./ClickToComponent.js');
|
|
619
613
|
clickToComponent.install();
|
|
620
614
|
}
|
|
@@ -632,6 +626,28 @@ function loadIntegrations() {
|
|
|
632
626
|
return integrations;
|
|
633
627
|
}
|
|
634
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Whether to install click-to-component. Naming an editor already means you
|
|
631
|
+
* want the feature on — no need to also set REACT_X11_CLICK_TO_COMPONENT=1
|
|
632
|
+
* just to pick one — and `0` turns it off whatever else is set, which is
|
|
633
|
+
* the way out of `react-x11 dev`, where it is on by default and
|
|
634
|
+
* REACT_X11_EDITOR also picks the error window's editor.
|
|
635
|
+
*/
|
|
636
|
+
function clickToComponentWanted() {
|
|
637
|
+
if (process.env.REACT_X11_CLICK_TO_COMPONENT === '0') return false;
|
|
638
|
+
return (
|
|
639
|
+
envFlag('REACT_X11_CLICK_TO_COMPONENT') ||
|
|
640
|
+
Boolean(process.env.REACT_X11_EDITOR)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** A switch in the environment: set, and not `0` — the way out of a
|
|
645
|
+
* default `react-x11 dev` turns on. */
|
|
646
|
+
function envFlag(name) {
|
|
647
|
+
const value = process.env[name];
|
|
648
|
+
return Boolean(value) && value !== '0';
|
|
649
|
+
}
|
|
650
|
+
|
|
635
651
|
/**
|
|
636
652
|
* Open a connection this process owns. The wrapper is here for one reason:
|
|
637
653
|
* to say what is wrong when there is no server, which is the first thing
|
package/src/cli/dev.js
CHANGED
|
@@ -27,12 +27,26 @@
|
|
|
27
27
|
// missing, names them with the versions the loader is written against, and
|
|
28
28
|
// installs them with the project's own package manager when the developer
|
|
29
29
|
// says yes. Without a terminal to ask on it prints the command instead.
|
|
30
|
+
// - **It turns click-to-component on.** Alt+Click (Option+Click on macOS)
|
|
31
|
+
// opens the JSX line an element came from (docs/click-to-component.md):
|
|
32
|
+
// a development tool, and this is the command a developer runs, so it is
|
|
33
|
+
// on unless REACT_X11_CLICK_TO_COMPONENT=0 says otherwise. Set here, in
|
|
34
|
+
// the environment, because that is where the renderer reads it when the
|
|
35
|
+
// first root is made — and where a toolchain restart inherits it from.
|
|
36
|
+
// - **It connects to React DevTools** whenever `npx react-devtools` is
|
|
37
|
+
// running, before or after the app started, the way a browser with the
|
|
38
|
+
// extension does: on by default where react-devtools-core is installed,
|
|
39
|
+
// unless REACT_X11_DEVTOOLS=0. Where it is not, and a DevTools UI is
|
|
40
|
+
// listening at start, it says what to install. Prepared before React
|
|
41
|
+
// Refresh loads, because Refresh installs a stub hook where it finds none,
|
|
42
|
+
// and the backend installs none where it finds one.
|
|
30
43
|
// - **It runs the project's react-x11.** A second copy of the renderer is a
|
|
31
44
|
// second React root nobody refreshes, so a global or npx-cached
|
|
32
45
|
// `react-x11` hands over to the copy the file itself imports.
|
|
33
46
|
import { spawnSync } from 'node:child_process';
|
|
34
47
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
35
48
|
import * as nodeModule from 'node:module';
|
|
49
|
+
import { connect } from 'node:net';
|
|
36
50
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
37
51
|
import { createInterface } from 'node:readline/promises';
|
|
38
52
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -122,6 +136,18 @@ export async function runDev(file, options = {}) {
|
|
|
122
136
|
return null;
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
process.env.REACT_X11_CLICK_TO_COMPONENT ??= '1';
|
|
140
|
+
if (process.env.REACT_X11_DEVTOOLS === undefined) {
|
|
141
|
+
if (resolves('react-devtools-core')) {
|
|
142
|
+
process.env.REACT_X11_DEVTOOLS = '1';
|
|
143
|
+
} else {
|
|
144
|
+
suggestDevTools({ projectDir, log });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (envFlag(process.env.REACT_X11_DEVTOOLS)) {
|
|
148
|
+
await (await import('../DevToolsIntegration.js')).prepare();
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
// Before anything below loads: a module's source maps are read when it
|
|
126
152
|
// loads, and only once this is on.
|
|
127
153
|
process.setSourceMapsEnabled(true);
|
|
@@ -386,6 +412,48 @@ function rootContent(node) {
|
|
|
386
412
|
return { windows, drawn };
|
|
387
413
|
}
|
|
388
414
|
|
|
415
|
+
/** Whether `specifier` resolves from react-x11, which is where it is
|
|
416
|
+
* imported from. */
|
|
417
|
+
function resolves(specifier) {
|
|
418
|
+
try {
|
|
419
|
+
import.meta.resolve(specifier);
|
|
420
|
+
return true;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The renderer's rule for a switch: set, and not `0`. */
|
|
427
|
+
function envFlag(value) {
|
|
428
|
+
return Boolean(value) && value !== '0';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Where react-devtools-core is missing: if a DevTools UI is listening (one
|
|
433
|
+
* TCP connect, in the background, never holding the process), say what to
|
|
434
|
+
* install for the app to show up in it. Silent otherwise — no DevTools
|
|
435
|
+
* running is the usual case, not one to print about.
|
|
436
|
+
*/
|
|
437
|
+
function suggestDevTools({ projectDir, log }) {
|
|
438
|
+
const host = process.env.REACT_X11_DEVTOOLS_HOST || 'localhost';
|
|
439
|
+
const port = Number(process.env.REACT_X11_DEVTOOLS_PORT) || 8097;
|
|
440
|
+
const socket = connect({ host, port });
|
|
441
|
+
socket.unref();
|
|
442
|
+
socket.setTimeout(1000, () => socket.destroy());
|
|
443
|
+
socket.on('error', () => {});
|
|
444
|
+
socket.on('connect', () => {
|
|
445
|
+
socket.destroy();
|
|
446
|
+
const line = shellLine(
|
|
447
|
+
installCommand(packageManagerFor(projectDir), ['react-devtools-core']),
|
|
448
|
+
);
|
|
449
|
+
log(
|
|
450
|
+
`React DevTools is listening on ${host}:${port}, and this project ` +
|
|
451
|
+
'cannot connect to it without react-devtools-core. Install it, and ' +
|
|
452
|
+
`the next run shows up there:\n\n ${line}\n`,
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
389
457
|
/** The react-x11 package `path` imports, or null when it imports none. */
|
|
390
458
|
export function reactX11For(path) {
|
|
391
459
|
let entry;
|
package/src/cli/index.js
CHANGED
|
@@ -17,6 +17,10 @@ Commands:
|
|
|
17
17
|
export is mounted, inside a window of its own when it
|
|
18
18
|
renders none; a file that mounts its own root runs as it is.
|
|
19
19
|
Arguments after the file are the app's.
|
|
20
|
+
Alt+Click (Option+Click on macOS) an element to open its
|
|
21
|
+
source; REACT_X11_CLICK_TO_COMPONENT=0 turns that off.
|
|
22
|
+
Connects to React DevTools (npx react-devtools) whenever
|
|
23
|
+
it runs; REACT_X11_DEVTOOLS=0 turns that off.
|
|
20
24
|
|
|
21
25
|
Options:
|
|
22
26
|
-h, --help Show this help.
|
package/src/cocoa/app.js
CHANGED
|
@@ -1447,6 +1447,9 @@ export class CocoaApp {
|
|
|
1447
1447
|
return this._animationEnds.get(ev.id)?.(ev);
|
|
1448
1448
|
case 'window-live-resize':
|
|
1449
1449
|
return this._routeLiveResize(ev);
|
|
1450
|
+
case 'window-fullscreen':
|
|
1451
|
+
case 'window-miniaturize':
|
|
1452
|
+
return this._routeWindowState(ev);
|
|
1450
1453
|
case 'window-created':
|
|
1451
1454
|
// a worker's window, made on the UI thread after the call that
|
|
1452
1455
|
// answered its handle
|
|
@@ -1611,6 +1614,8 @@ export class CocoaApp {
|
|
|
1611
1614
|
// this is the only thing the bridge says about it (`_recheckScreens`).
|
|
1612
1615
|
this._recheckScreens();
|
|
1613
1616
|
wnd._nativeResized(ev);
|
|
1617
|
+
// AppKit says nothing of a zoom but the resize it makes
|
|
1618
|
+
if (ev.type === 'window-resize') wnd._refreshStates();
|
|
1614
1619
|
wnd.emit('resize', {
|
|
1615
1620
|
width: wnd.width,
|
|
1616
1621
|
height: wnd.height,
|
|
@@ -1676,6 +1681,22 @@ export class CocoaApp {
|
|
|
1676
1681
|
wnd.emit('liveresize', { live: wnd.liveResizing });
|
|
1677
1682
|
}
|
|
1678
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* `window-fullscreen { fullscreen, phase }` and `window-miniaturize`
|
|
1686
|
+
* (@windowkit/appkit 0.30.0): the title bar's green button, the Dock, and
|
|
1687
|
+
* the same asked for through `setWmState`. The bridge has published the
|
|
1688
|
+
* state already — a full-screen transition's target from its `begin`,
|
|
1689
|
+
* before the animation, which is the moment an app takes its chrome away
|
|
1690
|
+
* — so the window reads it back and says `statechange` if it moved. A
|
|
1691
|
+
* user's own act, so it is input: its render goes out like a click's.
|
|
1692
|
+
*/
|
|
1693
|
+
_routeWindowState(ev) {
|
|
1694
|
+
const wnd = this._window(ev);
|
|
1695
|
+
if (!wnd || wnd.destroyed) return;
|
|
1696
|
+
wnd._refreshStates();
|
|
1697
|
+
this._afterInput();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1679
1700
|
/**
|
|
1680
1701
|
* `surface-released`: a worker's frame took an IOSurface off a layer and
|
|
1681
1702
|
* the frame that replaced it has committed (windowkit/appkit#52). The
|
|
@@ -1759,6 +1780,7 @@ export class CocoaApp {
|
|
|
1759
1780
|
const wnd = this._window(ev);
|
|
1760
1781
|
if (!wnd) return;
|
|
1761
1782
|
if (name === 'focus') this._lastKeyWindow = wnd;
|
|
1783
|
+
wnd._refreshStates();
|
|
1762
1784
|
wnd.emit(name, { buttons: 0, time: ev.time });
|
|
1763
1785
|
this._afterInput();
|
|
1764
1786
|
}
|
package/src/cocoa/relaunch.js
CHANGED
|
@@ -246,8 +246,10 @@ export function describeProcess(moduleUrl) {
|
|
|
246
246
|
platform: process.platform,
|
|
247
247
|
env: process.env,
|
|
248
248
|
execArgv: process.execArgv,
|
|
249
|
-
// Node's major version; Bun answers with one of its own choosing
|
|
250
|
-
|
|
249
|
+
// Node's major version; Bun answers with one of its own choosing, and a
|
|
250
|
+
// web page's `process` (the playground's) names no node at all — read
|
|
251
|
+
// here before any veto, so a throw would end every import of react-x11
|
|
252
|
+
node: bun ? null : Number(process.versions?.node?.split('.')[0]) || null,
|
|
251
253
|
entry: typeof entry === 'string' && fs.existsSync(entry) ? entry : null,
|
|
252
254
|
compiled: compiledExecutable(bun),
|
|
253
255
|
late: startedRunning(bun, moduleUrl, entry),
|
package/src/cocoa/window.js
CHANGED
|
@@ -11,6 +11,15 @@ import { CocoaDropTransport, dragSpec } from './dnd.js';
|
|
|
11
11
|
import { CocoaLayerPresenter } from './presenter.js';
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
13
13
|
|
|
14
|
+
/** The bridge verb each `_NET_WM_STATE` name maps onto (`setWmState`). */
|
|
15
|
+
const STATE_VERBS = {
|
|
16
|
+
fullscreen: 'setWindowFullScreen',
|
|
17
|
+
maximized: 'setWindowZoomed',
|
|
18
|
+
maximized_vert: 'setWindowZoomed',
|
|
19
|
+
maximized_horz: 'setWindowZoomed',
|
|
20
|
+
hidden: 'setWindowMiniaturized',
|
|
21
|
+
};
|
|
22
|
+
|
|
14
23
|
let nextWindowId = 1;
|
|
15
24
|
// The slack a frame clock allows under its interval, in ms: a timer's drift
|
|
16
25
|
// and no more (`CocoaApp._frameDue`, `_frameWait`, `nextFrameAt`). It lives
|
|
@@ -275,6 +284,10 @@ export class CocoaWindow {
|
|
|
275
284
|
// --- events --------------------------------------------------------------
|
|
276
285
|
|
|
277
286
|
on(name, fn) {
|
|
287
|
+
// what a change is measured from: the states when the first listener
|
|
288
|
+
// came, which it reads for itself with getWmStates
|
|
289
|
+
if (name === 'statechange' && !this._lastStates)
|
|
290
|
+
this._lastStates = this.getWmStatesNow();
|
|
278
291
|
let list = this._handlers.get(name);
|
|
279
292
|
if (!list) this._handlers.set(name, (list = []));
|
|
280
293
|
list.push(fn);
|
|
@@ -286,6 +299,14 @@ export class CocoaWindow {
|
|
|
286
299
|
};
|
|
287
300
|
}
|
|
288
301
|
|
|
302
|
+
/** ntk's spelling of the function `on` answers, for a listener held by
|
|
303
|
+
* the function it added (`useWindowState`'s `statechange`). */
|
|
304
|
+
off(name, fn) {
|
|
305
|
+
const list = this._handlers.get(name);
|
|
306
|
+
const at = list ? list.indexOf(fn) : -1;
|
|
307
|
+
if (at >= 0) list.splice(at, 1);
|
|
308
|
+
}
|
|
309
|
+
|
|
289
310
|
emit(name, ev) {
|
|
290
311
|
const list = this._handlers.get(name);
|
|
291
312
|
if (!list) return;
|
|
@@ -449,33 +470,95 @@ export class CocoaWindow {
|
|
|
449
470
|
setActions() {}
|
|
450
471
|
|
|
451
472
|
/**
|
|
452
|
-
* `_NET_WM_STATE` requests,
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
473
|
+
* `_NET_WM_STATE` requests, with the verb AppKit has for each:
|
|
474
|
+
*
|
|
475
|
+
* | name | verb |
|
|
476
|
+
* | --- | --- |
|
|
477
|
+
* | `fullscreen` | the green button's full-screen Space (`setWindowFullScreen`) |
|
|
478
|
+
* | `maximized`, `maximized_vert`, `maximized_horz` | the zoom to the standard frame (`setWindowZoomed`) — AppKit zooms both axes or neither |
|
|
479
|
+
* | `hidden` | to the Dock and back (`setWindowMiniaturized`) |
|
|
480
|
+
* | `demands_attention` | the Dock bounce (`requestUserAttention`), held until the state is removed or the window goes |
|
|
481
|
+
*
|
|
482
|
+
* Every other name resolves `false` — ntk's own contract for a state the
|
|
483
|
+
* server cannot honour — and so does a name whose verb a bridge older than
|
|
484
|
+
* @windowkit/appkit 0.30.0 does not have; WindowNode swallows the false.
|
|
485
|
+
* A transition is AppKit's to animate, and what happened comes back as
|
|
486
|
+
* `statechange` (`_refreshStates`).
|
|
458
487
|
*/
|
|
459
488
|
setWmState(names, action = 'add') {
|
|
460
489
|
const list = Array.isArray(names) ? names : [names];
|
|
490
|
+
const held = action === 'toggle' ? new Set(this.getWmStatesNow()) : null;
|
|
491
|
+
const wants = (name) => (held ? !held.has(name) : action !== 'remove');
|
|
461
492
|
let honoured = true;
|
|
462
493
|
for (const name of list) {
|
|
463
|
-
if (name
|
|
464
|
-
|
|
494
|
+
if (name === 'demands_attention') {
|
|
495
|
+
const holding = this._attentionRequest != null;
|
|
496
|
+
const on = action === 'toggle' ? !holding : action === 'add';
|
|
497
|
+
if (on && !holding && !this.destroyed) {
|
|
498
|
+
this._attentionRequest = this.app.requestAttention();
|
|
499
|
+
} else if (!on && holding) {
|
|
500
|
+
this.app.cancelAttention(this._attentionRequest);
|
|
501
|
+
this._attentionRequest = null;
|
|
502
|
+
}
|
|
465
503
|
continue;
|
|
466
504
|
}
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.app.cancelAttention(this._attentionRequest);
|
|
473
|
-
this._attentionRequest = null;
|
|
505
|
+
const verb = STATE_VERBS[name];
|
|
506
|
+
const fn = verb && this._native[verb];
|
|
507
|
+
if (typeof fn !== 'function' || this.destroyed) {
|
|
508
|
+
honoured = false;
|
|
509
|
+
continue;
|
|
474
510
|
}
|
|
511
|
+
// a toggle of an axis reads the pair: zoomed is both or neither
|
|
512
|
+
const on =
|
|
513
|
+
held && verb === 'setWindowZoomed'
|
|
514
|
+
? !held.has('maximized_vert')
|
|
515
|
+
: wants(name);
|
|
516
|
+
fn.call(this._native, this._h, on);
|
|
475
517
|
}
|
|
476
518
|
return Promise.resolve(honoured);
|
|
477
519
|
}
|
|
478
520
|
|
|
521
|
+
/**
|
|
522
|
+
* What AppKit has done with the window, in `_NET_WM_STATE` names — the
|
|
523
|
+
* names the X11 and Wayland windows report, so `useWindowState()` and
|
|
524
|
+
* `onStatesChange` read them the same way: `fullscreen`, the zoom as
|
|
525
|
+
* `maximized_vert` + `maximized_horz`, the Dock as `hidden`, the key
|
|
526
|
+
* window as `focused`. Read from the bridge's published window state, so
|
|
527
|
+
* a worker answers without a hop; `fullscreen` there says where a
|
|
528
|
+
* transition is going from the moment it starts, which is when an app
|
|
529
|
+
* hides its chrome. A bridge older than 0.30.0 publishes only `key`.
|
|
530
|
+
*/
|
|
531
|
+
getWmStatesNow() {
|
|
532
|
+
const f = this._h != null ? this._native.getWindowFrame?.(this._h) : null;
|
|
533
|
+
if (!f) return [];
|
|
534
|
+
const out = [];
|
|
535
|
+
if (f.fullscreen) out.push('fullscreen');
|
|
536
|
+
if (f.zoomed) out.push('maximized_vert', 'maximized_horz');
|
|
537
|
+
if (f.miniaturized) out.push('hidden');
|
|
538
|
+
if (f.key) out.push('focused');
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
getWmStates() {
|
|
543
|
+
return Promise.resolve(this.getWmStatesNow());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Something that can change the states happened — a full-screen
|
|
548
|
+
* transition, the Dock, a resize (AppKit says nothing of a zoom but the
|
|
549
|
+
* resize), the key window moving — so read them again and say so if they
|
|
550
|
+
* moved. Free for a window nobody asked: no listener, no read.
|
|
551
|
+
*/
|
|
552
|
+
_refreshStates() {
|
|
553
|
+
if (this.destroyed || !this._handlers.get('statechange')?.length) return;
|
|
554
|
+
const next = this.getWmStatesNow();
|
|
555
|
+
const was = this._lastStates ?? [];
|
|
556
|
+
this._lastStates = next;
|
|
557
|
+
if (was.length === next.length && was.every((n, i) => n === next[i]))
|
|
558
|
+
return;
|
|
559
|
+
this.emit('statechange', next);
|
|
560
|
+
}
|
|
561
|
+
|
|
479
562
|
// --- drag and drop (src/cocoa/dnd.js) ------------------------------------
|
|
480
563
|
|
|
481
564
|
/**
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -247,7 +247,10 @@ export const Scrollable = (Base) =>
|
|
|
247
247
|
// NaN, which a scroll pane in there measured its content from and
|
|
248
248
|
// threw on, out of the frame. The box itself has its empty rect;
|
|
249
249
|
// what is inside is placed when it is shown and laid out again.
|
|
250
|
-
|
|
250
|
+
// Both routes to it: the style, and the `hidden` React sets for
|
|
251
|
+
// `<Activity>` and `<Suspense>` (`setHidden`), which reaches yoga
|
|
252
|
+
// without touching the style.
|
|
253
|
+
if (this.style.display !== 'none' && !this.hidden) {
|
|
251
254
|
this._absolutizeChildren(
|
|
252
255
|
this.abs.x,
|
|
253
256
|
this.abs.y,
|
package/src/types/elements.d.ts
CHANGED
|
@@ -429,9 +429,12 @@ export interface WindowProps
|
|
|
429
429
|
* Applied before the window is mapped, which is the only way to open
|
|
430
430
|
* already fullscreen rather than flashing at the normal size first.
|
|
431
431
|
*
|
|
432
|
-
* On the cocoa backend `
|
|
432
|
+
* On the cocoa backend `fullscreen` is the green button's full-screen
|
|
433
|
+
* Space, `maximized` (either axis) the zoom to the standard frame,
|
|
434
|
+
* `hidden` the Dock, and `demands_attention` bounces the Dock icon until
|
|
433
435
|
* the app is activated and is cancelled when the state is removed; the
|
|
434
|
-
* other names have no
|
|
436
|
+
* other names have no AppKit equivalent and resolve to nothing, as the
|
|
437
|
+
* first three do on a bridge older than @windowkit/appkit 0.30.0.
|
|
435
438
|
*/
|
|
436
439
|
states?: WindowStateName[];
|
|
437
440
|
/** Sugar for `states={['fullscreen']}`; they union. */
|
package/src/types/system.d.ts
CHANGED
|
@@ -127,7 +127,10 @@ export interface WindowState {
|
|
|
127
127
|
/** Both axes. One axis alone shows up in `states`. */
|
|
128
128
|
readonly maximized: boolean;
|
|
129
129
|
/** What the window manager actually did, not what `<window fullscreen>`
|
|
130
|
-
* asked for.
|
|
130
|
+
* asked for — the title bar's button included. On the cocoa backend it
|
|
131
|
+
* turns as AppKit's transition begins, before the animation, so chrome
|
|
132
|
+
* an app hides in full screen goes while the window grows rather than
|
|
133
|
+
* after. */
|
|
131
134
|
readonly fullscreen: boolean;
|
|
132
135
|
/** The raw `_NET_WM_STATE` names, e.g. `['maximized_vert', 'focused']` —
|
|
133
136
|
* what the window manager put there, where `<window states>` is what was
|