react-x11 1.2.0 → 2.0.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/ntk.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `react-x11/ntk` — the toolkit underneath, re-exported so an extension
3
+ * package does not declare a second, independently-versioned `ntk`
4
+ * dependency. Two copies in a process means two font caches and two glyph
5
+ * atlases, and a node built against one cannot be painted by the other.
6
+ *
7
+ * The layout engine is not here: react-x11 owns Yoga now, and an element
8
+ * never needs it — `measureContent` states its constraints in words so that
9
+ * yoga's ABI stays out of the extension seam.
10
+ *
11
+ * Documents are not here either: ntk 8 removed `MarkdownView`, `HtmlView` and
12
+ * `layoutTex` with the engine. Use `@react-x11/components` for those;
13
+ * `SvgView` remains, since a drawing is not a document.
14
+ *
15
+ * ntk ships no types of its own, so these are deliberately loose rather
16
+ * than a hand-written mirror that would drift out of date silently. The
17
+ * named exports are the ones an extension actually reaches for; anything
18
+ * else ntk has is still there at runtime.
19
+ */
20
+ export const createClient: (
21
+ options?: Record<string, unknown>,
22
+ ) => Promise<unknown>;
23
+ export const StaticFontSource: new (...args: unknown[]) => unknown;
24
+ export const FontconfigFontSource: new (...args: unknown[]) => unknown;
25
+ export const Clipboard: new (...args: unknown[]) => unknown;
26
+ export const Path2D: new (...args: unknown[]) => unknown;
27
+ export const Image: new (...args: unknown[]) => unknown;
28
+ export const Pixmap: new (...args: unknown[]) => unknown;
29
+ /**
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).
33
+ */
34
+ export const Surface: new (...args: unknown[]) => unknown;
35
+ /** `code` values on a failed GL setup — see `<glarea onError>`. */
36
+ export const GLXError: {
37
+ NO_EXTENSION: 'GLX_NO_EXTENSION';
38
+ INDIRECT_DISABLED: 'GLX_INDIRECT_DISABLED';
39
+ NO_CONFIG: 'GLX_NO_CONFIG';
40
+ CONTEXT_FAILED: 'GLX_CONTEXT_FAILED';
41
+ };
42
+
43
+ declare const ntk: Record<string, unknown>;
44
+ export default ntk;
package/src/ntk.js ADDED
@@ -0,0 +1,25 @@
1
+ // `react-x11/ntk` — the toolkit underneath, re-exported.
2
+ //
3
+ // A package that adds an element draws with ntk's 2d context, may need a
4
+ // `Path2D`, an `Image`, a `Pixmap` or a `FontSource`, and an application
5
+ // embedding react-x11 may want `createClient` to build the connection it
6
+ // then passes as `createRoot({ app })`. Reaching those through here rather
7
+ // than declaring a second `ntk` dependency is what keeps one copy in the
8
+ // process: two copies mean two font caches and two glyph atlases, and a
9
+ // node built against one cannot be painted by the other.
10
+ //
11
+ // The layout engine is **not** here. It used to be — ntk owned Yoga while
12
+ // its own document widgets laid out with flexbox — but the renderer is the
13
+ // only layout consumer now and owns it directly (`src/yoga.js`). Nothing
14
+ // outside needs it either: an element's `measureContent` is handed its
15
+ // constraints in words (`'at-most'`, `'exactly'`), precisely so that yoga's
16
+ // ABI does not become part of the extension seam.
17
+ //
18
+ // Documents are not here either, and never were ntk's to give: ntk 8 removed
19
+ // `MarkdownView`, `HtmlView` and `layoutTex` along with the layout engine, so
20
+ // `export *` no longer carries them. A package that was reaching through here
21
+ // for one — they were reachable but never declared — wants
22
+ // `@react-x11/components` (`<Markdown>`, `<Formula>`). `SvgView` is still
23
+ // here; a drawing is not a document.
24
+ export * from 'ntk';
25
+ export { default } from 'ntk';
@@ -0,0 +1,366 @@
1
+ // Content-keyed cache of rendered node content (issue #149).
2
+ //
3
+ // Damage culling already skips subtrees *outside* the damage rect, so an
4
+ // unchanged icon in an untouched region costs nothing. This is for the other
5
+ // case: a node **inside damage but unchanged**. A theme change, an expose, a
6
+ // resize, a sibling's damage rect covering a row of icons, the strip a scroll
7
+ // exposes — all of them repaint content that is byte-identical to last frame.
8
+ //
9
+ // ## Keyed by content, not by node
10
+ //
11
+ // Per-node ownership would make 400 cells of the same icon into 400 identical
12
+ // surfaces. Keying on what is drawn collapses them to one entry per distinct
13
+ // drawing — the icon wall's 400 cells become 8 — and it removes invalidation
14
+ // entirely: a changed prop produces a different key, so there is never a stale
15
+ // entry to detect and evict.
16
+ //
17
+ // ## Coverage, where the drawing allows it
18
+ //
19
+ // A drawing that commits to a single colour is cached as an a8 coverage
20
+ // surface and painted through the current fill colour, so hover, `:disabled`
21
+ // and a theme flip all reuse one entry and the colour stays out of the key.
22
+ // Multi-colour drawings bake their colours in, which is right, because those
23
+ // colours belong to the drawing. `SvgView.paintKind` decides which is which.
24
+ //
25
+ // ## Self-limiting, on purpose
26
+ //
27
+ // X gives no back-pressure: the first sign of overspending is `BadAlloc` on
28
+ // `CreatePixmap`, delivered as an async error with no useful attribution. So
29
+ // there is a byte budget with LRU eviction (copying `trimGlyphPages` and its
30
+ // 8MB), a per-item area cap so one big illustration cannot evict the whole
31
+ // useful set, and a "seen twice" gate, because a node painted once and never
32
+ // again is a pure loss under a cache.
33
+ // Namespace import, not a named one: `Surface` arrives in a later ntk than
34
+ // this package pins, and a named import of something absent is a *load-time*
35
+ // SyntaxError — the whole renderer, not just the cache. Same shape as the
36
+ // `typeof wnd?.scrollRegion !== 'function'` guard for ntk without #139.
37
+ import * as ntk from 'ntk';
38
+
39
+ /** Stale pixels are undebuggable, and every other optimization here has an
40
+ * escape hatch — see NO_SCROLL_BLIT. */
41
+ const DISABLED = process.env.REACT_X11_NO_PAINT_CACHE === '1';
42
+
43
+ /**
44
+ * `verify` re-renders every hit and compares a digest of the drawing calls
45
+ * against the one recorded when the entry was made. A key that fails to name
46
+ * something the paint depends on then fails *loudly*, at the moment it would
47
+ * otherwise have shown a stale pixel — which is the one bug this design can
48
+ * have, and the one that is hardest to find by looking. Slow by construction:
49
+ * it does all the work the cache exists to avoid, plus the comparison.
50
+ */
51
+ const VERIFY = process.env.REACT_X11_PAINT_CACHE === 'verify';
52
+
53
+ const DEBUG = process.env.REACT_X11_DEBUG_PAINT_CACHE === '1';
54
+
55
+ /** Same budget as ntk's server-side glyph cache, for the same reason: it is
56
+ * enough for any real working set and small enough that a runaway shows up
57
+ * as eviction churn rather than as server memory. */
58
+ const DEFAULT_BUDGET = 8 << 20;
59
+
60
+ /**
61
+ * No single entry may take more than this. One 800x600 illustration is 1.9MB
62
+ * of argb32 and would evict the entire useful set for something drawn once.
63
+ * Widget-sized things stay well under it.
64
+ */
65
+ const MAX_ITEM_PIXELS = 256 * 256;
66
+
67
+ /** How many distinct keys the "seen twice" gate remembers before starting
68
+ * over. Bounded because a page cycling through unique content would otherwise
69
+ * grow this forever; the cost of forgetting is one extra live paint. */
70
+ const MAX_PENDING = 4096;
71
+
72
+ /** Whether a context is drawing 1:1 into device space.
73
+ *
74
+ * A cached surface is pixels at a fixed size and orientation. Under a scaled
75
+ * or rotated CTM, compositing it would *resample* those pixels where a live
76
+ * paint would re-rasterize at the right size, so the cache has to stand
77
+ * aside. react-x11 only ever translates during paint, and only inside
78
+ * `<canvas>`, so this is insurance rather than a live case.
79
+ */
80
+ function isDeviceSpace(ctx) {
81
+ const m = ctx.getTransform?.();
82
+ if (!m) return true;
83
+ return (
84
+ m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0
85
+ );
86
+ }
87
+
88
+ // --- the verify-mode digest ------------------------------------------------
89
+
90
+ /** Fold a string into a 32-bit FNV-1a hash. */
91
+ function fold(hash, text) {
92
+ let h = hash;
93
+ for (let i = 0; i < text.length; i++) {
94
+ h ^= text.charCodeAt(i);
95
+ h = Math.imul(h, 0x01000193);
96
+ }
97
+ return h >>> 0;
98
+ }
99
+
100
+ /** One drawing-call argument, as a string a digest can eat. Coordinates are
101
+ * quantized to 1/64 because that is the fixed-point grid X renders on, so
102
+ * float noise below it is not a difference. */
103
+ function stamp(value) {
104
+ if (typeof value === 'number') return String(Math.round(value * 64) / 64);
105
+ if (value && typeof value === 'object') {
106
+ if (value._cmds) return JSON.stringify(value._cmds);
107
+ return JSON.stringify(value);
108
+ }
109
+ return String(value);
110
+ }
111
+
112
+ /**
113
+ * A context that records everything drawn through it, then draws it. The
114
+ * digest of that recording is what verify mode compares — client-side, so
115
+ * catching a key bug costs no round trip and needs no pixel readback.
116
+ */
117
+ function recordingContext(ctx, state) {
118
+ return new Proxy(ctx, {
119
+ get(target, prop) {
120
+ const value = target[prop];
121
+ if (typeof value !== 'function') return value;
122
+ return (...args) => {
123
+ state.digest = fold(state.digest, prop);
124
+ for (const arg of args) state.digest = fold(state.digest, stamp(arg));
125
+ return value.apply(target, args);
126
+ };
127
+ },
128
+ set(target, prop, value) {
129
+ state.digest = fold(fold(state.digest, `=${String(prop)}`), stamp(value));
130
+ target[prop] = value;
131
+ return true;
132
+ },
133
+ });
134
+ }
135
+
136
+ // --- the cache -------------------------------------------------------------
137
+
138
+ /**
139
+ * One cache per X connection. Surfaces are server-side resources, so two
140
+ * roots on two displays must not share entries — and two roots on the *same*
141
+ * display should, which is why this hangs off the app rather than the root.
142
+ */
143
+ export class PaintCache {
144
+ constructor(app, { budget = DEFAULT_BUDGET, verify = VERIFY } = {}) {
145
+ this.app = app;
146
+ this.budget = budget;
147
+ this.verify = verify;
148
+ /** key -> entry. Map iteration is insertion order, so re-inserting on
149
+ * access makes this an LRU list for free — same trick as getGlyphPage. */
150
+ this.entries = new Map();
151
+ /** keys seen once, waiting to be seen again (the "seen twice" gate) */
152
+ this.pending = new Map();
153
+ this.bytes = 0;
154
+ /** entries touched this frame, which eviction must not take */
155
+ this.inUse = new Set();
156
+ this.stats = {
157
+ hits: 0,
158
+ misses: 0,
159
+ renders: 0,
160
+ evictions: 0,
161
+ tooBig: 0,
162
+ failed: 0,
163
+ };
164
+ }
165
+
166
+ beginFrame() {
167
+ this.inUse.clear();
168
+ }
169
+
170
+ endFrame() {
171
+ this._trim();
172
+ if (DEBUG) {
173
+ const s = this.stats;
174
+ console.log(
175
+ `react-x11: paint cache ${this.entries.size} entries, ` +
176
+ `${(this.bytes / 1024).toFixed(0)}KB — hits ${s.hits} misses ${s.misses} ` +
177
+ `renders ${s.renders} evictions ${s.evictions}` +
178
+ (s.tooBig ? ` too-big ${s.tooBig}` : '') +
179
+ (s.failed ? ` failed ${s.failed}` : ''),
180
+ );
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Paint `node`'s content, through the cache when it can be.
186
+ *
187
+ * Every path here falls back to painting live, so the cache can cost
188
+ * correctness nothing: the worst mistake it can make is not firing.
189
+ */
190
+ paint(node, ctx) {
191
+ const plan = node.paintCachePlan(ctx);
192
+ if (!plan) return node.paintContent(ctx);
193
+ return this.drawing(ctx, {
194
+ ...plan,
195
+ label: `<${node.kind}>`,
196
+ draw: (sctx, box) => node.paintCached(sctx, box),
197
+ live: () => node.paintContent(ctx),
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Paint a cacheable **drawing**, which a node's content is one kind of.
203
+ *
204
+ * `plan` is the node protocol's plan (`key`, `x`/`y`, `width`/`height`,
205
+ * `format`, `tint`) plus the three things a node supplies implicitly:
206
+ * `draw(sctx, box)` renders into the surface, `live()` paints the same
207
+ * thing straight to the target for every path that declines to cache, and
208
+ * the optional `after(surface)` runs once on a freshly rendered one — for
209
+ * a filter that belongs to the picture rather than to the pixels, which is
210
+ * how a blurred `boxShadow` is a cache entry at all (issue #345).
211
+ *
212
+ * `maxPixels` overrides the per-item cap for a caller whose drawing is
213
+ * legitimately box-sized rather than icon-sized.
214
+ */
215
+ drawing(ctx, plan) {
216
+ if (!isDeviceSpace(ctx)) return plan.live(ctx);
217
+
218
+ if (plan.width * plan.height > (plan.maxPixels ?? MAX_ITEM_PIXELS)) {
219
+ this.stats.tooBig++;
220
+ return plan.live(ctx);
221
+ }
222
+
223
+ const hit = this.entries.get(plan.key);
224
+ if (hit) {
225
+ // re-insert to mark recent
226
+ this.entries.delete(plan.key);
227
+ this.entries.set(plan.key, hit);
228
+ this.inUse.add(hit);
229
+ this.stats.hits++;
230
+ if (this.verify) this._verify(hit, plan);
231
+ return this._blit(ctx, hit, plan);
232
+ }
233
+
234
+ this.stats.misses++;
235
+ const seen = (this.pending.get(plan.key) ?? 0) + 1;
236
+ if (seen < 2) {
237
+ if (this.pending.size >= MAX_PENDING) this.pending.clear();
238
+ this.pending.set(plan.key, seen);
239
+ return plan.live(ctx);
240
+ }
241
+ this.pending.delete(plan.key);
242
+
243
+ const entry = this._render(plan);
244
+ if (!entry) return plan.live(ctx);
245
+ this.entries.set(plan.key, entry);
246
+ this.bytes += entry.bytes;
247
+ this.inUse.add(entry);
248
+ return this._blit(ctx, entry, plan);
249
+ }
250
+
251
+ _render(plan) {
252
+ try {
253
+ const surface = new ntk.Surface(this.app, {
254
+ width: plan.width,
255
+ height: plan.height,
256
+ format: plan.format,
257
+ });
258
+ const box = { x: 0, y: 0, width: plan.width, height: plan.height };
259
+ const state = { digest: 0x811c9dc5 };
260
+ surface.render((sctx) =>
261
+ plan.draw(this.verify ? recordingContext(sctx, state) : sctx, box),
262
+ );
263
+ plan.after?.(surface);
264
+ this.stats.renders++;
265
+ return {
266
+ key: plan.key,
267
+ surface,
268
+ bytes: surface.bytes,
269
+ digest: this.verify ? state.digest : 0,
270
+ };
271
+ } catch (err) {
272
+ // A server that will not give us a pixmap, a node whose paint threw:
273
+ // either way the live paint below is still correct.
274
+ this.stats.failed++;
275
+ if (DEBUG)
276
+ console.warn('react-x11: paint cache render failed:', err.message);
277
+ return null;
278
+ }
279
+ }
280
+
281
+ _blit(ctx, entry, plan) {
282
+ // an a8 entry is coverage: the fill colour is what actually gets painted
283
+ if (plan.format === 'a8') {
284
+ const before = ctx.fillStyle;
285
+ ctx.fillStyle = plan.tint;
286
+ ctx.drawImage(entry.surface, plan.x, plan.y);
287
+ ctx.fillStyle = before;
288
+ return;
289
+ }
290
+ ctx.drawImage(entry.surface, plan.x, plan.y);
291
+ }
292
+
293
+ /**
294
+ * Re-render a hit and compare digests. A mismatch means the key did not
295
+ * name everything the paint reads, which is the one way this design
296
+ * produces a wrong pixel rather than a slow frame.
297
+ */
298
+ _verify(entry, plan) {
299
+ const state = { digest: 0x811c9dc5 };
300
+ let scratch = null;
301
+ try {
302
+ scratch = new ntk.Surface(this.app, {
303
+ width: plan.width,
304
+ height: plan.height,
305
+ format: plan.format,
306
+ });
307
+ scratch.render((sctx) =>
308
+ plan.draw(recordingContext(sctx, state), {
309
+ x: 0,
310
+ y: 0,
311
+ width: plan.width,
312
+ height: plan.height,
313
+ }),
314
+ );
315
+ } catch {
316
+ return; // verification is best-effort; never break a frame over it
317
+ } finally {
318
+ scratch?.destroy();
319
+ }
320
+ if (state.digest === entry.digest) return;
321
+ console.error(
322
+ `react-x11: paint cache key does not cover the paint of ${plan.label ?? plan.key}.\n` +
323
+ ` key: ${plan.key}\n` +
324
+ ' The drawing changed while the key did not, so a cached frame would ' +
325
+ 'show stale pixels. Add whatever changed to the key.',
326
+ );
327
+ }
328
+
329
+ /** Evict least-recently-used entries until the budget is met. Entries drawn
330
+ * this frame are never taken — evicting one would guarantee re-rendering it
331
+ * next frame. */
332
+ _trim() {
333
+ if (this.bytes <= this.budget) return;
334
+ for (const [key, entry] of this.entries) {
335
+ if (this.bytes <= this.budget) break;
336
+ if (this.inUse.has(entry)) continue;
337
+ this.entries.delete(key);
338
+ this.bytes -= entry.bytes;
339
+ entry.surface.destroy();
340
+ this.stats.evictions++;
341
+ }
342
+ }
343
+
344
+ destroy() {
345
+ for (const entry of this.entries.values()) entry.surface.destroy();
346
+ this.entries.clear();
347
+ this.pending.clear();
348
+ this.bytes = 0;
349
+ this.inUse.clear();
350
+ }
351
+ }
352
+
353
+ /** Whether the installed ntk can make the surfaces this needs. Older ones
354
+ * cannot, and the answer is simply that nothing is cached. */
355
+ export const paintCacheSupported = () => typeof ntk.Surface === 'function';
356
+
357
+ /**
358
+ * The cache for an app, created on first use. Null when caching is off, when
359
+ * the installed ntk is too old, or when the app cannot make surfaces — the
360
+ * headless mock in the smoke tests has no Render extension, and every node
361
+ * there must paint live.
362
+ */
363
+ export function paintCacheFor(app) {
364
+ if (DISABLED || !paintCacheSupported() || !app?.display?.Render) return null;
365
+ return (app._paintCache ??= new PaintCache(app));
366
+ }