react-x11 2.10.2 → 2.12.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 (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +304 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/screencolor.js +62 -0
  20. package/src/cocoa/threaded.js +246 -0
  21. package/src/cocoa/window.js +256 -42
  22. package/src/components/Select.js +2 -2
  23. package/src/components/anchor.js +3 -3
  24. package/src/components/native.js +12 -7
  25. package/src/components/theme.js +2 -2
  26. package/src/debug.js +1 -1
  27. package/src/decorations.js +1 -1
  28. package/src/editmenu.js +2 -2
  29. package/src/errors.js +46 -0
  30. package/src/events.js +6 -6
  31. package/src/foreignnodes.js +3 -2
  32. package/src/frames.js +2 -2
  33. package/src/glnodes.js +1 -1
  34. package/src/grid.js +1653 -0
  35. package/src/host.d.ts +230 -0
  36. package/src/host.js +11 -3
  37. package/src/imagesource.js +1 -1
  38. package/src/index.d.ts +21 -4
  39. package/src/index.js +9 -1
  40. package/src/layouts.js +721 -0
  41. package/src/node.d.ts +4 -2
  42. package/src/node.js +19 -21
  43. package/src/nodes/animation.js +644 -0
  44. package/src/nodes/box.js +21 -0
  45. package/src/nodes/boxpaint.js +473 -0
  46. package/src/nodes/canvas.js +269 -0
  47. package/src/nodes/cascade.js +600 -0
  48. package/src/nodes/damage.js +183 -0
  49. package/src/nodes/edithistory.js +124 -0
  50. package/src/nodes/editmenupopup.js +260 -0
  51. package/src/nodes/hittest.js +185 -0
  52. package/src/nodes/image.js +266 -0
  53. package/src/nodes/install.js +75 -0
  54. package/src/nodes/invalidate.js +465 -0
  55. package/src/nodes/kinds.js +31 -0
  56. package/src/nodes/layout.js +439 -0
  57. package/src/nodes/layouthost.js +949 -0
  58. package/src/nodes/node.js +868 -0
  59. package/src/nodes/paint.js +466 -0
  60. package/src/nodes/position.js +366 -0
  61. package/src/nodes/preedit.js +127 -0
  62. package/src/nodes/queries.js +330 -0
  63. package/src/nodes/rects.js +102 -0
  64. package/src/nodes/scrollable.js +891 -0
  65. package/src/nodes/scrollbars.js +138 -0
  66. package/src/nodes/scrollblit.js +1034 -0
  67. package/src/nodes/selectable.js +142 -0
  68. package/src/nodes/styling.js +225 -0
  69. package/src/nodes/text.js +649 -0
  70. package/src/nodes/textarea.js +391 -0
  71. package/src/nodes/textinput.js +1146 -0
  72. package/src/nodes/util.js +17 -0
  73. package/src/nodes/window/anchoring.js +161 -0
  74. package/src/nodes/window/capabilities.js +190 -0
  75. package/src/nodes/window/debugpaint.js +83 -0
  76. package/src/nodes/window/droptarget.js +145 -0
  77. package/src/nodes/window/floors.js +577 -0
  78. package/src/nodes/window/flush.js +334 -0
  79. package/src/nodes/window/hints.js +482 -0
  80. package/src/nodes/window/listeners.js +222 -0
  81. package/src/nodes/window/popup.js +71 -0
  82. package/src/nodes/window/size.js +591 -0
  83. package/src/nodes/window/window.js +945 -0
  84. package/src/palette.js +1 -1
  85. package/src/registry.js +7 -3
  86. package/src/screencolor.js +212 -38
  87. package/src/screencolorhooks.js +6 -2
  88. package/src/styles.js +137 -15
  89. package/src/svgnodes.js +2 -1
  90. package/src/testing/harness.js +2 -2
  91. package/src/textselection.js +5 -3
  92. package/src/trace-registry.js +1 -1
  93. package/src/types/components.d.ts +38 -6
  94. package/src/types/elements.d.ts +11 -1
  95. package/src/types/nodes.d.ts +33 -5
  96. package/src/types/screencolor.d.ts +20 -14
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
@@ -0,0 +1,269 @@
1
+ // <canvas>: an element that draws with a 2d context, and the putImageData
2
+ // path into it.
3
+
4
+ import { ownerName } from '../errors.js';
5
+ import { isPaintedColor } from './boxpaint.js';
6
+ import { Node } from './node.js';
7
+ import { DEV } from './util.js';
8
+
9
+ /**
10
+ * The one call `onDraw`'s translation cannot reach (#366).
11
+ *
12
+ * `putImageData` ignores the context's transform and clip — the HTML canvas
13
+ * rule, kept by ntk — so inside `onDraw`, whose whole contract is "you are at
14
+ * the node's origin", it is the one call whose coordinates are still the
15
+ * drawable's own. Nothing errors; the pixels simply land at the window
16
+ * origin instead of in the node. `DrawInfo.x`/`.y` is the way to say it
17
+ * right, and development watches for the write that says it wrong: a
18
+ * destination outside the node's box. Once per process — one thread to pull
19
+ * is enough, and `onDraw` runs on every repaint.
20
+ */
21
+ let warnedPutImageData = false;
22
+ export function resetPutImageDataWarningForTests() {
23
+ warnedPutImageData = false;
24
+ }
25
+
26
+ /** The rect `putImageData(data, x, y, …dirty)` actually writes — the spec's
27
+ * dirty-rect normalisation, reproduced so the watch judges the write and
28
+ * not the whole source image (a correct call may blit a window of a bigger
29
+ * atlas). Null when the write is empty. */
30
+ function putImageDataDest(data, x, y, dx = 0, dy = 0, dw, dh) {
31
+ const width = data?.width ?? 0;
32
+ const height = data?.height ?? 0;
33
+ dw ??= width;
34
+ dh ??= height;
35
+ if (dw < 0) {
36
+ dx += dw;
37
+ dw = -dw;
38
+ }
39
+ if (dh < 0) {
40
+ dy += dh;
41
+ dh = -dh;
42
+ }
43
+ const sx = Math.max(0, dx);
44
+ const sy = Math.max(0, dy);
45
+ const sw = Math.min(width, dx + dw) - sx;
46
+ const sh = Math.min(height, dy + dh) - sy;
47
+ if (!(sw > 0) || !(sh > 0)) return null;
48
+ return { x: x + sx, y: y + sy, width: sw, height: sh };
49
+ }
50
+
51
+ /** Escape hatch: a retained node whose content is painted by props.onDraw. */
52
+ export class CanvasNode extends Node {
53
+ constructor(props, app) {
54
+ super('canvas', props, app);
55
+ }
56
+
57
+ applyProps(newProps, oldProps) {
58
+ const before = oldProps ?? this.props;
59
+ super.applyProps(newProps, oldProps);
60
+ // onDraw is read at paint time, so a new closure means new content — but
61
+ // it also matches /^on[A-Z]/, which is how the base class recognises an
62
+ // event handler, so `paintChanged` skips it and this is the only place
63
+ // that notices. Damage is bounded to this canvas: an unbounded call here
64
+ // made every re-render of a component that draws through <canvas> repaint
65
+ // the whole window, which is what a Checkbox's tick and a Select's chevron
66
+ // both do.
67
+ if (newProps.onDraw !== before.onDraw) {
68
+ this.root?.invalidate(false, this, 'props');
69
+ }
70
+ }
71
+
72
+ /**
73
+ * The ink a `mono` drawing is painted in: the node's own `color`, then what
74
+ * it inherits, then the palette's — exactly as `<text>` and `<svg>` resolve
75
+ * theirs, because it is literally the same resolution. An `<Icon>` in a
76
+ * row that dims itself dims with it, with nothing handed over at the call
77
+ * site.
78
+ */
79
+ _monoColor() {
80
+ return this.resolvedTextStyle().color;
81
+ }
82
+
83
+ /** Preset the ink a `mono` drawing inherits, so `onDraw` never names a
84
+ * colour of its own. Called inside the `save()`/`restore()` pair. */
85
+ _presetMono(ctx, color) {
86
+ ctx.fillStyle = color;
87
+ ctx.strokeStyle = color;
88
+ }
89
+
90
+ paintContent(ctx) {
91
+ const onDraw = this.props.onDraw;
92
+ if (typeof onDraw !== 'function') return;
93
+ ctx.save();
94
+ ctx.beginPath();
95
+ ctx.rect(this.abs.x, this.abs.y, this.abs.width, this.abs.height);
96
+ ctx.clip();
97
+ ctx.translate(this.abs.x, this.abs.y);
98
+ if (this.props.mono) this._presetMono(ctx, this._monoColor());
99
+ const unwatch = DEV ? this._watchPutImageData(ctx) : null;
100
+ try {
101
+ // Device pixels, the browser's own canvas contract: the backing
102
+ // store is the panel's grid and `scale` says how many of its pixels
103
+ // one logical pixel is worth. Deliberately NOT a ctx.scale() — ntk
104
+ // positions glyphs through the transform but sizes them from the
105
+ // font, so a scaled transform would move an app's fillText without
106
+ // growing it (src/scale.js).
107
+ onDraw(ctx, {
108
+ width: this.abs.width,
109
+ height: this.abs.height,
110
+ // the translation above, said out loud: raw-pixel calls address the
111
+ // drawable itself, so they are written at `info.x + x` (#366)
112
+ x: this.abs.x,
113
+ y: this.abs.y,
114
+ scale: this.scale,
115
+ node: this,
116
+ });
117
+ } finally {
118
+ unwatch?.();
119
+ ctx.restore();
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Shadow `ctx.putImageData` for the length of one `onDraw`, and warn on a
125
+ * write whose destination escapes the node's box — either coordinates
126
+ * that were never offset by `info.x`/`info.y`, which is the trap the
127
+ * watch exists for, or a drawing genuinely reaching outside bounds every
128
+ * other call is clipped to. The box test has a pixel of slack per edge:
129
+ * `abs` can sit on a fractional grid, and a rounded `info.x` must not
130
+ * read as an escape. Development only, and the shadow is not installed
131
+ * again once the warning has fired, so steady state costs nothing.
132
+ */
133
+ _watchPutImageData(ctx) {
134
+ const original = ctx.putImageData;
135
+ if (warnedPutImageData || typeof original !== 'function') return null;
136
+ const hadOwn = Object.hasOwn(ctx, 'putImageData');
137
+ const node = this;
138
+ ctx.putImageData = function (data, x, y, ...dirty) {
139
+ const dest = putImageDataDest(data, x, y, ...dirty);
140
+ const box = node.abs;
141
+ if (
142
+ !warnedPutImageData &&
143
+ dest &&
144
+ (dest.x < box.x - 1 ||
145
+ dest.y < box.y - 1 ||
146
+ dest.x + dest.width > box.x + box.width + 1 ||
147
+ dest.y + dest.height > box.y + box.height + 1)
148
+ ) {
149
+ warnedPutImageData = true;
150
+ const owner = ownerName(node);
151
+ console.warn(
152
+ `react-x11: putImageData in <canvas onDraw>${owner ? ` (in ${owner})` : ''} ` +
153
+ `wrote ${dest.width}x${dest.height} at ${dest.x},${dest.y} — outside the ` +
154
+ `node, which is ${box.width}x${box.height} at ${box.x},${box.y}. ` +
155
+ "putImageData ignores the context's transform (the HTML canvas rule), " +
156
+ "so unlike every other call in onDraw its coordinates are the drawable's, " +
157
+ "not the node's. Add the node's origin, which onDraw is handed: " +
158
+ 'ctx.putImageData(data, info.x + x, info.y + y). Better, draw through an ' +
159
+ 'image source — it honours the transform and the clip, and caches its ' +
160
+ 'upload server-side: ctx.drawImage(new Image({ width, height, data }), x, y), ' +
161
+ 'with Image from \'react-x11/ntk\'. See docs/elements.md, "<canvas>".',
162
+ );
163
+ }
164
+ return original.call(ctx, data, x, y, ...dirty);
165
+ };
166
+ return () => {
167
+ if (hadOwn) ctx.putImageData = original;
168
+ else delete ctx.putImageData;
169
+ };
170
+ }
171
+
172
+ /**
173
+ * `<canvas cacheKey>` opts a drawing into the paint cache.
174
+ *
175
+ * This one has to be opt-in, and the reason is worth stating: `onDraw` is
176
+ * an opaque closure. Nothing here can know what it reads — a prop, a ref, a
177
+ * clock, a module variable — and its identity changes on every render
178
+ * unless the app memoizes it, so it is not a key either. Only the author
179
+ * knows, so the author says:
180
+ *
181
+ * <canvas cacheKey={`spark:${series.id}:${w}x${h}`} onDraw={draw} />
182
+ *
183
+ * The rule is the protocol's rule: the key must name every input the
184
+ * drawing reads. A `cacheKey` that leaves one out shows stale pixels, so
185
+ * develop with `REACT_X11_PAINT_CACHE=verify`, which turns exactly that
186
+ * mistake into a loud complaint.
187
+ *
188
+ * `<canvas>` needs no `paintCached` of its own beyond the mono preset: it
189
+ * already draws origin-relative, so the cached render and the live one are
190
+ * the same code.
191
+ *
192
+ * ## `mono`: coverage, and the colour out of the key
193
+ *
194
+ * `<canvas mono>` is a promise about the drawing — *everything I paint is
195
+ * one colour, and it is not mine to choose*. `onDraw` then names no colour
196
+ * at all: `fillStyle` and `strokeStyle` arrive preset from `style.color`.
197
+ *
198
+ * That promise is what lets the entry be an **a8 coverage** surface with
199
+ * the colour applied at blit time, so the colour leaves the key: one
200
+ * rendered copy of a chevron serves the resting row, the highlighted row,
201
+ * the disabled one and both schemes. Without it each colour is a separate
202
+ * argb32 entry, which for an icon in four states is four rasterizations
203
+ * and four pixmaps of the same shape. `SvgView.paintKind` decides the same
204
+ * thing by scanning the document; a closure cannot be scanned, so here the
205
+ * author says it.
206
+ *
207
+ * A drawing that sets its own `fillStyle` under `mono` is a bug the digest
208
+ * catches: colour is out of the key, so two colours of one drawing collide
209
+ * on one entry and `REACT_X11_PAINT_CACHE=verify` complains.
210
+ *
211
+ * Needs **ntk ≥ 7.3.3**, and the floor is not cosmetic. Coverage
212
+ * composites through ntk's `_drawCoverage`, which routes a clip it cannot
213
+ * express as a rectangle through a scratch mask — and before 7.3.3 that
214
+ * path read the surface-sized mask from the origin rather than from the
215
+ * destination, so anything not drawn at (0, 0) was masked out entirely
216
+ * (sidorares/ntk#243). Nested rounded clips are the common case, not an
217
+ * exotic one: `examples/tasks.jsx` puts a checkbox tick under a rounded
218
+ * card, a scrolled list, a rounded row and a rounded well, and five of its
219
+ * six ticks came out blank. `package.json` carries the floor.
220
+ */
221
+ paintCachePlan() {
222
+ const { cacheKey, onDraw, mono } = this.props;
223
+ if (cacheKey == null || typeof onDraw !== 'function') return null;
224
+ const width = Math.ceil(this.abs.width);
225
+ const height = Math.ceil(this.abs.height);
226
+ if (width <= 0 || height <= 0) return null;
227
+ const tint = mono ? this._monoColor() : null;
228
+ // Nothing to composite through: an unpainted colour would blit the
229
+ // coverage as-is, which is not what "invisible" looks like.
230
+ if (mono && !isPaintedColor(tint)) return null;
231
+ return {
232
+ key: `canvas|${width}x${height}@1|${mono ? 'mono|' : ''}${cacheKey}`,
233
+ x: Math.round(this.abs.x),
234
+ y: Math.round(this.abs.y),
235
+ width,
236
+ height,
237
+ format: mono ? 'a8' : 'argb32',
238
+ tint,
239
+ };
240
+ }
241
+
242
+ paintCached(ctx, box, ink = '#ffffff') {
243
+ const onDraw = this.props.onDraw;
244
+ if (typeof onDraw !== 'function') return;
245
+ ctx.save();
246
+ ctx.beginPath();
247
+ ctx.rect(box.x, box.y, box.width, box.height);
248
+ ctx.clip();
249
+ ctx.translate(box.x, box.y);
250
+ // Into a coverage surface only the alpha of a paint survives and the
251
+ // tint arrives at blit time, so any opaque colour renders the same mask
252
+ // — and the cache says white then, or the tint where it bakes colour.
253
+ if (this.props.mono) this._presetMono(ctx, ink);
254
+ try {
255
+ onDraw(ctx, {
256
+ width: box.width,
257
+ height: box.height,
258
+ // the drawing goes into a surface of its own here, so the node's
259
+ // origin in it *is* the origin — and a raw-pixel write offset by
260
+ // `info.x`/`info.y` for the live path stays correct under a cacheKey
261
+ x: box.x,
262
+ y: box.y,
263
+ node: this,
264
+ });
265
+ } finally {
266
+ ctx.restore();
267
+ }
268
+ }
269
+ }