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
@@ -0,0 +1,306 @@
1
+ // `<svg>`: a retained-node wrapper over ntk's SvgView. The document is
2
+ // either JSX children — declarative SVG elements, one `SvgChildNode` each,
3
+ // serialized into the htmlparser2-style DOM SvgView consumes — or a `source`
4
+ // markup string, and it is sized like `<image>` from its viewBox.
5
+ //
6
+ // This file used to hold `<markdown>`, `<html>` and `<tex>` beside it, over
7
+ // ntk's document widgets. Those widgets rendered a document as one opaque
8
+ // drawing, which foreclosed both selection and per-block streaming; their
9
+ // successors are components in `@react-x11/components` (`<Markdown>`,
10
+ // `<Formula>`), composed from public host elements. `<svg>` stays here
11
+ // because it is not a document — it is a drawing, and one node is the right
12
+ // grain for it.
13
+ import { SvgView } from 'ntk';
14
+
15
+ import { Node, intrinsicSize } from './nodes.js';
16
+ import { isLayoutProp, isPaintProp } from './styles.js';
17
+
18
+ // SVG attributes that are camelCase in SVG itself; every other camelCase
19
+ // prop (strokeWidth, fillRule, textAnchor, …) kebab-cases, like React DOM.
20
+ const SVG_CAMEL_ATTRS = new Set([
21
+ 'viewBox',
22
+ 'preserveAspectRatio',
23
+ 'gradientUnits',
24
+ 'gradientTransform',
25
+ 'spreadMethod',
26
+ 'patternUnits',
27
+ 'patternContentUnits',
28
+ 'patternTransform',
29
+ 'clipPathUnits',
30
+ 'maskUnits',
31
+ 'maskContentUnits',
32
+ 'markerWidth',
33
+ 'markerHeight',
34
+ 'markerUnits',
35
+ 'refX',
36
+ 'refY',
37
+ 'textLength',
38
+ 'lengthAdjust',
39
+ 'stdDeviation',
40
+ ]);
41
+
42
+ const toSvgAttrib = (name) =>
43
+ SVG_CAMEL_ATTRS.has(name)
44
+ ? name
45
+ : name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
46
+
47
+ function svgAttribs(props, skip) {
48
+ const attribs = {};
49
+ for (const key of Object.keys(props)) {
50
+ const value = props[key];
51
+ if (key === 'children' || value == null) continue;
52
+ if (typeof value === 'function') continue;
53
+ if (skip && skip(key)) continue;
54
+ attribs[toSvgAttrib(key)] = String(value);
55
+ }
56
+ return attribs;
57
+ }
58
+
59
+ /**
60
+ * An element inside <svg>: no yoga node, no painting of its own — it only
61
+ * carries a tag name and props, and is serialized into the htmlparser2-style
62
+ * DOM that SvgView consumes. Prop/child changes bubble to the owning
63
+ * SvgNode via the _textContentChanged channel (also used by text chunks,
64
+ * so string children of an SVG <text> invalidate the same way).
65
+ */
66
+ export class SvgChildNode extends Node {
67
+ constructor(tag, props, app) {
68
+ super(tag, props, app, { yoga: false });
69
+ this.isSvgChild = true;
70
+ }
71
+
72
+ // <circle cx r fill>… is SVG's vocabulary, not the style channel's
73
+ get stylable() {
74
+ return false;
75
+ }
76
+
77
+ _textContentChanged() {
78
+ this.parent?._textContentChanged();
79
+ }
80
+
81
+ applyProps(newProps, oldProps) {
82
+ super.applyProps(newProps, oldProps);
83
+ this._textContentChanged();
84
+ }
85
+
86
+ toDom() {
87
+ return {
88
+ type: 'tag',
89
+ name: this.kind,
90
+ attribs: svgAttribs(this.props),
91
+ children: this.children
92
+ .map((c) =>
93
+ c.kind === 'textchunk'
94
+ ? { type: 'text', data: c.text }
95
+ : c.isSvgChild
96
+ ? c.toDom()
97
+ : null,
98
+ )
99
+ .filter(Boolean),
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * <svg>: ntk SvgView. Content is either JSX children (React-DOM style —
106
+ * <svg viewBox="0 0 24 24"><circle cx={12} … />; children win when both
107
+ * are present) or a `source` markup string. Sized like <image>: natural
108
+ * (viewBox) size, kept to its aspect ratio when the style constrains only
109
+ * one axis; scales to the content box.
110
+ */
111
+ export class SvgNode extends Node {
112
+ constructor(props, app) {
113
+ super('svg', props, app);
114
+ this.view = null;
115
+ this._stale = true;
116
+ /** Bumped every time the document is rebuilt, and sampled at paint time,
117
+ * so `paintCachePlan` can tell an animated document from a settled one.
118
+ * See the note there. */
119
+ this._docRevision = 0;
120
+ this._paintedRevision = -1;
121
+ }
122
+
123
+ /** The viewBox is the natural size; `<image>`'s rules do the rest —
124
+ * including the unit: natural sizes are logical pixels, multiplied to
125
+ * device here so an unsized drawing keeps its size relative to
126
+ * everything else. Unlike a bitmap it costs nothing: the document
127
+ * rasterizes at whatever box layout settles on (src/scale.js). */
128
+ measureContent(constraints) {
129
+ const view = this._ensureView();
130
+ const s = this.scale;
131
+ return intrinsicSize(
132
+ {
133
+ width: (view?.naturalWidth ?? 0) * s,
134
+ height: (view?.naturalHeight ?? 0) * s,
135
+ },
136
+ constraints,
137
+ );
138
+ }
139
+
140
+ /** Any change to the svg subtree (props, children, text) lands here. */
141
+ _textContentChanged() {
142
+ this._stale = true;
143
+ this.invalidateMeasure('props');
144
+ }
145
+
146
+ _ensureView() {
147
+ if (!this._stale) return this.view;
148
+ this._stale = false;
149
+ this._docRevision++;
150
+ this.view = null;
151
+ this._docKey = null;
152
+ try {
153
+ const children = this.children.filter(
154
+ (c) => c.isSvgChild || c.kind === 'textchunk',
155
+ );
156
+ if (children.length > 0) {
157
+ const skip = (key) =>
158
+ key === 'source' ||
159
+ isLayoutProp(key) ||
160
+ isPaintProp(key) ||
161
+ key === 'cursor' ||
162
+ key === 'focusable' ||
163
+ key === 'pointerEvents';
164
+ const dom = {
165
+ type: 'tag',
166
+ name: 'svg',
167
+ attribs: svgAttribs(this.props, skip),
168
+ children: children.map((c) =>
169
+ c.kind === 'textchunk' ? { type: 'text', data: c.text } : c.toDom(),
170
+ ),
171
+ };
172
+ // The document's identity, for the paint cache. Built here rather
173
+ // than at paint time on purpose: this runs once per content change,
174
+ // where paint runs every frame — and it is the serialization that
175
+ // makes two cells holding the same drawing one cache entry instead
176
+ // of two. Key order follows prop order, so two logically identical
177
+ // documents written differently miss rather than collide, which is
178
+ // the harmless direction.
179
+ this._docKey = JSON.stringify(dom);
180
+ this.view = new SvgView(null).setSvgDom(dom);
181
+ } else if (this.props.source) {
182
+ this._docKey = String(this.props.source);
183
+ this.view = new SvgView(null).setSvg(this._docKey);
184
+ }
185
+ } catch (err) {
186
+ console.error('react-x11: <svg> failed to parse:', err.message);
187
+ }
188
+ return this.view;
189
+ }
190
+
191
+ applyProps(newProps, oldProps) {
192
+ super.applyProps(newProps, oldProps);
193
+ // attributes live in props (children form) or in `source`; either way
194
+ // the SvgView is cheap to rebuild on the next measure/paint
195
+ this._textContentChanged();
196
+ }
197
+
198
+ paintContent(ctx) {
199
+ const view = this._ensureView();
200
+ if (!view) return;
201
+ const content = this.contentBox();
202
+ if (content.width <= 0 || content.height <= 0) return;
203
+ view.draw(ctx, content.x, content.y, content.width, content.height, {
204
+ color: this._currentColor(),
205
+ });
206
+ }
207
+
208
+ /**
209
+ * What `fill="currentColor"` resolves to here.
210
+ *
211
+ * The node's own `color`, then what it inherits, then the palette's — the
212
+ * same resolution `<text>` and `<canvas mono>` use, and it has to be, or
213
+ * `currentColor` means something different depending on which element is
214
+ * drawing. It used to stop at a hardcoded black, which is invisible on a
215
+ * dark desktop and does not follow a `<box style={{ color }}>` either.
216
+ */
217
+ _currentColor() {
218
+ return this.resolvedTextStyle().color;
219
+ }
220
+
221
+ /**
222
+ * Cache plan (see `Node.paintContent` for the protocol).
223
+ *
224
+ * The key is the document, the size it is drawn at, and the device scale.
225
+ * Everything else that could change the pixels is either handled at blit
226
+ * time or deliberately excluded:
227
+ *
228
+ * - `globalAlpha`, the clip and any ancestor translation are applied by
229
+ * `drawImage`, so an ancestor animating opacity is a cache *hit*;
230
+ * - colour is out of the key for a `mono` document, because the entry is
231
+ * coverage and the colour arrives at blit time — one entry then serves
232
+ * hover, `:disabled` and every theme;
233
+ * - a `multi` document bakes its colours, so the resolved colour joins the
234
+ * key. It rarely varies, so the extra component costs nothing.
235
+ *
236
+ * Sizes are rounded so the blit is unscaled: a scaled `drawImage` brackets
237
+ * every composite with SetPictureTransform and a filter change, which for
238
+ * a wall of icons is four extra requests each. The cost is that a cached
239
+ * drawing sits on the pixel grid where a live one could straddle it, which
240
+ * for icon-sized content is not visible.
241
+ *
242
+ * ## Animated documents opt themselves out
243
+ *
244
+ * A document that was rebuilt since this node's last paint is being
245
+ * animated, and caching it is a pure loss: the entry is written, composited
246
+ * once at most, and never matched again. The cache has a "seen twice" gate
247
+ * for exactly this, but content keying defeats it here — a wall of the same
248
+ * animated drawing at different phases produces documents that recur across
249
+ * *nodes* by coincidence, which is enough sightings to pass the gate and
250
+ * never enough to pay it back. Measured on a wall of 30 animated gauges:
251
+ * 450 of 1076 distinct documents recurred, 12% of lookups hit, and each
252
+ * miss-that-cached bought a CreatePixmap plus a second rasterization of a
253
+ * drawing that had already been painted live. On a server where offscreen
254
+ * rasterization is a software fallback that doubled the frame's fence.
255
+ *
256
+ * So the revision has to match the one this node last painted. A node that
257
+ * has never painted has nothing to compare and stays speculative, exactly
258
+ * as before — the "seen twice" gate is still the thing deciding, so a wall
259
+ * of identical icons fills on its first frame from its own sibling cells.
260
+ * Only a document that then *changes* opts out, which is the signal that it
261
+ * was never going to be matched again.
262
+ */
263
+ paintCachePlan() {
264
+ const view = this._ensureView();
265
+ if (!view || !this._docKey) return null;
266
+ const painted = this._paintedRevision;
267
+ this._paintedRevision = this._docRevision;
268
+ if (painted >= 0 && painted !== this._docRevision) return null;
269
+ const content = this.contentBox();
270
+ const width = Math.round(content.width);
271
+ const height = Math.round(content.height);
272
+ if (width <= 0 || height <= 0) return null;
273
+
274
+ const mono = view.paintKind === 'mono';
275
+ // a mono document paints in exactly one colour: its own, or ours
276
+ const tint = !mono
277
+ ? null
278
+ : view.soloPaint === 'currentColor'
279
+ ? this._currentColor()
280
+ : view.soloPaint;
281
+ if (mono && !tint) return null; // nothing in the document paints at all
282
+
283
+ const size = `${width}x${height}@1`;
284
+ return {
285
+ key: mono
286
+ ? `svg|${size}|${this._docKey}`
287
+ : `svg|${size}|${this._currentColor()}|${this._docKey}`,
288
+ x: Math.round(content.x),
289
+ y: Math.round(content.y),
290
+ width,
291
+ height,
292
+ format: mono ? 'a8' : 'argb32',
293
+ tint,
294
+ };
295
+ }
296
+
297
+ paintCached(ctx, box) {
298
+ const view = this._ensureView();
299
+ if (!view) return;
300
+ // Into a coverage surface, only the alpha of a paint survives, and the
301
+ // tint arrives at blit time — so any opaque colour renders the same mask.
302
+ view.draw(ctx, box.x, box.y, box.width, box.height, {
303
+ color: view.paintKind === 'mono' ? '#ffffff' : this._currentColor(),
304
+ });
305
+ }
306
+ }