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/package.json +4 -3
- package/src/Reconciler.js +6 -0
- package/src/cocoa/app.js +216 -14
- package/src/cocoa/context2d.js +37 -4
- package/src/cocoa/panehost.js +20 -2
- package/src/cocoa/panewindow.js +61 -9
- package/src/cocoa/presenter.js +191 -16
- package/src/cocoa/surface.js +235 -0
- package/src/cocoa/window.js +123 -13
- package/src/events.js +23 -5
- package/src/index.d.ts +25 -0
- package/src/nodes.js +849 -138
- package/src/ntk.d.ts +63 -4
- package/src/ntk.js +30 -0
- package/src/paintcache.js +71 -11
- package/src/svgnodes.js +4 -3
- package/src/types/elements.d.ts +13 -0
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
|
-
*
|
|
31
|
-
* buffer, `copyWithin(src, dx, dy)` shifts the
|
|
32
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
320
|
-
|
|
321
|
-
y: 0,
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
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
|
|
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' ?
|
|
304
|
+
color: view.paintKind === 'mono' ? ink : this._currentColor(),
|
|
304
305
|
});
|
|
305
306
|
}
|
|
306
307
|
}
|
package/src/types/elements.d.ts
CHANGED
|
@@ -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
|
/**
|