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.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- 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
|
+
}
|