react-x11 2.18.5 → 2.19.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.18.5",
3
+ "version": "2.19.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -103,14 +103,14 @@
103
103
  "dependencies": {
104
104
  "ical.js": "^2.2.1",
105
105
  "linebreak": "^1.1.0",
106
- "ntk": "^8.9.0",
106
+ "ntk": "^8.10.0",
107
107
  "react-reconciler": "^0.34.0",
108
108
  "yoga-layout": "^3.2.1"
109
109
  },
110
110
  "optionalDependencies": {
111
111
  "@windowkit/appkit": "^0.12.0",
112
112
  "@windowkit/wayland": "^3.1.1",
113
- "@windowkit/win32": "^0.0.1",
113
+ "@windowkit/win32": "^0.0.3",
114
114
  "dbus-native": "^0.15.1",
115
115
  "x11-dri": "^0.9.0"
116
116
  },
package/src/Reconciler.js CHANGED
@@ -7,7 +7,6 @@
7
7
  import { createRequire } from 'node:module';
8
8
  import React from 'react';
9
9
  import ReactReconciler from 'react-reconciler';
10
- import { createClient } from 'ntk';
11
10
 
12
11
  import {
13
12
  ConcurrentRoot,
@@ -22,6 +21,7 @@ import {
22
21
  flushWindowMaps,
23
22
  flushWindowRestacks,
24
23
  } from './nodes/window/window.js';
24
+ import { loadNtk } from './ntkroot.js';
25
25
  import { PopupNode } from './nodes/window/popup.js';
26
26
  import { BoxNode } from './nodes/box.js';
27
27
  import { THEME_SCOPE } from './nodes/kinds.js';
@@ -629,6 +629,8 @@ function loadIntegrations() {
629
629
  * anyone hits and the least self-explanatory failure in the library.
630
630
  */
631
631
  async function connect(options) {
632
+ // ntk's root is the X11 backend's, and loads with it (src/ntkroot.js)
633
+ const { createClient } = await loadNtk();
632
634
  try {
633
635
  return await createClient(options);
634
636
  } catch (err) {
@@ -822,7 +824,12 @@ export async function createRoot(options = {}) {
822
824
  const win32Asked =
823
825
  rest.backend === 'win32' || process.env.REACT_X11_BACKEND === 'win32';
824
826
  const connecting = !owned
825
- ? Promise.resolve(borrowed)
827
+ ? // an ntk app could only have come from ntk's own createClient: the
828
+ // root is imported already, and this hands it to the code that reads
829
+ // it synchronously (src/ntkroot.js)
830
+ isNtkApp(borrowed)
831
+ ? loadNtk().then(() => borrowed)
832
+ : Promise.resolve(borrowed)
826
833
  : backend === 'wayland'
827
834
  ? // Opt-in only, never through 'auto': X11 stays the default on Linux
828
835
  // because the remote case (docs/remote.md) is the flagship reason
@@ -23,7 +23,7 @@
23
23
  // class keeps the JS-visible state (fillStyle strings, gradient objects,
24
24
  // dash arrays) and re-syncs it when the backing surface is replaced after a
25
25
  // resize — `_gen` is that generation.
26
- import { cssColorStraight } from 'ntk';
26
+ import { cssColorStraight } from 'ntk/color';
27
27
 
28
28
  const BLACK = [0, 0, 0, 1];
29
29
 
@@ -558,13 +558,50 @@ export class BackendContext2D {
558
558
 
559
559
  setLineDash(segments) {
560
560
  this._state.dash = Array.isArray(segments) ? segments : [];
561
- this._native.ctxSetLineDash(this._s(), this._state.dash, 0);
561
+ // the offset is state of its own, and a new pattern keeps it
562
+ this._native.ctxSetLineDash(
563
+ this._s(),
564
+ this._state.dash,
565
+ this._state.dashOffset,
566
+ );
562
567
  }
563
568
 
564
569
  getLineDash() {
565
570
  return [...this._state.dash];
566
571
  }
567
572
 
573
+ /**
574
+ * Where along the pattern a stroke starts — the phase a marching dash
575
+ * moves. Missing here, a caller that asks `'lineDashOffset' in ctx` found
576
+ * no such thing and every stroke started its pattern at nought: the
577
+ * dashes of an animated edge were repainted sixteen times a second and
578
+ * never moved.
579
+ */
580
+ /**
581
+ * Text drawn under a transform that scales is drawn scaled, glyphs and
582
+ * all: both bridges lay a text layout out once and draw its outlines
583
+ * through the context's matrix (Direct2D's `DrawTextLayout`, CoreText's
584
+ * `CTFrameDraw`). ntk's context on X11 cannot — its glyphs come out of a
585
+ * cache rasterized at the size they were shaped at, and a transform
586
+ * moves where each lands, not how big it is — and says nothing, which is
587
+ * the same answer. So an element that would rather draw a layout it has
588
+ * than shape a new one at every step of a zoom asks this first.
589
+ */
590
+ get scalesText() {
591
+ return true;
592
+ }
593
+
594
+ get lineDashOffset() {
595
+ return this._state.dashOffset;
596
+ }
597
+
598
+ set lineDashOffset(value) {
599
+ // canvas ignores anything that is not a finite number
600
+ if (typeof value !== 'number' || !Number.isFinite(value)) return;
601
+ this._state.dashOffset = value;
602
+ this._native.ctxSetLineDash(this._s(), this._state.dash, value);
603
+ }
604
+
568
605
  save() {
569
606
  // Sync before pushing: a fresh surface empties the stack on its way in,
570
607
  // and a state pushed ahead of that sync was lost to it — the first
package/src/cocoa/app.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // (`_routeBatch`) with no pump at all (docs/macos.md §"JS on a worker").
17
17
  import { isMainThread } from 'node:worker_threads';
18
18
 
19
- import { cssColorStraight } from 'ntk';
19
+ import { cssColorStraight } from 'ntk/color';
20
20
 
21
21
  import { deliverActivate, deliverOpen } from '../application.js';
22
22
  import { flushPendingFrames } from '../frames.js';
@@ -26,7 +26,8 @@ import { readFileSync } from 'node:fs';
26
26
  import { inflateSync } from 'node:zlib';
27
27
 
28
28
  import LineBreaker from 'linebreak';
29
- import { cssColorStraight, Font } from 'ntk';
29
+ import { cssColorStraight } from 'ntk/color';
30
+ import Font from 'ntk/font';
30
31
 
31
32
  import { loadNative } from './native.js';
32
33
 
@@ -24,7 +24,7 @@
24
24
  // named repaints everything" rule the X11 damage model has), and geometry
25
25
  // is re-diffed every frame because comparing four numbers is cheaper than
26
26
  // knowing.
27
- import { cssColorStraight } from 'ntk';
27
+ import { cssColorStraight } from 'ntk/color';
28
28
 
29
29
  import { Node } from '../nodes/node.js';
30
30
  import { addDamageRect, damageToPaint } from '../nodes/damage.js';
@@ -34,7 +34,7 @@
34
34
  // derives is the padding a coverage surface has to carry. Both are pure
35
35
  // functions — no connection, nothing drawn — so this module stays testable
36
36
  // without a server.
37
- import { shadowReach, shadowSigma } from 'ntk';
37
+ import { shadowReach, shadowSigma } from 'ntk/shadow-math';
38
38
 
39
39
  /** A number, with or without the `px` CSS wants and this vocabulary does not. */
40
40
  const LENGTH = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:px)?$/i;
package/src/fonts.js CHANGED
@@ -37,7 +37,7 @@
37
37
 
38
38
  import { basename, extname } from 'node:path';
39
39
 
40
- import { Font } from 'ntk';
40
+ import Font from 'ntk/font';
41
41
 
42
42
  /**
43
43
  * Per-app bookkeeping. Keyed on the app rather than stored on it, like
@@ -30,7 +30,7 @@
30
30
  // save set, **never destroyed**. Destroying somebody else's window because a
31
31
  // React tree changed is the failure mode this element is most able to cause,
32
32
  // and `test/foreign.test.js` asserts against it first.
33
- import { XEMBED, XEmbedSocket } from 'ntk';
33
+ import { x11Ntk } from './ntkroot.js';
34
34
 
35
35
  import { isFocusable } from './a11y.js';
36
36
  import { canEmbed } from './embedding.js';
@@ -161,6 +161,8 @@ export class ForeignNode extends Node {
161
161
  if (!canEmbed(this.app)) return this._refuse();
162
162
  const rect = this._geometry();
163
163
  this.rect = rect;
164
+ // `<foreign>` embeds on an X connection only, where ntk's root is loaded
165
+ const { XEmbedSocket } = x11Ntk('<foreign>');
164
166
  const socket = new XEmbedSocket(parent, {
165
167
  ...rect,
166
168
  // react-x11 has a focus manager; a socket that answered
@@ -318,6 +320,8 @@ export class ForeignNode extends Node {
318
320
  defaultFocus(info) {
319
321
  const socket = this.socket;
320
322
  if (!socket || !this.client) return;
323
+ // a socket is an X connection's, so ntk's root is loaded (src/ntkroot.js)
324
+ const { XEMBED } = x11Ntk('<foreign>');
321
325
  const time = lastInputTime(this.app);
322
326
  socket.activate(true, { time });
323
327
  const detail =
@@ -338,6 +342,7 @@ export class ForeignNode extends Node {
338
342
  if (!socket) return;
339
343
  const time = lastInputTime(this.app);
340
344
  if (this._focusSent) {
345
+ const { XEMBED } = x11Ntk('<foreign>');
341
346
  socket.send(XEMBED.FOCUS_OUT, 0, 0, 0, { time });
342
347
  this._focusSent = false;
343
348
  }
package/src/glbackend.js CHANGED
@@ -19,7 +19,7 @@
19
19
  // `appcontext.js` (for `useSupports('shaders')`) and the node layer can use
20
20
  // it without the two importing each other.
21
21
 
22
- import { GLError } from 'ntk';
22
+ import { GLError } from 'ntk/gl';
23
23
 
24
24
  /** Does this connection's policy ever choose the direct backend? */
25
25
  const wantsDirect = (app) => {
package/src/glnodes.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // (NEXT_STEPS §4), sized and positioned by the parent's yoga layout like any
6
6
  // other drawn node. Everything about the surface is here; the scene graph
7
7
  // that draws into it comes later (docs/glx-plan.md).
8
- import { cssColorStraight } from 'ntk';
8
+ import { cssColorStraight } from 'ntk/color';
9
9
 
10
10
  // re-exported so the GL element layer stays one import for consumers
11
11
  export { directGLFailure, hasDirectGL } from './glbackend.js';
package/src/gloverlay.js CHANGED
@@ -60,7 +60,7 @@
60
60
  // A leaf module, like src/embedding.js: `appcontext.js` asks `canOverlay`
61
61
  // for `useSupports('glOverlay')`, and it imports nothing of ours but the
62
62
  // damage model's arithmetic, which imports nothing at all.
63
- import { cssColorStraight } from 'ntk';
63
+ import { cssColorStraight } from 'ntk/color';
64
64
 
65
65
  import { FULL_DAMAGE, addDamageRect } from './nodes/damage.js';
66
66
  import {
@@ -6,7 +6,9 @@
6
6
  // and the `cacheKey` cache live here, where a test needs no server; the node
7
7
  // half in nodes/image.js is only lifecycle — when to resolve, when to claim
8
8
  // damage, when to let go.
9
- import { Image, Picture, decodeImage } from 'ntk';
9
+ import { Image, decodeImage } from 'ntk/image';
10
+
11
+ import { x11Ntk } from './ntkroot.js';
10
12
 
11
13
  /**
12
14
  * A client-side object the 2d context composites as-is: an ntk `Image` or
@@ -429,6 +431,7 @@ export class DrawableSource {
429
431
  picture() {
430
432
  if (!this._picture) {
431
433
  const Render = this.app.display.Render;
434
+ const { Picture } = x11Ntk('<image> on a server-side picture');
432
435
  this._picture = new Picture(this.app, {
433
436
  drawable: { id: this.id },
434
437
  format: Render[DEPTH_FORMATS[this.depth]],
package/src/node.d.ts CHANGED
@@ -100,6 +100,9 @@ export interface Context2D {
100
100
  };
101
101
  setLineDash(segments: number[]): void;
102
102
  getLineDash(): number[];
103
+ /** Where along the dash pattern a stroke starts, in the same units as the
104
+ * pattern — the phase a marching dash moves. */
105
+ lineDashOffset: number;
103
106
  beginPath(): void;
104
107
  closePath(): void;
105
108
  moveTo(x: number, y: number): void;
@@ -139,6 +142,15 @@ export interface Context2D {
139
142
  strokeRect(x: number, y: number, width: number, height: number): void;
140
143
  clearRect(x: number, y: number, width: number, height: number): void;
141
144
  fillText(text: string, x: number, y: number): void;
145
+ /**
146
+ * `true` where text drawn under a transform that scales is drawn scaled,
147
+ * glyphs and all — the Windows and macOS contexts. Absent on X11, where
148
+ * ntk rasterizes glyphs at the size the text was shaped at and a
149
+ * transform moves only where each one lands. An element that would draw
150
+ * a layout it already has at another size, rather than shape a new one,
151
+ * asks this first.
152
+ */
153
+ readonly scalesText?: boolean;
142
154
  measureText(text: string): { width: number; [key: string]: unknown };
143
155
  /**
144
156
  * Composite an offscreen `Surface` (`react-x11/ntk`) — whole at a point,
@@ -490,6 +490,7 @@ export class NodeAnimation {
490
490
  if (this.root) {
491
491
  this.root.needsLayout = true;
492
492
  this.root._floorsDirty = true;
493
+ this.root._floorsSources.add(this);
493
494
  }
494
495
  }
495
496
  // A tick writes `this.style` without going through `_retarget`, so it
@@ -18,7 +18,7 @@ import {
18
18
  // `paintcache.js`: a named import of something an older ntk does not export
19
19
  // is a *load-time* SyntaxError, which would take the renderer down rather
20
20
  // than the one feature that needs it.
21
- import * as ntk from 'ntk';
21
+ import { ntkRoot, x11Ntk } from '../ntkroot.js';
22
22
  import { isFocusable as a11yFocusable } from '../a11y.js';
23
23
  import { DEV } from './util.js';
24
24
 
@@ -320,7 +320,7 @@ export class NodeBoxPaint {
320
320
  );
321
321
  sctx.fill();
322
322
  },
323
- after: (surface) => ntk.blurCoverage(surface, sigma),
323
+ after: (surface) => x11Ntk('a baked shadow').blurCoverage(surface, sigma),
324
324
  live: () => this._paintShadowLive(ctx, plan),
325
325
  };
326
326
  const cache = this.root?._paintCache;
@@ -338,7 +338,9 @@ export class NodeBoxPaint {
338
338
  * a k x k kernel, by the ratio of 2k to k squared.
339
339
  */
340
340
  _paintShadowLive(ctx, plan) {
341
- if (typeof ntk.Surface !== 'function' || !this.app?.display?.Render) return;
341
+ const ntk = ntkRoot();
342
+ if (typeof ntk?.Surface !== 'function' || !this.app?.display?.Render)
343
+ return;
342
344
  let surface = null;
343
345
  try {
344
346
  surface = new ntk.Surface(this.app, {
@@ -20,7 +20,7 @@ import {
20
20
  // `paintcache.js`: a named import of something an older ntk does not export
21
21
  // is a *load-time* SyntaxError, which would take the renderer down rather
22
22
  // than the one feature that needs it.
23
- import * as ntk from 'ntk';
23
+ import { loadImage } from 'ntk/image';
24
24
  import { symbolWeight, symbolsFor, warnOnce } from '../symbols.js';
25
25
  import { intrinsicSize } from './layout.js';
26
26
  import { Node } from './node.js';
@@ -164,7 +164,7 @@ export class ImageNode extends Node {
164
164
  _loadEntry(src) {
165
165
  if (isPathImageSource(src)) {
166
166
  return {
167
- promise: ntk.loadImage(toLoadablePath(src)).then(
167
+ promise: loadImage(toLoadablePath(src)).then(
168
168
  (image) => image,
169
169
  (err) => {
170
170
  console.error(
@@ -196,7 +196,7 @@ export class ImageNode extends Node {
196
196
  async _loadFile(src) {
197
197
  const token = ++this._loadToken;
198
198
  try {
199
- const image = await ntk.loadImage(toLoadablePath(src));
199
+ const image = await loadImage(toLoadablePath(src));
200
200
  if (token !== this._loadToken || this.destroyed) return;
201
201
  this._ownedImage = image;
202
202
  this._setImage(image);
@@ -322,6 +322,11 @@ export class WindowInvalidate {
322
322
  // panned — was a full measurement of the content floors a step.
323
323
  if (reason !== 'scroll' && reason !== 'position') {
324
324
  this._floorsDirty = true;
325
+ // …and which node changed, so that a change inside a box that sizes
326
+ // itself measures that box and nothing around it (`_floorsScope`).
327
+ // A claim that names no node, or names the window, is the tree's.
328
+ if (source && source !== this) this._floorsSources.add(source);
329
+ else this._floorsUnscoped = true;
325
330
  }
326
331
  if (reason !== 'scroll' && reason !== 'resize' && reason !== 'position') {
327
332
  this._floorsContentDirty = true;
@@ -477,6 +482,27 @@ export class WindowInvalidate {
477
482
  return WINDOW_ONLY;
478
483
  }
479
484
 
485
+ /**
486
+ * One rect the layout diff says a node moved through (`layoutDiff.sink`,
487
+ * nodes/window/flush.js), to the lists that paint that node — the same
488
+ * reach a claim of it has (`_paneReach`). A node inside a surface is the
489
+ * panes' alone: the window's pass under the surface would repaint pixels
490
+ * no child of it ever had, and it went to both, so a card dragged across
491
+ * a graph drawn through GL was a window pass under the surface on every
492
+ * step. Answers whether the window's list took it.
493
+ */
494
+ _claimLayoutMove(rect, node, cap) {
495
+ const reach =
496
+ this._overlaid.size !== 0 ? this._paneReach(node ?? null) : WINDOW_ONLY;
497
+ let windowTook = false;
498
+ if (reach !== PANES_ONLY && this._damage !== FULL_DAMAGE) {
499
+ this._damage = addDamageRect(this._damage, rect, cap);
500
+ windowTook = true;
501
+ }
502
+ if (reach !== WINDOW_ONLY) this._addPaneDamage(rect);
503
+ return windowTook;
504
+ }
505
+
480
506
  /** One rect more for the panes, unless they repaint whole already. */
481
507
  _addPaneDamage(rect) {
482
508
  if (this._paneDamage === FULL_DAMAGE) return;
@@ -313,17 +313,17 @@ export class NodeLayout {
313
313
  ) {
314
314
  return;
315
315
  }
316
- if (had) layoutDiff.sink(insetRect(was, -grow));
316
+ if (had) layoutDiff.sink(insetRect(was, -grow), this);
317
317
  if (width > 0 && height > 0) {
318
- layoutDiff.sink(insetRect(this.abs, -grow));
318
+ layoutDiff.sink(insetRect(this.abs, -grow), this);
319
319
  }
320
320
  return;
321
321
  }
322
322
  if (had) {
323
- layoutDiff.sink(insetRect(old, -grow));
323
+ layoutDiff.sink(insetRect(old, -grow), this);
324
324
  }
325
325
  if (width > 0 && height > 0) {
326
- layoutDiff.sink(insetRect(this.abs, -grow));
326
+ layoutDiff.sink(insetRect(this.abs, -grow), this);
327
327
  }
328
328
  }
329
329
  }
@@ -415,6 +415,7 @@ export class NodeLayoutHost {
415
415
  // content floors were measured from, as any style change is
416
416
  root._floorsDirty = true;
417
417
  root._floorsContentDirty = true;
418
+ root._floorsUnscoped = true;
418
419
  }
419
420
  }
420
421
 
@@ -944,6 +945,7 @@ export class WindowLayoutHost {
944
945
  this._failedHosts.clear();
945
946
  this._floorsDirty = true;
946
947
  this._floorsContentDirty = true;
948
+ this._floorsUnscoped = true;
947
949
  return true;
948
950
  }
949
951
  }
@@ -3,7 +3,7 @@
3
3
  // window, background first.
4
4
 
5
5
  import { isPlaced } from '../layouts.js';
6
- import { Surface } from '../ntk.js';
6
+ import { Surface } from '../offscreen.js';
7
7
  import { isPaintedColor } from './boxpaint.js';
8
8
  import { DAMAGE_SLOP } from './damage.js';
9
9
  import { DRAWN_KINDS } from './kinds.js';
@@ -196,6 +196,7 @@ export class NodeQueries {
196
196
  if (applyLayoutStyle(this.yoga, this.style, before) && this.root) {
197
197
  this.root._floorsDirty = true;
198
198
  this.root._floorsContentDirty = true;
199
+ this.root._floorsSources.add(this);
199
200
  }
200
201
  }
201
202
  }
@@ -233,6 +234,7 @@ export class WindowQueries {
233
234
  // a query block may carry layout properties, so the floors measured from
234
235
  // the styles it is replacing are not the answer any more
235
236
  this._floorsDirty = true;
237
+ this._floorsUnscoped = true;
236
238
  for (const node of [...this._sizeQueryNodes]) {
237
239
  if (node.destroyed) this._sizeQueryNodes.delete(node);
238
240
  else node._sizeQueriesChanged();
@@ -324,7 +326,10 @@ export class WindowQueries {
324
326
  // a block may carry layout properties, so the floors measured from the
325
327
  // styles it is replacing are not the answer any more — the same debt a
326
328
  // window query leaves (`_resolveSizeQueries`)
327
- if (changed) this._floorsDirty = true;
329
+ if (changed) {
330
+ this._floorsDirty = true;
331
+ this._floorsUnscoped = true;
332
+ }
328
333
  return changed;
329
334
  }
330
335
  }
@@ -301,11 +301,11 @@ export const Scrollable = (Base) =>
301
301
  // for a `<glarea>` child's move that this pane rides (issue
302
302
  // #644): its box claimed nothing, and a pane laid out again may
303
303
  // have moved rows the move's copy carries where they were.
304
- if (clamped || layoutDiff.ride) outer(vp);
304
+ if (clamped || layoutDiff.ride) outer(vp, this);
305
305
  } else {
306
- layoutDiff.sink = (rect) => {
306
+ layoutDiff.sink = (rect, node) => {
307
307
  const clipped = intersectRects(rect, vp);
308
- if (clipped) outer(clipped);
308
+ if (clipped) outer(clipped, node);
309
309
  };
310
310
  }
311
311
  this._claimThumbs(thumbsWere, outer);
@@ -863,8 +863,8 @@ export const Scrollable = (Base) =>
863
863
  if (sameThumb(before, after)) continue;
864
864
  // a pixel of slop for the rounded corners' antialiasing, as the
865
865
  // blit's own thumb repair takes
866
- if (before) sink(insetRect(before, -1));
867
- if (after) sink(insetRect(after, -1));
866
+ if (before) sink(insetRect(before, -1), this);
867
+ if (after) sink(insetRect(after, -1), this);
868
868
  }
869
869
  }
870
870
 
package/src/nodes/text.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // selection paints, and the glyph strip.
3
3
 
4
4
  import { localTextStyleChanged, TEXT_REMEASURE } from '../styles.js';
5
- import { cssColorStraight } from 'ntk';
5
+ import { cssColorStraight } from 'ntk/color';
6
6
  import { hooks as a11yHooks } from '../a11y.js';
7
7
  import { codePointAtOffset, codeUnitOffsets } from '../textrange.js';
8
8
  import { NO_DAMAGE } from './damage.js';
@@ -1,7 +1,7 @@
1
1
  // What a window can paint: compositing and ARGB transparency, the window
2
2
  // background, and the capabilities `@supports` blocks read.
3
3
 
4
- import { cssColorStraight } from 'ntk';
4
+ import { cssColorStraight } from 'ntk/color';
5
5
  import {
6
6
  argbVisual,
7
7
  compositingActive,
@@ -197,6 +197,88 @@ export function collectFloorStale(node, stale, found, sweep) {
197
197
  }
198
198
  }
199
199
 
200
+ /**
201
+ * Whether `node` is a box its content cannot size and its surroundings cannot
202
+ * size either — an edge a floor measurement may stop at
203
+ * (`WindowNode._floorsScope`).
204
+ *
205
+ * Absolutely positioned, with a width and a height its style names as
206
+ * numbers: it is out of its parent's flow, so nothing measured inside it
207
+ * reaches anything above it (`contentSpan` skips it, and no floor is written
208
+ * on it), and every pass lays it out at those numbers wherever it sits, so
209
+ * what is inside it is measured the same in a pass over the window and in a
210
+ * pass over it alone. Nothing in its own box resolves against its parent —
211
+ * a percentage of padding would — and neither it nor its parent is a layout
212
+ * host, whose children are measured tree by tree.
213
+ *
214
+ * A card on a graph, a popover laid out inside the window, a sized overlay:
215
+ * the boxes a change inside is most often confined to. An in-flow box with a
216
+ * size of its own would do as well along its cross axis, but on its main
217
+ * axis a flex line can grow it past its width or shrink it under a minimum
218
+ * the author named, and that is a question about its siblings.
219
+ */
220
+ export function isFloorBoundary(node) {
221
+ const style = node.style;
222
+ return (
223
+ node.yoga != null &&
224
+ !node.isWindow &&
225
+ style.position === 'absolute' &&
226
+ style.display !== 'none' &&
227
+ typeof style.width === 'number' &&
228
+ typeof style.height === 'number' &&
229
+ typeof style.minWidth !== 'string' &&
230
+ typeof style.minHeight !== 'string' &&
231
+ typeof style.maxWidth !== 'string' &&
232
+ typeof style.maxHeight !== 'string' &&
233
+ !PADDINGS.some((key) => typeof style[key] === 'string') &&
234
+ node._host === null &&
235
+ (node.parent?._host ?? null) === null
236
+ );
237
+ }
238
+
239
+ const PADDINGS = [
240
+ 'padding',
241
+ 'paddingTop',
242
+ 'paddingRight',
243
+ 'paddingBottom',
244
+ 'paddingLeft',
245
+ 'paddingHorizontal',
246
+ 'paddingVertical',
247
+ 'paddingStart',
248
+ 'paddingEnd',
249
+ ];
250
+
251
+ /**
252
+ * The box that holds a change to `node` in (`isFloorBoundary`), or null when
253
+ * none does below `root`. Looked for from the parent up: a node that changed
254
+ * may have changed its own size, and it is what is *around* the change that
255
+ * has to hold still.
256
+ */
257
+ export function floorBoundaryOf(node, root) {
258
+ for (let n = node.parent; n && n !== root; n = n.parent) {
259
+ if (n.isWindow) return null;
260
+ if (isFloorBoundary(n)) return n;
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Whether yoga has a change on record anywhere but inside `boundaries` and on
267
+ * the way down to them (`paths`) — something that changed with no node
268
+ * saying so, which a measurement confined to the boundaries would miss. A
269
+ * dirty node's ancestors are dirty too, so the walk goes down dirty nodes
270
+ * only, and a clean one has nothing under it to find.
271
+ */
272
+ export function dirtyOutside(node, boundaries, paths) {
273
+ for (const child of node.children) {
274
+ if (!child.yoga || child.isWindow || !child.yoga.isDirty()) continue;
275
+ if (boundaries.has(child)) continue;
276
+ if (!paths.has(child)) return true;
277
+ if (dirtyOutside(child, boundaries, paths)) return true;
278
+ }
279
+ return false;
280
+ }
281
+
200
282
  /**
201
283
  * After a pass that settled the widths: which of the height floors, each a
202
284
  * height *for a width*, were measured for a width their node no longer has.
@@ -216,7 +298,7 @@ export function collectFloorStale(node, stale, found, sweep) {
216
298
  * the pixel grid off and the real one with it on, so the same layout reads
217
299
  * a fraction apart between them.
218
300
  */
219
- export function probeHeightFloors(node, root, hit) {
301
+ export function probeHeightFloors(node, root, hit, stop = root) {
220
302
  for (const child of node.children) {
221
303
  if (!child.yoga || child.isWindow) continue;
222
304
  if (child.style.display === 'none') continue;
@@ -230,10 +312,10 @@ export function probeHeightFloors(node, root, hit) {
230
312
  at !== undefined &&
231
313
  child._heightForWidth(width) !== child._heightForWidth(at)
232
314
  ) {
233
- markHeightStale(child, root, hit);
315
+ markHeightStale(child, root, hit, stop);
234
316
  }
235
317
  } else {
236
- probeHeightFloors(child, root, hit);
318
+ probeHeightFloors(child, root, hit, stop);
237
319
  }
238
320
  // whatever the extent is, it is the extent for this width now — a
239
321
  // stale one is about to be measured here, a clean one was just checked
@@ -241,10 +323,12 @@ export function probeHeightFloors(node, root, hit) {
241
323
  }
242
324
  }
243
325
 
244
- /** A leaf whose height moved takes every extent above it with it. */
245
- function markHeightStale(node, root, hit) {
326
+ /** A leaf whose height moved takes every extent above it with it — up to
327
+ * `stop`, a box that sizes itself (`isFloorBoundary`), whose extent is its
328
+ * style's whatever moved inside it. */
329
+ function markHeightStale(node, root, hit, stop = root) {
246
330
  hit.marked = true;
247
- for (let n = node; n && n !== root; n = n.parent) {
331
+ for (let n = node; n && n !== stop; n = n.parent) {
248
332
  if (n._floorH === undefined) continue;
249
333
  n._floorH = undefined;
250
334
  root._floorsStale.add(n);