react-x11 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
package/src/cocoa/app.js CHANGED
@@ -24,13 +24,14 @@ import { CocoaGlobalMenuExport } from './globalmenu.js';
24
24
  import { CocoaPaneHost } from './panehost.js';
25
25
  import { CocoaPaneWindow } from './panewindow.js';
26
26
  import { CocoaFontManager } from './fonts.js';
27
+ import { CocoaSurface } from './surface.js';
27
28
  import { CocoaWindow } from './window.js';
28
29
  import { decodeKey, modifierMask } from './keymap.js';
29
30
  import { loadNative } from './native.js';
30
31
 
31
32
  const RAF_INTERVAL_MS = 16;
32
33
 
33
- class CocoaApp {
34
+ export class CocoaApp {
34
35
  constructor(native, options = {}) {
35
36
  this._native = native;
36
37
  this.options = options;
@@ -53,7 +54,10 @@ class CocoaApp {
53
54
  process.env.REACT_X11_COCOA_PRESENTER ??
54
55
  'surface';
55
56
 
56
- this.fonts = new CocoaFontManager();
57
+ // the app's own bridge, so an app over a fake one (the tests) needs no
58
+ // real bridge on the machine — the manager's default loads it only when
59
+ // it is built standalone
60
+ this.fonts = new CocoaFontManager(native);
57
61
 
58
62
  // AppKit-rendered control bezels. Its *presence* is the capability:
59
63
  // `useSupports('nativeControls')` and the widget set's `controls:
@@ -189,6 +193,17 @@ class CocoaApp {
189
193
  return new CocoaPaneHost(this, wnd);
190
194
  }
191
195
 
196
+ /**
197
+ * The offscreen-surface seam `react-x11/ntk`'s `Surface` dispatches on:
198
+ * ntk's `Surface` contract over a CG bitmap (src/cocoa/surface.js). Its
199
+ * presence is what makes `new Surface(app, { width, height })` answer a
200
+ * surface here rather than ntk's pixmap, which needs an X connection — a
201
+ * backend without the method gets ntk's, so an X app is never asked.
202
+ */
203
+ createSurface(options) {
204
+ return new CocoaSurface(this, options);
205
+ }
206
+
192
207
  /**
193
208
  * The `useGlobalMenu` transport seam: same owner shape as the D-Bus
194
209
  * GlobalMenuExport (start/stop/update), pointed at the macOS menu bar.
@@ -279,16 +279,34 @@ export class CocoaContext2D {
279
279
  }
280
280
 
281
281
  save() {
282
+ // Sync before pushing: a fresh surface empties the stack on its way in,
283
+ // and a state pushed ahead of that sync was lost to it — the first
284
+ // save/restore pair on a new context restored nothing.
285
+ const surface = this._s();
282
286
  this._stack.push({ ...this._state, dash: [...this._state.dash] });
283
- this._native.ctxSave(this._s());
287
+ this._native.ctxSave(surface);
284
288
  }
285
289
 
286
290
  restore() {
291
+ // Canvas's rule: a restore with nothing saved does nothing. It is also
292
+ // what keeps a surface's base state safe under an unbalanced painter —
293
+ // the native stack below the JS one is the surface's own — and what a
294
+ // replaced backing surface wants, since it has nothing saved either.
295
+ const surface = this._s();
287
296
  const prev = this._stack.pop();
288
- if (prev) this._state = prev;
289
- this._native.ctxRestore(this._s());
297
+ if (!prev) return;
298
+ this._state = prev;
299
+ this._native.ctxRestore(surface);
290
300
  }
291
301
 
302
+ /**
303
+ * ntk's contract has a caller who took a context owing it a `destroy()`
304
+ * — there it is a GC and a Picture. Here a context is JS state over the
305
+ * surface's own graphics state, so there is nothing to free; the call is
306
+ * honoured so a caller written against ntk needs no branch.
307
+ */
308
+ destroy() {}
309
+
292
310
  _concat(a2, b2, c2, d2, e2, f2) {
293
311
  const [a, b, c, d, e, f] = this._state.ctm;
294
312
  this._state.ctm = [
@@ -0,0 +1,229 @@
1
+ // An offscreen drawing surface on the Cocoa backend — ntk's `Surface`
2
+ // contract (draw once, composite many, and shift a retained band in place)
3
+ // over one @windowkit/appkit CG bitmap. `react-x11/ntk`'s `Surface` hands
4
+ // out one of these when the app it is given is a Cocoa app (the app's
5
+ // `createSurface` seam, src/cocoa/app.js), so a component allocates its
6
+ // buffer the same way on both backends and names neither:
7
+ //
8
+ // const surface = new Surface(app, { width, height }); // device pixels
9
+ // const ctx = surface.getContext('2d'); // a CocoaContext2D
10
+ // ctx.fillRect(0, 0, width, height);
11
+ // surface.copyWithin({ x: 0, y: 0, width, height }, 0, -rowHeight);
12
+ // windowCtx.drawImage(surface, x, y); // one composite
13
+ //
14
+ // What is the same as ntk's: the constructor, `width`/`height`/`format`/
15
+ // `depth`/`bytes`, `getContext`, `render`, `clear`, `copyWithin` down to its
16
+ // clamping (ntk#252 — integer deltas, the band that survives, false when
17
+ // nothing does), `destroy`/`Symbol.dispose`, and `drawImage` taking the
18
+ // surface as a source. What differs is stated here, because this is where
19
+ // it lives:
20
+ //
21
+ // - **One graphics state per surface.** CoreGraphics keeps the CTM, the
22
+ // clip and the path in the bitmap context itself, where an X connection
23
+ // keeps them per Picture/GC. So `getContext('2d')` answers the same
24
+ // context every time — a JS object over that one state, nothing to free,
25
+ // and `destroy()` on it is a no-op — and `render()` brackets its callback
26
+ // in save/restore from the identity transform, so a one-shot draw leaves
27
+ // no residue for the next painter, which is what ntk gets from building a
28
+ // fresh context per call.
29
+ // - **`format: 'a8'` is not here yet.** Coverage surfaces are what the paint
30
+ // cache's masks and the shadows use, and both stay on their X path; every
31
+ // consumer that allocates a surface of its own asks for argb32. Asking
32
+ // for a8 throws rather than answering a colour surface that would
33
+ // composite differently.
34
+ // - **No Picture.** `picture()` is X's compositing handle; here a surface
35
+ // composites through `ctx.drawImage`, and asking for the picture says so.
36
+ // - **Freed on collection.** The bridge frees the bitmap from the handle's
37
+ // finalizer; `destroy()` drops the handle and refuses further use, so the
38
+ // memory goes with the next GC rather than on the call.
39
+ //
40
+ // Units are device pixels, like the window's backing store: a caller sizes
41
+ // one from `contentBox()` numbers, which are device pixels already
42
+ // (docs/scale.md). The bridge is told the app's scale so the bitmap carries
43
+ // it — inert for a `drawImage` source, right for a layer's contents.
44
+ import { CocoaContext2D } from './context2d.js';
45
+
46
+ export class CocoaSurface {
47
+ constructor(app, { width, height, format = 'argb32' } = {}) {
48
+ if (
49
+ !Number.isInteger(width) ||
50
+ !Number.isInteger(height) ||
51
+ width <= 0 ||
52
+ height <= 0
53
+ ) {
54
+ throw new Error('Surface: width and height must be positive integers');
55
+ }
56
+ if (format === 'a8') {
57
+ throw new Error(
58
+ "Surface: format 'a8' (a coverage surface) is not on the cocoa " +
59
+ 'backend yet — allocate argb32, which every backend has, and ' +
60
+ 'tint through fillStyle/globalAlpha; track docs/macos.md ' +
61
+ '"Custom drawing on a layer tree".',
62
+ );
63
+ }
64
+ if (format !== 'argb32') {
65
+ throw new Error(
66
+ `Surface: unknown format ${JSON.stringify(format)} (argb32 or a8)`,
67
+ );
68
+ }
69
+ this.app = app;
70
+ this.width = width;
71
+ this.height = height;
72
+ this.format = format;
73
+ this.depth = 32;
74
+ this._native = app._native;
75
+ this._fonts = app.fonts ?? null;
76
+ this._ctx = null;
77
+ this._destroyed = false;
78
+ this._surfaceHandle = this._native.createSurface(
79
+ width,
80
+ height,
81
+ app.scale ?? 1,
82
+ );
83
+ // a fresh bitmap's contents are the allocator's; a surface that is only
84
+ // partly drawn must composite nothing where nothing was drawn
85
+ this.clear();
86
+ }
87
+
88
+ /** bytes of backing storage — what a cache budgets against */
89
+ get bytes() {
90
+ return this.width * this.height * 4;
91
+ }
92
+
93
+ /** X's compositing handle, which this backend does not have. */
94
+ picture() {
95
+ throw new Error(
96
+ 'Surface: a surface on the cocoa backend has no XRender Picture — ' +
97
+ 'composite it with ctx.drawImage(surface, x, y), which takes a ' +
98
+ 'surface directly on both backends.',
99
+ );
100
+ }
101
+
102
+ _handle() {
103
+ if (this._destroyed) {
104
+ throw new Error(
105
+ 'Surface: destroyed — a context on a destroyed surface cannot ' +
106
+ 'draw; allocate a new Surface and draw into that.',
107
+ );
108
+ }
109
+ return this._surfaceHandle;
110
+ }
111
+
112
+ _context() {
113
+ if (!this._ctx) {
114
+ this._ctx = new CocoaContext2D(
115
+ this._native,
116
+ () => this._handle(),
117
+ () => 1,
118
+ );
119
+ this._ctx._fonts = this._fonts;
120
+ }
121
+ return this._ctx;
122
+ }
123
+
124
+ /**
125
+ * The 2d context on the bitmap — the same one every time, since the
126
+ * bitmap has one graphics state (see the header). ntk's contract has the
127
+ * caller owning it and owing it a `destroy()`; that call is honoured as a
128
+ * no-op, so a caller written against ntk needs no branch.
129
+ */
130
+ getContext(name = '2d') {
131
+ this._handle();
132
+ if (name !== '2d') {
133
+ throw new Error(
134
+ `Surface: getContext(${JSON.stringify(name)}) — a surface on the ` +
135
+ "cocoa backend has a '2d' context and nothing else.",
136
+ );
137
+ }
138
+ return this._context();
139
+ }
140
+
141
+ /**
142
+ * Draw into the surface through a context that starts clean — identity
143
+ * transform, the fill and line state as they were — and leaves the
144
+ * surface's state as it found it: the save/restore bracket stands in for
145
+ * the per-call context ntk builds and destroys.
146
+ */
147
+ render(fn) {
148
+ const ctx = this.getContext('2d');
149
+ ctx.save();
150
+ try {
151
+ ctx.resetTransform();
152
+ fn(ctx);
153
+ } finally {
154
+ ctx.restore();
155
+ }
156
+ return this;
157
+ }
158
+
159
+ /** Reset every pixel to fully transparent, whatever transform a live
160
+ * context holds — the clear is issued from the identity. */
161
+ clear() {
162
+ if (this._destroyed) return this;
163
+ const ctx = this._context();
164
+ ctx.save();
165
+ try {
166
+ ctx.resetTransform();
167
+ ctx.clearRect(0, 0, this.width, this.height);
168
+ } finally {
169
+ ctx.restore();
170
+ }
171
+ return this;
172
+ }
173
+
174
+ /**
175
+ * Scroll the pixels of `src` (surface coordinates, `{x, y, width,
176
+ * height}`) by (dx, dy) in place: one in-place copy of the band that
177
+ * survives the shift (the bridge's `scrollSurface`, a memmove per row), in
178
+ * place of redrawing everything that merely moved. True when the copy was
179
+ * issued; false means "nothing survives the shift here" and the caller
180
+ * repaints `src` exactly as it would have without this method.
181
+ *
182
+ * ntk#252's contract, clamp for clamp: refused when the delta is
183
+ * fractional (a sub-pixel shift changes every pixel), when it is zero,
184
+ * when nothing of `src` survives after clamping to the surface, or on a
185
+ * destroyed surface. The band is `clamped src ∩ (clamped src + delta)`,
186
+ * so nothing outside `src` is written; the overlap is safe because the
187
+ * copy walks rows in the direction that reads before it overwrites.
188
+ */
189
+ copyWithin(src, dx, dy) {
190
+ if (this._destroyed) return false;
191
+ if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
192
+ if (dx === 0 && dy === 0) return false;
193
+ const x0 = Math.max(0, Math.floor(src.x));
194
+ const y0 = Math.max(0, Math.floor(src.y));
195
+ const x1 = Math.min(this.width, Math.ceil(src.x + src.width));
196
+ const y1 = Math.min(this.height, Math.ceil(src.y + src.height));
197
+ const dstX0 = Math.max(x0, x0 + dx);
198
+ const dstY0 = Math.max(y0, y0 + dy);
199
+ const dstX1 = Math.min(x1, x1 + dx);
200
+ const dstY1 = Math.min(y1, y1 + dy);
201
+ // written as the positive test so a NaN edge (a rect with no numbers
202
+ // in it) is a refusal too, never a native call with garbage
203
+ if (!(dstX1 > dstX0 && dstY1 > dstY0)) return false;
204
+ return Boolean(
205
+ this._native.scrollSurface(
206
+ this._surfaceHandle,
207
+ x0,
208
+ y0,
209
+ x1 - x0,
210
+ y1 - y0,
211
+ dx,
212
+ dy,
213
+ ),
214
+ );
215
+ }
216
+
217
+ destroy() {
218
+ if (this._destroyed) return;
219
+ this._destroyed = true;
220
+ this._surfaceHandle = null;
221
+ this._ctx = null;
222
+ }
223
+
224
+ [Symbol.dispose]() {
225
+ this.destroy();
226
+ }
227
+ }
228
+
229
+ export default CocoaSurface;
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
+ }