react-x11 2.4.0 → 2.6.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/src/ntk.d.ts CHANGED
@@ -16,7 +16,13 @@
16
16
  * than a hand-written mirror that would drift out of date silently. The
17
17
  * named exports are the ones an extension actually reaches for; anything
18
18
  * else ntk has is still there at runtime.
19
+ *
20
+ * `Surface` is the exception, typed in full: it is react-x11's own class,
21
+ * answering ntk's pixmap on an X connection and a CG bitmap on the cocoa
22
+ * backend, so its shape is this package's to declare.
19
23
  */
24
+ import type { Context2D } from './node.js';
25
+
20
26
  export const createClient: (
21
27
  options?: Record<string, unknown>,
22
28
  ) => Promise<unknown>;
@@ -26,12 +32,65 @@ export const Clipboard: new (...args: unknown[]) => unknown;
26
32
  export const Path2D: new (...args: unknown[]) => unknown;
27
33
  export const Image: new (...args: unknown[]) => unknown;
28
34
  export const Pixmap: new (...args: unknown[]) => unknown;
35
+
36
+ /** What `new Surface(app, options)` takes: a size in device pixels. */
37
+ export interface SurfaceOptions {
38
+ width: number;
39
+ height: number;
40
+ /**
41
+ * `'argb32'` (the default) on every backend. `'a8'`, a coverage surface
42
+ * that composites as a mask for the fill style, is X11-only today and
43
+ * throws on the cocoa backend.
44
+ */
45
+ format?: 'argb32' | 'a8';
46
+ }
47
+
48
+ /** A rectangle in surface coordinates — what `copyWithin` shifts. */
49
+ export interface SurfaceRect {
50
+ x: number;
51
+ y: number;
52
+ width: number;
53
+ height: number;
54
+ }
55
+
29
56
  /**
30
- * Draw once, composite many — and, for an element that scrolls a retained
31
- * buffer, `copyWithin(src, dx, dy)` shifts the surviving band server-side.
32
- * See [extending.md](../docs/extending.md).
57
+ * An offscreen surface: draw once, composite many — and, for an element
58
+ * that scrolls a retained buffer, `copyWithin(src, dx, dy)` shifts the
59
+ * surviving band in place. On an X connection it is ntk's pixmap and
60
+ * Picture; on the cocoa backend a CG bitmap; the same object shape either
61
+ * way, and `ctx.drawImage(surface, …)` takes it as a source on both. See
62
+ * [extending.md](../docs/extending.md) "Scrolling the pixels, not just the
63
+ * offset".
33
64
  */
34
- export const Surface: new (...args: unknown[]) => unknown;
65
+ export interface Surface {
66
+ readonly app: unknown;
67
+ readonly width: number;
68
+ readonly height: number;
69
+ readonly format: 'argb32' | 'a8';
70
+ readonly depth: 8 | 32;
71
+ /** Bytes of backing storage — what a cache budgets against. */
72
+ readonly bytes: number;
73
+ /**
74
+ * A 2d context on the surface. The caller owns it and owes it a
75
+ * `destroy()` — real on X11 (a GC and a Picture), a no-op on cocoa, where
76
+ * a surface has one context for its whole life.
77
+ */
78
+ getContext(name: '2d', ...args: unknown[]): Context2D;
79
+ /** Draw through a context that exists for the call. */
80
+ render(fn: (ctx: Context2D) => void): this;
81
+ /** Reset every pixel to fully transparent. */
82
+ clear(): this;
83
+ /**
84
+ * Shift `src` by a whole-pixel delta in place; true when a band survived
85
+ * the shift and was copied, false when the caller should repaint `src`.
86
+ */
87
+ copyWithin(src: SurfaceRect, dx: number, dy: number): boolean;
88
+ /** X11 only — the server-side Picture, for `<image picture>`. Throws on the cocoa backend. */
89
+ picture(app?: unknown): unknown;
90
+ destroy(): void;
91
+ [Symbol.dispose](): void;
92
+ }
93
+ export const Surface: new (app: unknown, options: SurfaceOptions) => Surface;
35
94
  /** `code` values on a failed GL setup — see `<glarea onError>`. */
36
95
  export const GLXError: {
37
96
  NO_EXTENSION: 'GLX_NO_EXTENSION';
package/src/ntk.js CHANGED
@@ -21,5 +21,35 @@
21
21
  // for one — they were reachable but never declared — wants
22
22
  // `@react-x11/components` (`<Markdown>`, `<Formula>`). `SvgView` is still
23
23
  // here; a drawing is not a document.
24
+ //
25
+ // One name is not a plain re-export. `Surface` below asks the app it is
26
+ // handed for the implementation, because ntk's own is a pixmap and a
27
+ // Picture — an X connection's — and a component allocates its buffer
28
+ // without knowing which backend it was mounted on. This subpath is where a
29
+ // drawing-adjacent name gets its backend-neutral answer; the X-only names
30
+ // (`createClient`, `Pixmap`, `Picture`, `XEmbedSocket`) stay X-only.
31
+ import { Surface as NtkSurface } from 'ntk';
32
+
24
33
  export * from 'ntk';
25
34
  export { default } from 'ntk';
35
+
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) — and an ntk
41
+ * connection has no such method and gets ntk's pixmap. The result is
42
+ * whichever implementation answered, not an instance of this class: the
43
+ * contract is the shape — `width`/`height`, `getContext('2d')`, `render`,
44
+ * `clear`, `copyWithin`, `destroy`, and `ctx.drawImage(surface, …)` —
45
+ * (docs/extending.md "Scrolling the pixels, not just the offset"), and
46
+ * nothing needs `instanceof`.
47
+ */
48
+ export class Surface {
49
+ constructor(app, options) {
50
+ if (typeof app?.createSurface === 'function') {
51
+ return app.createSurface(options);
52
+ }
53
+ return new NtkSurface(app, options);
54
+ }
55
+ }
package/src/paintcache.js CHANGED
@@ -22,6 +22,14 @@
22
22
  // Multi-colour drawings bake their colours in, which is right, because those
23
23
  // colours belong to the drawing. `SvgView.paintKind` decides which is which.
24
24
  //
25
+ // Coverage needs a backend that composites a mask through a colour, which
26
+ // X Render does and the Cocoa backend's surfaces do not (src/cocoa/
27
+ // surface.js — no `a8`). There a mono drawing is cached as argb32 with its
28
+ // colour in the key: one entry per colour it is seen in rather than one for
29
+ // all of them, which is still one render per colour instead of one per
30
+ // cell per frame. A coverage-only plan — a blurred shadow, whose blur is a
31
+ // pass over the mask — stays live on that backend.
32
+ //
25
33
  // ## Self-limiting, on purpose
26
34
  //
27
35
  // X gives no back-pressure: the first sign of overspending is `BadAlloc` on
@@ -36,6 +44,12 @@
36
44
  // `typeof wnd?.scrollRegion !== 'function'` guard for ntk without #139.
37
45
  import * as ntk from 'ntk';
38
46
 
47
+ // The surface this cache draws into is `react-x11/ntk`'s: ntk's pixmap on
48
+ // an X connection, the app's own (`app.createSurface`) on a backend that
49
+ // makes them — the Cocoa backend's CG bitmap. One import, so the cache
50
+ // names neither.
51
+ import { Surface } from './ntk.js';
52
+
39
53
  /** Stale pixels are undebuggable, and every other optimization here has an
40
54
  * escape hatch — see NO_SCROLL_BLIT. */
41
55
  const DISABLED = process.env.REACT_X11_NO_PAINT_CACHE === '1';
@@ -145,6 +159,9 @@ export class PaintCache {
145
159
  this.app = app;
146
160
  this.budget = budget;
147
161
  this.verify = verify;
162
+ /** whether an `a8` entry can be painted through a colour here — X
163
+ * Render composites a mask through the fill; nothing else does yet */
164
+ this.coverage = Boolean(app?.display?.Render);
148
165
  /** key -> entry. Map iteration is insertion order, so re-inserting on
149
166
  * access makes this an LRU list for free — same trick as getGlyphPage. */
150
167
  this.entries = new Map();
@@ -193,7 +210,7 @@ export class PaintCache {
193
210
  return this.drawing(ctx, {
194
211
  ...plan,
195
212
  label: `<${node.kind}>`,
196
- draw: (sctx, box) => node.paintCached(sctx, box),
213
+ draw: (sctx, box, ink) => node.paintCached(sctx, box, ink),
197
214
  live: () => node.paintContent(ctx),
198
215
  });
199
216
  }
@@ -211,9 +228,22 @@ export class PaintCache {
211
228
  *
212
229
  * `maxPixels` overrides the per-item cap for a caller whose drawing is
213
230
  * legitimately box-sized rather than icon-sized.
231
+ *
232
+ * `draw` is handed the ink a mono drawing paints in as its third
233
+ * argument: white into a coverage surface, where only the alpha survives
234
+ * and the tint arrives at blit time, and the tint itself on a backend
235
+ * without coverage, where the entry bakes its colour and carries it in
236
+ * the key (`_bakeTint`).
214
237
  */
215
238
  drawing(ctx, plan) {
216
239
  if (!isDeviceSpace(ctx)) return plan.live(ctx);
240
+ if (plan.format === 'a8' && !this.coverage) {
241
+ // A plan whose `after` pass works on the mask — a blurred shadow —
242
+ // has no argb32 equivalent: it paints live, which is what the frame
243
+ // owed with no cache at all.
244
+ if (plan.after) return plan.live(ctx);
245
+ plan = this._bakeTint(plan);
246
+ }
217
247
 
218
248
  if (plan.width * plan.height > (plan.maxPixels ?? MAX_ITEM_PIXELS)) {
219
249
  this.stats.tooBig++;
@@ -254,9 +284,30 @@ export class PaintCache {
254
284
  return this._blit(ctx, entry, plan);
255
285
  }
256
286
 
287
+ /**
288
+ * A coverage plan on a backend that cannot composite coverage, as an
289
+ * argb32 plan that paints its tint: the same pixels, one entry per colour
290
+ * instead of one per drawing.
291
+ */
292
+ _bakeTint(plan) {
293
+ const tint = plan.tint;
294
+ return {
295
+ ...plan,
296
+ format: 'argb32',
297
+ key: `${plan.key}|ink:${tint}`,
298
+ draw: (sctx, box) => plan.draw(sctx, box, tint),
299
+ };
300
+ }
301
+
302
+ /** The colour a drawing paints in: white into coverage, the tint into a
303
+ * baked entry, and nothing a multi-colour drawing reads. */
304
+ static inkFor(plan) {
305
+ return plan.format === 'a8' ? '#ffffff' : (plan.tint ?? '#ffffff');
306
+ }
307
+
257
308
  _render(plan) {
258
309
  try {
259
- const surface = new ntk.Surface(this.app, {
310
+ const surface = new Surface(this.app, {
260
311
  width: plan.width,
261
312
  height: plan.height,
262
313
  format: plan.format,
@@ -264,7 +315,11 @@ export class PaintCache {
264
315
  const box = { x: 0, y: 0, width: plan.width, height: plan.height };
265
316
  const state = { digest: 0x811c9dc5 };
266
317
  surface.render((sctx) =>
267
- plan.draw(this.verify ? recordingContext(sctx, state) : sctx, box),
318
+ plan.draw(
319
+ this.verify ? recordingContext(sctx, state) : sctx,
320
+ box,
321
+ PaintCache.inkFor(plan),
322
+ ),
268
323
  );
269
324
  // `after` may hand back a *different* surface than it was given — a
270
325
  // blurred shadow bakes its convolution into a second one and destroys
@@ -310,18 +365,17 @@ export class PaintCache {
310
365
  const state = { digest: 0x811c9dc5 };
311
366
  let scratch = null;
312
367
  try {
313
- scratch = new ntk.Surface(this.app, {
368
+ scratch = new Surface(this.app, {
314
369
  width: plan.width,
315
370
  height: plan.height,
316
371
  format: plan.format,
317
372
  });
318
373
  scratch.render((sctx) =>
319
- plan.draw(recordingContext(sctx, state), {
320
- x: 0,
321
- y: 0,
322
- width: plan.width,
323
- height: plan.height,
324
- }),
374
+ plan.draw(
375
+ recordingContext(sctx, state),
376
+ { x: 0, y: 0, width: plan.width, height: plan.height },
377
+ PaintCache.inkFor(plan),
378
+ ),
325
379
  );
326
380
  } catch {
327
381
  return; // verification is best-effort; never break a frame over it
@@ -365,6 +419,12 @@ export class PaintCache {
365
419
  * cannot, and the answer is simply that nothing is cached. */
366
420
  export const paintCacheSupported = () => typeof ntk.Surface === 'function';
367
421
 
422
+ /** Whether `app` can make an offscreen surface at all: an X connection
423
+ * with the Render extension (ntk's pixmap surface), or a backend with a
424
+ * surface of its own — the Cocoa app's `createSurface` (docs/macos.md). */
425
+ const canMakeSurfaces = (app) =>
426
+ Boolean(app?.display?.Render) || typeof app?.createSurface === 'function';
427
+
368
428
  /**
369
429
  * The cache for an app, created on first use. Null when caching is off, when
370
430
  * the installed ntk is too old, or when the app cannot make surfaces — the
@@ -372,6 +432,6 @@ export const paintCacheSupported = () => typeof ntk.Surface === 'function';
372
432
  * there must paint live.
373
433
  */
374
434
  export function paintCacheFor(app) {
375
- if (DISABLED || !paintCacheSupported() || !app?.display?.Render) return null;
435
+ if (DISABLED || !paintCacheSupported() || !canMakeSurfaces(app)) return null;
376
436
  return (app._paintCache ??= new PaintCache(app));
377
437
  }
package/src/svgnodes.js CHANGED
@@ -294,13 +294,14 @@ export class SvgNode extends Node {
294
294
  };
295
295
  }
296
296
 
297
- paintCached(ctx, box) {
297
+ paintCached(ctx, box, ink = '#ffffff') {
298
298
  const view = this._ensureView();
299
299
  if (!view) return;
300
300
  // Into a coverage surface, only the alpha of a paint survives, and the
301
- // tint arrives at blit time — so any opaque colour renders the same mask.
301
+ // tint arrives at blit time — so any opaque colour renders the same
302
+ // mask, and the cache says white then, or the tint where it bakes colour.
302
303
  view.draw(ctx, box.x, box.y, box.width, box.height, {
303
- color: view.paintKind === 'mono' ? '#ffffff' : this._currentColor(),
304
+ color: view.paintKind === 'mono' ? ink : this._currentColor(),
304
305
  });
305
306
  }
306
307
  }
@@ -188,6 +188,19 @@ export interface DrawnProps<T = DrawnNode>
188
188
  SelectionProps<T>,
189
189
  EventHandlers<T> {
190
190
  ref?: Ref<T>;
191
+ /**
192
+ * Zoom this subtree, CSS `zoom` rather than a transform: every length
193
+ * under here — and this element's *own* style — is multiplied by it, so
194
+ * `scale={2}` is a card twice the size with text shaped at twice the size
195
+ * inside it, not a 1x picture stretched. Nested props multiply, and the
196
+ * display scale is the floor they multiply
197
+ * ([scale.md](scale.md#a-subtree-of-its-own)).
198
+ *
199
+ * A positive number; leaving it out means 1. `<window>` and `<popup>`
200
+ * take no `scale`: a real X window is its own root, so a menu opened from
201
+ * a zoomed card comes up at the app's own size.
202
+ */
203
+ scale?: number;
191
204
  }
192
205
 
193
206
  /**