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.
@@ -6,7 +6,7 @@ import { resolveFramePolicy } from '../../pacing.js';
6
6
  import { paintCacheFor } from '../../paintcache.js';
7
7
  import { hooks as traceHooks } from '../../trace-registry.js';
8
8
  import { now } from '../animation.js';
9
- import { FULL_DAMAGE, layoutDiff, addDamageRect } from '../damage.js';
9
+ import { FULL_DAMAGE, layoutDiff } from '../damage.js';
10
10
  import { BLIT_POISONED } from '../scrollblit.js';
11
11
  import { debugPaint } from './debugpaint.js';
12
12
 
@@ -204,12 +204,10 @@ export class WindowFlush {
204
204
  (panes && this._paneDamage !== FULL_DAMAGE)
205
205
  ) {
206
206
  const cap = this._damageRectCap();
207
- layoutDiff.sink = (rect) => {
208
- if (this._damage !== FULL_DAMAGE) {
209
- layoutMoved = true;
210
- this._damage = addDamageRect(this._damage, rect, cap);
211
- }
212
- if (panes) this._addPaneDamage(rect);
207
+ // each to the lists that paint the node that moved
208
+ // (`_claimLayoutMove`): a move inside a `<glarea>` is its panes'
209
+ layoutDiff.sink = (rect, node) => {
210
+ if (this._claimLayoutMove(rect, node, cap)) layoutMoved = true;
213
211
  };
214
212
  }
215
213
  try {
@@ -259,14 +257,8 @@ export class WindowFlush {
259
257
  if (sv && !sv._recordBlitClaim(after)) {
260
258
  sv._pendingBlitFrom = BLIT_POISONED;
261
259
  }
262
- if (this._damage !== FULL_DAMAGE) {
263
- this._damage = addDamageRect(
264
- this._damage,
265
- after,
266
- this._damageRectCap(),
267
- );
268
- }
269
- if (panes) this._addPaneDamage(after);
260
+ // to the lists that paint it, as the layout diff's claims go
261
+ this._claimLayoutMove(after, node, this._damageRectCap());
270
262
  }
271
263
  this._reflowed.clear();
272
264
  } else if (this._reflowed.size) {
@@ -4,9 +4,12 @@
4
4
 
5
5
  import { measuringExactly } from '../../styles.js';
6
6
  import { availableArea } from '../../screens.js';
7
+ import { Yoga } from '../../yoga.js';
7
8
  import {
8
9
  captureLeafHeights,
9
10
  collectFloorStale,
11
+ dirtyOutside,
12
+ floorBoundaryOf,
10
13
  probeHeightFloors,
11
14
  setMeasuringShrink,
12
15
  restoreShrink,
@@ -27,6 +30,26 @@ import {
27
30
  scaleWindowGeometry,
28
31
  } from './hints.js';
29
32
 
33
+ // Escape hatch, read once like the others: for measuring the scoped floors
34
+ // against the whole-tree ones on the same build, and as first aid if a
35
+ // change inside a card is ever measured wrong. `=== '1'` for the reason
36
+ // REACT_X11_NO_SCROLL_BLIT gives.
37
+ const NO_SCOPED_FLOORS = process.env.REACT_X11_NO_SCOPED_FLOORS === '1';
38
+
39
+ /**
40
+ * Mark a box laid out alone (`_measureBoundary`) for the window's pass to lay
41
+ * out again. It was laid out at the size that pass will give it, so yoga
42
+ * would answer from its cache and keep what the measurement left inside —
43
+ * placed off the pixel grid, with the shrink borrowed. A style set to
44
+ * something else and back is how a box with no measure function says it
45
+ * changed.
46
+ */
47
+ function relayOut(node) {
48
+ const shrink = node.style.flexShrink ?? 1;
49
+ node.yoga.setFlexShrink(shrink + 1);
50
+ node.yoga.setFlexShrink(shrink);
51
+ }
52
+
30
53
  /** Content-sized windows, installed onto `WindowNode.prototype` by window.js. */
31
54
  export class WindowSize {
32
55
  /**
@@ -102,6 +125,21 @@ export class WindowSize {
102
125
  this._sweepLayoutHosts();
103
126
  const found = { width: false, height: false };
104
127
  this._floorsStale.clear();
128
+ // Decided afresh on every call rather than once a frame: something
129
+ // between the two calls a frame makes — a size query resolving — can
130
+ // turn a scoped change into one that is not, and the wider collection
131
+ // is the one that is always right.
132
+ const scope = this._floorsScope();
133
+ this._floorsScopeNow = scope;
134
+ if (scope !== null) {
135
+ for (const boundary of scope) {
136
+ // its children's floors are written from here; its own extent is
137
+ // its style's, and stands
138
+ this._floorsStale.add(boundary);
139
+ collectFloorStale(boundary, this._floorsStale, found, false);
140
+ }
141
+ return found;
142
+ }
105
143
  // the root's own children are written from here too, and its direction
106
144
  // can move like any node's
107
145
  this._floorsStale.add(this);
@@ -109,6 +147,123 @@ export class WindowSize {
109
147
  return found;
110
148
  }
111
149
 
150
+ /**
151
+ * The boxes the changes since the last measurement are confined to, or
152
+ * null when a measurement has to look at the whole tree.
153
+ *
154
+ * Every change to a box tree announces itself through `invalidate` with
155
+ * the node it changed (`_floorsSources`), and every node above it is
156
+ * dirty in yoga for it — which is all `collectFloorStale` can see, so a
157
+ * text that changed inside a card took every extent from the card up to
158
+ * the window with it, and the measurement that followed laid the whole
159
+ * tree out three times to read them back. A box that sizes itself
160
+ * (`isFloorBoundary`) holds the change in: its extent is its style's, the
161
+ * extents above it are what they were, and what is inside it can be
162
+ * measured by laying it out alone.
163
+ *
164
+ * Scoped only when all of that is known to hold: every change has such a
165
+ * box around it, yoga has nothing dirty outside them and the way down to
166
+ * them (a change nobody named), the tree has been measured once already,
167
+ * the window is the width it was measured at and sizes itself from no
168
+ * content, and no layout host or container query is in play. Anything
169
+ * else is the whole tree, as it always was.
170
+ */
171
+ _floorsScope() {
172
+ const sources = this._floorsSources;
173
+ if (
174
+ NO_SCOPED_FLOORS ||
175
+ this._floorsUnscoped ||
176
+ sources.size === 0 ||
177
+ !this._floorsSwept ||
178
+ this._layoutHosts.size !== 0 ||
179
+ this._containerQueryNodes.size !== 0 ||
180
+ this._floorsWidth !== (this.window?.width ?? this._requestedSize?.width)
181
+ ) {
182
+ return null;
183
+ }
184
+ const props = this.props;
185
+ if (
186
+ isAutoSize(props.width) ||
187
+ isAutoSize(props.height) ||
188
+ CONTENT_BOUND_PROPS.some((key) => isContentBound(props[key]))
189
+ ) {
190
+ return null;
191
+ }
192
+ const boundaries = new Set();
193
+ for (const source of sources) {
194
+ // gone, or moved to another window: its old parent's child list is
195
+ // the change that counts, and that parent said so itself
196
+ if (source.destroyed || source.root !== this) continue;
197
+ const boundary = floorBoundaryOf(source, this);
198
+ if (boundary === null) return null;
199
+ boundaries.add(boundary);
200
+ }
201
+ if (boundaries.size === 0) return null;
202
+ // the outermost of nested ones: measuring it measures what it holds
203
+ const paths = new Set();
204
+ for (const boundary of [...boundaries]) {
205
+ for (let n = boundary.parent; n && n !== this; n = n.parent) {
206
+ if (boundaries.has(n)) {
207
+ boundaries.delete(boundary);
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ for (const boundary of boundaries) {
213
+ for (let n = boundary.parent; n && n !== this; n = n.parent) {
214
+ paths.add(n);
215
+ }
216
+ }
217
+ if (dirtyOutside(this, boundaries, paths)) return null;
218
+ return boundaries;
219
+ }
220
+
221
+ /**
222
+ * One boundary's floors, measured with it laid out alone — the passes
223
+ * `_measureContentSpans` runs over the whole tree, run over the box: its
224
+ * size is its style's in either, so the space everything inside it is
225
+ * laid out in is the same. `probe` is the height measurement's check for
226
+ * leaves whose width moved, stopped at the box, whose own extent stands.
227
+ */
228
+ _measureBoundary(boundary, axis, probe = false) {
229
+ const yoga = boundary.yoga;
230
+ const dir =
231
+ boundary.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
232
+ if (axis === 'width') {
233
+ const shrunk = [];
234
+ setMeasuringShrink(boundary, axis, shrunk);
235
+ this._scopedFloorPasses += 1;
236
+ yoga.calculateLayout(undefined, undefined, dir);
237
+ contentSpan(boundary, axis, null, this);
238
+ restoreShrink(shrunk);
239
+ relayOut(boundary);
240
+ return;
241
+ }
242
+ this._scopedFloorPasses += 1;
243
+ yoga.calculateLayout(undefined, undefined, dir);
244
+ if (probe) {
245
+ const hit = { marked: false, owed: false };
246
+ probeHeightFloors(boundary, this, hit, boundary);
247
+ if (hit.marked) {
248
+ this._writeFloors('height');
249
+ this._scopedFloorPasses += 1;
250
+ yoga.calculateLayout(undefined, undefined, dir);
251
+ }
252
+ }
253
+ const intrinsic = new Map();
254
+ captureLeafHeights(boundary, intrinsic);
255
+ const frozen = [];
256
+ freezeWidths(boundary, frozen);
257
+ const shrunk = [];
258
+ setMeasuringShrink(boundary, axis, shrunk);
259
+ this._scopedFloorPasses += 1;
260
+ yoga.calculateLayout(undefined, 0, dir);
261
+ contentSpan(boundary, axis, intrinsic, this);
262
+ restoreWidths(frozen);
263
+ restoreShrink(shrunk);
264
+ relayOut(boundary);
265
+ }
266
+
112
267
  /** The floors on the children of every node found stale, from the extents
113
268
  * they carry — `writeFloors` for each. */
114
269
  _writeFloors(axis) {
@@ -203,28 +358,77 @@ export class WindowSize {
203
358
  */
204
359
  _applyContentFloors(width, height) {
205
360
  const found = this._collectFloorStale();
361
+ const scope = this._floorsScopeNow;
362
+ if (scope !== null) {
363
+ this._applyScopedFloors(scope, found, width, height);
364
+ } else {
365
+ measuringExactly(() => {
366
+ if (found.width) this._measureWidthFloors();
367
+ else this._writeFloors('width');
368
+ // from the extents on hand; a stale one writes the style's minimum,
369
+ // which is the floor coming off ahead of its measurement below
370
+ this._writeFloors('height');
371
+ });
372
+ let heights = found.height;
373
+ let probed = false;
374
+ if (!heights) {
375
+ this._layoutRoot(width, height);
376
+ heights = this._probeHeightFloors().owed;
377
+ probed = true;
378
+ }
379
+ if (heights) {
380
+ measuringExactly(() => this._measureHeightFloors(width, !probed));
381
+ this._layoutRoot(width, height);
382
+ }
383
+ }
384
+ this._floorsDirty = false;
385
+ this._floorsContentDirty = false;
386
+ this._floorsSwept = true;
387
+ this._floorsWidth = width;
388
+ this._floorsSources.clear();
389
+ this._floorsUnscoped = false;
390
+ this._floorsScopeNow = null;
391
+ }
392
+
393
+ /**
394
+ * `_applyContentFloors` for changes confined to boxes that size
395
+ * themselves (`_floorsScope`): the same measurements in the same order,
396
+ * each over one box laid out alone rather than over the window, and the
397
+ * window's one real pass after them — which yoga answers from its cache
398
+ * everywhere but the boxes and the way down to them.
399
+ */
400
+ _applyScopedFloors(boundaries, found, width, height) {
206
401
  measuringExactly(() => {
207
- if (found.width) this._measureWidthFloors();
208
- else this._writeFloors('width');
209
- // from the extents on hand; a stale one writes the style's minimum,
210
- // which is the floor coming off ahead of its measurement below
402
+ if (found.width) {
403
+ this._writeFloors('width');
404
+ for (const boundary of boundaries) {
405
+ this._measureBoundary(boundary, 'width');
406
+ }
407
+ }
408
+ this._writeFloors('width');
211
409
  this._writeFloors('height');
212
410
  });
213
411
  let heights = found.height;
214
412
  let probed = false;
215
413
  if (!heights) {
216
414
  this._layoutRoot(width, height);
217
- heights = this._probeHeightFloors().owed;
415
+ const hit = { marked: false, owed: false };
416
+ for (const boundary of boundaries) {
417
+ probeHeightFloors(boundary, this, hit, boundary);
418
+ }
419
+ heights = hit.owed;
218
420
  probed = true;
219
421
  }
220
422
  if (heights) {
221
- measuringExactly(() => this._measureHeightFloors(width, !probed));
423
+ measuringExactly(() => {
424
+ this._writeFloors('height');
425
+ for (const boundary of boundaries) {
426
+ this._measureBoundary(boundary, 'height', !probed);
427
+ }
428
+ this._writeFloors('height');
429
+ });
222
430
  this._layoutRoot(width, height);
223
431
  }
224
- this._floorsDirty = false;
225
- this._floorsContentDirty = false;
226
- this._floorsSwept = true;
227
- this._floorsWidth = width;
228
432
  }
229
433
 
230
434
  /**
@@ -267,6 +267,12 @@ export class WindowNode extends Scrollable(Node) {
267
267
  this._floorsDirty = true;
268
268
  this._floorsWidth = null;
269
269
  this._floorsStale = new Set();
270
+ // the nodes whose changes the next measurement owes, and whether one
271
+ // came with no node to name (`_floorsScope`)
272
+ this._floorsSources = new Set();
273
+ this._floorsUnscoped = false;
274
+ this._floorsScopeNow = null;
275
+ this._scopedFloorPasses = 0;
270
276
  // whether the first floors pass has run (`collectFloorStale`'s sweep)
271
277
  this._floorsSwept = false;
272
278
  this._floorsMeasured = 0;
package/src/ntk.js CHANGED
@@ -22,43 +22,20 @@
22
22
  // `@react-x11/components` (`<Markdown>`, `<Formula>`). `SvgView` is still
23
23
  // here; a drawing is not a document.
24
24
  //
25
- // One name is not a plain re-export. `Surface` below asks the app it is
25
+ // One name is not a plain re-export. `Surface` (src/offscreen.js) asks the app it is
26
26
  // handed for the implementation, because ntk's own is a pixmap and a
27
27
  // Picture — an X connection's — and a component allocates its buffer
28
28
  // without knowing which backend it was mounted on. This subpath is where a
29
29
  // drawing-adjacent name gets its backend-neutral answer; the X-only names
30
30
  // (`createClient`, `Pixmap`, `Picture`, `XEmbedSocket`) stay X-only.
31
- import { Surface as NtkSurface } from 'ntk';
31
+ import * as ntk from 'ntk';
32
+
33
+ import { adoptNtk } from './ntkroot.js';
32
34
 
33
35
  export * from 'ntk';
34
36
  export { default } from 'ntk';
37
+ export { Surface } from './offscreen.js';
35
38
 
36
- /**
37
- * ntk's offscreen `Surface`, on whichever backend `app` is.
38
- *
39
- * An app that makes its own surfaces answers `createSurface(options)` —
40
- * the Cocoa app does, over a CG bitmap (src/cocoa/surface.js), the Wayland
41
- * app over a GL render target (src/wayland/surface.js) — and an ntk
42
- * connection has no such method and gets ntk's pixmap. The result is
43
- * whichever implementation answered, not an instance of this class: the
44
- * contract is the shape — `width`/`height`, `getContext('2d')`, `render`,
45
- * `clear`, `copyWithin`, `destroy`, and `ctx.drawImage(surface, …)` —
46
- * (docs/extending.md "Scrolling the pixels, not just the offset"), and
47
- * nothing needs `instanceof`.
48
- *
49
- * Part of that shape is that a context may be **held**: ntk tells a caller
50
- * doing many draws to take `getContext('2d')` once rather than one per
51
- * frame, and that holds on every backend here, including the ones where a
52
- * surface is a GPU target sharing a device with the window (#566). Draws
53
- * through a held context land in the surface whenever they are made, and
54
- * leave nothing for the caller to restore; `render(fn)` is the scoped form,
55
- * for a caller that wants a frame's clean transform and clip.
56
- */
57
- export class Surface {
58
- constructor(app, options) {
59
- if (typeof app?.createSurface === 'function') {
60
- return app.createSurface(options);
61
- }
62
- return new NtkSurface(app, options);
63
- }
64
- }
39
+ // what src/ntkroot.js would load, already loaded: handed over rather than
40
+ // imported twice
41
+ adoptNtk(ntk);
package/src/ntkroot.js ADDED
@@ -0,0 +1,50 @@
1
+ // ntk's package root — the X11 client, ntk's windows and their contexts, the
2
+ // GL rungs, fontconfig and the rest of what the X11 backend draws with —
3
+ // loaded where an X connection is made rather than wherever react-x11 is
4
+ // imported.
5
+ //
6
+ // Only that backend needs it. The Cocoa, Windows and Wayland backends use a
7
+ // colour parser, the shadow arithmetic, the image and SVG decoders and the
8
+ // font parser of ntk's, each from a subpath of its own (`ntk/color`,
9
+ // `ntk/shadow-math`, `ntk/image`, `ntk/svg`, `ntk/font`, …), and importing
10
+ // the root for them was ~210 ms of module loading on Windows before the first
11
+ // window of an app that never opens an X connection.
12
+ //
13
+ // `createRoot` loads it on the X11 path, before the connection, and for an
14
+ // ntk app it is handed rather than makes — an app that could only have come
15
+ // from ntk's own `createClient`, so the import is already done and this is a
16
+ // lookup. `react-x11/ntk` hands over the root it imports. After that, the
17
+ // code that only ever runs against an X connection reads it synchronously.
18
+ let root = null;
19
+ let loading = null;
20
+
21
+ /** Load ntk's root, once; resolves to the module. */
22
+ export function loadNtk() {
23
+ return (loading ??= import('ntk').then((mod) => (root = mod)));
24
+ }
25
+
26
+ /** A root imported some other way (`react-x11/ntk`): the same module, so
27
+ * nothing is loaded twice by taking it. */
28
+ export function adoptNtk(mod) {
29
+ root ??= mod;
30
+ loading ??= Promise.resolve(mod);
31
+ }
32
+
33
+ /** ntk's root, or null while no X connection has been made. */
34
+ export function ntkRoot() {
35
+ return root;
36
+ }
37
+
38
+ /**
39
+ * ntk's root, for code that only runs against an X connection — which
40
+ * `createRoot` has loaded it for. Throws where it is not: reaching for an X
41
+ * resource on a backend that has none is the mistake, and this says so
42
+ * rather than failing inside ntk on an app that is not its own.
43
+ */
44
+ export function x11Ntk(what) {
45
+ if (root) return root;
46
+ throw new Error(
47
+ `react-x11: ${what} needs an X connection, and this process has none ` +
48
+ '(ntk loads with the X11 backend, in createRoot).',
49
+ );
50
+ }
@@ -0,0 +1,33 @@
1
+ // An offscreen surface on whichever backend an app is — what the paint cache,
2
+ // a group's opacity and the symbol icons draw into, and what `react-x11/ntk`
3
+ // hands a component as `Surface`.
4
+ //
5
+ // An app that makes its own surfaces answers `createSurface(options)` — the
6
+ // Cocoa app over a CG bitmap (src/cocoa/surface.js), the Windows app over a
7
+ // Direct2D bitmap, the Wayland app over a GL render target
8
+ // (src/wayland/surface.js) — and an X connection has no such method and gets
9
+ // ntk's pixmap, from ntk's root, which only an X connection loads
10
+ // (src/ntkroot.js). The result is whichever implementation answered, not an
11
+ // instance of this class: the contract is the shape — `width`/`height`,
12
+ // `getContext('2d')`, `render`, `clear`, `copyWithin`, `destroy`, and
13
+ // `ctx.drawImage(surface, …)` — (docs/extending.md "Scrolling the pixels, not
14
+ // just the offset"), and nothing needs `instanceof`.
15
+ //
16
+ // Part of that shape is that a context may be **held**: ntk tells a caller
17
+ // doing many draws to take `getContext('2d')` once rather than one per frame,
18
+ // and that holds on every backend here, including the ones where a surface is
19
+ // a GPU target sharing a device with the window (#566). Draws through a held
20
+ // context land in the surface whenever they are made, and leave nothing for
21
+ // the caller to restore; `render(fn)` is the scoped form, for a caller that
22
+ // wants a frame's clean transform and clip.
23
+ import { x11Ntk } from './ntkroot.js';
24
+
25
+ export class Surface {
26
+ constructor(app, options) {
27
+ if (typeof app?.createSurface === 'function') {
28
+ return app.createSurface(options);
29
+ }
30
+ const { Surface: NtkSurface } = x11Ntk('an offscreen surface');
31
+ return new NtkSurface(app, options);
32
+ }
33
+ }
package/src/paintcache.js CHANGED
@@ -42,13 +42,13 @@
42
42
  // this package pins, and a named import of something absent is a *load-time*
43
43
  // SyntaxError — the whole renderer, not just the cache. Same shape as the
44
44
  // `typeof wnd?.scrollRegion !== 'function'` guard for ntk without #139.
45
- import * as ntk from 'ntk';
46
45
 
47
46
  // The surface this cache draws into is `react-x11/ntk`'s: ntk's pixmap on
48
47
  // an X connection, the app's own (`app.createSurface`) on a backend that
49
48
  // makes them — the Cocoa backend's CG bitmap. One import, so the cache
50
49
  // names neither.
51
- import { Surface } from './ntk.js';
50
+ import { ntkRoot } from './ntkroot.js';
51
+ import { Surface } from './offscreen.js';
52
52
 
53
53
  /** Stale pixels are undebuggable, and every other optimization here has an
54
54
  * escape hatch — see NO_SCROLL_BLIT. */
@@ -83,19 +83,26 @@ const MAX_ITEM_PIXELS = 256 * 256;
83
83
  * grow this forever; the cost of forgetting is one extra live paint. */
84
84
  const MAX_PENDING = 4096;
85
85
 
86
- /** Whether a context is drawing 1:1 into device space.
87
- *
88
- * A cached surface is pixels at a fixed size and orientation. Under a scaled
89
- * or rotated CTM, compositing it would *resample* those pixels where a live
90
- * paint would re-rasterize at the right size, so the cache has to stand
91
- * aside. react-x11 only ever translates during paint, and only inside
92
- * `<canvas>`, so this is insurance rather than a live case.
86
+ /**
87
+ * Whether `ctx` draws on the device's pixel grid, wherever its origin is: a
88
+ * surface rendered at device resolution composites through it pixel for
89
+ * pixel. An integer translation keeps that — and it is how an overlay pane
90
+ * over a `<glarea>` draws, from the window's coordinates into its layer's
91
+ * (src/win32/overlay.js) — so declining it painted every cached drawing on a
92
+ * surface's children live, every time a pane repainted: every card of a
93
+ * graph, on every step of a pan. A scale, a rotation or a fraction of a
94
+ * pixel does not, and paints live.
93
95
  */
94
96
  function isDeviceSpace(ctx) {
95
97
  const m = ctx.getTransform?.();
96
98
  if (!m) return true;
97
99
  return (
98
- m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0
100
+ m.a === 1 &&
101
+ m.b === 0 &&
102
+ m.c === 0 &&
103
+ m.d === 1 &&
104
+ Number.isInteger(m.e) &&
105
+ Number.isInteger(m.f)
99
106
  );
100
107
  }
101
108
 
@@ -415,15 +422,19 @@ export class PaintCache {
415
422
  }
416
423
  }
417
424
 
418
- /** Whether the installed ntk can make the surfaces this needs. Older ones
419
- * cannot, and the answer is simply that nothing is cached. */
420
- export const paintCacheSupported = () => typeof ntk.Surface === 'function';
425
+ /** Whether the installed ntk can make the surfaces this needs on an X
426
+ * connection — ntk's root, which only an X connection loads
427
+ * (src/ntkroot.js). Older ones cannot, and the answer is simply that nothing
428
+ * is cached there. */
429
+ export const paintCacheSupported = () =>
430
+ typeof ntkRoot()?.Surface === 'function';
421
431
 
422
432
  /** Whether `app` can make an offscreen surface at all: an X connection
423
433
  * with the Render extension (ntk's pixmap surface), or a backend with a
424
434
  * surface of its own — the Cocoa app's `createSurface` (docs/macos.md). */
425
435
  const canMakeSurfaces = (app) =>
426
- Boolean(app?.display?.Render) || typeof app?.createSurface === 'function';
436
+ (Boolean(app?.display?.Render) && paintCacheSupported()) ||
437
+ typeof app?.createSurface === 'function';
427
438
 
428
439
  /**
429
440
  * The cache for an app, created on first use. Null when caching is off, when
@@ -432,6 +443,6 @@ const canMakeSurfaces = (app) =>
432
443
  * there must paint live.
433
444
  */
434
445
  export function paintCacheFor(app) {
435
- if (DISABLED || !paintCacheSupported() || !canMakeSurfaces(app)) return null;
446
+ if (DISABLED || !canMakeSurfaces(app)) return null;
436
447
  return (app._paintCache ??= new PaintCache(app));
437
448
  }
@@ -326,6 +326,28 @@ function buildTransformer({ babel, jsxTransform, refreshTransform }, options) {
326
326
 
327
327
  let registered = false;
328
328
 
329
+ /**
330
+ * Which files stay out of the hot graph: every package in node_modules,
331
+ * react-x11's own src/, and whatever the app's `ignore` names.
332
+ *
333
+ * Compared with forward slashes, whichever the path arrives with: on
334
+ * Windows a file path's separators are backslashes, and a forward-slash
335
+ * test let every package in node_modules into the hot graph — where the
336
+ * HMR layer's rewrite of a package's modules could leave one reading an
337
+ * import before it was bound (entities, under htmlparser2).
338
+ */
339
+ export function hotIgnoreFor(ignore, srcDir = SRC_DIR) {
340
+ const src = srcDir.replaceAll('\\', '/');
341
+ return (path) => {
342
+ const slashed = path.replaceAll('\\', '/');
343
+ return (
344
+ slashed.includes('/node_modules/') ||
345
+ slashed.startsWith(src) ||
346
+ (ignore ? ignore(path) === true : false)
347
+ );
348
+ };
349
+ }
350
+
329
351
  /**
330
352
  * Register the hot-reload loader. `react-x11/refresh/register` calls this
331
353
  * with no options; a tool that needs the seams writes its own two-line
@@ -362,10 +384,7 @@ export async function registerRefresh(options = {}) {
362
384
  const { ignore } = validated;
363
385
  registered = true;
364
386
 
365
- const hotIgnore = (path) =>
366
- path.includes('/node_modules/') ||
367
- path.startsWith(SRC_DIR) ||
368
- (ignore ? ignore(path) === true : false);
387
+ const hotIgnore = hotIgnoreFor(ignore);
369
388
 
370
389
  nodeModule.registerHooks({
371
390
  load(url, context, nextLoad) {
@@ -56,7 +56,7 @@
56
56
  // and losing the grab to someone else is detected (the server says so, with
57
57
  // a LeaveNotify of mode Ungrab) rather than waited out.
58
58
 
59
- import { pixelLayout, toStraightRgba } from 'ntk';
59
+ import { pixelLayout, toStraightRgba } from 'ntk/imagedata';
60
60
 
61
61
  import { sessionBus } from './bus.js';
62
62
  import { XK_ESCAPE, XK_KP_ENTER, XK_RETURN, XK_SPACE } from './keysyms.js';
package/src/styles.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Numbers are pixels; strings like '50%' / 'auto' pass through to yoga.
4
4
  // The engine is ours (`./yoga.js`) — the enum tables below are built at
5
5
  // module scope, which is what that module's synchronous half is for.
6
- import { cssColorStraight } from 'ntk';
6
+ import { cssColorStraight } from 'ntk/color';
7
7
 
8
8
  import { parseBoxShadow, parseLinearGradient } from './decorations.js';
9
9
  import {
package/src/svgnodes.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // `<Formula>`), composed from public host elements. `<svg>` stays here
11
11
  // because it is not a document — it is a drawing, and one node is the right
12
12
  // grain for it.
13
- import { SvgView } from 'ntk';
13
+ import SvgView from 'ntk/svg';
14
14
 
15
15
  import { Node } from './nodes/node.js';
16
16
  import { intrinsicSize } from './nodes/layout.js';
package/src/symbols.js CHANGED
@@ -17,11 +17,13 @@
17
17
  // the same bargain a tray icon's name makes — so an app that runs on both
18
18
  // picks its names by platform, as it does for the tray.
19
19
 
20
- import { cssColorStraight, decodeImage, SvgView } from 'ntk';
20
+ import { cssColorStraight } from 'ntk/color';
21
+ import { decodeImage } from 'ntk/image';
22
+ import SvgView from 'ntk/svg';
21
23
  import * as nodeFs from 'node:fs';
22
24
 
23
25
  import { IconTheme } from './icontheme.js';
24
- import { Surface } from './ntk.js';
26
+ import { Surface } from './offscreen.js';
25
27
  import { xsettings } from './xsettings.js';
26
28
 
27
29
  /** A `fontWeight` as the number both icon systems configure with. */
package/src/trayhooks.js CHANGED
@@ -54,7 +54,7 @@
54
54
  // frame is not available for anything that has to ask the desktop.
55
55
 
56
56
  import { useEffect, useRef, useState } from 'react';
57
- import { decodeImage } from 'ntk';
57
+ import { decodeImage } from 'ntk/image';
58
58
 
59
59
  import { useAppOrNull } from './appcontext.js';
60
60
  import { currentRegistration } from './application.js';
@@ -42,7 +42,8 @@
42
42
  import { createRequire } from 'node:module';
43
43
  import path from 'node:path';
44
44
  import { pathToFileURL } from 'node:url';
45
- import { Path2D, Image as NtkImage } from 'ntk';
45
+ import { Image as NtkImage } from 'ntk/image';
46
+ import { Path2D } from 'ntk/path';
46
47
  import { glDevice } from './device.js';
47
48
  import { GlyphAtlas } from './glyphatlas.js';
48
49
  import { GLTarget } from './target.js';