react-x11 2.11.0 → 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 (96) 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 +292 -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/threaded.js +246 -0
  20. package/src/cocoa/window.js +256 -42
  21. package/src/components/Select.js +2 -2
  22. package/src/components/anchor.js +3 -3
  23. package/src/components/native.js +12 -7
  24. package/src/components/theme.js +2 -2
  25. package/src/debug.js +1 -1
  26. package/src/decorations.js +1 -1
  27. package/src/editmenu.js +2 -2
  28. package/src/errors.js +46 -0
  29. package/src/events.js +6 -6
  30. package/src/foreignnodes.js +3 -2
  31. package/src/frames.js +2 -2
  32. package/src/glnodes.js +1 -1
  33. package/src/grid.js +1653 -0
  34. package/src/host.d.ts +230 -0
  35. package/src/host.js +11 -3
  36. package/src/imagesource.js +1 -1
  37. package/src/index.d.ts +21 -4
  38. package/src/index.js +9 -1
  39. package/src/layouts.js +721 -0
  40. package/src/node.d.ts +4 -2
  41. package/src/node.js +19 -21
  42. package/src/nodes/animation.js +644 -0
  43. package/src/nodes/box.js +21 -0
  44. package/src/nodes/boxpaint.js +473 -0
  45. package/src/nodes/canvas.js +269 -0
  46. package/src/nodes/cascade.js +600 -0
  47. package/src/nodes/damage.js +183 -0
  48. package/src/nodes/edithistory.js +124 -0
  49. package/src/nodes/editmenupopup.js +260 -0
  50. package/src/nodes/hittest.js +185 -0
  51. package/src/nodes/image.js +266 -0
  52. package/src/nodes/install.js +75 -0
  53. package/src/nodes/invalidate.js +465 -0
  54. package/src/nodes/kinds.js +31 -0
  55. package/src/nodes/layout.js +439 -0
  56. package/src/nodes/layouthost.js +949 -0
  57. package/src/nodes/node.js +868 -0
  58. package/src/nodes/paint.js +466 -0
  59. package/src/nodes/position.js +366 -0
  60. package/src/nodes/preedit.js +127 -0
  61. package/src/nodes/queries.js +330 -0
  62. package/src/nodes/rects.js +102 -0
  63. package/src/nodes/scrollable.js +891 -0
  64. package/src/nodes/scrollbars.js +138 -0
  65. package/src/nodes/scrollblit.js +1034 -0
  66. package/src/nodes/selectable.js +142 -0
  67. package/src/nodes/styling.js +225 -0
  68. package/src/nodes/text.js +649 -0
  69. package/src/nodes/textarea.js +391 -0
  70. package/src/nodes/textinput.js +1146 -0
  71. package/src/nodes/util.js +17 -0
  72. package/src/nodes/window/anchoring.js +161 -0
  73. package/src/nodes/window/capabilities.js +190 -0
  74. package/src/nodes/window/debugpaint.js +83 -0
  75. package/src/nodes/window/droptarget.js +145 -0
  76. package/src/nodes/window/floors.js +577 -0
  77. package/src/nodes/window/flush.js +334 -0
  78. package/src/nodes/window/hints.js +482 -0
  79. package/src/nodes/window/listeners.js +222 -0
  80. package/src/nodes/window/popup.js +71 -0
  81. package/src/nodes/window/size.js +591 -0
  82. package/src/nodes/window/window.js +945 -0
  83. package/src/palette.js +1 -1
  84. package/src/registry.js +7 -3
  85. package/src/styles.js +137 -15
  86. package/src/svgnodes.js +2 -1
  87. package/src/testing/harness.js +2 -2
  88. package/src/textselection.js +5 -3
  89. package/src/trace-registry.js +1 -1
  90. package/src/types/components.d.ts +38 -6
  91. package/src/types/elements.d.ts +11 -1
  92. package/src/types/nodes.d.ts +17 -2
  93. package/src/types/style.d.ts +94 -3
  94. package/src/windowstate.js +1 -1
  95. package/src/yoga.js +1 -1
  96. 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
@@ -161,7 +161,7 @@ export class CocoaGLArea {
161
161
  if (this.destroyed) return;
162
162
  this.rect = rect;
163
163
  const s = this.scale;
164
- this._native.setLayerProps(this.layer, {
164
+ this._setLayerProps({
165
165
  frame: [rect.x / s, rect.y / s, rect.width / s, rect.height / s],
166
166
  // above both presenters' content: the surface presenter's contents
167
167
  // live on the root layer itself, the layers presenter's visuals top
@@ -193,8 +193,25 @@ export class CocoaGLArea {
193
193
  }
194
194
 
195
195
  map() {
196
- if (!this.destroyed) {
197
- this._native.setLayerProps(this.layer, { hidden: false });
196
+ if (!this.destroyed) this._setLayerProps({ hidden: false });
197
+ }
198
+
199
+ /**
200
+ * Everything this layer is handed goes out with Core Animation's implicit
201
+ * actions off. The layer is ours, not a presenter's, so no frame's
202
+ * transaction covers it, and a bare set animates each key for a quarter
203
+ * of a second from wherever the layer was: the surface grew into place
204
+ * at mount and trailed every step of a live resize. A present needs
205
+ * nothing here — `setLayerContentsIOSurface` opens its own transaction
206
+ * for the flip.
207
+ */
208
+ _setLayerProps(props) {
209
+ const native = this._native;
210
+ native.txBegin({ disableActions: true });
211
+ try {
212
+ native.setLayerProps(this.layer, props);
213
+ } finally {
214
+ native.txCommit();
198
215
  }
199
216
  }
200
217
 
@@ -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
+ }
@@ -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
  /**
@@ -26,12 +26,9 @@
26
26
  // knowing.
27
27
  import { cssColorStraight } from 'ntk';
28
28
 
29
- import {
30
- Node,
31
- addDamageRect,
32
- damageToPaint,
33
- intersectRects,
34
- } from '../nodes.js';
29
+ import { Node } from '../nodes/node.js';
30
+ import { addDamageRect, damageToPaint } from '../nodes/damage.js';
31
+ import { intersectRects } from '../nodes/rects.js';
35
32
  import { EASING_CONTROL_POINTS, TRANSITION_CONTROL_POINTS } from '../styles.js';
36
33
  import { CocoaContext2D } from './context2d.js';
37
34
 
@@ -489,7 +486,7 @@ export function propBoxProps(node, app, scale, parentOrigin, order) {
489
486
  // --- animations the render server runs ---------------------------------------
490
487
  //
491
488
  // The node model keeps deciding what is animating and when it ends
492
- // (nodes.js `_retarget` / `_updateLoops`); what moves here is who
489
+ // (nodes/animation.js `_retarget` / `_updateLoops`); what moves here is who
493
490
  // interpolates. Taken means the node's style goes to its target — the
494
491
  // layer's model value, sent by the next frame's property diff — and that
495
492
  // frame attaches an explicit animation carrying the pixels there; no frame
@@ -594,7 +591,12 @@ export class LayerAnimations {
594
591
  // transition reverses from where it got to" means here. A colour
595
592
  // cannot be additive, so the one before it is replaced.
596
593
  this._removeLive(node, prop);
597
- const shown = this.native.presentationValue?.(layer, map.keyPath);
594
+ // On a worker the render server's value is an answer that comes
595
+ // later (windowkit/appkit#52), and the animation is added now: it
596
+ // runs from the declared value instead.
597
+ const shown = this.app._threaded
598
+ ? null
599
+ : this.native.presentationValue?.(layer, map.keyPath);
598
600
  opts.from = Array.isArray(shown) ? shown : this._value(map, entry.from);
599
601
  opts.to = this._value(map, entry.to);
600
602
  opts.timing = TRANSITION_CONTROL_POINTS;
@@ -806,7 +808,9 @@ export class CocoaLayerPresenter {
806
808
  }
807
809
  presented = true;
808
810
  } finally {
809
- native.txCommit();
811
+ // the size this frame was laid out at, which a worker's live resize
812
+ // waits for (CocoaWindow._flip); the main thread's commit ignores it
813
+ native.txCommit(this.window._frameSize?.());
810
814
  this._claims = null;
811
815
  // a frame that threw half-way presents what it got to; the claims it
812
816
  // was answering are still owed, so the next frame answers them again
@@ -32,12 +32,9 @@
32
32
  // clock. Overlays, toasts, drag ghosts, spinners and floating cards pass by
33
33
  // construction; a hover fade on a row in the middle of a list does not,
34
34
  // and stays on the clock. docs/macos.md §"Layer promotion" is the account.
35
- import {
36
- BoxNode,
37
- addDamageRect,
38
- damageToPaint,
39
- intersectRects,
40
- } from '../nodes.js';
35
+ import { BoxNode } from '../nodes/box.js';
36
+ import { addDamageRect, damageToPaint } from '../nodes/damage.js';
37
+ import { intersectRects } from '../nodes/rects.js';
41
38
  import { resolveBorderWidths } from '../styles.js';
42
39
  import {
43
40
  LayerAnimations,
@@ -181,7 +178,7 @@ function scrollbarStrip(bar, scale) {
181
178
  * The surface window's promoted nodes: which ones have a layer, what each
182
179
  * layer shows, and the animations the render server runs on them.
183
180
  * `frame()` is the whole of the per-frame work, called by the window from
184
- * nodes.js's `prepareFrame` seam — after layout, before the damage is taken.
181
+ * the `prepareFrame` seam in nodes/window/flush.js — after layout, before the damage is taken.
185
182
  */
186
183
  export class CocoaPromotion {
187
184
  constructor(window) {