react-x11 2.24.0 → 2.25.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.
@@ -15,8 +15,13 @@
15
15
  //
16
16
  // - Down a column whose height is its content's, the measuring passes have
17
17
  // no free space to squash into, so a block in it is laid out at its
18
- // content height in every pass. Laid out alone with no height on offer,
19
- // it comes out the same.
18
+ // content height in every pass. What it holds is laid out in the height
19
+ // the pass offers the column, though — none under a pane that scrolls,
20
+ // and 0 under the window, which the collapse lays out at 0 and every box
21
+ // on the way down passes on as a bound — and a bound is not nothing: a
22
+ // pane in the block that names a floor is squashed to it, and a basis
23
+ // counts. Laid out alone as the one item of a column that offers the
24
+ // same (`frameFor`), the block comes out the same.
20
25
  // - Across, a block stretched over its column is as wide as the column's
21
26
  // content box, in every pass: now, at the width the heights are measured
22
27
  // for, and in the width pass, whose width the way down from the root
@@ -30,7 +35,12 @@
30
35
  // Anything else — a row on the way, a margin or a width that is not a
31
36
  // length — is not a spine, and the tree is measured whole.
32
37
 
33
- import { declaresOwnMinimum, inFlow, mainAxisOf } from './floors.js';
38
+ import {
39
+ declaresOwnMinimum,
40
+ inFlow,
41
+ mainAxisOf,
42
+ namesOwnFloor,
43
+ } from './floors.js';
34
44
  import { Yoga } from '../../yoga.js';
35
45
 
36
46
  const LENGTHS_ACROSS = [
@@ -149,7 +159,7 @@ export function spineScope(root, sources, listOnly) {
149
159
  if (!child.yoga || child.isWindow || child.style.display === 'none') {
150
160
  continue;
151
161
  }
152
- const widths = rootWidths(child, offered);
162
+ const widths = rootWidths(child, offered, node);
153
163
  if (widths === null) return null;
154
164
  roots.set(child, widths);
155
165
  }
@@ -161,6 +171,11 @@ export function spineScope(root, sources, listOnly) {
161
171
  return false;
162
172
  };
163
173
  for (const node of [...roots.keys()]) if (within(node)) roots.delete(node);
174
+ // …and one that is not has to be laid out alone as the whole tree lays it
175
+ // out, which only the pass knows for some (`frameFor`)
176
+ for (const [node, widths] of roots) {
177
+ if (!heightHolds(node, widths.frame)) return null;
178
+ }
164
179
  for (const node of [...spine]) {
165
180
  if (roots.has(node) || within(node)) spine.delete(node);
166
181
  }
@@ -186,6 +201,121 @@ export function spineScope(root, sources, listOnly) {
186
201
  };
187
202
  }
188
203
 
204
+ /**
205
+ * The column a root is laid out alone as the one item of, for its heights
206
+ * (`_measureSpineRoot`): one that lays it out in what the collapse pass
207
+ * over the whole tree offers the column it sits in.
208
+ *
209
+ * - `COLLAPSED` under the window, which that pass lays out at 0, and under
210
+ * a stop that only names its floors or clips when every box on the way
211
+ * up from it passes that 0 on (`passesBound`). Every column the spine
212
+ * sums passes it on too, having no height of its own — unless it names
213
+ * a basis or an aspect ratio to lay out what it holds in instead.
214
+ * - `UNBOUNDED` in a column under a pane that scrolls, which measures what
215
+ * it holds with no bound. The column is laid out at its content's height,
216
+ * as long as the pane's own item on the way has no basis to start from.
217
+ * - An item of the pane itself is laid out in the pane's height: 0 when
218
+ * the pane is squashed to nothing, as a pane under the window is, which
219
+ * the frame copies by scrolling too (`SQUASHED_PANE`); in any other
220
+ * pane's, which only the pass knows (`OPEN_PANE`).
221
+ *
222
+ * `null` where a box on the way lays out what it holds in a height of its
223
+ * own, which only the pass knows too.
224
+ */
225
+ function frameFor(column, stop) {
226
+ if (stop.style.overflow === 'scroll') {
227
+ if (column === stop) {
228
+ return boundFrom(stop) ? SQUASHED_PANE : OPEN_PANE;
229
+ }
230
+ for (let node = column; node !== stop; node = node.parent) {
231
+ if (node.style.aspectRatio > 0) return null;
232
+ if (node.parent === stop && basisOf(node.style) !== 'auto') return null;
233
+ }
234
+ return UNBOUNDED;
235
+ }
236
+ for (let node = column; node !== stop; node = node.parent) {
237
+ if (!passesBound(node)) return null;
238
+ }
239
+ return boundFrom(stop) ? COLLAPSED : null;
240
+ }
241
+
242
+ const COLLAPSED = Object.freeze({ height: 0, scrolls: false });
243
+ const UNBOUNDED = Object.freeze({ height: undefined, scrolls: false });
244
+ const SQUASHED_PANE = Object.freeze({ height: 0, scrolls: true });
245
+ // laid out with no bound, as `UNBOUNDED` — for the roots that nothing about
246
+ // the pane's height decides (`heightHolds`)
247
+ const OPEN_PANE = Object.freeze({ height: undefined, scrolls: false });
248
+
249
+ /** Whether the collapse's 0 reaches what `node` holds: no box from it up to
250
+ * the window has a height of its own to replace it with, and none above
251
+ * it scrolls, measuring what it holds with no bound. */
252
+ function boundFrom(node) {
253
+ for (let n = node; !n.isWindow; n = n.parent) {
254
+ if (!n.parent) return false;
255
+ if (n !== node && n.style.overflow === 'scroll') return false;
256
+ if (!passesBound(n)) return false;
257
+ }
258
+ return true;
259
+ }
260
+
261
+ /**
262
+ * Whether a box lays out what it holds in the bound it is laid out in
263
+ * itself: no height of its own named, no floor above nothing, no basis but
264
+ * 0 and no aspect ratio to take one from. Asked of the style, as everything
265
+ * here is — yoga's getters are a call into the engine and an object each,
266
+ * and a fling asks this of every box up to the window — and it is the
267
+ * style's floor that counts: the one the floors wrote is taken off a box a
268
+ * change is inside before anything is measured (`collectFloorStale`), and
269
+ * every box asked here is one.
270
+ */
271
+ function passesBound(node) {
272
+ const style = node.style;
273
+ if (style.height !== undefined && style.height !== 'auto') return false;
274
+ if (style.minHeight !== undefined && style.minHeight !== 0) return false;
275
+ const basis = basisOf(style);
276
+ if (share(basis) || basis > 0) return false;
277
+ return !(style.aspectRatio > 0);
278
+ }
279
+
280
+ /** A length given as a share of something — a percentage — rather than as
281
+ * a number or `auto`. */
282
+ const share = (value) => typeof value === 'string' && value !== 'auto';
283
+
284
+ /** The basis a style gives a flex item, as yoga takes it: the longhand, or
285
+ * the shorthand's — 0 for a number, `auto` for either keyword. */
286
+ function basisOf(style) {
287
+ if (style.flexBasis !== undefined) return style.flexBasis;
288
+ return typeof style.flex === 'number' ? 0 : 'auto';
289
+ }
290
+
291
+ /**
292
+ * Whether a root laid out alone in `frame` comes out as the whole tree lays
293
+ * it out: there is a frame; no length on its height axis is a share of the
294
+ * column it is not laid out in here; and in a pane that is not squashed,
295
+ * whose height only the pass knows, the root's own height is not decided
296
+ * by it — it does not grow into it, give way to it with a floor, or start
297
+ * from a basis.
298
+ */
299
+ function heightHolds(root, frame) {
300
+ if (frame === null) return false;
301
+ const style = root.style;
302
+ const basis = basisOf(style);
303
+ if (
304
+ share(style.height) ||
305
+ share(style.minHeight) ||
306
+ share(style.maxHeight) ||
307
+ share(basis)
308
+ ) {
309
+ return false;
310
+ }
311
+ if (frame !== OPEN_PANE) return true;
312
+ return !(
313
+ root.yoga.getFlexGrow() > 0 ||
314
+ namesOwnFloor(root, 'height') ||
315
+ basis !== 'auto'
316
+ );
317
+ }
318
+
189
319
  const OUT_OF_FLOW = 1;
190
320
  const RELATIVE_MARGIN = 2;
191
321
  const STYLE_FLAGS = new WeakMap();
@@ -240,7 +370,7 @@ function staleChildren(node) {
240
370
  * that is not a length, a percentage or `auto`, ends one: `auto` centres the
241
371
  * box instead of stretching it.
242
372
  */
243
- function rootWidths(node, offered) {
373
+ function rootWidths(node, offered, stop) {
244
374
  const style = node.style;
245
375
  const parent = node.parent;
246
376
  if (!parent || !inFlow(node) || node._host !== null) return null;
@@ -249,12 +379,13 @@ function rootWidths(node, offered) {
249
379
  if (!stretched(node)) return null;
250
380
  if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
251
381
  if (PADDINGS.some((key) => typeof style[key] === 'string')) return null;
252
- return offered.in(parent);
382
+ return offered.in(parent, stop);
253
383
  }
254
384
 
255
385
  /**
256
386
  * The widths a column offers the roots in it (`rootWidths`), worked out once
257
- * a column rather than once a root.
387
+ * a column rather than once a root — and the column they are laid out in
388
+ * for their heights (`frameFor`), the same for every root in it.
258
389
  *
259
390
  * Every root in a column is offered the same two, and working out the width
260
391
  * pass's walks the way up to the window, with four yoga reads a box for its
@@ -272,14 +403,16 @@ class OfferedWidths {
272
403
  this.passWidths = new Map();
273
404
  }
274
405
 
275
- /** `{ width, widthPass }` for a root in `column`: its content box now,
276
- * and in the width pass over the whole tree. */
277
- in(column) {
406
+ /** `{ width, widthPass, frame }` for a root in `column`: its content box
407
+ * now, and in the width pass over the whole tree; and the column it is
408
+ * laid out in, under `stop`. */
409
+ in(column, stop) {
278
410
  let offer = this.offers.get(column);
279
411
  if (offer === undefined) {
280
412
  offer = {
281
413
  width: Math.max(0, column.yoga.getComputedWidth() - this.inset(column)),
282
414
  widthPass: this.passContent(column),
415
+ frame: frameFor(column, stop),
283
416
  };
284
417
  this.offers.set(column, offer);
285
418
  }
@@ -185,11 +185,17 @@ export class WindowNode extends Scrollable(Node) {
185
185
  // over and its cost is known (`flush`)
186
186
  this._inFlush = false;
187
187
  this._claimAfterFlush = false;
188
+ // a frame laid out, so what is under the pointer may have moved
189
+ // (`flush`, `EventManager.refreshHover`)
190
+ this._hoverRecheck = false;
188
191
  // the nodes answering `opaqueRect()`, and — during a paint pass one of
189
192
  // them covers — that node with its ancestors, whose fills are skipped
190
193
  // (`_coverFor`, `Node._paintBackground`)
191
194
  this._opaqueNodes = new Set();
192
195
  this._coverChain = null;
196
+ // the nodes answering `viewportFixedRects()`, whose rects a scroll pane
197
+ // they are in repairs when it blits (`_viewportFixedPins`)
198
+ this._viewportFixedNodes = new Set();
193
199
  // Nodes that want the `attention` event (ntk#37) — an
194
200
  // `unstable_onAttention` prop,
195
201
  // an `:attention` block, or both. Built before the EventManager so the
package/src/xsettings.js CHANGED
@@ -127,6 +127,9 @@ class XSettingsSession {
127
127
  this.stopped = false;
128
128
  this._selectionAtom = 0;
129
129
  this._propertyAtom = 0;
130
+ /** What `stop` takes off the connection: the event listeners and the
131
+ * XFixes address window `watchOwner` put on it. */
132
+ this._teardown = [];
130
133
  }
131
134
 
132
135
  _set(values) {
@@ -150,6 +153,17 @@ class XSettingsSession {
150
153
  stop() {
151
154
  this.stopped = true;
152
155
  this.listeners.clear();
156
+ // Taken off, not just ignored: a listener that checks `stopped` and
157
+ // returns still runs for every event on the connection, and an app that
158
+ // mounts one root after another — a test suite, a hot reload — gained a
159
+ // listener and a window a root.
160
+ for (const undo of this._teardown.splice(0)) {
161
+ try {
162
+ undo();
163
+ } catch {
164
+ // a connection already gone takes all of it with it
165
+ }
166
+ }
153
167
  }
154
168
  }
155
169
 
@@ -238,8 +252,12 @@ async function readSettings(session) {
238
252
  async function watchOwner(session) {
239
253
  const app = session.app;
240
254
  const X = app.X;
255
+ // Started without being awaited, so a root unmounted in the meantime has
256
+ // already stopped the session — and a listener added now would be one
257
+ // `stop` never saw.
258
+ if (session.stopped) return;
241
259
 
242
- X.on('event', (ev) => {
260
+ const onProperty = (ev) => {
243
261
  if (session.stopped) return;
244
262
  if (
245
263
  ev.type === PROPERTY_NOTIFY &&
@@ -251,7 +269,9 @@ async function watchOwner(session) {
251
269
  () => {},
252
270
  );
253
271
  }
254
- });
272
+ };
273
+ X.on('event', onProperty);
274
+ session._teardown.push(() => X.removeListener('event', onProperty));
255
275
 
256
276
  const fixes = await requireExtension(app, 'fixes');
257
277
  if (!fixes || session.stopped) return;
@@ -261,11 +281,17 @@ async function watchOwner(session) {
261
281
  X.CreateWindow(id, app.display.screen[0].root, -10, -10, 1, 1, 0, 0, 2, 0, {
262
282
  eventMask: 0,
263
283
  });
264
- X.on('event', (ev) => {
284
+ const onOwner = (ev) => {
265
285
  if (session.stopped) return;
266
286
  if (ev.type !== fixes.firstEvent) return;
267
287
  if (ev.selection !== session._selectionAtom) return;
268
288
  latch(session).catch(() => {});
289
+ };
290
+ X.on('event', onOwner);
291
+ session._teardown.push(() => {
292
+ X.removeListener('event', onOwner);
293
+ // the selection input goes with the window it was addressed to
294
+ X.DestroyWindow(id);
269
295
  });
270
296
  fixes.SelectSelectionInput(
271
297
  id,