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
@@ -0,0 +1,100 @@
1
+ // `useIdle()` and `useKeepAwake()` — the two halves of "is the user still
2
+ // there", as things a component declares rather than manages.
3
+ //
4
+ // The machinery lives in `idle.js`, keyed by connection: idleness is a fact
5
+ // about one display's input devices, and the inhibition is held for as long
6
+ // as the component that asked for it is mounted.
7
+
8
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react';
9
+
10
+ import { useApp } from './appcontext.js';
11
+ import { idleSnapshot, keepAwake, watchIdle } from './idle.js';
12
+
13
+ /**
14
+ * Whether the user has been away from the keyboard for `timeout`
15
+ * milliseconds.
16
+ *
17
+ * ```jsx
18
+ * const away = useIdle(5 * 60_000);
19
+ *
20
+ * <Avatar status={away ? 'away' : 'online'} />
21
+ * ```
22
+ *
23
+ * Idleness is the **whole display's**, not this window's: it counts input on
24
+ * every device, whichever application it went to. That is the right meaning
25
+ * for a presence indicator, an auto-save, or a dashboard that stops
26
+ * refreshing when the desk is empty, and the wrong one for "has the user
27
+ * ignored *my* window" — which is `useWindowState().focused`.
28
+ *
29
+ * Where the X server carries a `IDLETIME` counter — Xorg does — this costs
30
+ * **no timer at all**: a SYNC alarm fires when the counter crosses `timeout`
31
+ * and again when input pulls it back down. On a server without one (XQuartz)
32
+ * it falls back to polling MIT-SCREEN-SAVER, scheduled against the remaining
33
+ * time rather than on a tick. On a display with neither it stays `false`,
34
+ * which is the honest answer for a display that cannot be asked.
35
+ *
36
+ * `timeout` is a dependency: passing a computed value re-arms on every change,
37
+ * so hold it in a constant or a `useMemo` rather than building it inline from
38
+ * state that moves.
39
+ */
40
+ export function useIdle(timeout) {
41
+ const app = useApp();
42
+ const subscribe = useCallback(
43
+ (onChange) => watchIdle(app, timeout, onChange),
44
+ [app, timeout],
45
+ );
46
+ const snapshot = useCallback(
47
+ () => idleSnapshot(app, timeout),
48
+ [app, timeout],
49
+ );
50
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
51
+ }
52
+
53
+ /**
54
+ * Keep the screen awake while `active` is true.
55
+ *
56
+ * ```jsx
57
+ * useKeepAwake(playing, 'Playing a video');
58
+ * ```
59
+ *
60
+ * Held for as long as the flag is true and the component is mounted, and
61
+ * released on either — including on an unmount mid-playback, which is the
62
+ * case that leaves a desktop permanently un-blanking when it is done by hand.
63
+ *
64
+ * Three rungs, and **all three inhibit screen blanking only**: the settings
65
+ * portal's `Inhibit`, the older `org.freedesktop.ScreenSaver` service, and
66
+ * `ScreenSaverSuspend`, which is a plain X request and needs no session bus.
67
+ * Nothing here stops a **suspend** — only the portal rung could, and a seam
68
+ * that works on one desktop in three is worse than not offering it.
69
+ *
70
+ * `reason` is shown to the user by desktops that list what is holding the
71
+ * screen on, so it reads best as a sentence about the app's state rather than
72
+ * the app's name, which they already show.
73
+ *
74
+ * Failure is silent by design. A machine with no portal, no screensaver
75
+ * service and no MIT-SCREEN-SAVER cannot be asked, and a video player is not
76
+ * a place to surface that.
77
+ */
78
+ export function useKeepAwake(active, reason = 'Busy') {
79
+ const app = useApp();
80
+ // Read through a ref so that editing the reason mid-playback does not drop
81
+ // the inhibition and take out a new one — a gap the screen can blank in.
82
+ const latest = useRef(reason);
83
+ latest.current = reason;
84
+
85
+ useEffect(() => {
86
+ if (!active) return undefined;
87
+ let release = null;
88
+ let cancelled = false;
89
+ keepAwake({ reason: latest.current, app }).then((fn) => {
90
+ // Unmounted while the bus round trip was in flight: take it out and
91
+ // give it straight back rather than leaking it for the process's life.
92
+ if (cancelled) fn();
93
+ else release = fn;
94
+ });
95
+ return () => {
96
+ cancelled = true;
97
+ release?.();
98
+ };
99
+ }, [active, app]);
100
+ }
@@ -0,0 +1,349 @@
1
+ // What `<image>` can show beyond a file path (issue #367): in-memory pixels
2
+ // — encoded bytes, raw RGBA, an ntk `Image` — and pictures that already live
3
+ // on the server, composited without a pixel ever crossing the wire.
4
+ //
5
+ // The split follows `decorations.js`: classification, validation, decoding
6
+ // and the `cacheKey` cache live here, where a test needs no server; the node
7
+ // half in nodes.js is only lifecycle — when to resolve, when to claim
8
+ // damage, when to let go.
9
+ import { Image, Picture, decodeImage } from 'ntk';
10
+
11
+ /**
12
+ * A client-side object the 2d context composites as-is: an ntk `Image` or
13
+ * `Surface`, or anything with a size and a `picture(app)`. The same
14
+ * duck-typing ntk's own `drawImage` uses, so the two answers cannot drift —
15
+ * whatever passes here is something the paint will accept.
16
+ */
17
+ export function isDirectImageSource(src) {
18
+ return (
19
+ src != null &&
20
+ typeof src === 'object' &&
21
+ typeof src.picture === 'function' &&
22
+ Number.isFinite(src.width) &&
23
+ Number.isFinite(src.height)
24
+ );
25
+ }
26
+
27
+ /**
28
+ * A file path or file URL. URLs are matched structurally, mirroring the
29
+ * declared `FileUrl` — the declarations use only ES lib types, so `URL`
30
+ * cannot be named there, and the runtime accepting exactly what the type
31
+ * accepts is what keeps the two from disagreeing.
32
+ */
33
+ export function isPathImageSource(src) {
34
+ if (typeof src === 'string' || src instanceof URL) return true;
35
+ return (
36
+ src != null &&
37
+ typeof src === 'object' &&
38
+ typeof src.href === 'string' &&
39
+ typeof src.protocol === 'string'
40
+ );
41
+ }
42
+
43
+ /** …normalized for ntk's `loadImage`, which wants a string or a real URL. */
44
+ export const toLoadablePath = (src) =>
45
+ typeof src === 'string' || src instanceof URL ? src : new URL(src.href);
46
+
47
+ /** Raw straight-RGBA pixels: `{ width, height, data }` — the shape
48
+ * `getImageData` hands back. (A `Buffer` of encoded bytes is a `Uint8Array`
49
+ * subclass, so the two byte forms are one check elsewhere.) */
50
+ export function isRawImageSource(src) {
51
+ return (
52
+ src != null &&
53
+ typeof src === 'object' &&
54
+ !(src instanceof Uint8Array) &&
55
+ !isPathImageSource(src) &&
56
+ !isDirectImageSource(src) &&
57
+ 'data' in src
58
+ );
59
+ }
60
+
61
+ const positiveInt = (v) => Number.isInteger(v) && v > 0;
62
+
63
+ const describe = (value) =>
64
+ value === null
65
+ ? 'null'
66
+ : typeof value === 'object'
67
+ ? (value.constructor?.name ?? 'an object')
68
+ : typeof value;
69
+
70
+ /** Stated once, so every error lists the same set of accepted forms. */
71
+ const SRC_FORMS =
72
+ 'a file path or file URL (PNG/JPEG), encoded PNG/JPEG bytes (Buffer or ' +
73
+ 'Uint8Array), raw RGBA ({ width, height, data }), or an ntk Image/Surface';
74
+
75
+ function validateServerSource(kind, desc) {
76
+ const shape =
77
+ kind === 'drawable'
78
+ ? '{ id, width, height, depth? }'
79
+ : '{ id, width, height }';
80
+ const explain =
81
+ `react-x11: <image ${kind}> takes ${shape} — the ${kind === 'picture' ? 'Picture' : 'Pixmap/Window'}'s ` +
82
+ 'X id and its size in pixels. The size is stated by the caller because ' +
83
+ 'asking the server for it would be a round trip, which this prop exists to avoid.';
84
+ if (desc == null || typeof desc !== 'object') {
85
+ throw new Error(`${explain} Got ${describe(desc)}.`);
86
+ }
87
+ if (!positiveInt(desc.id)) {
88
+ throw new Error(
89
+ `${explain} \`id\` must be an X resource id, got ${desc.id}.`,
90
+ );
91
+ }
92
+ if (!positiveInt(desc.width) || !positiveInt(desc.height)) {
93
+ throw new Error(
94
+ `${explain} Got a size of ${desc.width}x${desc.height} for id ${desc.id}.`,
95
+ );
96
+ }
97
+ if (
98
+ kind === 'drawable' &&
99
+ desc.depth != null &&
100
+ !(desc.depth in DEPTH_FORMATS)
101
+ ) {
102
+ throw new Error(
103
+ `react-x11: <image drawable> supports depth 24 (rgb, the default), 32 ` +
104
+ `(argb) and 8 (alpha only, composited as ink through its coverage) — ` +
105
+ `got ${desc.depth}. A depth the RENDER standard formats do not cover ` +
106
+ `cannot be composited without knowing the visual, so wrap the drawable ` +
107
+ `in a Picture yourself and pass <image picture>.`,
108
+ );
109
+ }
110
+ }
111
+
112
+ function validateRawSource(src) {
113
+ if (!positiveInt(src.width) || !positiveInt(src.height)) {
114
+ throw new Error(
115
+ 'react-x11: <image src={{ width, height, data }}> needs positive ' +
116
+ `integer dimensions, got ${src.width}x${src.height}.`,
117
+ );
118
+ }
119
+ const { data } = src;
120
+ const bytes =
121
+ data instanceof Uint8Array || data instanceof Uint8ClampedArray
122
+ ? data.length
123
+ : null;
124
+ const want = src.width * src.height * 4;
125
+ if (bytes !== want) {
126
+ throw new Error(
127
+ 'react-x11: <image src={{ width, height, data }}> wants straight ' +
128
+ '(non-premultiplied) RGBA bytes — data.length must be ' +
129
+ `width × height × 4 = ${want}, got ${bytes ?? describe(data)}. ` +
130
+ 'For encoded PNG/JPEG bytes pass the buffer itself as `src`.',
131
+ );
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Structural validation for the source props, at prop-arrival time — so the
137
+ * throw lands in render/commit where React can report it against the
138
+ * component, never inside a layout or paint pass. Content failures (a file
139
+ * that is not there, bytes that do not decode) are not structural and are
140
+ * handled where they surface: logged, and the element shows nothing.
141
+ */
142
+ export function validateImageProps(props) {
143
+ const named = ['src', 'picture', 'drawable'].filter((k) => props[k] != null);
144
+ if (named.length > 1) {
145
+ throw new Error(
146
+ `react-x11: <image> shows one source, got ${named.length} ` +
147
+ `(${named.join(', ')}). Pass exactly one of \`src\` (client-side ` +
148
+ 'pixels), `picture` (an existing server-side Picture) or `drawable` ' +
149
+ '(an existing server-side Pixmap/Window).',
150
+ );
151
+ }
152
+ if (
153
+ props.cacheKey != null &&
154
+ (props.picture != null || props.drawable != null)
155
+ ) {
156
+ throw new Error(
157
+ 'react-x11: <image cacheKey> names decoded client-side pixels, and a ' +
158
+ '`picture`/`drawable` is already server-side — there is nothing to ' +
159
+ 'cache. Drop the cacheKey; the composite is already upload-free.',
160
+ );
161
+ }
162
+ if (props.picture != null) validateServerSource('picture', props.picture);
163
+ if (props.drawable != null) validateServerSource('drawable', props.drawable);
164
+ const src = props.src;
165
+ if (src == null) return;
166
+ if (isPathImageSource(src)) return;
167
+ if (src instanceof Uint8Array) return;
168
+ if (isDirectImageSource(src)) return;
169
+ if (typeof src === 'object' && 'data' in src) return validateRawSource(src);
170
+ throw new Error(
171
+ `react-x11: <image src> must be ${SRC_FORMS} — got ${describe(src)}.`,
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Decode a synchronous source into an ntk `Image`: encoded bytes through the
177
+ * PNG/JPEG decoders, raw RGBA wrapped as-is (no copy — the object is treated
178
+ * as immutable content from here on). File paths stay async and do not come
179
+ * here. May throw on corrupt bytes; the caller treats that as a content
180
+ * failure, because encoded bytes usually arrive from outside the program.
181
+ */
182
+ export function decodeImageSource(src) {
183
+ if (src instanceof Uint8Array) return decodeImage(src);
184
+ return new Image({ width: src.width, height: src.height, data: src.data });
185
+ }
186
+
187
+ const sameServerSource = (a, b) =>
188
+ (a?.id ?? null) === (b?.id ?? null) &&
189
+ (a?.width ?? null) === (b?.width ?? null) &&
190
+ (a?.height ?? null) === (b?.height ?? null) &&
191
+ (a?.depth ?? null) === (b?.depth ?? null);
192
+
193
+ /**
194
+ * Did the *content* behind the source props change?
195
+ *
196
+ * By value for the server descriptors — React rebuilds inline objects every
197
+ * render, and `{ id: 7, … }` is the same picture however fresh the object.
198
+ * By identity for client sources, except that an unchanged `cacheKey`
199
+ * vouches for a rebuilt buffer: same key, same picture. A direct source (an
200
+ * ntk `Image`) is its own identity, so the key never overrides one — a
201
+ * caller switching between bytes and an `Image` under a stable key still
202
+ * gets the switch.
203
+ */
204
+ export function imageSourceChanged(next, prev) {
205
+ if (!sameServerSource(next.picture, prev.picture)) return true;
206
+ if (!sameServerSource(next.drawable, prev.drawable)) return true;
207
+ if (next.cacheKey !== prev.cacheKey) return true;
208
+ if (next.src === prev.src) return false;
209
+ if ((next.src == null) !== (prev.src == null)) return true;
210
+ if (isDirectImageSource(next.src) || isDirectImageSource(prev.src))
211
+ return true;
212
+ return next.cacheKey == null;
213
+ }
214
+
215
+ // --- the cacheKey cache -----------------------------------------------------
216
+ //
217
+ // ntk's `Image` already caches its server upload per connection; what it
218
+ // cannot know is that the buffer an app re-derived this render is the same
219
+ // picture as last render's. `cacheKey` is the caller saying so — the same
220
+ // contract as `<canvas cacheKey>`: the key names the content, and two
221
+ // `<image>`s with one key share one decoded image and one upload.
222
+ //
223
+ // Refcounted rather than LRU, unlike the paint cache, for two reasons: an
224
+ // entry holds a server pixmap, the resource X gives no back-pressure on, and
225
+ // unlike a rendered widget an image cannot be re-made from its key once the
226
+ // source is gone. So an entry lives exactly as long as some mounted <image>
227
+ // holds it and is freed with the last one; a remount decodes again, which is
228
+ // the honest cost of not keeping dead pixmaps around.
229
+
230
+ /** app -> Map<key, entry>. Per connection, because the upload is. */
231
+ const sourceCaches = new WeakMap();
232
+
233
+ /**
234
+ * Take a hold on the entry for `key`, creating it with `load()` on first
235
+ * use. `load` returns `{ image }` for a synchronous source or `{ promise }`
236
+ * for a file read — the promise resolves an `Image` or, on content failure,
237
+ * `null` (never rejects; the loader reports the failure once, not per
238
+ * holder). Every acquire is paired with a `releaseImageSource`.
239
+ */
240
+ export function acquireImageSource(app, key, load) {
241
+ let cache = sourceCaches.get(app);
242
+ if (!cache) sourceCaches.set(app, (cache = new Map()));
243
+ let entry = cache.get(key);
244
+ if (!entry) {
245
+ entry = { key, refs: 0, image: null, promise: null, released: false };
246
+ cache.set(key, entry);
247
+ const made = load();
248
+ if (made.promise) {
249
+ entry.promise = made.promise.then((image) => {
250
+ entry.promise = null;
251
+ // every holder unmounted while it decoded — free, don't adopt
252
+ if (entry.released) {
253
+ image?.destroy();
254
+ return null;
255
+ }
256
+ entry.image = image;
257
+ return image;
258
+ });
259
+ } else {
260
+ entry.image = made.image;
261
+ }
262
+ }
263
+ entry.refs++;
264
+ return entry;
265
+ }
266
+
267
+ /** Drop one hold; the last one out frees the entry and its server upload. */
268
+ export function releaseImageSource(app, entry) {
269
+ if (--entry.refs > 0) return;
270
+ sourceCaches.get(app)?.delete(entry.key);
271
+ entry.released = true;
272
+ entry.image?.destroy();
273
+ entry.image = null;
274
+ }
275
+
276
+ // --- server-side sources ----------------------------------------------------
277
+
278
+ /** RENDER's depth-implied standard formats — the ones a drawable can be
279
+ * composited through without knowing its visual. */
280
+ const DEPTH_FORMATS = { 8: 'a8', 24: 'rgb24', 32: 'rgba32' };
281
+
282
+ /**
283
+ * An existing server-side Picture, as a source `drawImage` accepts: showing
284
+ * it is one `RenderComposite` — no `PutImage`, no readback, no round trip.
285
+ *
286
+ * The picture is the caller's: nothing here creates or frees anything. The
287
+ * one liberty taken is temporary — drawing it *scaled* sets the picture's
288
+ * transform and filter for the composite and resets them to
289
+ * identity/nearest after, which is the same bracket ntk puts around its own
290
+ * cached uploads. A picture that must keep a transform of its own should be
291
+ * composited 1:1 (style the box to the stated size).
292
+ */
293
+ export class PictureSource {
294
+ constructor(app, { id, width, height }) {
295
+ this.app = app;
296
+ this.id = id;
297
+ this.width = width;
298
+ this.height = height;
299
+ this._handle = null;
300
+ }
301
+
302
+ /** Built on first paint, so a headless tree never touches Render. */
303
+ picture() {
304
+ return (this._handle ??= {
305
+ id: this.id,
306
+ setFilter: (name, params) =>
307
+ this.app.display.Render.SetPictureFilter(this.id, name, params),
308
+ });
309
+ }
310
+ }
311
+
312
+ /**
313
+ * An existing server-side Pixmap or Window, composited through a Picture
314
+ * created over it — created lazily at first paint, owned here, freed by
315
+ * `destroy()`. The drawable itself stays the caller's.
316
+ *
317
+ * `depth` picks the picture format and defaults to 24, the screen's default
318
+ * depth — what a window pixmap from Composite's `NameWindowPixmap` is. A
319
+ * caller with an ARGB drawable says 32; 8 composites as ink through its
320
+ * alpha, which is what previewing a mask looks like. A wrong depth is a
321
+ * server-side BadMatch, which is why the mismatch the client *can* catch
322
+ * (an unsupported number) throws in validation instead.
323
+ */
324
+ export class DrawableSource {
325
+ constructor(app, { id, width, height, depth = 24 }) {
326
+ this.app = app;
327
+ this.id = id;
328
+ this.width = width;
329
+ this.height = height;
330
+ this.depth = depth;
331
+ this._picture = null;
332
+ }
333
+
334
+ picture() {
335
+ if (!this._picture) {
336
+ const Render = this.app.display.Render;
337
+ this._picture = new Picture(this.app, {
338
+ drawable: { id: this.id },
339
+ format: Render[DEPTH_FORMATS[this.depth]],
340
+ });
341
+ }
342
+ return this._picture;
343
+ }
344
+
345
+ destroy() {
346
+ this._picture?.destroy();
347
+ this._picture = null;
348
+ }
349
+ }