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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. 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
- import { createRequire } from 'node:module';
6
-
7
- const require = createRequire(import.meta.url);
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
- api.initialize();
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
- renderer.injectIntoDevTools({
52
- bundleType: 1,
53
- version: require('../package.json').version,
54
- rendererPackageName: 'react-x11',
55
- findFiberByHostInstance: (instance) => instance._reactFiber,
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
- let highlightedRoot = null;
68
-
69
- const show = (payload) => {
70
- // newer devtools versions pass an array of public instances
71
- const target = Array.isArray(payload) ? payload[0] : payload;
72
- // public instances are ntk windows for <window>/<popup>, nodes otherwise
73
- const node = target?._reactX11Node ?? target;
74
- const root = node?.root;
75
- if (!root || typeof root.setHighlight !== 'function') return;
76
- if (highlightedRoot && highlightedRoot !== root) {
77
- highlightedRoot.setHighlight(null);
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
- highlightedRoot = root;
80
- root.setHighlight(node);
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 hide = () => {
84
- highlightedRoot?.setHighlight(null);
85
- highlightedRoot = null;
461
+ const start = () => {
462
+ if (picking) return;
463
+ picking = true;
464
+ setInspectHandler(onPointer);
86
465
  };
87
466
 
88
- agent.addListener?.('showNativeHighlight', show);
89
- agent.addListener?.('hideNativeHighlight', hide);
90
- agent.addListener?.('shutdown', hide);
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
- attachHighlightAgent(hook.reactDevtoolsAgent);
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) => attachHighlightAgent(agent));
509
+ hook.on('react-devtools', (agent) => attachAgent(agent));
102
510
  }
103
511
  } catch {
104
- // highlight support is best-effort; the tree view still works without it
512
+ // the overlays are best-effort; the tree view still works without them
105
513
  }
106
514
  }