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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// `useScreens()` — the monitor layout as something a component re-renders on.
|
|
2
|
+
//
|
|
3
|
+
// The store lives in `screens.js`, keyed by connection rather than shared
|
|
4
|
+
// across the process the way `appearance.js` is: the screen layout is a fact
|
|
5
|
+
// about one X display, and a process driving two of them is exactly what the
|
|
6
|
+
// test suite does routinely.
|
|
7
|
+
|
|
8
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
9
|
+
|
|
10
|
+
import { useApp } from './appcontext.js';
|
|
11
|
+
import { screensSnapshot, watchScreens } from './screens.js';
|
|
12
|
+
import { scaleOf, monitorScalesOf } from './scale.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The raw snapshot is device pixels — `screens.js` clamps CreateWindow
|
|
16
|
+
* geometry with it and must stay that way — and the hook's callers are
|
|
17
|
+
* application code, which thinks in logical pixels like every prop it
|
|
18
|
+
* writes. Divided here, per snapshot object so referential stability
|
|
19
|
+
* survives (useSyncExternalStore re-renders on identity).
|
|
20
|
+
*
|
|
21
|
+
* Each screen also carries its own `scale`: on a desktop where the ladder
|
|
22
|
+
* read the hardware (src/scale.js), a retina lid and an office monitor
|
|
23
|
+
* really do answer differently, and an app that places windows can honour
|
|
24
|
+
* that. Where the desktop configured one factor, every entry carries it.
|
|
25
|
+
*/
|
|
26
|
+
const logicalSnapshots = new WeakMap();
|
|
27
|
+
|
|
28
|
+
function logicalScreens(app, raw) {
|
|
29
|
+
if (!raw) return raw;
|
|
30
|
+
const cached = logicalSnapshots.get(raw);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const s = scaleOf(app);
|
|
33
|
+
const perMonitor = monitorScalesOf(app);
|
|
34
|
+
const rect = (r) =>
|
|
35
|
+
r == null
|
|
36
|
+
? r
|
|
37
|
+
: Object.freeze({
|
|
38
|
+
...r,
|
|
39
|
+
x: r.x / s,
|
|
40
|
+
y: r.y / s,
|
|
41
|
+
width: r.width / s,
|
|
42
|
+
height: r.height / s,
|
|
43
|
+
});
|
|
44
|
+
const out = Object.freeze({
|
|
45
|
+
...raw,
|
|
46
|
+
screens: Object.freeze(
|
|
47
|
+
raw.screens.map((screen) =>
|
|
48
|
+
Object.freeze({
|
|
49
|
+
...rect(screen),
|
|
50
|
+
available: rect(screen.available),
|
|
51
|
+
scale:
|
|
52
|
+
(screen.name && perMonitor.get(screen.name)?.scale) ??
|
|
53
|
+
perMonitor.get(screen.outputs?.[0])?.scale ??
|
|
54
|
+
s,
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
primary: null, // reattached below so it stays an identity into `screens`
|
|
59
|
+
workArea: rect(raw.workArea),
|
|
60
|
+
virtual: rect(raw.virtual),
|
|
61
|
+
});
|
|
62
|
+
const primary =
|
|
63
|
+
out.screens.find((screen) => screen.primary) ??
|
|
64
|
+
(out.screens.length === 1 ? out.screens[0] : null);
|
|
65
|
+
const finished = Object.freeze({ ...out, primary });
|
|
66
|
+
logicalSnapshots.set(raw, finished);
|
|
67
|
+
return finished;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The monitors this display has, live.
|
|
72
|
+
*
|
|
73
|
+
* ```jsx
|
|
74
|
+
* const { screens, primary } = useScreens();
|
|
75
|
+
*
|
|
76
|
+
* <Select
|
|
77
|
+
* value={monitor}
|
|
78
|
+
* onChange={setMonitor}
|
|
79
|
+
* options={screens.map((s) => ({
|
|
80
|
+
* value: s.name,
|
|
81
|
+
* label: `${s.name} — ${s.width}×${s.height}`,
|
|
82
|
+
* }))}
|
|
83
|
+
* />
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* Each entry is:
|
|
87
|
+
*
|
|
88
|
+
* | | |
|
|
89
|
+
* | --- | --- |
|
|
90
|
+
* | `name` | `'HDMI-1'`, `'eDP-1'` — **null** where the server has no RandR |
|
|
91
|
+
* | `x` `y` `width` `height` | the monitor's rect in virtual-screen coordinates |
|
|
92
|
+
* | `available` | that rect minus the panels — where a window can go |
|
|
93
|
+
* | `primary` | the desktop's main monitor, where panels and new windows land |
|
|
94
|
+
* | `widthMM` `heightMM` | physical size, or null |
|
|
95
|
+
* | `refreshRate` | Hz to two decimals (`59.99`), or null |
|
|
96
|
+
* | `rotation` | `0`, `90`, `180` or `270` |
|
|
97
|
+
* | `outputs` | every output on this monitor — two names means it is mirrored |
|
|
98
|
+
*
|
|
99
|
+
* and the object around them carries `primary` (the entry, or null), the
|
|
100
|
+
* desktop-wide `workArea`, the whole `virtual` screen, and `source`.
|
|
101
|
+
*
|
|
102
|
+
* **`name` is null before RandR answers, and on a server without it.** The
|
|
103
|
+
* geometry resolves during `createRoot()` from Xinerama, which is one round
|
|
104
|
+
* trip; the names, the primary flag and the physical sizes take a ten-round-
|
|
105
|
+
* trip RandR walk that deliberately does not hold startup up, so they appear
|
|
106
|
+
* a moment later. The rects do not move when they land — Xinerama on a
|
|
107
|
+
* modern server *is* RandR's emulation of it — so a component that rendered
|
|
108
|
+
* against the early answer sees fields fill in, not values change. Where a
|
|
109
|
+
* name is what gets persisted, treat null as "not known yet" and keep the
|
|
110
|
+
* last one, rather than writing it.
|
|
111
|
+
*
|
|
112
|
+
* `source` says which tier answered: `'randr'`, `'xinerama'`, `'screen'`
|
|
113
|
+
* (one entry covering the whole display, for a server with neither
|
|
114
|
+
* extension), `'test'`, or null on a headless mock with no display at all.
|
|
115
|
+
*
|
|
116
|
+
* **`available` is an approximation and the only one here.** `_NET_WORKAREA`
|
|
117
|
+
* is published for the whole virtual desktop rather than per monitor, so it
|
|
118
|
+
* is applied as a per-axis bound: exact on one head, and on several it still
|
|
119
|
+
* takes a top or bottom panel off the height. Deriving a true per-monitor
|
|
120
|
+
* work area means reading `_NET_WM_STRUT_PARTIAL` off every window on the
|
|
121
|
+
* screen — see the note in `screens.js`.
|
|
122
|
+
*
|
|
123
|
+
* Re-renders when a monitor is plugged in or unplugged, when the arrangement
|
|
124
|
+
* changes, and when a panel appears, moves or auto-hides.
|
|
125
|
+
*/
|
|
126
|
+
export function useScreens() {
|
|
127
|
+
const app = useApp();
|
|
128
|
+
const subscribe = useCallback(
|
|
129
|
+
(onChange) => watchScreens(app, onChange),
|
|
130
|
+
[app],
|
|
131
|
+
);
|
|
132
|
+
const snapshot = useCallback(
|
|
133
|
+
() => logicalScreens(app, screensSnapshot(app)),
|
|
134
|
+
[app],
|
|
135
|
+
);
|
|
136
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
137
|
+
}
|
package/src/startup.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Startup notification (freedesktop): tell the desktop the app has finished
|
|
2
|
+
// starting, and tell the window manager which user action started it.
|
|
3
|
+
//
|
|
4
|
+
// Two visible defects without it, for anyone who launches from a launcher
|
|
5
|
+
// rather than a terminal. The launcher opens a startup sequence when it
|
|
6
|
+
// spawns us and closes it when we say we are up; say nothing and it runs to
|
|
7
|
+
// mutter's STARTUP_TIMEOUT_MS, which is 15 seconds of busy cursor over a
|
|
8
|
+
// window the user is already clicking. And `_NET_WM_USER_TIME` is the
|
|
9
|
+
// evidence focus-stealing prevention weighs when deciding whether a new
|
|
10
|
+
// window may come forward; with none, a strict desktop opens us behind
|
|
11
|
+
// whatever the user was doing.
|
|
12
|
+
//
|
|
13
|
+
// This is core rather than an integration package: it is X11 over the
|
|
14
|
+
// connection the renderer already has, using calls already in ntk and
|
|
15
|
+
// node-x11 — no dependency, no engines floor, nothing to opt into. It sits
|
|
16
|
+
// next to `WM_DELETE_WINDOW` and `_NET_WM_PID` in kind.
|
|
17
|
+
//
|
|
18
|
+
// See docs/desktop.md. Issue #174.
|
|
19
|
+
import { eventMask } from 'x11/lib/eventmask.js';
|
|
20
|
+
|
|
21
|
+
/** mutter gives up at 15s. This is the backstop for an app that never
|
|
22
|
+
* paints at all — early enough to beat that by a margin, late enough that
|
|
23
|
+
* no honest first frame trips it. */
|
|
24
|
+
const BACKSTOP_MS = 10_000;
|
|
25
|
+
|
|
26
|
+
const BEGIN = '_NET_STARTUP_INFO_BEGIN';
|
|
27
|
+
const CONTINUE = '_NET_STARTUP_INFO';
|
|
28
|
+
/** The protocol's chunk size: format 8, 20 bytes per ClientMessage. */
|
|
29
|
+
const CHUNK = 20;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Take `DESKTOP_STARTUP_ID` out of the environment.
|
|
33
|
+
*
|
|
34
|
+
* **Deleted, not merely read.** The variable names exactly one launch, and
|
|
35
|
+
* a child process that inherits it claims a sequence that is not its own
|
|
36
|
+
* and ends it early — the parent's busy cursor stops when the child starts.
|
|
37
|
+
* Every toolkit that gets this wrong produces that same bug, which is why
|
|
38
|
+
* the read is destructive rather than tidy.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately not memoized: the deletion is the memo, and a cache here
|
|
41
|
+
* would outlive the launch it belongs to.
|
|
42
|
+
*/
|
|
43
|
+
function consumeEnv() {
|
|
44
|
+
const found = process.env?.DESKTOP_STARTUP_ID || null;
|
|
45
|
+
if (process.env) delete process.env.DESKTOP_STARTUP_ID;
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The id in force: whatever a root was given, else the environment's.
|
|
50
|
+
* `undefined` until something has looked. */
|
|
51
|
+
let currentId;
|
|
52
|
+
|
|
53
|
+
/** The live session, if any. One per process, because one launch is. */
|
|
54
|
+
let current = null;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The X server timestamp of the user action that launched this app, from
|
|
58
|
+
* the `_TIME` suffix of the startup id, or `null`.
|
|
59
|
+
*
|
|
60
|
+
* `null` is a real answer rather than a failure: an app started from a
|
|
61
|
+
* shell has no launch timestamp and never will. Callers that want to raise
|
|
62
|
+
* a window use it as the "when", and `0` is not a substitute — EWMH gives
|
|
63
|
+
* zero its own meaning ("do not focus this on map").
|
|
64
|
+
*/
|
|
65
|
+
export function launchTimestamp() {
|
|
66
|
+
if (currentId === undefined) currentId = consumeEnv();
|
|
67
|
+
return parseLaunchTime(currentId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `foo_TIME12345` → `12345`. Anything else, including a `_TIME` that is
|
|
71
|
+
* not a number, is `null`. Never throws. */
|
|
72
|
+
export function parseLaunchTime(id) {
|
|
73
|
+
if (typeof id !== 'string') return null;
|
|
74
|
+
const at = id.lastIndexOf('_TIME');
|
|
75
|
+
if (at < 0) return null;
|
|
76
|
+
const digits = id.slice(at + 5);
|
|
77
|
+
if (!/^\d+$/.test(digits)) return null;
|
|
78
|
+
const value = Number(digits);
|
|
79
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A value as the protocol's parser wants it. Always quoted, which is always
|
|
84
|
+
* legal and saves deciding; inside the quotes only `\` and `"` are escaped.
|
|
85
|
+
*
|
|
86
|
+
* Note what this is *not*: C escaping. The spec is explicit that `\n` here
|
|
87
|
+
* means the letter n, so a newline passes through as itself and must not be
|
|
88
|
+
* turned into a backslash and an n.
|
|
89
|
+
*/
|
|
90
|
+
function quote(value) {
|
|
91
|
+
return `"${String(value).replace(/[\\"]/g, (c) => `\\${c}`)}"`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `remove: ID="foo"` — a message type and its key/value pairs. */
|
|
95
|
+
export function encodeStartupMessage(type, fields) {
|
|
96
|
+
const pairs = Object.entries(fields)
|
|
97
|
+
.filter(([, v]) => v != null)
|
|
98
|
+
.map(([k, v]) => `${k}=${quote(v)}`);
|
|
99
|
+
return `${type}: ${pairs.join(' ')}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The message split into the ClientMessages that carry it: 20 bytes each,
|
|
104
|
+
* nul-terminated, zero-padded.
|
|
105
|
+
*
|
|
106
|
+
* The trailing nul is part of the message rather than padding, so a message
|
|
107
|
+
* whose bytes land on an exact multiple of 20 still needs the chunk after
|
|
108
|
+
* it — otherwise the last byte used is not a nul and a strict reader waits
|
|
109
|
+
* forever for the rest.
|
|
110
|
+
*/
|
|
111
|
+
export function messageChunks(text) {
|
|
112
|
+
const body = Buffer.from(text, 'utf8');
|
|
113
|
+
const bytes = Buffer.concat([body, Buffer.from([0])]);
|
|
114
|
+
const chunks = [];
|
|
115
|
+
for (let at = 0; at < bytes.length; at += CHUNK) {
|
|
116
|
+
const chunk = new Array(CHUNK).fill(0);
|
|
117
|
+
for (let i = 0; i < CHUNK && at + i < bytes.length; i++) {
|
|
118
|
+
chunk[i] = bytes[at + i];
|
|
119
|
+
}
|
|
120
|
+
chunks.push(chunk);
|
|
121
|
+
}
|
|
122
|
+
return chunks;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the `startupNotification` option into `{ id, completeOn }`, or
|
|
127
|
+
* `null` for opted out.
|
|
128
|
+
*
|
|
129
|
+
* `completeOn` defaults to `'paint'` — see `StartupSession.painted`.
|
|
130
|
+
*/
|
|
131
|
+
function settings(option) {
|
|
132
|
+
if (option === false) return null;
|
|
133
|
+
// Consumed either way, even when an explicit id wins: leaving it set
|
|
134
|
+
// would hand this launch's id to the next child process spawned.
|
|
135
|
+
const fromEnv = consumeEnv();
|
|
136
|
+
const given =
|
|
137
|
+
typeof option === 'string'
|
|
138
|
+
? { id: option }
|
|
139
|
+
: option && typeof option === 'object'
|
|
140
|
+
? option
|
|
141
|
+
: {};
|
|
142
|
+
return { id: given.id ?? fromEnv, completeOn: given.completeOn ?? 'paint' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class StartupSession {
|
|
146
|
+
constructor(app, { id, completeOn }) {
|
|
147
|
+
this.app = app;
|
|
148
|
+
this.id = id;
|
|
149
|
+
this.completeOn = completeOn;
|
|
150
|
+
this.time = parseLaunchTime(id);
|
|
151
|
+
this.done = false;
|
|
152
|
+
this.timer = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `_NET_STARTUP_ID` and `_NET_WM_USER_TIME` on the first toplevel, and
|
|
157
|
+
* **before it maps**: EWMH's guarantee is about the state of the window at
|
|
158
|
+
* the moment it is mapped, so setting them afterwards is too late and
|
|
159
|
+
* looks identical in a log.
|
|
160
|
+
*/
|
|
161
|
+
decorate(wnd) {
|
|
162
|
+
if (this.claimed || !this.id) return;
|
|
163
|
+
this.claimed = wnd;
|
|
164
|
+
wnd.setProperty?.('_NET_STARTUP_ID', this.id);
|
|
165
|
+
if (this.time !== null) {
|
|
166
|
+
wnd.setProperty?.('_NET_WM_USER_TIME', [this.time], {
|
|
167
|
+
type: 'CARDINAL',
|
|
168
|
+
format: 32,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Intern now rather than at the moment of completion. Both are round
|
|
172
|
+
// trips, and the whole point of this feature is that the second one
|
|
173
|
+
// happens the instant the app is up — spending it here, while the first
|
|
174
|
+
// frame is still being built, costs nothing anybody can see.
|
|
175
|
+
this._primeAtoms(wnd);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_primeAtoms(wnd) {
|
|
179
|
+
const X = wnd.X;
|
|
180
|
+
if (!X?.InternAtom) return;
|
|
181
|
+
const atom = (name) =>
|
|
182
|
+
new Promise((resolve, reject) =>
|
|
183
|
+
X.InternAtom(false, name, (err, id) =>
|
|
184
|
+
err ? reject(err) : resolve(id),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
this.atomsReady = Promise.all([atom(BEGIN), atom(CONTINUE)])
|
|
188
|
+
.then((ids) => {
|
|
189
|
+
this.atoms = ids;
|
|
190
|
+
})
|
|
191
|
+
.catch(() => {
|
|
192
|
+
// A display that cannot intern an atom has worse problems than a
|
|
193
|
+
// busy cursor, and the launcher's own timeout still covers this.
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The first toplevel is up. Arms the backstop; completes now if that is
|
|
198
|
+
* what this app asked for. */
|
|
199
|
+
mapped(wnd) {
|
|
200
|
+
if (this.done || !this.id || this.claimed !== wnd) return;
|
|
201
|
+
if (this.completeOn === 'map') return this.complete();
|
|
202
|
+
// Whichever comes first. An app that never paints — headless, a throw in
|
|
203
|
+
// the first render, a window mounted hidden — must still end the
|
|
204
|
+
// sequence, or this reproduces the bug it exists to fix.
|
|
205
|
+
this.timer ??= setTimeout(() => this.complete(), BACKSTOP_MS);
|
|
206
|
+
this.timer.unref?.();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A flush actually painted — the default moment, and the one this
|
|
211
|
+
* renderer can name where a toolkit that paints on map cannot.
|
|
212
|
+
*
|
|
213
|
+
* Not the map: `invalidate()` schedules through the frame clock and the
|
|
214
|
+
* drawing happens in `flush()` a frame later, so a mapped window is an
|
|
215
|
+
* empty one. Ending the sequence there stops the busy cursor over a blank
|
|
216
|
+
* rectangle — compliant, and a worse answer than the timeout.
|
|
217
|
+
*
|
|
218
|
+
* Not "real" content either. If the first frame is a spinner because the
|
|
219
|
+
* tree is suspended, that is exactly the right moment to stop the
|
|
220
|
+
* *system's* spinner: the app is up and is telling the user what it is
|
|
221
|
+
* doing. There is no signal for "finished loading" and guessing at one is
|
|
222
|
+
* how this ends up back at fifteen seconds.
|
|
223
|
+
*/
|
|
224
|
+
painted() {
|
|
225
|
+
if (this.completeOn === 'paint') this.complete();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Send `remove:` and stand down. Idempotent, because three things race to
|
|
230
|
+
* call it and the protocol should see exactly one.
|
|
231
|
+
*/
|
|
232
|
+
complete() {
|
|
233
|
+
if (this.done) return;
|
|
234
|
+
this.done = true;
|
|
235
|
+
if (this.timer) clearTimeout(this.timer);
|
|
236
|
+
this.timer = null;
|
|
237
|
+
if (this.app._reactX11Startup === this) this.app._reactX11Startup = null;
|
|
238
|
+
const wnd = this.claimed;
|
|
239
|
+
if (!this.id || !wnd) return;
|
|
240
|
+
const text = encodeStartupMessage('remove', { ID: this.id });
|
|
241
|
+
// Normally the atoms landed while the first frame was being built, and
|
|
242
|
+
// this goes out in the same turn as the paint that triggered it. The
|
|
243
|
+
// fallback is for completing before they arrive, which is what
|
|
244
|
+
// `completeOn: 'map'` does on a cold connection.
|
|
245
|
+
if (this.atoms) send(wnd, this.atoms, text);
|
|
246
|
+
else this.atomsReady?.then(() => this.atoms && send(wnd, this.atoms, text));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Broadcast a startup message to the root window.
|
|
252
|
+
*
|
|
253
|
+
* Two things the transport gets wrong by default. The event mask has to be
|
|
254
|
+
* `PropertyChange` — `SendClientMessage` defaults to the substructure pair
|
|
255
|
+
* EWMH wants for root messages, and this protocol is not that. And the
|
|
256
|
+
* `window` field names a window the *sender* owns, while the destination is
|
|
257
|
+
* the root, which is why both are arguments.
|
|
258
|
+
*/
|
|
259
|
+
function send(wnd, [begin, cont], text) {
|
|
260
|
+
const X = wnd.X;
|
|
261
|
+
const root = X?.display?.screen?.[0]?.root;
|
|
262
|
+
if (!X?.SendClientMessage || !root) return;
|
|
263
|
+
messageChunks(text).forEach((chunk, i) => {
|
|
264
|
+
X.SendClientMessage(
|
|
265
|
+
root,
|
|
266
|
+
wnd.id,
|
|
267
|
+
i === 0 ? begin : cont,
|
|
268
|
+
8,
|
|
269
|
+
chunk,
|
|
270
|
+
eventMask.PropertyChange,
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Begin a session for a root, or `null` when there is nothing to do — no
|
|
277
|
+
* id in the environment (a terminal, CI, XQuartz) or the app opted out.
|
|
278
|
+
* Nothing is sent and nothing is set in that case.
|
|
279
|
+
*/
|
|
280
|
+
export function beginStartup(app, option) {
|
|
281
|
+
const resolved = settings(option);
|
|
282
|
+
currentId = resolved?.id ?? null;
|
|
283
|
+
if (!resolved?.id) return null;
|
|
284
|
+
const session = new StartupSession(app, resolved);
|
|
285
|
+
app._reactX11Startup = session;
|
|
286
|
+
current = session;
|
|
287
|
+
return session;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* End the startup sequence now. Idempotent, and a no-op when there is none,
|
|
292
|
+
* so an app may call it unconditionally.
|
|
293
|
+
*
|
|
294
|
+
* This is the seam behind `completeOn: 'manual'`, for an app that knows
|
|
295
|
+
* better than "the first frame" — one whose first frame is a splash it does
|
|
296
|
+
* not want to be judged by, or which is up only once a session is restored.
|
|
297
|
+
* No arguments, because a process has one launch however many roots it
|
|
298
|
+
* builds.
|
|
299
|
+
*/
|
|
300
|
+
export function notifyStartupComplete() {
|
|
301
|
+
current?.complete();
|
|
302
|
+
}
|
package/src/style.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `react-x11/style` — the style vocabulary, for code outside the package
|
|
3
|
+
* that has to speak it: a registered element asking whether a prop name is
|
|
4
|
+
* style, resolving `$token` references against a theme, or flattening the
|
|
5
|
+
* array/object `style` shape the built-ins accept.
|
|
6
|
+
*/
|
|
7
|
+
import type {
|
|
8
|
+
Animation,
|
|
9
|
+
AnimationSpec,
|
|
10
|
+
Easing,
|
|
11
|
+
Style,
|
|
12
|
+
StyleProperties,
|
|
13
|
+
} from './types/style.js';
|
|
14
|
+
|
|
15
|
+
export type { Animation, AnimationSpec, Easing, Style, StyleProperties };
|
|
16
|
+
|
|
17
|
+
/** Freeze a stylesheet object, the `StyleSheet.create` of this renderer. */
|
|
18
|
+
export function createStyles<T extends Record<string, Style>>(sheet: T): T;
|
|
19
|
+
|
|
20
|
+
/** Collapse an array/nested `style` prop into one object. */
|
|
21
|
+
export function flattenStyle(
|
|
22
|
+
style: Style | Style[] | null | undefined,
|
|
23
|
+
): StyleProperties;
|
|
24
|
+
|
|
25
|
+
/** Is this prop name part of the style vocabulary? The question a
|
|
26
|
+
* registered element asks before treating a prop as its own semantics. */
|
|
27
|
+
export function isStyleProp(name: string): boolean;
|
|
28
|
+
export function isLayoutProp(name: string): boolean;
|
|
29
|
+
export function isPaintProp(name: string): boolean;
|
|
30
|
+
export function isAnimatableProp(name: string): boolean;
|
|
31
|
+
|
|
32
|
+
/** Overlay the `:hover` / `:focus` / `:active` / `:disabled` blocks that
|
|
33
|
+
* the given states select. */
|
|
34
|
+
export function resolveStyleStates(
|
|
35
|
+
style: StyleProperties,
|
|
36
|
+
states: Record<string, boolean>,
|
|
37
|
+
): StyleProperties;
|
|
38
|
+
export function hasStateStyles(style: StyleProperties): boolean;
|
|
39
|
+
|
|
40
|
+
/** Does this style reference any `$token`? */
|
|
41
|
+
export function styleUsesTokens(style: StyleProperties): boolean;
|
|
42
|
+
export function tokenNames(
|
|
43
|
+
style: StyleProperties,
|
|
44
|
+
out?: Set<string>,
|
|
45
|
+
): Set<string>;
|
|
46
|
+
/** Replace `$token` references with values from the theme. */
|
|
47
|
+
export function resolveTokens(
|
|
48
|
+
style: StyleProperties,
|
|
49
|
+
theme: Record<string, unknown> | null | undefined,
|
|
50
|
+
where?: string,
|
|
51
|
+
strict?: boolean,
|
|
52
|
+
): StyleProperties;
|
|
53
|
+
|
|
54
|
+
export function styleHasSizeQueries(style: StyleProperties): boolean;
|
|
55
|
+
/** Does the style carry a `'@supports …'` block? Those are re-resolved when
|
|
56
|
+
* the server's answer changes, not when the window is laid out. */
|
|
57
|
+
export function styleHasSupportsQueries(style: StyleProperties): boolean;
|
|
58
|
+
/** `resolveQueries` with only the size half — the shape this had before
|
|
59
|
+
* capability blocks existed. */
|
|
60
|
+
export function resolveSizeQueries(
|
|
61
|
+
style: StyleProperties,
|
|
62
|
+
size: { width: number; height: number },
|
|
63
|
+
): StyleProperties;
|
|
64
|
+
/** Merge every matching `@` block — size and capability alike — in
|
|
65
|
+
* declaration order. `supports` maps feature name to whether the window can
|
|
66
|
+
* actually do it; a missing map matches nothing, which is the safe way
|
|
67
|
+
* round. */
|
|
68
|
+
export function resolveQueries(
|
|
69
|
+
style: StyleProperties,
|
|
70
|
+
context?: {
|
|
71
|
+
size?: { width: number; height: number } | null;
|
|
72
|
+
supports?: Record<string, boolean> | null;
|
|
73
|
+
},
|
|
74
|
+
): StyleProperties;
|
|
75
|
+
|
|
76
|
+
export function interpolate(from: unknown, to: unknown, t: number): unknown;
|
|
77
|
+
/**
|
|
78
|
+
* A colour at a given opacity — the way to build a status *panel* out of a
|
|
79
|
+
* status colour: `tint(theme.danger, 0.12)` is a wash of it that the ink on
|
|
80
|
+
* top keeps its own contrast against, where an opaque tint would have to
|
|
81
|
+
* have been chosen against an ink it does not own.
|
|
82
|
+
*/
|
|
83
|
+
export function tint(color: string, alpha: number): string;
|
|
84
|
+
/**
|
|
85
|
+
* Which of `inks` can be read on `fill` — the highest WCAG contrast ratio.
|
|
86
|
+
* The palette derives `accentText` and the status inks with this; an app
|
|
87
|
+
* choosing ink for a fill of its own can use the same rule.
|
|
88
|
+
*/
|
|
89
|
+
export function readableInk(fill: string, inks: string[]): string;
|
|
90
|
+
export function transitionFor(
|
|
91
|
+
style: StyleProperties,
|
|
92
|
+
prop: string,
|
|
93
|
+
): { duration: number; delay?: number } | null;
|
|
94
|
+
export function ease(t: number): number;
|
|
95
|
+
|
|
96
|
+
/** A resolved loop declaration: `from` filled in from the style, both ends
|
|
97
|
+
* checked for a midpoint, the easing looked up. */
|
|
98
|
+
export interface ResolvedAnimation extends Required<
|
|
99
|
+
Omit<AnimationSpec, 'easing' | 'alternate'>
|
|
100
|
+
> {
|
|
101
|
+
prop: keyof StyleProperties;
|
|
102
|
+
easing: Easing;
|
|
103
|
+
alternate: boolean;
|
|
104
|
+
ease(t: number): number;
|
|
105
|
+
}
|
|
106
|
+
/** The loops a style declares, or null. Throws on a declaration that could
|
|
107
|
+
* never run — see `style.animation` in docs/styling.md. */
|
|
108
|
+
export function animationsOf(
|
|
109
|
+
style: StyleProperties,
|
|
110
|
+
where?: string,
|
|
111
|
+
): ResolvedAnimation[] | null;
|
|
112
|
+
/** Whether two resolved loops describe the same motion — what decides
|
|
113
|
+
* between keeping a running loop's phase and starting it over. */
|
|
114
|
+
export function sameAnimation(
|
|
115
|
+
a: ResolvedAnimation,
|
|
116
|
+
b: ResolvedAnimation,
|
|
117
|
+
): boolean;
|
|
118
|
+
/** Where a loop is `elapsed` ms after it started. */
|
|
119
|
+
export function animationValueAt(
|
|
120
|
+
spec: ResolvedAnimation,
|
|
121
|
+
elapsed: number,
|
|
122
|
+
): unknown;
|
|
123
|
+
export const EASING_NAMES: readonly Easing[];
|
|
124
|
+
|
|
125
|
+
export const EMPTY_STYLE: Readonly<StyleProperties>;
|
|
126
|
+
export const STATE_KEYS: readonly string[];
|
package/src/style.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `react-x11/style` — the style vocabulary, for code that has to speak it
|
|
2
|
+
// from outside the package: a registered element deciding whether a prop
|
|
3
|
+
// name is style (`isStyleProp`), resolving `$token` references against a
|
|
4
|
+
// theme, or flattening the same array/object `style` prop shape the
|
|
5
|
+
// built-ins take.
|
|
6
|
+
export {
|
|
7
|
+
createStyles,
|
|
8
|
+
flattenStyle,
|
|
9
|
+
isStyleProp,
|
|
10
|
+
isLayoutProp,
|
|
11
|
+
isPaintProp,
|
|
12
|
+
isAnimatableProp,
|
|
13
|
+
resolveStyleStates,
|
|
14
|
+
hasStateStyles,
|
|
15
|
+
styleUsesTokens,
|
|
16
|
+
tokenNames,
|
|
17
|
+
resolveTokens,
|
|
18
|
+
styleHasSizeQueries,
|
|
19
|
+
styleHasSupportsQueries,
|
|
20
|
+
resolveSizeQueries,
|
|
21
|
+
resolveQueries,
|
|
22
|
+
interpolate,
|
|
23
|
+
tint,
|
|
24
|
+
readableInk,
|
|
25
|
+
transitionFor,
|
|
26
|
+
animationsOf,
|
|
27
|
+
sameAnimation,
|
|
28
|
+
animationValueAt,
|
|
29
|
+
ease,
|
|
30
|
+
EASING_NAMES,
|
|
31
|
+
EMPTY_STYLE,
|
|
32
|
+
STATE_KEYS,
|
|
33
|
+
} from './styles.js';
|