react-x11 1.2.0 → 2.0.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/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -2,13 +2,108 @@
|
|
|
2
2
|
// the environment; requires the `react-devtools-core` and `ws` dev
|
|
3
3
|
// dependencies. Start the standalone UI first (`npx react-devtools`), then
|
|
4
4
|
// run the app: REACT_X11_DEVTOOLS=1 npm run examples:dashboard
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
//
|
|
6
|
+
// Most of what DevTools can do costs this file nothing: the component tree,
|
|
7
|
+
// props/state/hooks, and every edit the frontend makes go through the
|
|
8
|
+
// renderer interface `injectIntoDevTools()` registers. What is left is the
|
|
9
|
+
// part a browser gets from the DOM and an X11 app has to answer for itself
|
|
10
|
+
// — the overlays (hover highlight, update outlines), the element picker,
|
|
11
|
+
// the style editor's measurements, and the storage a restart survives.
|
|
12
|
+
// Each of those is an agent listener or an option below.
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { setInspectHandler } from './events.js';
|
|
18
|
+
import { flattenStyle, isStyleProp, STYLE_PROP_NAMES } from './styles.js';
|
|
8
19
|
|
|
9
20
|
let api = null;
|
|
10
21
|
let connected = false;
|
|
11
22
|
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// State that outlives the process
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The backend keeps its preferences in `localStorage` — component filters,
|
|
29
|
+
* console patching, the "reload and profile" flags — and a browser hands it
|
|
30
|
+
* one. Without it every DevTools setting resets on each run, and the
|
|
31
|
+
* backend's own reload-and-profile support check fails on the first line.
|
|
32
|
+
* A file in the cache directory is the same deal the appearance cache
|
|
33
|
+
* takes (see appearance.js): ordinary user-writable JSON, absent on the
|
|
34
|
+
* first run, and doing without it costs only persistence.
|
|
35
|
+
*/
|
|
36
|
+
function stateFile() {
|
|
37
|
+
if (process.env.REACT_X11_DEVTOOLS_STATE) {
|
|
38
|
+
return process.env.REACT_X11_DEVTOOLS_STATE;
|
|
39
|
+
}
|
|
40
|
+
let base = process.env.XDG_CACHE_HOME;
|
|
41
|
+
if (!base) {
|
|
42
|
+
let home;
|
|
43
|
+
try {
|
|
44
|
+
home = os.homedir();
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!home || home === '/') return null;
|
|
49
|
+
base =
|
|
50
|
+
process.platform === 'darwin'
|
|
51
|
+
? path.join(home, 'Library', 'Caches')
|
|
52
|
+
: path.join(home, '.cache');
|
|
53
|
+
}
|
|
54
|
+
return path.join(base, 'react-x11', 'devtools.json');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A Storage that reads and writes one JSON object. `persist` false is
|
|
58
|
+
* sessionStorage: same interface, nothing on disk. Exported for its own
|
|
59
|
+
* test — a store that quietly stops persisting looks exactly like DevTools
|
|
60
|
+
* settings that never stuck. */
|
|
61
|
+
export function createStorage(persist) {
|
|
62
|
+
const file = persist ? stateFile() : null;
|
|
63
|
+
let entries = new Map();
|
|
64
|
+
if (file) {
|
|
65
|
+
try {
|
|
66
|
+
entries = new Map(Object.entries(JSON.parse(fs.readFileSync(file))));
|
|
67
|
+
} catch {
|
|
68
|
+
// absent, unreadable or not JSON — start from nothing
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const flush = () => {
|
|
72
|
+
if (!file) return;
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
75
|
+
fs.writeFileSync(file, JSON.stringify(Object.fromEntries(entries)));
|
|
76
|
+
} catch {
|
|
77
|
+
// a read-only or full disk costs persistence, not the session
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
get length() {
|
|
82
|
+
return entries.size;
|
|
83
|
+
},
|
|
84
|
+
key: (i) => [...entries.keys()][i] ?? null,
|
|
85
|
+
getItem: (key) => entries.get(String(key)) ?? null,
|
|
86
|
+
setItem(key, value) {
|
|
87
|
+
entries.set(String(key), String(value));
|
|
88
|
+
flush();
|
|
89
|
+
},
|
|
90
|
+
removeItem(key) {
|
|
91
|
+
entries.delete(String(key));
|
|
92
|
+
flush();
|
|
93
|
+
},
|
|
94
|
+
clear() {
|
|
95
|
+
entries.clear();
|
|
96
|
+
flush();
|
|
97
|
+
},
|
|
98
|
+
/** What a reload-and-profile restart carries into the child. */
|
|
99
|
+
toJSON: () => Object.fromEntries(entries),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Install
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
12
107
|
/**
|
|
13
108
|
* Install the DevTools global hook. Must run before the first React commit
|
|
14
109
|
* so mounted roots are observed — Reconciler.js awaits this at module load
|
|
@@ -22,10 +117,47 @@ export async function prepare() {
|
|
|
22
117
|
if (!global.WebSocket) {
|
|
23
118
|
global.WebSocket = (await import('ws')).default;
|
|
24
119
|
}
|
|
120
|
+
// The update-tracing overlay schedules its redraws on animation
|
|
121
|
+
// frames, which Node does not have; without these the first traced
|
|
122
|
+
// commit throws inside the backend. Unref'd, because an overlay must
|
|
123
|
+
// not be a reason for the process to stay up.
|
|
124
|
+
if (!global.requestAnimationFrame) {
|
|
125
|
+
global.requestAnimationFrame = (callback) => {
|
|
126
|
+
const timer = setTimeout(() => callback(Date.now()), 16);
|
|
127
|
+
timer.unref?.();
|
|
128
|
+
return timer;
|
|
129
|
+
};
|
|
130
|
+
global.cancelAnimationFrame = (timer) => clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
// Defined rather than assigned: *reading* `global.localStorage` first
|
|
133
|
+
// is what makes node print "localStorage is not available because
|
|
134
|
+
// --localstorage-file was not provided" on every DevTools run, and its
|
|
135
|
+
// storage is an unrelated opt-in feature this has no business waiting
|
|
136
|
+
// for.
|
|
137
|
+
for (const [name, persist] of [
|
|
138
|
+
['localStorage', true],
|
|
139
|
+
['sessionStorage', false],
|
|
140
|
+
]) {
|
|
141
|
+
Object.defineProperty(global, name, {
|
|
142
|
+
value: createStorage(persist),
|
|
143
|
+
configurable: true,
|
|
144
|
+
writable: true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// A reload-and-profile restart is the one thing that should look like a
|
|
148
|
+
// page reload rather than a new session: the selection and the
|
|
149
|
+
// profiling flags are handed to the child through its environment (see
|
|
150
|
+
// reloadAndProfile) and seeded back here.
|
|
151
|
+
for (const [key, value] of Object.entries(readSession())) {
|
|
152
|
+
global.sessionStorage.setItem(key, value);
|
|
153
|
+
}
|
|
25
154
|
const mod = await import('react-devtools-core');
|
|
26
155
|
// v7 exposes the API on the default export under node ESM interop
|
|
27
156
|
api = mod.default?.initialize ? mod.default : mod;
|
|
28
|
-
|
|
157
|
+
// The hook settings the user last chose in the DevTools UI — console
|
|
158
|
+
// patching and friends. `initialize()` takes them, `onSettingsUpdated`
|
|
159
|
+
// (below) records them, so a setting survives a restart.
|
|
160
|
+
api.initialize(readHookSettings());
|
|
29
161
|
} catch (err) {
|
|
30
162
|
api = null;
|
|
31
163
|
console.warn(
|
|
@@ -36,58 +168,334 @@ export async function prepare() {
|
|
|
36
168
|
}
|
|
37
169
|
}
|
|
38
170
|
|
|
171
|
+
const HOOK_SETTINGS_KEY = 'react-x11:hookSettings';
|
|
172
|
+
|
|
173
|
+
function readHookSettings() {
|
|
174
|
+
try {
|
|
175
|
+
const raw = global.localStorage?.getItem(HOOK_SETTINGS_KEY);
|
|
176
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
177
|
+
} catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readSession() {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(process.env.REACT_X11_DEVTOOLS_SESSION ?? '{}') ?? {};
|
|
185
|
+
} catch {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The profiling a restart was asked to start with, or null for a normal
|
|
191
|
+
* run. Set by reloadAndProfile() in the process that asked for the
|
|
192
|
+
* restart, read here in the one that came back. */
|
|
193
|
+
function readProfilingOnStart() {
|
|
194
|
+
try {
|
|
195
|
+
const raw = process.env.REACT_X11_DEVTOOLS_PROFILING;
|
|
196
|
+
return raw ? JSON.parse(raw) : null;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* DevTools' "Reload and start profiling": profiling that covers the initial
|
|
204
|
+
* mount cannot be turned on in a running app, so the app restarts with it
|
|
205
|
+
* already on. A browser reloads the page; here that is re-exec'ing this
|
|
206
|
+
* process with the same node flags, the same argv and two extra
|
|
207
|
+
* environment variables — the profiling settings, and the session storage
|
|
208
|
+
* a reload would have kept.
|
|
209
|
+
*/
|
|
210
|
+
export function reloadAndProfile(recordChangeDescriptions, recordTimeline) {
|
|
211
|
+
const child = spawn(
|
|
212
|
+
process.execPath,
|
|
213
|
+
[...process.execArgv, ...process.argv.slice(1)],
|
|
214
|
+
{
|
|
215
|
+
stdio: 'inherit',
|
|
216
|
+
detached: true,
|
|
217
|
+
env: {
|
|
218
|
+
...process.env,
|
|
219
|
+
REACT_X11_DEVTOOLS: '1',
|
|
220
|
+
REACT_X11_DEVTOOLS_PROFILING: JSON.stringify({
|
|
221
|
+
recordChangeDescriptions,
|
|
222
|
+
recordTimeline,
|
|
223
|
+
}),
|
|
224
|
+
REACT_X11_DEVTOOLS_SESSION: JSON.stringify(
|
|
225
|
+
global.sessionStorage?.toJSON?.() ?? {},
|
|
226
|
+
),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
// Only stand down once the replacement is actually running: a spawn that
|
|
231
|
+
// fails must leave the app the user was profiling alive.
|
|
232
|
+
child.on('spawn', () => {
|
|
233
|
+
child.unref();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
});
|
|
236
|
+
child.on('error', (err) => {
|
|
237
|
+
console.warn(
|
|
238
|
+
'react-x11: DevTools asked for a restart to profile, but re-exec ' +
|
|
239
|
+
`failed (${err.code ?? err.message}). The app is still running; ` +
|
|
240
|
+
'profile from here instead.',
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Connect
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
39
249
|
/** Connect the backend to the standalone DevTools app and register the
|
|
40
250
|
* renderer. Called by Reconciler.js right after the renderer is created. */
|
|
41
251
|
export function connect(renderer) {
|
|
42
252
|
if (!api || connected) return;
|
|
43
253
|
connected = true;
|
|
254
|
+
const profiling = readProfilingOnStart();
|
|
44
255
|
|
|
45
256
|
api.connectToDevTools({
|
|
46
257
|
isAppActive: () => true,
|
|
47
258
|
host: process.env.REACT_X11_DEVTOOLS_HOST || 'localhost',
|
|
48
259
|
port: Number(process.env.REACT_X11_DEVTOOLS_PORT) || 8097,
|
|
260
|
+
// The style editor: `style` is a react-x11 style prop (an object, or a
|
|
261
|
+
// nested array of them), which is the shape DevTools' own resolver
|
|
262
|
+
// expects to be handed flat. `validAttributes` is what it offers to
|
|
263
|
+
// add to an element.
|
|
264
|
+
resolveRNStyle: resolveStyle,
|
|
265
|
+
nativeStyleEditorValidAttributes: STYLE_PROP_NAMES,
|
|
266
|
+
// Hook settings (console patching and so on) are the user's, not the
|
|
267
|
+
// run's: keep them where the next run will find them.
|
|
268
|
+
onSettingsUpdated: (settings) => {
|
|
269
|
+
try {
|
|
270
|
+
global.localStorage?.setItem(
|
|
271
|
+
HOOK_SETTINGS_KEY,
|
|
272
|
+
JSON.stringify(settings),
|
|
273
|
+
);
|
|
274
|
+
} catch {
|
|
275
|
+
// an unwritable store is not worth a warning per keystroke
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
// The backend's own check for this asks whether synchronous XHR works,
|
|
279
|
+
// which is a browser question — the honest answer for a node process
|
|
280
|
+
// that can re-exec itself is yes.
|
|
281
|
+
isReloadAndProfileSupported: true,
|
|
282
|
+
isProfiling: profiling != null,
|
|
283
|
+
onReloadAndProfile: reloadAndProfile,
|
|
284
|
+
onReloadAndProfileFlagsReset: () => {
|
|
285
|
+
delete process.env.REACT_X11_DEVTOOLS_PROFILING;
|
|
286
|
+
delete process.env.REACT_X11_DEVTOOLS_SESSION;
|
|
287
|
+
},
|
|
49
288
|
});
|
|
50
289
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
290
|
+
// No argument: react-reconciler 0.33 takes none, and the object this used
|
|
291
|
+
// to pass was discarded in silence. The renderer's name and version now
|
|
292
|
+
// come from the host config, where Reconciler.js already sets them
|
|
293
|
+
// (`rendererPackageName`, `rendererVersion`).
|
|
294
|
+
//
|
|
295
|
+
// Unless it is already registered: `react-x11/test`'s inspect() drives
|
|
296
|
+
// the same backend in-process and may have injected first — a second
|
|
297
|
+
// inject would register a duplicate renderer with the hook, and the
|
|
298
|
+
// standalone app would show two copies of every tree.
|
|
299
|
+
if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__?.rendererInterfaces?.size) {
|
|
300
|
+
renderer.injectIntoDevTools();
|
|
301
|
+
}
|
|
302
|
+
if (profiling) startProfiling(profiling);
|
|
57
303
|
|
|
58
304
|
watchForAgent();
|
|
59
305
|
}
|
|
60
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Turn profiling on before the first commit, which is the whole point of
|
|
309
|
+
* the restart: registering a renderer does not start it (the agent only
|
|
310
|
+
* does that when the frontend asks), so the run that came back from
|
|
311
|
+
* `reloadAndProfile` starts it itself.
|
|
312
|
+
*/
|
|
313
|
+
function startProfiling({ recordChangeDescriptions, recordTimeline }) {
|
|
314
|
+
const interfaces = global.__REACT_DEVTOOLS_GLOBAL_HOOK__?.rendererInterfaces;
|
|
315
|
+
const ri = interfaces && [...interfaces.values()].at(-1);
|
|
316
|
+
try {
|
|
317
|
+
ri?.startProfiling?.(recordChangeDescriptions, recordTimeline);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.warn(
|
|
320
|
+
`react-x11: could not start profiling after restart: ${err.message}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A style prop as the style editor wants it: flat, and only the properties
|
|
326
|
+
* that are really style properties — a state block (`:hover`) or a query is
|
|
327
|
+
* neither editable nor a value, and listing them among the rest would
|
|
328
|
+
* invite editing something that cannot be edited. */
|
|
329
|
+
export function resolveStyle(style) {
|
|
330
|
+
const flat = flattenStyle(style);
|
|
331
|
+
const resolved = {};
|
|
332
|
+
for (const key of Object.keys(flat)) {
|
|
333
|
+
if (isStyleProp(key)) resolved[key] = flat[key];
|
|
334
|
+
}
|
|
335
|
+
return resolved;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// The overlays and the picker
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
// One highlight at a time, whoever asked for it: the tree hover and the
|
|
343
|
+
// picker share the window's single tint, and the loser of a race would
|
|
344
|
+
// otherwise leave its tint behind forever.
|
|
345
|
+
let highlightedRoot = null;
|
|
346
|
+
|
|
347
|
+
/** The retained node behind whatever DevTools handed us — public instances
|
|
348
|
+
* are ntk windows for `<window>`/`<popup>`, nodes everywhere else. */
|
|
349
|
+
function nodeOf(target) {
|
|
350
|
+
const value = Array.isArray(target) ? target[0] : target;
|
|
351
|
+
return value?._reactX11Node ?? value ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function showHighlight(target) {
|
|
355
|
+
const node = nodeOf(target);
|
|
356
|
+
const root = node?.root;
|
|
357
|
+
if (!root || typeof root.setHighlight !== 'function') return;
|
|
358
|
+
if (highlightedRoot && highlightedRoot !== root) {
|
|
359
|
+
highlightedRoot.setHighlight(null);
|
|
360
|
+
}
|
|
361
|
+
highlightedRoot = root;
|
|
362
|
+
root.setHighlight(node);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function hideHighlight() {
|
|
366
|
+
highlightedRoot?.setHighlight(null);
|
|
367
|
+
highlightedRoot = null;
|
|
368
|
+
}
|
|
369
|
+
|
|
61
370
|
/**
|
|
62
371
|
* Wire a DevTools backend agent's element-hover events to the renderer:
|
|
63
372
|
* hovering an element in the DevTools tree tints its rect in the window.
|
|
64
373
|
* Exported separately so it can be tested without react-devtools-core.
|
|
65
374
|
*/
|
|
66
375
|
export function attachHighlightAgent(agent) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
376
|
+
agent.addListener?.('showNativeHighlight', showHighlight);
|
|
377
|
+
agent.addListener?.('hideNativeHighlight', hideHighlight);
|
|
378
|
+
agent.addListener?.('shutdown', hideHighlight);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* "Highlight updates when components render". The backend counts the
|
|
383
|
+
* updates, picks each rect's colour off its own ramp and expires them on
|
|
384
|
+
* its own clock; all it wants from a host is somewhere to draw. It hands
|
|
385
|
+
* over the host instances that re-rendered — every live one, every time —
|
|
386
|
+
* so this replaces the overlay rather than adding to it.
|
|
387
|
+
*/
|
|
388
|
+
export function attachTraceUpdatesAgent(agent) {
|
|
389
|
+
let painted = new Set();
|
|
390
|
+
|
|
391
|
+
const draw = (entries) => {
|
|
392
|
+
const byRoot = new Map();
|
|
393
|
+
for (const entry of entries ?? []) {
|
|
394
|
+
const node = nodeOf(entry?.node);
|
|
395
|
+
const root = node?.root;
|
|
396
|
+
if (!root || root.destroyed) continue;
|
|
397
|
+
if (typeof root.setTraceUpdates !== 'function') continue;
|
|
398
|
+
const rect = node.abs;
|
|
399
|
+
if (!rect?.width) continue;
|
|
400
|
+
const rects = byRoot.get(root) ?? [];
|
|
401
|
+
rects.push({ ...rect, color: entry.color });
|
|
402
|
+
byRoot.set(root, rects);
|
|
78
403
|
}
|
|
79
|
-
|
|
80
|
-
|
|
404
|
+
for (const root of painted) {
|
|
405
|
+
if (!byRoot.has(root) && !root.destroyed) root.setTraceUpdates(null);
|
|
406
|
+
}
|
|
407
|
+
for (const [root, rects] of byRoot) root.setTraceUpdates(rects);
|
|
408
|
+
painted = new Set(byRoot.keys());
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const clear = () => {
|
|
412
|
+
// a window that closed while its outlines were up has nothing to clear
|
|
413
|
+
// and nothing to repaint
|
|
414
|
+
for (const root of painted) {
|
|
415
|
+
if (!root.destroyed) root.setTraceUpdates(null);
|
|
416
|
+
}
|
|
417
|
+
painted = new Set();
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
agent.addListener?.('drawTraceUpdates', draw);
|
|
421
|
+
agent.addListener?.('disableTraceUpdates', clear);
|
|
422
|
+
agent.addListener?.('shutdown', clear);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The element picker — the crosshair in the DevTools toolbar. In a browser
|
|
427
|
+
* the backend listens on the page itself; with no DOM to listen to it says
|
|
428
|
+
* `startInspectingNative` and leaves the pointer to the host. While it is
|
|
429
|
+
* on, the pointer belongs to DevTools (see setInspectHandler in events.js):
|
|
430
|
+
* motion tints whatever is under it, a press selects that element in the
|
|
431
|
+
* tree, Escape gives up.
|
|
432
|
+
*/
|
|
433
|
+
export function attachPickerAgent(agent) {
|
|
434
|
+
let picking = false;
|
|
435
|
+
|
|
436
|
+
const stop = () => {
|
|
437
|
+
if (!picking) return;
|
|
438
|
+
picking = false;
|
|
439
|
+
setInspectHandler(null);
|
|
440
|
+
hideHighlight();
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const onPointer = (kind, node) => {
|
|
444
|
+
if (kind === 'move') {
|
|
445
|
+
if (node) showHighlight(node);
|
|
446
|
+
else hideHighlight();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (kind === 'select' && node) {
|
|
450
|
+
// select first: the frontend leaves picking mode on the second
|
|
451
|
+
// message, and an element that arrives after it is ignored
|
|
452
|
+
agent.selectNode?.(node);
|
|
453
|
+
stop();
|
|
454
|
+
agent.stopInspectingNative?.(true);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
stop();
|
|
458
|
+
agent.stopInspectingNative?.(false);
|
|
81
459
|
};
|
|
82
460
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
461
|
+
const start = () => {
|
|
462
|
+
if (picking) return;
|
|
463
|
+
picking = true;
|
|
464
|
+
setInspectHandler(onPointer);
|
|
86
465
|
};
|
|
87
466
|
|
|
88
|
-
agent.addListener?.('
|
|
89
|
-
agent.addListener?.('
|
|
90
|
-
|
|
467
|
+
agent.addListener?.('startInspectingNative', start);
|
|
468
|
+
agent.addListener?.('shutdown', stop);
|
|
469
|
+
// Giving up from the DevTools side is a bridge message the backend
|
|
470
|
+
// answers by hiding its overlay, with nothing left for a host listener —
|
|
471
|
+
// so this reads the bridge directly rather than leave a picker running
|
|
472
|
+
// that swallows every event the app would have had.
|
|
473
|
+
agent._bridge?.addListener?.('stopInspectingHost', stop);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Select a node in the DevTools tree, if DevTools is attached and knows it.
|
|
478
|
+
* Click-to-component calls this: an Alt+Click that opens the source is also
|
|
479
|
+
* the click that says "this element", and saying it twice is silly.
|
|
480
|
+
*/
|
|
481
|
+
export function selectInDevTools(node) {
|
|
482
|
+
const agent = global.__REACT_DEVTOOLS_GLOBAL_HOOK__?.reactDevtoolsAgent;
|
|
483
|
+
if (!agent || !node) return false;
|
|
484
|
+
try {
|
|
485
|
+
// an element DevTools has no id for is one its filters hid; selecting
|
|
486
|
+
// it would be a message the frontend drops
|
|
487
|
+
if (!agent.getIDForHostInstance?.(node)) return false;
|
|
488
|
+
agent.selectNode(node);
|
|
489
|
+
return true;
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function attachAgent(agent) {
|
|
496
|
+
attachHighlightAgent(agent);
|
|
497
|
+
attachTraceUpdatesAgent(agent);
|
|
498
|
+
attachPickerAgent(agent);
|
|
91
499
|
}
|
|
92
500
|
|
|
93
501
|
function watchForAgent() {
|
|
@@ -95,12 +503,12 @@ function watchForAgent() {
|
|
|
95
503
|
if (!hook) return;
|
|
96
504
|
try {
|
|
97
505
|
if (hook.reactDevtoolsAgent) {
|
|
98
|
-
|
|
506
|
+
attachAgent(hook.reactDevtoolsAgent);
|
|
99
507
|
} else if (typeof hook.on === 'function') {
|
|
100
508
|
// the backend emits 'react-devtools' once the agent is initialized
|
|
101
|
-
hook.on('react-devtools', (agent) =>
|
|
509
|
+
hook.on('react-devtools', (agent) => attachAgent(agent));
|
|
102
510
|
}
|
|
103
511
|
} catch {
|
|
104
|
-
//
|
|
512
|
+
// the overlays are best-effort; the tree view still works without them
|
|
105
513
|
}
|
|
106
514
|
}
|