react-x11 2.11.0 → 2.13.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 +12 -4
  3. package/src/Reconciler.js +19 -31
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/appcontext.js +59 -30
  7. package/src/bootstrap.js +14 -0
  8. package/src/clientmessage.js +1 -1
  9. package/src/cocoa/app.js +303 -49
  10. package/src/cocoa/bezels.js +175 -30
  11. package/src/cocoa/dnd.js +27 -13
  12. package/src/cocoa/fonts.js +3 -3
  13. package/src/cocoa/glarea.js +24 -5
  14. package/src/cocoa/main.d.ts +8 -0
  15. package/src/cocoa/main.js +43 -0
  16. package/src/cocoa/overlay.js +159 -0
  17. package/src/cocoa/panehost.js +15 -5
  18. package/src/cocoa/presenter.js +13 -9
  19. package/src/cocoa/promotion.js +17 -7
  20. package/src/cocoa/relaunch.js +207 -0
  21. package/src/cocoa/threaded.js +246 -0
  22. package/src/cocoa/window.js +256 -42
  23. package/src/components/Select.js +2 -2
  24. package/src/components/anchor.js +3 -3
  25. package/src/components/native.js +12 -7
  26. package/src/components/theme.js +2 -2
  27. package/src/debug.js +1 -1
  28. package/src/decorations.js +1 -1
  29. package/src/editmenu.js +2 -2
  30. package/src/embedding.js +31 -0
  31. package/src/errors.js +46 -0
  32. package/src/events.js +78 -18
  33. package/src/foreignnodes.js +59 -5
  34. package/src/frames.js +2 -2
  35. package/src/glnodes.js +172 -41
  36. package/src/gloverlay.js +383 -0
  37. package/src/grid.js +1653 -0
  38. package/src/host.d.ts +230 -1
  39. package/src/host.js +11 -3
  40. package/src/imagesource.js +1 -1
  41. package/src/index.d.ts +34 -4
  42. package/src/index.js +9 -1
  43. package/src/layouts.js +721 -0
  44. package/src/node.d.ts +16 -3
  45. package/src/node.js +19 -21
  46. package/src/nodes/animation.js +644 -0
  47. package/src/nodes/box.js +21 -0
  48. package/src/nodes/boxpaint.js +473 -0
  49. package/src/nodes/canvas.js +269 -0
  50. package/src/nodes/cascade.js +600 -0
  51. package/src/nodes/damage.js +183 -0
  52. package/src/nodes/edithistory.js +124 -0
  53. package/src/nodes/editmenupopup.js +260 -0
  54. package/src/nodes/hittest.js +185 -0
  55. package/src/nodes/image.js +266 -0
  56. package/src/nodes/install.js +75 -0
  57. package/src/nodes/invalidate.js +465 -0
  58. package/src/nodes/kinds.js +31 -0
  59. package/src/nodes/layout.js +439 -0
  60. package/src/nodes/layouthost.js +949 -0
  61. package/src/nodes/node.js +868 -0
  62. package/src/nodes/paint.js +466 -0
  63. package/src/nodes/position.js +366 -0
  64. package/src/nodes/preedit.js +127 -0
  65. package/src/nodes/queries.js +330 -0
  66. package/src/nodes/rects.js +102 -0
  67. package/src/nodes/scrollable.js +891 -0
  68. package/src/nodes/scrollbars.js +138 -0
  69. package/src/nodes/scrollblit.js +1034 -0
  70. package/src/nodes/selectable.js +142 -0
  71. package/src/nodes/styling.js +225 -0
  72. package/src/nodes/text.js +649 -0
  73. package/src/nodes/textarea.js +391 -0
  74. package/src/nodes/textinput.js +1146 -0
  75. package/src/nodes/util.js +17 -0
  76. package/src/nodes/window/anchoring.js +161 -0
  77. package/src/nodes/window/capabilities.js +190 -0
  78. package/src/nodes/window/debugpaint.js +83 -0
  79. package/src/nodes/window/droptarget.js +145 -0
  80. package/src/nodes/window/floors.js +577 -0
  81. package/src/nodes/window/flush.js +369 -0
  82. package/src/nodes/window/hints.js +482 -0
  83. package/src/nodes/window/listeners.js +222 -0
  84. package/src/nodes/window/popup.js +71 -0
  85. package/src/nodes/window/size.js +591 -0
  86. package/src/nodes/window/window.js +954 -0
  87. package/src/palette.js +1 -1
  88. package/src/registry.js +7 -3
  89. package/src/styles.js +137 -15
  90. package/src/svgnodes.js +2 -1
  91. package/src/testing/harness.js +2 -2
  92. package/src/textselection.js +5 -3
  93. package/src/trace-registry.js +1 -1
  94. package/src/types/components.d.ts +38 -6
  95. package/src/types/elements.d.ts +26 -14
  96. package/src/types/nodes.d.ts +17 -2
  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
@@ -18,11 +18,44 @@
18
18
  // frame padded back out by those insets and blitted from the ink region.
19
19
  // Scanned rather than hard-coded so a macOS release that redraws its
20
20
  // controls moves the answer instead of breaking it.
21
+ //
22
+ // ## On a worker
23
+ //
24
+ // In threaded mode (src/cocoa/main.js) the bridge makes the control on the
25
+ // UI thread and answers a measure or a draw through a callback
26
+ // (windowkit/appkit#54) — a cell made on the worker drew correct pixels
27
+ // and then crashed the process at exit. So nothing here can ask and use
28
+ // the answer in one call. The metrics layout reads synchronously, `natural`
29
+ // and `shadow`, are fetched for every kind at startup (`prefetch`). A bezel
30
+ // `get` has not drawn yet answers null and calls back once it has, and the
31
+ // caller draws what it had meanwhile. A bezel drawn at rest brings its
32
+ // pressed twin along, so the press — the input whose answer is the bezel —
33
+ // never waits for one (AGENTS.md, "Answer the input").
34
+
35
+ const KINDS = ['push', 'checkbox', 'radio', 'popup', 'slider', 'switch'];
36
+ // the sizes the widget set lays its controls out at (components/native.js)
37
+ const SIZES = ['regular', 'small'];
38
+ // the fullest state, so the scan sees the whole footprint
39
+ const SCAN_STATE = Object.freeze({
40
+ state: 1,
41
+ value: 0.5,
42
+ enabled: true,
43
+ appearance: 'light',
44
+ });
45
+
21
46
  export class BezelStore {
22
- constructor(native) {
47
+ /**
48
+ * `answersLater` says whether the bridge answers through a callback —
49
+ * asked at each call, since the app knows only once it has started.
50
+ */
51
+ constructor(native, { answersLater = () => false } = {}) {
23
52
  this._native = native;
53
+ this._answersLater = answersLater;
24
54
  this._canonical = new Map(); // kind|size|scale → { insets, natural }
55
+ this._scanning = new Map(); // kind|size|scale → the scan in flight
25
56
  this._cache = new Map(); // full param key → { surface, sx, sy, sw, sh }
57
+ this._drawing = new Map(); // full param key → callbacks awaiting it
58
+ this._gen = 0;
26
59
  this._MAX = 160;
27
60
  }
28
61
 
@@ -40,16 +73,21 @@ export class BezelStore {
40
73
  clear() {
41
74
  // the surfaces are freed by their External finalizer
42
75
  this._cache.clear();
76
+ // a draw still out is in the old accent: it goes to its callers, and
77
+ // not into the cache
78
+ this._gen++;
43
79
  }
44
80
 
45
81
  /**
46
82
  * The control's natural size in logical px — the size the bezel is
47
83
  * designed at, which layout adopts for the kinds that must not stretch
48
84
  * (checkbox, radio, switch). For the stretchable kinds only `height` is
49
- * meaningful: a push button is as wide as its label needs.
85
+ * meaningful: a push button is as wide as its label needs. Null on a
86
+ * worker for a size `prefetch` did not cover, until its scan is in.
50
87
  */
51
88
  natural(kind, controlSize = 'regular') {
52
89
  const c = this._scan(kind, controlSize, 2);
90
+ if (!c) return null;
53
91
  return {
54
92
  width: Math.round(c.natural.width),
55
93
  height: Math.round(c.natural.height),
@@ -65,20 +103,38 @@ export class BezelStore {
65
103
  */
66
104
  shadow(kind, controlSize = 'regular') {
67
105
  const c = this._scan(kind, controlSize, 2);
106
+ if (!c) return { top: 0, bottom: 0 };
68
107
  return {
69
108
  top: Math.round(c.body.top),
70
109
  bottom: Math.round(c.body.bottom),
71
110
  };
72
111
  }
73
112
 
113
+ /**
114
+ * Scan every kind at the widget set's sizes, now: at scale 2 for the
115
+ * metrics (`natural` measures there) and at `scale` for the bezels the
116
+ * app will draw. Resolves once all are in. For a worker, where a scan is
117
+ * two answers from the UI thread; on the main thread it is not needed,
118
+ * each scan running the first time it is asked for.
119
+ */
120
+ prefetch(scale) {
121
+ const jobs = [];
122
+ for (const s of new Set([2, scale])) {
123
+ for (const kind of KINDS) {
124
+ for (const size of SIZES) jobs.push(this._scanLater(kind, size, s));
125
+ }
126
+ }
127
+ return Promise.all(jobs).then(() => undefined);
128
+ }
129
+
74
130
  /**
75
131
  * The bezel for one laid-out box: `w`/`h` in device px, blit-ready.
76
132
  * Returns `{ surface, sx, sy, sw, sh }` — draw with the 9-arg
77
- * `ctx.drawImage` so the ink region lands exactly on the box.
133
+ * `ctx.drawImage` so the ink region lands exactly on the box. On a worker
134
+ * a bezel not drawn yet is null, and `onReady` is called once it is in.
78
135
  */
79
- get(params, w, h, scale) {
136
+ get(params, w, h, scale, onReady = null) {
80
137
  const controlSize = params.controlSize ?? 'regular';
81
- const c = this._scan(params.kind, controlSize, scale);
82
138
  const key = JSON.stringify([
83
139
  params.kind,
84
140
  controlSize,
@@ -92,7 +148,7 @@ export class BezelStore {
92
148
  h,
93
149
  scale,
94
150
  ]);
95
- let entry = this._cache.get(key);
151
+ const entry = this._cache.get(key);
96
152
  if (entry) {
97
153
  // Map order is the recency order: re-inserting keeps the hot bezels
98
154
  // at the young end when the cache is over budget.
@@ -100,6 +156,23 @@ export class BezelStore {
100
156
  this._cache.set(key, entry);
101
157
  return entry;
102
158
  }
159
+ if (this._answersLater()) {
160
+ this._drawLater(key, { ...params, controlSize }, w, h, scale, onReady);
161
+ return null;
162
+ }
163
+ const c = this._scan(params.kind, controlSize, scale);
164
+ const frame = this._frame(c, w, h, scale);
165
+ this._native.drawControlIntoSurface(frame.surface, {
166
+ ...params,
167
+ controlSize,
168
+ });
169
+ this._store(key, frame.entry);
170
+ return frame.entry;
171
+ }
172
+
173
+ /** A surface padded out by the kind's insets, and the region of it a box
174
+ * `w` × `h` blits from. */
175
+ _frame(c, w, h, scale) {
103
176
  const fw = w / scale + c.insets.left + c.insets.right;
104
177
  const fh = h / scale + c.insets.top + c.insets.bottom;
105
178
  const surface = this._native.createSurface(
@@ -107,51 +180,123 @@ export class BezelStore {
107
180
  Math.max(1, Math.round(fh * scale)),
108
181
  scale,
109
182
  );
110
- this._native.drawControlIntoSurface(surface, {
111
- ...params,
112
- controlSize,
113
- });
114
- entry = {
183
+ return {
115
184
  surface,
116
- sx: Math.round(c.insets.left * scale),
117
- sy: Math.round(c.insets.top * scale),
118
- sw: w,
119
- sh: h,
185
+ entry: {
186
+ surface,
187
+ sx: Math.round(c.insets.left * scale),
188
+ sy: Math.round(c.insets.top * scale),
189
+ sw: w,
190
+ sh: h,
191
+ },
120
192
  };
193
+ }
194
+
195
+ _store(key, entry) {
121
196
  this._cache.set(key, entry);
122
197
  if (this._cache.size > this._MAX) {
123
198
  // eldest first; the surface itself is freed by its External finalizer
124
199
  const oldest = this._cache.keys().next().value;
125
200
  this._cache.delete(oldest);
126
201
  }
127
- return entry;
202
+ }
203
+
204
+ /** A worker's `get`: one draw per key however many ask, each asker called
205
+ * back when it lands. The surface is left alone until then — the bridge
206
+ * draws into its bitmap on the UI thread. */
207
+ _drawLater(key, params, w, h, scale, onReady) {
208
+ const waiting = this._drawing.get(key);
209
+ if (waiting) {
210
+ if (onReady) waiting.add(onReady);
211
+ return;
212
+ }
213
+ const callbacks = new Set(onReady ? [onReady] : []);
214
+ this._drawing.set(key, callbacks);
215
+ const gen = this._gen;
216
+ this._scanLater(params.kind, params.controlSize, scale).then((c) => {
217
+ const frame = this._frame(c, w, h, scale);
218
+ this._native.drawControlIntoSurface(frame.surface, params, () => {
219
+ this._drawing.delete(key);
220
+ if (gen === this._gen) this._store(key, frame.entry);
221
+ for (const fn of callbacks) fn();
222
+ // the twin a press will ask for, drawn before the press
223
+ if (!params.pressed && params.enabled !== false) {
224
+ this.get({ ...params, pressed: true }, w, h, scale);
225
+ }
226
+ });
227
+ });
128
228
  }
129
229
 
130
230
  /**
131
231
  * One render + alpha scan per (kind, size, scale): where does this cell
132
232
  * actually put ink inside the frame it is given? The frame is the natural
133
233
  * cellSize widened by 24pt so a stretchable bezel's side margins are
134
- * visible as margins rather than crowding the ends.
234
+ * visible as margins rather than crowding the ends. Null on a worker when
235
+ * the scan is not in yet: it is asked for, and answers later.
135
236
  */
136
237
  _scan(kind, controlSize, scale) {
137
238
  const key = `${kind}|${controlSize}|${scale}`;
138
- let c = this._canonical.get(key);
139
- if (c) return c;
239
+ const done = this._canonical.get(key);
240
+ if (done) return done;
241
+ if (this._answersLater()) {
242
+ this._scanLater(kind, controlSize, scale);
243
+ return null;
244
+ }
140
245
  const m = this._native.measureControl({ kind, controlSize });
246
+ const frame = this._scanFrame(m, scale);
247
+ this._native.drawControlIntoSurface(frame.surface, {
248
+ kind,
249
+ controlSize,
250
+ ...SCAN_STATE,
251
+ });
252
+ return this._inkBox(key, frame, scale);
253
+ }
254
+
255
+ /** `_scan` through the bridge's callbacks: once per key, however many
256
+ * ask. */
257
+ _scanLater(kind, controlSize, scale) {
258
+ const key = `${kind}|${controlSize}|${scale}`;
259
+ const done = this._canonical.get(key);
260
+ if (done) return Promise.resolve(done);
261
+ let job = this._scanning.get(key);
262
+ if (job) return job;
263
+ const native = this._native;
264
+ job = new Promise((resolve) =>
265
+ native.measureControl({ kind, controlSize }, resolve),
266
+ )
267
+ .then((m) => {
268
+ const frame = this._scanFrame(m, scale);
269
+ return new Promise((resolve) =>
270
+ native.drawControlIntoSurface(
271
+ frame.surface,
272
+ { kind, controlSize, ...SCAN_STATE },
273
+ () => resolve(frame),
274
+ ),
275
+ );
276
+ })
277
+ .then((frame) => {
278
+ this._scanning.delete(key);
279
+ return this._inkBox(key, frame, scale);
280
+ });
281
+ this._scanning.set(key, job);
282
+ return job;
283
+ }
284
+
285
+ _scanFrame(m, scale) {
141
286
  const fw = m.width + 24;
142
287
  const fh = m.height;
143
288
  const pw = Math.max(1, Math.round(fw * scale));
144
289
  const ph = Math.max(1, Math.round(fh * scale));
145
- const surface = this._native.createSurface(pw, ph, scale);
146
- this._native.drawControlIntoSurface(surface, {
147
- kind,
148
- controlSize,
149
- // the fullest state, so the scan sees the whole footprint
150
- state: 1,
151
- value: 0.5,
152
- enabled: true,
153
- appearance: 'light',
154
- });
290
+ return {
291
+ surface: this._native.createSurface(pw, ph, scale),
292
+ fw,
293
+ fh,
294
+ pw,
295
+ ph,
296
+ };
297
+ }
298
+
299
+ _inkBox(key, { surface, fw, fh, pw, ph }, scale) {
155
300
  const buf = this._native.ctxGetImageData(surface, 0, 0, pw, ph);
156
301
  let x0 = pw;
157
302
  let y0 = ph;
@@ -189,7 +334,7 @@ export class BezelStore {
189
334
  by0 = y0;
190
335
  by1 = y1;
191
336
  }
192
- c = {
337
+ const c = {
193
338
  insets: {
194
339
  left: x0 / scale,
195
340
  top: y0 / scale,
package/src/cocoa/dnd.js CHANGED
@@ -149,28 +149,42 @@ export function requestedAction(operations) {
149
149
  * `DropSession.localDrop` hands the handler: `items` by type, the parsed
150
150
  * `files`, the best `text`, and a `getData` answering from the same read.
151
151
  */
152
- export function readPayload(native, types) {
152
+ export function readPayload(native, types, carried = null) {
153
153
  const values = {};
154
154
  const urls = [];
155
- const items = (() => {
156
- try {
157
- return native.dragItems?.() ?? [];
158
- } catch {
159
- return [];
160
- }
161
- })();
162
- items.forEach((item, index) => {
155
+ // On a worker the drop carries its payload's text and URL forms itself
156
+ // (`items: [{ types, strings }]`, windowkit/appkit#51), since the drag
157
+ // pasteboard cannot be read back from there inside the callback; its
158
+ // other types are offered, and read as nothing.
159
+ const read = carried
160
+ ? {
161
+ items: carried,
162
+ string: (index, uti) => carried[index]?.strings?.[uti] ?? null,
163
+ data: () => null,
164
+ }
165
+ : {
166
+ items: (() => {
167
+ try {
168
+ return native.dragItems?.() ?? [];
169
+ } catch {
170
+ return [];
171
+ }
172
+ })(),
173
+ string: (index, uti) => native.dragItemString(index, uti),
174
+ data: (index, uti) => native.dragItemData(index, uti),
175
+ };
176
+ read.items.forEach((item, index) => {
163
177
  for (const uti of item?.types ?? []) {
164
178
  if (uti === 'public.file-url' || uti === 'public.url') {
165
- const url = native.dragItemString(index, uti);
179
+ const url = read.string(index, uti);
166
180
  if (url) urls.push(url);
167
181
  continue;
168
182
  }
169
183
  const mime = mimeFromUti(uti, native);
170
184
  if (!mime || values[mime] !== undefined) continue;
171
185
  const value = isTextual(uti, mime)
172
- ? native.dragItemString(index, uti)
173
- : native.dragItemData(index, uti);
186
+ ? read.string(index, uti)
187
+ : read.data(index, uti);
174
188
  if (value == null) continue;
175
189
  values[mime] = value;
176
190
  if (
@@ -360,7 +374,7 @@ export class CocoaDropTransport {
360
374
  const offer = this._offer(ev);
361
375
  const extras = drag
362
376
  ? drag._dropExtras()
363
- : readPayload(this.wnd._native, offer.types);
377
+ : readPayload(this.wnd._native, offer.types, ev.items ?? null);
364
378
  const outcome = this.session.localDrop(offer, extras, Date.now());
365
379
  if (drag && outcome.handled) drag.currentAction = outcome.action;
366
380
  const response = { accept: outcome.handled };
@@ -17,7 +17,7 @@
17
17
  // fontShapeText / fontWithSize / ctxDrawGlyphs).
18
18
  //
19
19
  // Index spaces, because two meet here: `lines[].start/end` and
20
- // `runs[].start/end` are UTF-16 code units (what `rangeBands` in nodes.js
20
+ // `runs[].start/end` are UTF-16 code units (what `rangeBands` in nodes/text.js
21
21
  // compares against), while `caretPosition()` takes and `indexAt()` returns
22
22
  // code points (what the selection and caret code speak). CoreText itself is
23
23
  // UTF-16 end to end; the code-point conversion happens at this boundary and
@@ -936,7 +936,7 @@ export class CocoaFontManager {
936
936
  }
937
937
  // A width offer of zero is a question, not a degenerate layout: yoga
938
938
  // asks it to find the node's min-content floor (`minWidth: 'auto'`,
939
- // nodes.js). It used to fall into the `undefined` below and answer
939
+ // nodes/window/floors.js). It used to fall into the `undefined` below and answer
940
940
  // max-content — the whole paragraph on one line — so a `<text>` in a
941
941
  // flex item held its container open at its longest line and two equal
942
942
  // columns came out 823px and 34px wide. `brokenAtEveryOpportunity` is
@@ -944,7 +944,7 @@ export class CocoaFontManager {
944
944
  const minContent = Number.isFinite(maxWidth) && maxWidth <= 0;
945
945
  // Whether this paragraph is one that ends in a `…`. Both halves: ntk
946
946
  // elides off the line *count*, so an ellipsis with nothing to cap can
947
- // never fire (nodes.js `_maxLines`), and neither can this.
947
+ // never fire (nodes/text.js `_maxLines`), and neither can this.
948
948
  const elides = overflow === 'ellipsis' && Number.isFinite(maxLines);
949
949
  // An eliding paragraph's floor is the mark, not its longest word, and
950
950
  // it is cut here because the native cannot cut without a width to cut
@@ -14,7 +14,10 @@
14
14
  // `destroy()`, `requestAnimationFrame`. On X11 that child is a real X
15
15
  // window stacked above the parent's drawing; here it is a sublayer of the
16
16
  // window's root layer with a high zPosition — the same "GL sits above the
17
- // 2D" semantics, by the same mechanism the platform gives us.
17
+ // 2D" semantics, by the same mechanism the platform gives us. The layer
18
+ // takes no input: pointer events are the NSWindow's, and the window's hit
19
+ // test answers the surface for a point over it (`GlAreaNode.hitSurface`),
20
+ // which is where X11's event propagation ends up too.
18
21
  //
19
22
  // ## The API ladder
20
23
  //
@@ -142,7 +145,6 @@ export class CocoaGLArea {
142
145
  this.scale = this.parent.scale ?? app.scale ?? 1;
143
146
  this.destroyed = false;
144
147
  this._reactX11Node = null;
145
- this.onWheel = options.onWheel ?? null;
146
148
  this.layer = this._native.createLayer();
147
149
  this._native.addSublayer(this.parent._layer, this.layer);
148
150
  this.rect = null;
@@ -161,7 +163,7 @@ export class CocoaGLArea {
161
163
  if (this.destroyed) return;
162
164
  this.rect = rect;
163
165
  const s = this.scale;
164
- this._native.setLayerProps(this.layer, {
166
+ this._setLayerProps({
165
167
  frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
166
168
  // above both presenters' content: the surface presenter's contents
167
169
  // live on the root layer itself, the layers presenter's visuals top
@@ -193,8 +195,25 @@ export class CocoaGLArea {
193
195
  }
194
196
 
195
197
  map() {
196
- if (!this.destroyed) {
197
- this._native.setLayerProps(this.layer, { hidden: false });
198
+ if (!this.destroyed) this._setLayerProps({ hidden: false });
199
+ }
200
+
201
+ /**
202
+ * Everything this layer is handed goes out with Core Animation's implicit
203
+ * actions off. The layer is ours, not a presenter's, so no frame's
204
+ * transaction covers it, and a bare set animates each key for a quarter
205
+ * of a second from wherever the layer was: the surface grew into place
206
+ * at mount and trailed every step of a live resize. A present needs
207
+ * nothing here — `setLayerContentsIOSurface` opens its own transaction
208
+ * for the flip.
209
+ */
210
+ _setLayerProps(props) {
211
+ const native = this._native;
212
+ native.txBegin({ disableActions: true });
213
+ try {
214
+ native.setLayerProps(this.layer, props);
215
+ } finally {
216
+ native.txCommit();
198
217
  }
199
218
  }
200
219
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `react-x11/cocoa-main` — the cocoa backend's threaded mode, a side-effect
3
+ * entry for `node --import` (`bun --preload`): the app's entry runs on a
4
+ * worker while AppKit keeps the main thread, so menus, drags, live resizes
5
+ * and modal panels no longer stop the app's JS. Does nothing off macOS.
6
+ * No exports.
7
+ */
8
+ export {};
@@ -0,0 +1,43 @@
1
+ // react-x11/cocoa-main — threaded mode asked for by name, for `--import`:
2
+ //
3
+ // node --import react-x11/cocoa-main app.jsx
4
+ // bun --preload react-x11/cocoa-main app.jsx
5
+ //
6
+ // Threaded mode is the default on macOS without it: the first import of
7
+ // react-x11 moves the app onto a worker (src/cocoa/relaunch.js). This does
8
+ // the same before the entry is even loaded, and skips the checks that keep
9
+ // the automatic move from firing where it should not — so it is the way in
10
+ // for an app the automatic move declines (react-x11 imported late, say),
11
+ // and it spares the main thread from loading the entry's imports once for
12
+ // nothing (docs/packaging.md).
13
+ //
14
+ // On the worker it starts, Node runs this `--import` again, before the
15
+ // entry, and it sets the worker up there — earlier than the package's own
16
+ // import would (Bun does not apply a `--preload` to a Worker, and gets the
17
+ // import's). Anywhere else it does nothing, so one command line serves
18
+ // every platform: on Linux the entry runs as it would without it, and so
19
+ // does a Worker the app starts itself (it inherits this `--import`).
20
+ import { isMainThread, workerData } from 'node:worker_threads';
21
+
22
+ import { loadNative } from './native.js';
23
+ import { bootstrapWorker, relaunch } from './relaunch.js';
24
+
25
+ if (!isMainThread && workerData?.reactX11State) {
26
+ bootstrapWorker(workerData.reactX11State);
27
+ } else if (isMainThread && process.platform === 'darwin') {
28
+ const entry = process.argv[1];
29
+ if (!entry) {
30
+ throw new Error(
31
+ 'react-x11/cocoa-main: no entry script to run — start the app with ' +
32
+ '`node --import react-x11/cocoa-main app.js`.',
33
+ );
34
+ }
35
+ const native = loadNative();
36
+ if (typeof native.runMain !== 'function') {
37
+ throw new Error(
38
+ 'react-x11/cocoa-main: the installed @windowkit/appkit has no ' +
39
+ 'runMain() — threaded mode needs bridge 0.10 or later.',
40
+ );
41
+ }
42
+ relaunch(entry, native);
43
+ }
@@ -0,0 +1,159 @@
1
+ // The pane a `<glarea>`'s children are drawn on, on the Cocoa backend: a
2
+ // transparent bitmap layer over the surface (src/gloverlay.js).
3
+ //
4
+ // A sublayer of the window's root layer like the surface's own
5
+ // (src/cocoa/glarea.js), one step above it: the GL layer sits at zPosition
6
+ // 1e7, over everything both presenters put on the root layer, and this one
7
+ // at 1e7 + 1, over that. Core Animation composites it, so what the children
8
+ // leave transparent shows the GL frame, and a translucent fill, an
9
+ // antialiased edge or a shadow blends with it — the one backend where the
10
+ // overlay is not opaque. Every surface's GL layer shares the one zPosition,
11
+ // so where two surfaces overlap, both overlays are above both frames.
12
+ //
13
+ // It speaks the verbs of a window the overlay drives on X11 — `setState`,
14
+ // `map`, `unmap`, `getContext`, `destroy` — plus `present`, which puts what
15
+ // was painted on the layer: ntk blits an X window's backing store on its
16
+ // own, where a layer's contents are a copy the bitmap has to be pushed to.
17
+ import { CocoaContext2D } from './context2d.js';
18
+
19
+ export const OVERLAY_Z = 1e7 + 1;
20
+
21
+ export class CocoaOverlayPane {
22
+ constructor(app, options) {
23
+ this.app = app;
24
+ this.parent = options.parent;
25
+ this._native = app._native;
26
+ this.scale = this.parent.scale ?? app.scale ?? 1;
27
+ this.destroyed = false;
28
+ this.layer = this._native.createLayer();
29
+ this._native.addSublayer(this.parent._layer, this.layer);
30
+ this.rect = null;
31
+ this._surface = null;
32
+ this._surfaceSize = null;
33
+ this._gen = 0;
34
+ this._ctx = null;
35
+ this._dirty = false;
36
+ // Hidden until something is on it: a layer shows its contents from the
37
+ // moment it is added, and before the first present there are none to
38
+ // show, only whatever the render server makes of that.
39
+ this._presented = false;
40
+ this._hidden = false;
41
+ this.setState({
42
+ x: options.x ?? 0,
43
+ y: options.y ?? 0,
44
+ width: options.width ?? 1,
45
+ height: options.height ?? 1,
46
+ });
47
+ }
48
+
49
+ get width() {
50
+ return this.rect?.width ?? 0;
51
+ }
52
+
53
+ get height() {
54
+ return this.rect?.height ?? 0;
55
+ }
56
+
57
+ /** Geometry in device px, the unit the overlay's rects are in. */
58
+ setState(rect) {
59
+ if (this.destroyed) return;
60
+ this.rect = rect;
61
+ const s = this.scale;
62
+ this._setLayerProps({
63
+ frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
64
+ zPosition: OVERLAY_Z,
65
+ hidden: this._hidden || !this._presented,
66
+ });
67
+ }
68
+
69
+ map() {
70
+ this._hidden = false;
71
+ if (this._presented) this._setLayerProps({ hidden: false });
72
+ }
73
+
74
+ unmap() {
75
+ this._hidden = true;
76
+ this._setLayerProps({ hidden: true });
77
+ }
78
+
79
+ /** Implicit animations off, for the reason `CocoaGLArea._setLayerProps`
80
+ * gives: no frame's transaction covers a layer that is not a presenter's. */
81
+ _setLayerProps(props) {
82
+ if (this.destroyed) return;
83
+ const native = this._native;
84
+ native.txBegin({ disableActions: true });
85
+ try {
86
+ native.setLayerProps(this.layer, props);
87
+ } finally {
88
+ native.txCommit();
89
+ }
90
+ }
91
+
92
+ /** The bitmap, the pane's size — a new size is a new bitmap, cleared, and
93
+ * the old one freed now rather than by the handle's finalizer. */
94
+ _ensureSurface() {
95
+ const w = Math.max(1, this.rect?.width ?? 1);
96
+ const h = Math.max(1, this.rect?.height ?? 1);
97
+ const size = this._surfaceSize;
98
+ if (!this._surface || size.width !== w || size.height !== h) {
99
+ this._release();
100
+ this._surface = this._native.createSurface(w, h, this.scale);
101
+ this._native.ctxClearRect(this._surface, 0, 0, w, h);
102
+ this._surfaceSize = { width: w, height: h };
103
+ this._gen++;
104
+ }
105
+ return this._surface;
106
+ }
107
+
108
+ _release() {
109
+ const surface = this._surface;
110
+ this._surface = null;
111
+ if (surface && typeof this._native.releaseSurface === 'function') {
112
+ this._native.releaseSurface(surface);
113
+ }
114
+ }
115
+
116
+ getContext() {
117
+ if (!this._ctx) {
118
+ this._ctx = new CocoaContext2D(
119
+ this._native,
120
+ () => this._ensureSurface(),
121
+ () => {
122
+ this._ensureSurface();
123
+ return this._gen;
124
+ },
125
+ );
126
+ this._ctx._fonts = this.app.fonts;
127
+ this._ctx._onDirty = () => {
128
+ this._dirty = true;
129
+ };
130
+ }
131
+ return this._ctx;
132
+ }
133
+
134
+ /** What was painted, onto the layer — a copy, so the bitmap is free to be
135
+ * painted again at once. */
136
+ present() {
137
+ if (this.destroyed || !this._dirty || !this._surface) return;
138
+ this._dirty = false;
139
+ const native = this._native;
140
+ native.txBegin({ disableActions: true });
141
+ try {
142
+ native.surfaceToLayer(this._surface, this.layer);
143
+ if (!this._presented) {
144
+ this._presented = true;
145
+ if (!this._hidden) native.setLayerProps(this.layer, { hidden: false });
146
+ }
147
+ } finally {
148
+ native.txCommit();
149
+ }
150
+ }
151
+
152
+ destroy() {
153
+ if (this.destroyed) return;
154
+ this.destroyed = true;
155
+ this._release();
156
+ this._ctx = null;
157
+ this._native.removeFromSuperlayer(this.layer);
158
+ }
159
+ }
@@ -29,11 +29,21 @@ export class CocoaPaneHost {
29
29
  return;
30
30
  }
31
31
  this._rect = { ...rect };
32
- this._native.setLayerProps(this.layer, {
33
- frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
34
- zPosition: 1e7,
35
- hidden: false,
36
- });
32
+ // Actions off: the layer is ours, not a presenter's, so no frame's
33
+ // transaction covers it, and a bare set tweens the pane into place at
34
+ // mount and after every resize — the trap CocoaGLArea._setLayerProps
35
+ // describes.
36
+ const native = this._native;
37
+ native.txBegin({ disableActions: true });
38
+ try {
39
+ native.setLayerProps(this.layer, {
40
+ frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
41
+ zPosition: 1e7,
42
+ hidden: false,
43
+ });
44
+ } finally {
45
+ native.txCommit();
46
+ }
37
47
  }
38
48
 
39
49
  /**