react-x11 2.47.4 → 2.48.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.47.4",
3
+ "version": "2.48.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -109,7 +109,7 @@
109
109
  "yoga-layout": "^3.2.1"
110
110
  },
111
111
  "optionalDependencies": {
112
- "@windowkit/appkit": "^0.28.0",
112
+ "@windowkit/appkit": "^0.29.0",
113
113
  "@windowkit/wayland": "^3.1.1",
114
114
  "@windowkit/win32": "^0.0.9",
115
115
  "dbus-native": "^0.16.0",
package/src/cocoa/app.js CHANGED
@@ -71,6 +71,9 @@ const PUMP_INTERVAL_MS = 8;
71
71
  // to 100, docs/windows.md §"Resize"). `createRoot({ cocoa: { resizeWait } })`
72
72
  // sets it; 0 is no handshake at all.
73
73
  const RESIZE_WAIT_MS = 50;
74
+ // What `createRoot({ cocoa: { paneStretch } })` and
75
+ // REACT_X11_COCOA_PANE_STRETCH take (src/cocoa/panehost.js, `_stretch`).
76
+ const PANE_STRETCH = ['behind', 'never', 'always'];
74
77
  // How often the screen layout is re-read while something is watching it, in
75
78
  // ms. There is no event to wait for — the bridge keeps its `NSScreen` copy
76
79
  // current on macOS's notification and emits nothing (#617) — so
@@ -149,6 +152,24 @@ export class CocoaApp {
149
152
  );
150
153
  }
151
154
  this._resizeWait = resizeWait;
155
+ // How a `<Frame>` pane's last frame is shown while the frame of the
156
+ // size its layer was just given is on its way (src/cocoa/panehost.js,
157
+ // `_stretch`): 'behind' stretches it across once the pane falls behind
158
+ // `resizeWait`, 'never' keeps it at its size however late the pane is,
159
+ // and 'always' stretches it both ways. Apart from `resizeWait`, which
160
+ // is also how long AppKit holds the window's edge for a frame, so a
161
+ // pane that is never to be stretched would otherwise need a budget
162
+ // that held the drag back as long.
163
+ const paneStretch =
164
+ options.cocoa?.paneStretch ??
165
+ (process.env.REACT_X11_COCOA_PANE_STRETCH || 'behind');
166
+ if (!PANE_STRETCH.includes(paneStretch)) {
167
+ throw new TypeError(
168
+ 'react-x11: cocoa.paneStretch (REACT_X11_COCOA_PANE_STRETCH) is ' +
169
+ `one of ${PANE_STRETCH.join(', ')} — got ${paneStretch}.`,
170
+ );
171
+ }
172
+ this._paneStretch = paneStretch;
152
173
 
153
174
  // The activation policy has to be fixed before the app finishes
154
175
  // launching — a Regular launch registers a Dock tile, so an agent app
package/src/cocoa/dnd.js CHANGED
@@ -34,7 +34,8 @@
34
34
  // the pasteboard, so an in-app drop still gets `e.items` by reference and
35
35
  // `e.source === 'internal'`, the contract the X11 transport keeps. Other
36
36
  // applications read the pasteboard, where a thunk is a promise the bridge
37
- // asks `provide` to keep.
37
+ // asks `provide` to keep — on the main thread. On a worker the bridge takes
38
+ // no `provide`, and a thunk resolves as the drag begins (`dragSpec`).
38
39
  //
39
40
  // ## Types
40
41
  //
@@ -223,6 +224,16 @@ export const wire = (value) =>
223
224
  * to keep. The files thunk alone resolves now, because the items cannot
224
225
  * be counted without it.
225
226
  *
227
+ * Unless the app is on a worker (`upFront`, threaded mode): AppKit asks a
228
+ * provider from inside its own callback on the main thread, which a worker
229
+ * cannot answer in time, so the bridge refuses `provide` there with a
230
+ * TypeError. That was every drag on a Mac, since an app runs on a worker
231
+ * by default. There every thunk resolves now, as the win32 backend has
232
+ * them, and this is the external promotion `_resolve` caches for: an
233
+ * in-app drop is handed the same value by reference. A value that resolves
234
+ * to nothing, or that JSON cannot carry (a cycle, a node), is left off
235
+ * the pasteboard rather than failing the drag; an in-app drop still has it.
236
+ *
226
237
  * No image, and so no slide-back. The picture of the drag is the app's own
227
238
  * `<popup dragPreview>`; what the system session carries is a blank 1×1.
228
239
  * Left at AppKit's default, a refused or cancelled drop plays
@@ -236,7 +247,7 @@ export const wire = (value) =>
236
247
  * to animate from `onDragEnd`. Not a seam: there is no system image for
237
248
  * a caller to want slid back.
238
249
  */
239
- export function dragSpec(session, native, scale) {
250
+ export function dragSpec(session, native, scale, { upFront = false } = {}) {
240
251
  const types = session.types;
241
252
  const reverse = new Map(); // uti -> react-x11 type
242
253
  const items = [];
@@ -256,7 +267,17 @@ export function dragSpec(session, native, scale) {
256
267
  if (!uti || uti in first) continue;
257
268
  reverse.set(uti, type);
258
269
  const raw = session._data[type];
259
- first[uti] = typeof raw === 'function' ? null : wire(raw);
270
+ if (!upFront) {
271
+ first[uti] = typeof raw === 'function' ? null : wire(raw);
272
+ continue;
273
+ }
274
+ const value = session._resolve(type);
275
+ if (value == null) continue;
276
+ try {
277
+ first[uti] = wire(value);
278
+ } catch {
279
+ // not something the pasteboard can carry; the in-app drop has it
280
+ }
260
281
  }
261
282
  if (Object.keys(first).length) {
262
283
  if (items.length) Object.assign(items[0], first);
@@ -269,7 +290,11 @@ export function dragSpec(session, native, scale) {
269
290
  items,
270
291
  operations: session.actions,
271
292
  slideBack: false,
272
- provide: (uti) => wire(session._resolve(reverse.get(uti) ?? uti)),
293
+ ...(upFront
294
+ ? null
295
+ : {
296
+ provide: (uti) => wire(session._resolve(reverse.get(uti) ?? uti)),
297
+ }),
273
298
  };
274
299
  }
275
300
 
@@ -91,7 +91,8 @@ class PaneLayer {
91
91
  * and a smeared strip. The budget is the window's live-resize
92
92
  * handshake's (`createRoot({ cocoa: { resizeWait } })`), how long a frame
93
93
  * of a new size may take before the old one is shown in its place: 0
94
- * stretches always, both ways, as before.
94
+ * stretches always, both ways, as before. And the app may say otherwise
95
+ * whatever the budget (`_stretch`).
95
96
  *
96
97
  * Down the layer the frame is at its size whatever this says. A width
97
98
  * moves where a page's lines break, and what comes is a page laid out
@@ -116,8 +117,9 @@ class PaneLayer {
116
117
  * decides the rest (`_answered`).
117
118
  */
118
119
  _anchoredNow() {
120
+ const stretch = this._stretch();
121
+ if (stretch !== 'behind') return stretch === 'never';
119
122
  const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
120
- if (!(budget > 0)) return false;
121
123
  // a size asked longer ago than the budget, and no frame of any since
122
124
  const oldest = this._asked[0];
123
125
  if (oldest && now() - Math.max(oldest.at, this._answeredAt) > budget) {
@@ -126,6 +128,23 @@ class PaneLayer {
126
128
  return !this._behind;
127
129
  }
128
130
 
131
+ /**
132
+ * When the frame the layer holds is stretched to bounds that are not its
133
+ * size: `'behind'`, across, once the pane falls behind the budget
134
+ * (`_anchoredNow`); `'never'`, kept at its size and its edge carried over
135
+ * however late the pane is; `'always'`, both ways, Core Animation's own.
136
+ * The app's `cocoa.paneStretch` (REACT_X11_COCOA_PANE_STRETCH), and
137
+ * `'behind'` where it names none, or `'always'` at a budget of 0, which
138
+ * waits for no frame. An app that says `'never'` was not served by the
139
+ * budget: as long as AppKit holds the window's edge for a frame, so a
140
+ * budget a slow page never misses held every step of the drag back.
141
+ */
142
+ _stretch() {
143
+ const stretch = this.app._paneStretch ?? 'behind';
144
+ if (stretch !== 'behind') return stretch;
145
+ return (this.app._resizeWait ?? RESIZE_WAIT_MS) > 0 ? 'behind' : 'always';
146
+ }
147
+
129
148
  /**
130
149
  * The props that show the frame the layer holds in bounds that are not
131
150
  * its size, where they changed. At its size and from the top left, as a
@@ -138,7 +157,8 @@ class PaneLayer {
138
157
  * the frame is stretched, and down it is still at its size
139
158
  * (`_anchoredNow`): the whole of each row stretched to the new width,
140
159
  * cropped or with its last row carried down. Both rects are the whole
141
- * when the frame is the layer's size, and at a budget of 0.
160
+ * when the frame is the layer's size, and where it is stretched always
161
+ * (`_stretch`).
142
162
  *
143
163
  * Anchored by gravity alone, the strip the frame did not cover showed
144
164
  * whatever was under the pane, which is the `<Frame>`'s background: in
@@ -159,8 +179,7 @@ class PaneLayer {
159
179
  const rect = this._rect;
160
180
  let crop = null;
161
181
  let edge = null;
162
- const budget = this.app._resizeWait ?? RESIZE_WAIT_MS;
163
- if (frame && rect && budget > 0) {
182
+ if (frame && rect && this._stretch() !== 'always') {
164
183
  // across at its size while the pane keeps up, and down always; an
165
184
  // axis stretched is shown whole, the frame's every column across
166
185
  const across = this._anchoredNow();
@@ -520,7 +520,9 @@ export class CocoaWindow {
520
520
  if (this.destroyed) return null;
521
521
  const began = this._native.beginDrag(
522
522
  this._h,
523
- dragSpec(session, this._native, this.scale),
523
+ dragSpec(session, this._native, this.scale, {
524
+ upFront: this.app._threaded,
525
+ }),
524
526
  );
525
527
  this.app._afterInput();
526
528
  return began;
package/src/index.d.ts CHANGED
@@ -306,6 +306,11 @@ export interface RootOptions {
306
306
  * the next comes, and past it, it is stretched across to the pane's new
307
307
  * width and stays at its size down it; at 0 every one is stretched both
308
308
  * ways.
309
+ * `paneStretch` says when a `<Frame>` pane's last frame is stretched
310
+ * instead, whatever `resizeWait` is: `'behind'` (the default) as above,
311
+ * `'never'` keeps it at its size however late the pane's next frame is,
312
+ * and `'always'` stretches it both ways. `REACT_X11_COCOA_PANE_STRETCH`
313
+ * says the same from the environment.
309
314
  * `screenPoll` is how often the screen layout is re-read while a
310
315
  * {@link useScreens} subscriber is mounted, in ms — 500 by default, 0 for
311
316
  * never. macOS has no event for a display plugged in, unplugged or
@@ -333,6 +338,7 @@ export interface RootOptions {
333
338
  frameInterval?: number;
334
339
  pumpInterval?: number;
335
340
  resizeWait?: number;
341
+ paneStretch?: 'behind' | 'never' | 'always';
336
342
  screenPoll?: number;
337
343
  appName?: string;
338
344
  activationPolicy?: 'regular' | 'accessory' | 'prohibited';
@@ -333,7 +333,8 @@ export function dirtyOutside(node, boundaries, paths) {
333
333
  * Undefined where it cannot be summed: a child with no extent on hand, a
334
334
  * negative margin (the boxes would overlap, and the last would not have to
335
335
  * be the lowest), or nothing in flow at all — a leaf's answer, which is
336
- * `contentSpan`'s to give.
336
+ * `contentSpan`'s to give. All but the first are the column's own, and a
337
+ * spine never sums a column that has them (`columnSums`).
337
338
  */
338
339
  export function columnHeightSpan(node) {
339
340
  const yoga = node.yoga;
@@ -363,6 +364,30 @@ export function columnHeightSpan(node) {
363
364
  );
364
365
  }
365
366
 
367
+ /**
368
+ * Whether `columnHeightSpan` has an answer for this column once its children
369
+ * carry extents: everything else it asks — something in flow, margins down
370
+ * that are lengths and pull no box over the one before it, a gap that is a
371
+ * length — asked ahead of the measurement, from the same readings.
372
+ *
373
+ * A spine asks it before it sums a column (`isSummableColumn`), because once
374
+ * the measurement is under way a sum with no answer has nowhere to go: it is
375
+ * written as the column's extent, an extent of none writes the style's own
376
+ * minimum, and the column — and every column summed above it — lost its
377
+ * floor to a label that changed beside a block with `marginTop: -4`.
378
+ */
379
+ export function columnSums(node) {
380
+ if (columnGap(node) === undefined) return false;
381
+ let count = 0;
382
+ for (const child of node.children) {
383
+ if (!inFlow(child)) continue;
384
+ const { top, bottom } = marginsDown(child);
385
+ if (!(top >= 0 && bottom >= 0)) return false;
386
+ count += 1;
387
+ }
388
+ return count !== 0;
389
+ }
390
+
366
391
  /**
367
392
  * `marginDown` for both edges of a box, kept on the box with the style object
368
393
  * it was read under. A margin reaches yoga only from the style
@@ -36,6 +36,7 @@
36
36
  // length — is not a spine, and the tree is measured whole.
37
37
 
38
38
  import {
39
+ columnSums,
39
40
  declaresOwnMinimum,
40
41
  inFlow,
41
42
  mainAxisOf,
@@ -74,7 +75,9 @@ const PADDINGS = [
74
75
  * main axis without wrapping, and with a height that is its content's —
75
76
  * nothing named on the height axis (`declaresOwnMinimum`), so neither a
76
77
  * size, a floor, a ceiling nor a clip stands between its children and its
77
- * extent.
78
+ * extent. And one the sum has an answer for (`columnSums`): a column with a
79
+ * block pulled up over the one before it is measured, as a root, rather
80
+ * than summed into no extent and no floor.
78
81
  */
79
82
  export function isSummableColumn(node) {
80
83
  const style = node.style;
@@ -86,7 +89,8 @@ export function isSummableColumn(node) {
86
89
  mainAxisOf(node) === 'height' &&
87
90
  style.flexDirection !== 'column-reverse' &&
88
91
  (style.flexWrap == null || style.flexWrap === 'nowrap') &&
89
- !declaresOwnMinimum(node, 'height')
92
+ !declaresOwnMinimum(node, 'height') &&
93
+ columnSums(node)
90
94
  );
91
95
  }
92
96
 
@@ -129,6 +133,17 @@ export function spineScope(root, sources, listOnly) {
129
133
  const spine = new Set();
130
134
  const stops = new Set();
131
135
  const offered = new OfferedWidths(root);
136
+ // asked once a column: a dozen rows that arrive in one column of a
137
+ // thousand ask it of that column a dozen times (`columnSums`)
138
+ const summable = new Map();
139
+ const sums = (column) => {
140
+ let answer = summable.get(column);
141
+ if (answer === undefined) {
142
+ answer = isSummableColumn(column);
143
+ summable.set(column, answer);
144
+ }
145
+ return answer;
146
+ };
132
147
  for (const source of sources) {
133
148
  const list = listOnly(source);
134
149
  // the way up to where the change stops mattering
@@ -148,7 +163,7 @@ export function spineScope(root, sources, listOnly) {
148
163
  let top = -1;
149
164
  for (let i = path.length - 1; i >= 0; i--) {
150
165
  const summed = i > 0 || list;
151
- if (!summed || !isSummableColumn(path[i])) {
166
+ if (!summed || !sums(path[i])) {
152
167
  top = i;
153
168
  break;
154
169
  }
@@ -379,6 +394,9 @@ function rootWidths(node, offered, stop) {
379
394
  if (!stretched(node)) return null;
380
395
  if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
381
396
  if (PADDINGS.some((key) => typeof style[key] === 'string')) return null;
397
+ // …nor a gap that is a share of something: laid out alone, it is a share
398
+ // of something else
399
+ if (typeof (style.rowGap ?? style.gap) === 'string') return null;
382
400
  return offered.in(parent, stop);
383
401
  }
384
402