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/idlehooks.js
ADDED
|
@@ -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
|
+
}
|