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
@@ -0,0 +1,330 @@
1
+ // Size and container queries (#470): the blocks a node's style declares,
2
+ // the container sizes they are answered against, and the passes a window
3
+ // spends settling them.
4
+
5
+ import {
6
+ applyLayoutStyle,
7
+ localTextStyleChanged,
8
+ containerAnswers,
9
+ } from '../styles.js';
10
+ import { DEV } from './util.js';
11
+
12
+ /** How many extra layout passes a flush spends settling `@container` blocks
13
+ * before it takes the layout it has — see `_settleContainerQueries`. */
14
+ const CONTAINER_QUERY_PASSES = 3;
15
+
16
+ /** The sizes a node's container blocks were resolved against, as one
17
+ * comparable string — what a pinned node is held at. */
18
+ function containersKey(containers) {
19
+ if (!containers) return '';
20
+ let key = '';
21
+ for (const name of Object.keys(containers)) {
22
+ const c = containers[name];
23
+ key += `${name}:${c.width}x${c.height};`;
24
+ }
25
+ return key;
26
+ }
27
+
28
+ /** Node's half of size and container queries, installed onto `Node.prototype` by node.js. */
29
+ export class NodeQueries {
30
+ /** Join the owning window's size-query registry, and take the current
31
+ * size into account — a node mounted after a resize has to match against
32
+ * the size the window is now, not the one it started at. */
33
+ _registerSizeQueries() {
34
+ if (this._queried && this.root?._sizeQueryNodes) {
35
+ this.root._sizeQueryNodes.add(this);
36
+ if (this.root.querySize) this._sizeQueriesChanged();
37
+ }
38
+ if (this._supportsQueried && this.root?._supportsQueryNodes) {
39
+ this.root._supportsQueryNodes.add(this);
40
+ // a node mounted into a window that already knows its capabilities
41
+ // has to match against those, not against the startup default
42
+ this._sizeQueriesChanged();
43
+ }
44
+ if (this._wantsAttention && this.root?._attentionNodes) {
45
+ this.root._attentionNodes.add(this);
46
+ }
47
+ if (this._cq !== null && this.root?._containerQueryNodes) {
48
+ this.root._containerQueryNodes.add(this);
49
+ // it can see the containers above it now — the constructor's
50
+ // resolution had no ancestors to find one in
51
+ this._sizeQueriesChanged();
52
+ }
53
+ for (const child of this.children) {
54
+ if (!child.isWindow) child._registerSizeQueries();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The sizes this node's `@container` blocks resolve against: for each name
60
+ * the style asks about, the nearest ancestor declaring it, in this node's
61
+ * **logical** pixels — the unit the threshold beside `width: 400` was
62
+ * written in, so the two numbers mean the same thing. Yoga's computed
63
+ * size rather than `abs`: inside a flush, `abs` is still the previous
64
+ * frame's.
65
+ *
66
+ * A container that has not been laid out yet contributes nothing, so its
67
+ * blocks do not apply — the way a capability block does not before the
68
+ * window exists: the fallback design is the one that works everywhere.
69
+ * "Laid out" is `_placed` between frames and every attached node while
70
+ * the window is settling a pass it just ran (`_cqFresh`); a fresh yoga
71
+ * node answers NaN. Null when no container is known, which is the
72
+ * identity `resolveQueries` keeps.
73
+ */
74
+ _containerSizes() {
75
+ const names = this._cq?.names;
76
+ if (!names) return null;
77
+ let sizes = null;
78
+ const s = this.scale || 1;
79
+ const fresh = Boolean(this.root?._cqFresh);
80
+ for (const name of names) {
81
+ const c = this._containerFor(name);
82
+ if (!c) {
83
+ // In a window and nothing above declares one: a forgotten
84
+ // declaration, not a component rendered outside its context — that
85
+ // is what the *named* form is for, and a missing name applies
86
+ // nothing quietly. `root` rather than `parent`: React builds a
87
+ // subtree bottom-up, so a node can have a parent and no window yet,
88
+ // and the container it will find is further up.
89
+ if (DEV && name === '' && this.root) this._noContainer();
90
+ continue;
91
+ }
92
+ if (!c.yoga || !(fresh || c._placed)) continue;
93
+ const width = c.yoga.getComputedWidth() / s;
94
+ const height = c.yoga.getComputedHeight() / s;
95
+ if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
96
+ (sizes ??= {})[name] = { width, height };
97
+ }
98
+ return sizes;
99
+ }
100
+
101
+ /** `_containerSizes()`, unless this node is pinned at the sizes it is
102
+ * looking at — then the sizes its held answer came from, so a restyle
103
+ * arriving from React does not undo what the layout pass decided. */
104
+ _pinnedContainerSizes() {
105
+ const live = this._containerSizes();
106
+ const cq = this._cq;
107
+ const pin = cq.pin;
108
+ if (!pin) return live;
109
+ if (pin.key === containersKey(live)) return pin.containers;
110
+ cq.pin = null;
111
+ return live;
112
+ }
113
+
114
+ /**
115
+ * The nearest ancestor whose style declares `container` — any container
116
+ * for the unnamed query (`''`), the one carrying `name` otherwise, however
117
+ * many nearer containers that reaches past. A window ends the walk after
118
+ * offering itself, and a window asks nothing: a `<popup>` inside a
119
+ * container is a root of its own and asks its own window with `@width`.
120
+ *
121
+ * Walked rather than cached: it is a dozen property reads per dependent
122
+ * per layout pass, and a cache would have to follow every insert, every
123
+ * reorder and every `container` value that changes above.
124
+ */
125
+ _containerFor(name) {
126
+ if (this.isWindow) return null;
127
+ for (let n = this.parent; n; n = n.parent) {
128
+ const c = n.style?.container;
129
+ if (name === '' ? c === true || typeof c === 'string' : c === name) {
130
+ return n;
131
+ }
132
+ if (n.isWindow) break;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ _noContainer() {
138
+ this._tokenProblem(
139
+ [
140
+ `react-x11: <${this.kind}> has an "@container" block and no ` +
141
+ 'container above it — declare one with `container: true` in an ' +
142
+ "ancestor's style (or name it and ask for it by name), or ask " +
143
+ 'the window with "@width"',
144
+ ],
145
+ true,
146
+ 'The block does not apply and the app carries on',
147
+ );
148
+ }
149
+
150
+ /** Said once per node, in development: a design that cannot settle looks
151
+ * like a layout bug, and the frame it is pinned at is the only clue. */
152
+ _warnContainerOscillation() {
153
+ const cq = this._cq;
154
+ if (cq.warned) return;
155
+ cq.warned = true;
156
+ const asked = [...(cq.names ?? [])]
157
+ .map((n) => (n === '' ? 'its container' : `"${n}"`))
158
+ .join(', ');
159
+ console.warn(
160
+ `react-x11: the "@container" blocks on <${this.kind}> cannot settle: ` +
161
+ `a block that matches at one size of ${asked} changes that size to ` +
162
+ 'one where it no longer matches, and back. A container query must ' +
163
+ 'not move the size it asks about — give the container a size of its ' +
164
+ 'own, or minWidth: 0 and a flexBasis so its content cannot grow it. ' +
165
+ 'The current answer is held until the container moves for another ' +
166
+ 'reason (docs/styling.md#container-queries).',
167
+ );
168
+ }
169
+
170
+ /** The owning window resized, the server's answer moved, or a layout pass
171
+ * moved a container this node asks about: re-resolve, since a query block
172
+ * may now match that did not, or the other way round. */
173
+ _sizeQueriesChanged() {
174
+ if (
175
+ !(this._queried || this._supportsQueried || this._cq !== null) ||
176
+ this.destroyed
177
+ ) {
178
+ return;
179
+ }
180
+ const before = this.style;
181
+ // a query block may name `fontSize`, and `_syncStyle` → `_retarget` is
182
+ // what pushes that into the subtree; only the node-local text props are
183
+ // left to notice here
184
+ this._syncStyle(this.props);
185
+ if (localTextStyleChanged(this.style, before)) this._textContentChanged();
186
+ if (this.yoga && this.style !== before) {
187
+ // A block that moved a layout property changed the tree the content
188
+ // floors were measured from — the debt a style change from React
189
+ // leaves too (`invalidate`, reason 'props'). It has to be marked as a
190
+ // *content* change: the live-resize deferral takes a plain
191
+ // `_floorsDirty` for the drag itself and lays out against the floors
192
+ // in hand, which are the old arrangement's, and by the time the
193
+ // catch-up looks the dirty flags are spent and no leaf's height moved
194
+ // — so the floor of a card that turned from a row into a column would
195
+ // stay the row's, and yoga would squeeze the column down to it.
196
+ if (applyLayoutStyle(this.yoga, this.style, before) && this.root) {
197
+ this.root._floorsDirty = true;
198
+ this.root._floorsContentDirty = true;
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ /** WindowNode's half of size and container queries, installed onto `WindowNode.prototype` by window/window.js. */
205
+ export class WindowQueries {
206
+ /**
207
+ * Re-evaluate the size-query blocks for this window's current size, just
208
+ * before laying out. This is the whole reason a size query may carry
209
+ * layout properties while a state block may not: it only ever runs inside
210
+ * a layout pass the resize already required.
211
+ *
212
+ * Callers pass the size in device pixels — it comes off the window or out
213
+ * of yoga, and both live on the device grid — but `querySize` is stored
214
+ * in **logical** pixels, because that is the unit the thresholds were
215
+ * written in: `'@width >= 620'` sits in a style block next to `width:
216
+ * 620`, and the same number must mean the same thing. At scale 1 the two
217
+ * coincide, which is how comparing device pixels survived every 1x
218
+ * display it was ever run on and broke on the first retina one (every
219
+ * query read double, so none of them ever changed answer under a drag).
220
+ */
221
+ _resolveSizeQueries(deviceWidth, deviceHeight) {
222
+ const s = this.scale || 1;
223
+ const width = deviceWidth / s;
224
+ const height = deviceHeight / s;
225
+ if (this._sizeQueryNodes.size === 0) {
226
+ this.querySize = this.querySize ?? { width, height };
227
+ return false;
228
+ }
229
+ if (this.querySize?.width === width && this.querySize?.height === height) {
230
+ return false;
231
+ }
232
+ this.querySize = { width, height };
233
+ // a query block may carry layout properties, so the floors measured from
234
+ // the styles it is replacing are not the answer any more
235
+ this._floorsDirty = true;
236
+ for (const node of [...this._sizeQueryNodes]) {
237
+ if (node.destroyed) this._sizeQueryNodes.delete(node);
238
+ else node._sizeQueriesChanged();
239
+ }
240
+ // Whether the layout may have moved under this, which is what an
241
+ // auto-sizing pass needs to know: it resolves these against a size it is
242
+ // still working out, and has to look again if the answer changed.
243
+ return true;
244
+ }
245
+
246
+ /**
247
+ * Resolve the `@container` blocks against the layout just produced, and
248
+ * lay out again while an answer moves — a container's size is what a pass
249
+ * *produces*, so it can only be asked about afterwards, and a block that
250
+ * changed may carry layout properties. `relayout` is whatever pass the
251
+ * caller runs: `_layoutStep` in a flush, `measure()` while an auto-sized
252
+ * window is working out how big to be.
253
+ *
254
+ * Bounded. Two passes settle the common case (a block that matches at the
255
+ * width the content took), and nested containers can honestly need a
256
+ * third — an outer answer moving an inner container past one of its own
257
+ * thresholds — so the cap is a small fixed number rather than "once".
258
+ * What it must not do is chase a design no size satisfies; that is
259
+ * detected per node inside `_resolveContainerQueries`, and pinned.
260
+ */
261
+ _settleContainerQueries(relayout) {
262
+ if (this._containerQueryNodes.size === 0) return;
263
+ const held = new Map();
264
+ this._cqFresh = true;
265
+ try {
266
+ for (let pass = 0; pass < CONTAINER_QUERY_PASSES; pass++) {
267
+ if (!this._resolveContainerQueries(held)) return;
268
+ relayout();
269
+ }
270
+ if (DEV && this._resolveContainerQueries(held, false)) {
271
+ console.warn(
272
+ 'react-x11: "@container" blocks did not settle in ' +
273
+ `${CONTAINER_QUERY_PASSES} layout passes; the last one stands. ` +
274
+ 'More than two nested containers, each changing the next, is ' +
275
+ 'the shape that gets here (docs/styling.md#container-queries).',
276
+ );
277
+ }
278
+ } finally {
279
+ this._cqFresh = false;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * One round of the above: every dependent whose blocks answer differently
285
+ * against the containers as they are now is re-resolved, and the caller
286
+ * hears whether any was. With `apply` false it only answers.
287
+ *
288
+ * `held` is what each node has answered so far this frame. A node that
289
+ * comes back to an answer it already held is a design that oscillates —
290
+ * the block moves the size it asks about, which CSS forbids by
291
+ * construction (size containment) and yoga cannot — so it is **pinned**:
292
+ * the answer it has stands, and stays until the container's size moves
293
+ * for some other reason. Without the pin the next frame would find the
294
+ * other answer, apply it, get the other size, and strobe on every layout.
295
+ */
296
+ _resolveContainerQueries(held, apply = true) {
297
+ let changed = false;
298
+ for (const node of [...this._containerQueryNodes]) {
299
+ if (node.destroyed) {
300
+ this._containerQueryNodes.delete(node);
301
+ continue;
302
+ }
303
+ const cq = node._cq;
304
+ const containers = node._containerSizes();
305
+ const key = containersKey(containers);
306
+ if (cq.pin) {
307
+ if (cq.pin.key === key) continue;
308
+ cq.pin = null;
309
+ }
310
+ const answers = containerAnswers(node._baseStyle, containers);
311
+ if (answers === cq.answers) continue;
312
+ if (!apply) return true;
313
+ let seen = held.get(node);
314
+ if (seen?.includes(answers)) {
315
+ cq.pin = { key, containers: cq.containers };
316
+ if (DEV) node._warnContainerOscillation();
317
+ continue;
318
+ }
319
+ if (!seen) held.set(node, (seen = [cq.answers]));
320
+ seen.push(answers);
321
+ node._sizeQueriesChanged();
322
+ changed = true;
323
+ }
324
+ // a block may carry layout properties, so the floors measured from the
325
+ // styles it is replacing are not the answer any more — the same debt a
326
+ // window query leaves (`_resolveSizeQueries`)
327
+ if (changed) this._floorsDirty = true;
328
+ return changed;
329
+ }
330
+ }
@@ -0,0 +1,102 @@
1
+ // Rect algebra: the pure geometry the damage model, hit testing, positions
2
+ // and the scroll blit do their sums with. Nothing in here knows about nodes.
3
+
4
+ export const rectContains = (outer, inner) =>
5
+ outer.x <= inner.x &&
6
+ outer.y <= inner.y &&
7
+ outer.x + outer.width >= inner.x + inner.width &&
8
+ outer.y + outer.height >= inner.y + inner.height;
9
+
10
+ export const isIntegerRect = (r) =>
11
+ Number.isInteger(r.x) &&
12
+ Number.isInteger(r.y) &&
13
+ Number.isInteger(r.width) &&
14
+ Number.isInteger(r.height);
15
+
16
+ /** `rect` shrunk by `by` on every side. */
17
+ export function insetRect(rect, by) {
18
+ return {
19
+ x: rect.x + by,
20
+ y: rect.y + by,
21
+ width: rect.width - 2 * by,
22
+ height: rect.height - 2 * by,
23
+ };
24
+ }
25
+
26
+ /** The whole pixels inside `rect` — a fractional edge left out — or null
27
+ * when none are. */
28
+ export function innerPixels(rect) {
29
+ const x = Math.ceil(rect.x);
30
+ const y = Math.ceil(rect.y);
31
+ const right = Math.floor(rect.x + rect.width);
32
+ const bottom = Math.floor(rect.y + rect.height);
33
+ if (right <= x || bottom <= y) return null;
34
+ return { x, y, width: right - x, height: bottom - y };
35
+ }
36
+
37
+ /** The overlap of two rects, or null when they have none. */
38
+ export function intersectRects(a, b) {
39
+ const x = Math.max(a.x, b.x);
40
+ const y = Math.max(a.y, b.y);
41
+ const right = Math.min(a.x + a.width, b.x + b.width);
42
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
43
+ if (right <= x || bottom <= y) return null;
44
+ return { x, y, width: right - x, height: bottom - y };
45
+ }
46
+
47
+ export function unionRect(a, b) {
48
+ if (!b) return a ?? null;
49
+ if (!a) return { ...b };
50
+ const x = Math.min(a.x, b.x);
51
+ const y = Math.min(a.y, b.y);
52
+ return {
53
+ x,
54
+ y,
55
+ width: Math.max(a.x + a.width, b.x + b.width) - x,
56
+ height: Math.max(a.y + a.height, b.y + b.height) - y,
57
+ };
58
+ }
59
+
60
+ export function rectArea(r) {
61
+ return Math.max(0, r.width) * Math.max(0, r.height);
62
+ }
63
+
64
+ /** The box around a non-empty list of rects. */
65
+ export function rectsBounds(rects) {
66
+ let out = rects[0];
67
+ for (let i = 1; i < rects.length; i++) out = unionRect(out, rects[i]);
68
+ return out;
69
+ }
70
+
71
+ /** Does `rect` reach into any of the four `radius`-sized corner squares of
72
+ * `box` — the only part of a rounded border a translation cannot keep? */
73
+ export function cornerSquaresOverlap(box, radius, rect) {
74
+ const r = Math.min(radius, box.width / 2, box.height / 2);
75
+ if (!(r > 0)) return false;
76
+ const corners = [
77
+ { x: box.x, y: box.y, width: r, height: r },
78
+ { x: box.x + box.width - r, y: box.y, width: r, height: r },
79
+ { x: box.x, y: box.y + box.height - r, width: r, height: r },
80
+ {
81
+ x: box.x + box.width - r,
82
+ y: box.y + box.height - r,
83
+ width: r,
84
+ height: r,
85
+ },
86
+ ];
87
+ return corners.some((square) => rectsOverlap(square, rect));
88
+ }
89
+
90
+ /**
91
+ * Do two rects share any area? Touching edges do not count. With a
92
+ * `margin`, do they come within that many pixels of each other: the same
93
+ * question asked of either one grown by `margin` on every side.
94
+ */
95
+ export function rectsOverlap(a, b, margin = 0) {
96
+ return (
97
+ a.x < b.x + b.width + margin &&
98
+ b.x < a.x + a.width + margin &&
99
+ a.y < b.y + b.height + margin &&
100
+ b.y < a.y + a.height + margin
101
+ );
102
+ }