react-x11 2.18.5 → 2.18.6

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.18.6",
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.2",
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,36 @@ 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
+ get lineDashOffset() {
581
+ return this._state.dashOffset;
582
+ }
583
+
584
+ set lineDashOffset(value) {
585
+ // canvas ignores anything that is not a finite number
586
+ if (typeof value !== 'number' || !Number.isFinite(value)) return;
587
+ this._state.dashOffset = value;
588
+ this._native.ctxSetLineDash(this._s(), this._state.dash, value);
589
+ }
590
+
568
591
  save() {
569
592
  // Sync before pushing: a fresh surface empties the stack on its way in,
570
593
  // 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;
@@ -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);
@@ -477,6 +477,27 @@ export class WindowInvalidate {
477
477
  return WINDOW_ONLY;
478
478
  }
479
479
 
480
+ /**
481
+ * One rect the layout diff says a node moved through (`layoutDiff.sink`,
482
+ * nodes/window/flush.js), to the lists that paint that node — the same
483
+ * reach a claim of it has (`_paneReach`). A node inside a surface is the
484
+ * panes' alone: the window's pass under the surface would repaint pixels
485
+ * no child of it ever had, and it went to both, so a card dragged across
486
+ * a graph drawn through GL was a window pass under the surface on every
487
+ * step. Answers whether the window's list took it.
488
+ */
489
+ _claimLayoutMove(rect, node, cap) {
490
+ const reach =
491
+ this._overlaid.size !== 0 ? this._paneReach(node ?? null) : WINDOW_ONLY;
492
+ let windowTook = false;
493
+ if (reach !== PANES_ONLY && this._damage !== FULL_DAMAGE) {
494
+ this._damage = addDamageRect(this._damage, rect, cap);
495
+ windowTook = true;
496
+ }
497
+ if (reach !== WINDOW_ONLY) this._addPaneDamage(rect);
498
+ return windowTook;
499
+ }
500
+
480
501
  /** One rect more for the panes, unless they repaint whole already. */
481
502
  _addPaneDamage(rect) {
482
503
  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
  }
@@ -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';
@@ -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,
@@ -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) {
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. */
@@ -415,15 +415,19 @@ export class PaintCache {
415
415
  }
416
416
  }
417
417
 
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';
418
+ /** Whether the installed ntk can make the surfaces this needs on an X
419
+ * connection — ntk's root, which only an X connection loads
420
+ * (src/ntkroot.js). Older ones cannot, and the answer is simply that nothing
421
+ * is cached there. */
422
+ export const paintCacheSupported = () =>
423
+ typeof ntkRoot()?.Surface === 'function';
421
424
 
422
425
  /** Whether `app` can make an offscreen surface at all: an X connection
423
426
  * with the Render extension (ntk's pixmap surface), or a backend with a
424
427
  * surface of its own — the Cocoa app's `createSurface` (docs/macos.md). */
425
428
  const canMakeSurfaces = (app) =>
426
- Boolean(app?.display?.Render) || typeof app?.createSurface === 'function';
429
+ (Boolean(app?.display?.Render) && paintCacheSupported()) ||
430
+ typeof app?.createSurface === 'function';
427
431
 
428
432
  /**
429
433
  * The cache for an app, created on first use. Null when caching is off, when
@@ -432,6 +436,6 @@ const canMakeSurfaces = (app) =>
432
436
  * there must paint live.
433
437
  */
434
438
  export function paintCacheFor(app) {
435
- if (DISABLED || !paintCacheSupported() || !canMakeSurfaces(app)) return null;
439
+ if (DISABLED || !canMakeSurfaces(app)) return null;
436
440
  return (app._paintCache ??= new PaintCache(app));
437
441
  }
@@ -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';
@@ -32,7 +32,7 @@
32
32
  // buttons do what they say. input.js routes; this answers where things are.
33
33
 
34
34
  import fs from 'node:fs';
35
- import { Path2D } from 'ntk';
35
+ import { Path2D } from 'ntk/path';
36
36
  import { RESIZE_EDGE, TOPLEVEL_STATE } from './window.js';
37
37
  import { DEFAULTS, parseButtonLayout, titleFont } from './framestyle.js';
38
38
 
@@ -15,7 +15,7 @@
15
15
  // `indexAt()` returns code points, which is what the selection and the caret
16
16
  // speak. DirectWrite is UTF-16 end to end, like CoreText, so the conversion
17
17
  // happens at this boundary and nowhere else.
18
- import { cssColorStraight } from 'ntk';
18
+ import { cssColorStraight } from 'ntk/color';
19
19
 
20
20
  // REACT_X11_WIN32_DEBUG=1 reports the size each paragraph is shaped at, which
21
21
  // is how to tell a scale that never reached the text engine from one that did.