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.
- package/README.md +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +292 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
package/src/cocoa/bezels.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
...params,
|
|
112
|
-
controlSize,
|
|
113
|
-
});
|
|
114
|
-
entry = {
|
|
183
|
+
return {
|
|
115
184
|
surface,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
if (
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 =
|
|
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
|
-
?
|
|
173
|
-
:
|
|
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 };
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -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
|
package/src/cocoa/glarea.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/cocoa/panehost.js
CHANGED
|
@@ -29,11 +29,21 @@ export class CocoaPaneHost {
|
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
this._rect = { ...rect };
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
/**
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -26,12 +26,9 @@
|
|
|
26
26
|
// knowing.
|
|
27
27
|
import { cssColorStraight } from 'ntk';
|
|
28
28
|
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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
|
-
*
|
|
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) {
|