react-x11 2.15.2 → 2.16.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 +37 -0
- package/package.json +4 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/application.js +25 -1
- package/src/capabilities.js +349 -0
- package/src/cocoa/app.js +28 -9
- package/src/cocoa/context2d.js +139 -6
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +160 -0
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/globalmenu.js +3 -205
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +98 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +11 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +65 -2
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +752 -0
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +193 -29
- package/src/types/capabilities.d.ts +139 -0
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/launcher.d.ts +50 -4
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +64 -6
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// What this desktop can actually do — feature discovery for the things an app
|
|
2
|
+
// does *outside* its own windows.
|
|
3
|
+
//
|
|
4
|
+
// ## Why this is not `useSupports()`
|
|
5
|
+
//
|
|
6
|
+
// `useSupports()` answers questions about the **display**: is there a
|
|
7
|
+
// compositor, is there a 32-bit visual, did this connection get the direct GL
|
|
8
|
+
// backend. All of them are local, synchronous, and knowable before the first
|
|
9
|
+
// frame.
|
|
10
|
+
//
|
|
11
|
+
// Everything on this page is the opposite on all three counts. Whether there
|
|
12
|
+
// is a notification daemon, a tray host or a launcher listening is a fact
|
|
13
|
+
// about **another process on a bus**, it takes a round trip to learn, and it
|
|
14
|
+
// changes while the app runs — a panel restarts, an extension is toggled, a
|
|
15
|
+
// user logs into a different session type. So the shape has to be render
|
|
16
|
+
// state that settles and then follows, not a boolean that is right on the
|
|
17
|
+
// first frame.
|
|
18
|
+
//
|
|
19
|
+
// ## Why a boolean is not enough
|
|
20
|
+
//
|
|
21
|
+
// "Does this desktop have notifications" is the wrong question, because the
|
|
22
|
+
// answer is yes on machines that mean four different things by it:
|
|
23
|
+
//
|
|
24
|
+
// - a freedesktop daemon with `actions` — a banner with buttons that call
|
|
25
|
+
// back into the app, updated in place, reporting what the user did;
|
|
26
|
+
// - a freedesktop daemon **without** `actions` — GNOME's own for years,
|
|
27
|
+
// and several minimal ones — where the same call shows a banner and the
|
|
28
|
+
// buttons silently never appear;
|
|
29
|
+
// - macOS's notification centre — actions and callbacks, but only for a
|
|
30
|
+
// signed bundle, and a different vocabulary underneath;
|
|
31
|
+
// - `notify-send` or `osascript` — one-way text with an urgency and an
|
|
32
|
+
// icon, no update, no close, no events, ever.
|
|
33
|
+
//
|
|
34
|
+
// An app that wants "reply from the notification" has to know which of those
|
|
35
|
+
// it has, and the honest unit is therefore a **feature set**, not a flag. The
|
|
36
|
+
// freedesktop daemons already publish exactly this through `GetCapabilities`;
|
|
37
|
+
// this module's job is to translate every backend's answer into one portable
|
|
38
|
+
// vocabulary so an app branches on the feature and never on the platform.
|
|
39
|
+
//
|
|
40
|
+
// ## The rule for adding one
|
|
41
|
+
//
|
|
42
|
+
// A capability name is a *thing an app wants to do*, and its features are the
|
|
43
|
+
// parts of it that a backend can honestly lack. If a feature is missing
|
|
44
|
+
// everywhere but one backend it is still a feature — `badgeText` is macOS's
|
|
45
|
+
// alone and is listed — but if a "feature" is really a different way of doing
|
|
46
|
+
// the same thing, it belongs in `backend` instead. Backends are named after
|
|
47
|
+
// the mechanism (`statusnotifier`, `cocoa`, `notify-send`), never after the
|
|
48
|
+
// platform, so that a second Linux mechanism does not need a second name for
|
|
49
|
+
// Linux.
|
|
50
|
+
|
|
51
|
+
import { sessionBus } from './bus.js';
|
|
52
|
+
import { currentRegistration, currentRegistrationRole } from './application.js';
|
|
53
|
+
import {
|
|
54
|
+
notificationBackend,
|
|
55
|
+
NOTIFICATIONS_NAME,
|
|
56
|
+
NOTIFICATIONS_PATH,
|
|
57
|
+
} from './notifications.js';
|
|
58
|
+
import { WATCHER_NAME } from './statusnotifier.js';
|
|
59
|
+
import { liveApps } from './trace-registry.js';
|
|
60
|
+
|
|
61
|
+
/** Every capability this module can answer for. */
|
|
62
|
+
export const CAPABILITIES = ['notifications', 'tray', 'launcher'];
|
|
63
|
+
|
|
64
|
+
/** The shape a probe resolves to when there is no mechanism at all. */
|
|
65
|
+
const NONE = Object.freeze({ available: false, backend: null, features: {} });
|
|
66
|
+
|
|
67
|
+
const frozen = (backend, features) =>
|
|
68
|
+
Object.freeze({
|
|
69
|
+
available: true,
|
|
70
|
+
backend,
|
|
71
|
+
features: Object.freeze(features),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** The app to ask when the caller did not say. Mirrors `launcher.js`. */
|
|
75
|
+
function soleApp() {
|
|
76
|
+
const apps = liveApps();
|
|
77
|
+
if (apps.length <= 1) return apps[0] ?? null;
|
|
78
|
+
const showing = apps.filter((app) => (app._rootChildren ?? []).length > 0);
|
|
79
|
+
return showing.length === 1 ? showing[0] : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// notifications
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The freedesktop daemon's own `GetCapabilities`, translated.
|
|
88
|
+
*
|
|
89
|
+
* The daemon publishes a flat list of strings; the names below are the ones
|
|
90
|
+
* that change what an app may do. Anything not in the list is absent, which
|
|
91
|
+
* is why every field is read with `includes` rather than defaulted true —
|
|
92
|
+
* a daemon that lists nothing supports nothing but a banner.
|
|
93
|
+
*/
|
|
94
|
+
function fromDaemonCaps(caps) {
|
|
95
|
+
const has = (name) => caps.includes(name);
|
|
96
|
+
return {
|
|
97
|
+
// The two that decide whether a notification is a conversation or a sign.
|
|
98
|
+
actions: has('actions'),
|
|
99
|
+
events: has('actions'),
|
|
100
|
+
// Every fd.o daemon can replace and close by id; neither is advertised
|
|
101
|
+
// as a capability because the protocol requires both.
|
|
102
|
+
update: true,
|
|
103
|
+
close: true,
|
|
104
|
+
body: has('body'),
|
|
105
|
+
bodyMarkup: has('body-markup'),
|
|
106
|
+
bodyImage: has('body-images'),
|
|
107
|
+
icon: has('icon-static') || has('icon-multi'),
|
|
108
|
+
sound: has('sound'),
|
|
109
|
+
// The banner survives in a tray/centre rather than expiring unseen.
|
|
110
|
+
persistence: has('persistence'),
|
|
111
|
+
urgency: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function probeNotifications({ app } = {}) {
|
|
116
|
+
const backend = await notificationBackend({ app });
|
|
117
|
+
if (!backend) return NONE;
|
|
118
|
+
|
|
119
|
+
if (backend === 'dbus') {
|
|
120
|
+
const ref = await sessionBus();
|
|
121
|
+
if (!ref) return NONE;
|
|
122
|
+
try {
|
|
123
|
+
const iface = await ref.bus.getInterface(
|
|
124
|
+
NOTIFICATIONS_NAME,
|
|
125
|
+
NOTIFICATIONS_PATH,
|
|
126
|
+
NOTIFICATIONS_NAME,
|
|
127
|
+
);
|
|
128
|
+
const caps = await new Promise((resolve) => {
|
|
129
|
+
iface.GetCapabilities((err, list) => resolve(err ? [] : (list ?? [])));
|
|
130
|
+
});
|
|
131
|
+
return frozen('dbus', fromDaemonCaps(caps));
|
|
132
|
+
} catch {
|
|
133
|
+
// The name is there but the object will not answer. A banner will
|
|
134
|
+
// probably still post; nothing richer should be promised.
|
|
135
|
+
return frozen('dbus', fromDaemonCaps([]));
|
|
136
|
+
} finally {
|
|
137
|
+
await ref.release();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (backend === 'cocoa') {
|
|
142
|
+
return frozen('cocoa', {
|
|
143
|
+
actions: true,
|
|
144
|
+
events: true,
|
|
145
|
+
update: true,
|
|
146
|
+
close: true,
|
|
147
|
+
body: true,
|
|
148
|
+
bodyMarkup: false, // the centre renders plain text
|
|
149
|
+
bodyImage: true,
|
|
150
|
+
icon: true,
|
|
151
|
+
sound: true,
|
|
152
|
+
persistence: true,
|
|
153
|
+
urgency: true, // mapped onto interruption levels
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// `osascript` and `notify-send`: one way, and that is the whole of it.
|
|
158
|
+
// `notify-send -p` prints an id on newer libnotify, which is why `update`
|
|
159
|
+
// is not flatly false there — see notifications.js.
|
|
160
|
+
return frozen(backend, {
|
|
161
|
+
actions: false,
|
|
162
|
+
events: false,
|
|
163
|
+
update: backend === 'notify-send',
|
|
164
|
+
close: false,
|
|
165
|
+
body: true,
|
|
166
|
+
bodyMarkup: false,
|
|
167
|
+
bodyImage: false,
|
|
168
|
+
icon: true,
|
|
169
|
+
sound: false,
|
|
170
|
+
persistence: false,
|
|
171
|
+
urgency: backend === 'notify-send',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// tray
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/** The tray on an app that has one of its own, which needs nothing asked. */
|
|
180
|
+
function trayNow(target) {
|
|
181
|
+
if (typeof target?.createStatusItem === 'function') {
|
|
182
|
+
return frozen('cocoa', {
|
|
183
|
+
menu: true,
|
|
184
|
+
iconName: true, // SF Symbols
|
|
185
|
+
iconBytes: true,
|
|
186
|
+
attention: false, // no NeedsAttention equivalent on a status item
|
|
187
|
+
overlay: false,
|
|
188
|
+
tooltip: true,
|
|
189
|
+
title: true,
|
|
190
|
+
click: true,
|
|
191
|
+
clickPosition: true,
|
|
192
|
+
clickRect: true,
|
|
193
|
+
clickModifiers: true,
|
|
194
|
+
scroll: false,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function probeTray({ app } = {}) {
|
|
201
|
+
const now = trayNow(app ?? soleApp());
|
|
202
|
+
if (now) return now;
|
|
203
|
+
|
|
204
|
+
const ref = await sessionBus();
|
|
205
|
+
if (!ref) return NONE;
|
|
206
|
+
try {
|
|
207
|
+
// A **live owner**, not an activatable name — see `statusnotifier.js`.
|
|
208
|
+
if (!(await ref.bus.nameHasOwner(WATCHER_NAME))) return NONE;
|
|
209
|
+
} catch {
|
|
210
|
+
return NONE;
|
|
211
|
+
} finally {
|
|
212
|
+
await ref.release();
|
|
213
|
+
}
|
|
214
|
+
return frozen('statusnotifier', {
|
|
215
|
+
menu: true,
|
|
216
|
+
iconName: true, // themed icon names, which is the good path here
|
|
217
|
+
iconBytes: true, // ARGB pixmaps
|
|
218
|
+
attention: true,
|
|
219
|
+
overlay: true,
|
|
220
|
+
tooltip: true,
|
|
221
|
+
title: true,
|
|
222
|
+
click: true,
|
|
223
|
+
clickPosition: true,
|
|
224
|
+
// The protocol carries none of these — see `statusnotifier.js`.
|
|
225
|
+
clickRect: false,
|
|
226
|
+
clickModifiers: false,
|
|
227
|
+
scroll: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// launcher
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
/** The Dock tile on an app that has one, which needs nothing asked. */
|
|
236
|
+
function launcherNow(target) {
|
|
237
|
+
if (typeof target?.setDockBadge === 'function') {
|
|
238
|
+
return frozen('cocoa', {
|
|
239
|
+
badge: true,
|
|
240
|
+
badgeText: true, // the tile takes any label
|
|
241
|
+
progress: false, // NSDockTile has no progress bar
|
|
242
|
+
urgent: true, // requestUserAttention, via window states
|
|
243
|
+
menu: typeof target.setDockMenu === 'function',
|
|
244
|
+
// The Dock always shows the app; nothing has to be installed for it.
|
|
245
|
+
needsDesktopFile: false,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function probeLauncher({ app } = {}) {
|
|
252
|
+
const now = launcherNow(app ?? soleApp());
|
|
253
|
+
if (now) return now;
|
|
254
|
+
|
|
255
|
+
// The Linux rung needs two things that are not the bus: an app id to
|
|
256
|
+
// attribute the entry to, and a `.desktop` file of that name for the
|
|
257
|
+
// launcher to hang it on. The first is knowable here; the second is not
|
|
258
|
+
// (it is a file on a path the launcher chooses), which is why it is
|
|
259
|
+
// reported as a *requirement* rather than as availability.
|
|
260
|
+
const ref = await sessionBus();
|
|
261
|
+
if (!ref) return NONE;
|
|
262
|
+
await ref.release();
|
|
263
|
+
if (!currentRegistration()?.appId) {
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
available: false,
|
|
266
|
+
backend: null,
|
|
267
|
+
features: {},
|
|
268
|
+
// Two different "no"s, and only one is a mistake. A **secondary**
|
|
269
|
+
// instance called `registerApplication()` and lost the race for the
|
|
270
|
+
// name — the first copy owns the badge and the quicklist, which is the
|
|
271
|
+
// whole point of single-instance — so telling its author to call a
|
|
272
|
+
// function they already called sends them after a bug that is not
|
|
273
|
+
// there. The launcher is genuinely unavailable *to this process*
|
|
274
|
+
// either way; only the advice differs.
|
|
275
|
+
reason:
|
|
276
|
+
currentRegistrationRole() === 'secondary' ? 'not-primary' : 'no-app-id',
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return frozen('launcherentry', {
|
|
280
|
+
badge: true,
|
|
281
|
+
badgeText: false, // the protocol carries a count and nothing else
|
|
282
|
+
progress: true,
|
|
283
|
+
urgent: true,
|
|
284
|
+
menu: true, // the quicklist
|
|
285
|
+
needsDesktopFile: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
const PROBES = {
|
|
292
|
+
notifications: probeNotifications,
|
|
293
|
+
tray: probeTray,
|
|
294
|
+
launcher: probeLauncher,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* What this desktop can do for one feature, as a promise.
|
|
299
|
+
*
|
|
300
|
+
* ```js
|
|
301
|
+
* const n = await desktopCapability('notifications');
|
|
302
|
+
* if (n.features.actions) postWithReplyButton();
|
|
303
|
+
* else postPlainBanner();
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* Resolves to `{ available, backend, features }`. `available` false means
|
|
307
|
+
* there is no mechanism at all and `features` is empty; otherwise `backend`
|
|
308
|
+
* names the mechanism — `'dbus'`, `'cocoa'`, `'statusnotifier'`,
|
|
309
|
+
* `'launcherentry'`, `'notify-send'`, `'osascript'` — and `features` is the
|
|
310
|
+
* portable vocabulary for this capability.
|
|
311
|
+
*
|
|
312
|
+
* **Never cached.** A panel restarting, an extension being enabled or a
|
|
313
|
+
* daemon being installed all change the answer, and a cached "no" would
|
|
314
|
+
* outlive every one of them. {@link useDesktopCapability} re-probes on the same
|
|
315
|
+
* events that would change it.
|
|
316
|
+
*/
|
|
317
|
+
export async function desktopCapability(name, options = {}) {
|
|
318
|
+
const probe = PROBES[name];
|
|
319
|
+
if (!probe) {
|
|
320
|
+
throw new TypeError(
|
|
321
|
+
`react-x11: desktopCapability(${JSON.stringify(name)}) — no such ` +
|
|
322
|
+
`capability. Expected ${CAPABILITIES.join(', ')}.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return await probe(options);
|
|
327
|
+
} catch {
|
|
328
|
+
// A probe that throws is a desktop that could not be asked, which is the
|
|
329
|
+
// same outcome for a caller as one that answered no.
|
|
330
|
+
return NONE;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The "nothing here" answer, exported so a caller can compare against it and
|
|
335
|
+
* so the hook has something stable to return on the first frame. */
|
|
336
|
+
export const NO_CAPABILITY = NONE;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The answer for a capability where it is known without asking anything —
|
|
340
|
+
* the Cocoa app's own tray and Dock tile — or null where finding out takes a
|
|
341
|
+
* round trip. What lets `useDesktopCapability` be settled on its first frame
|
|
342
|
+
* where the answer never needed waiting for.
|
|
343
|
+
*/
|
|
344
|
+
export function capabilityNow(name, { app } = {}) {
|
|
345
|
+
const target = app ?? soleApp();
|
|
346
|
+
if (name === 'tray') return trayNow(target);
|
|
347
|
+
if (name === 'launcher') return launcherNow(target);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
package/src/cocoa/app.js
CHANGED
|
@@ -38,7 +38,9 @@ import { CocoaPaneWindow } from './panewindow.js';
|
|
|
38
38
|
import { CocoaColorSampler } from './screencolor.js';
|
|
39
39
|
import { CocoaFilePanels } from './filepanels.js';
|
|
40
40
|
import { CocoaFontManager } from './fonts.js';
|
|
41
|
+
import { releaseImageUpload } from './context2d.js';
|
|
41
42
|
import { CocoaSurface } from './surface.js';
|
|
43
|
+
import { CocoaSymbols } from './symbols.js';
|
|
42
44
|
import { CocoaWindow } from './window.js';
|
|
43
45
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
44
46
|
import { loadNative } from './native.js';
|
|
@@ -199,6 +201,8 @@ export class CocoaApp {
|
|
|
199
201
|
// real bridge on the machine — the manager's default loads it only when
|
|
200
202
|
// it is built standalone
|
|
201
203
|
this.fonts = new CocoaFontManager(native);
|
|
204
|
+
// `<image src={{ symbol }}>`'s names are SF Symbols here (src/symbols.js)
|
|
205
|
+
this.symbols = new CocoaSymbols(native);
|
|
202
206
|
|
|
203
207
|
// Native open/save panels (src/cocoa/filepanels.js). Present exactly
|
|
204
208
|
// when the bridge has them (>= 0.5), and its presence is what puts the
|
|
@@ -449,6 +453,18 @@ export class CocoaApp {
|
|
|
449
453
|
return new CocoaSurface(this, options);
|
|
450
454
|
}
|
|
451
455
|
|
|
456
|
+
/**
|
|
457
|
+
* The release seam for an `Image`'s upload (`freeImage`, src/imagesource.js).
|
|
458
|
+
* `ctx.drawImage(image)` here composites from a CG bitmap made for the
|
|
459
|
+
* Image on its first draw, which ntk's `Image.destroy()` — written for X,
|
|
460
|
+
* where the upload is a pixmap it tracks itself — cannot see; an owner
|
|
461
|
+
* letting go of an Image calls this too, and the bitmap is freed on the
|
|
462
|
+
* call. An Image nobody releases takes its bitmap with it when collected.
|
|
463
|
+
*/
|
|
464
|
+
releaseImage(image) {
|
|
465
|
+
releaseImageUpload(image);
|
|
466
|
+
}
|
|
467
|
+
|
|
452
468
|
/**
|
|
453
469
|
* The `useGlobalMenu` transport seam: same owner shape as the D-Bus
|
|
454
470
|
* GlobalMenuExport (start/stop/update), pointed at the macOS menu bar.
|
|
@@ -628,6 +644,8 @@ export class CocoaApp {
|
|
|
628
644
|
_unregisterWindow(wnd) {
|
|
629
645
|
this._windows.delete(wnd._key);
|
|
630
646
|
if (this._grabWindow === wnd) this._grabWindow = null;
|
|
647
|
+
// a popup that took the keyboard and closed is not where keys go next
|
|
648
|
+
if (this._lastKeyWindow === wnd) this._lastKeyWindow = null;
|
|
631
649
|
}
|
|
632
650
|
|
|
633
651
|
// --- the pump ------------------------------------------------------------
|
|
@@ -789,8 +807,8 @@ export class CocoaApp {
|
|
|
789
807
|
this._rafQueue = [];
|
|
790
808
|
let soonest = Infinity;
|
|
791
809
|
for (const entry of queue) {
|
|
792
|
-
// A window whose last flip has not
|
|
793
|
-
//
|
|
810
|
+
// A window whose last flip has not reached its layer yet (threaded
|
|
811
|
+
// mode's fence, `CocoaWindow.frameInFlight`) waits before
|
|
794
812
|
// its clock is asked, or the clock would count a frame that did not
|
|
795
813
|
// run. The release is an event, and the batch it arrives in ticks
|
|
796
814
|
// again.
|
|
@@ -861,7 +879,7 @@ export class CocoaApp {
|
|
|
861
879
|
* paid once, when the batch is done: ten moves and a click that crossed
|
|
862
880
|
* while this thread was busy are one frame, not eleven. Then what a pump
|
|
863
881
|
* tick does: the frames that are due, and the ones a window's visibility
|
|
864
|
-
* or its
|
|
882
|
+
* or its last flip was holding, which an occlusion change or a
|
|
865
883
|
* `surface-released` in this very batch may just have freed.
|
|
866
884
|
*/
|
|
867
885
|
_routeBatch(batch) {
|
|
@@ -1077,9 +1095,9 @@ export class CocoaApp {
|
|
|
1077
1095
|
// the notch and the scroll, most of a refresh period of nothing.
|
|
1078
1096
|
//
|
|
1079
1097
|
// Mostly: a trackpad's momentum lands two in one tick often, and a
|
|
1080
|
-
// second flip inside the refresh
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1098
|
+
// second flip inside the refresh is a frame the display never shows
|
|
1099
|
+
// (`CocoaWindow._flippedRecently` says what it cost). So the rest of a
|
|
1100
|
+
// burst lands React's half and leaves the
|
|
1083
1101
|
// paint to the paced frame the scroll already asked for — the model
|
|
1084
1102
|
// has scrolled, and the next refresh shows all of it.
|
|
1085
1103
|
if (wnd._flippedRecently()) {
|
|
@@ -1188,9 +1206,10 @@ export class CocoaApp {
|
|
|
1188
1206
|
/**
|
|
1189
1207
|
* `surface-released`: a worker's frame took an IOSurface off a layer and
|
|
1190
1208
|
* the frame that replaced it has committed (windowkit/appkit#52). The
|
|
1191
|
-
* window whose
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1209
|
+
* window whose buffer that was stops waiting on it, and may draw into it
|
|
1210
|
+
* again once the WindowServer lets go of it too
|
|
1211
|
+
* (`CocoaWindow._surfaceReleased`); the tick at the end of this batch
|
|
1212
|
+
* runs the frame it was holding.
|
|
1194
1213
|
*/
|
|
1195
1214
|
_routeSurfaceReleased(ev) {
|
|
1196
1215
|
for (const wnd of this._windows.values()) {
|
package/src/cocoa/context2d.js
CHANGED
|
@@ -33,6 +33,100 @@ function parseColor(value) {
|
|
|
33
33
|
return parsed;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Bytes as a Buffer over the same memory — for a view, its own window of
|
|
37
|
+
* the ArrayBuffer, not the whole buffer from offset 0. */
|
|
38
|
+
function toBuffer(data) {
|
|
39
|
+
if (Buffer.isBuffer(data)) return data;
|
|
40
|
+
if (ArrayBuffer.isView(data)) {
|
|
41
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
42
|
+
}
|
|
43
|
+
return Buffer.from(data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- ntk Images as drawImage sources -----------------------------------------
|
|
47
|
+
//
|
|
48
|
+
// An ntk `Image` is straight RGBA in JS memory. On X, ntk uploads it to a
|
|
49
|
+
// pixmap per connection and caches that on the Image; here the upload is a
|
|
50
|
+
// CG bitmap made on the first draw and kept in this map, then composited
|
|
51
|
+
// through `ctxDrawSurface` like any surface, scaling and cropping included.
|
|
52
|
+
// The bridge's `ctxPutImageData` does the conversion — it premultiplies the
|
|
53
|
+
// straight bytes into the bitmap's BGRA (ByteOrder32Host + AlphaFirst) —
|
|
54
|
+
// so the bytes go over as the Image holds them.
|
|
55
|
+
//
|
|
56
|
+
// Images are immutable content (ntk's contract, and `<image>`'s), so an
|
|
57
|
+
// entry is never refreshed. The map is keyed weakly: an Image that is
|
|
58
|
+
// dropped takes its entry along and the handle's finalizer frees the
|
|
59
|
+
// bitmap; an owner that is done with one frees it on the call instead,
|
|
60
|
+
// through `releaseImageUpload` (the app's `releaseImage` seam).
|
|
61
|
+
|
|
62
|
+
/** Image -> { native, handle } */
|
|
63
|
+
const imageUploads = new WeakMap();
|
|
64
|
+
|
|
65
|
+
/** An ntk `Image`: the duck type `isDirectImageSource` and ntk's own
|
|
66
|
+
* `drawImage` accept, plus the pixels this backend reads in place of the
|
|
67
|
+
* picture. A bare `{ width, height, data }` is not one — `ImageData` is
|
|
68
|
+
* written between draws, and caching it by identity would show stale
|
|
69
|
+
* pixels. */
|
|
70
|
+
function isImagePixels(image) {
|
|
71
|
+
const { width, height, data } = image;
|
|
72
|
+
return (
|
|
73
|
+
typeof image.picture === 'function' &&
|
|
74
|
+
Number.isInteger(width) &&
|
|
75
|
+
Number.isInteger(height) &&
|
|
76
|
+
width > 0 &&
|
|
77
|
+
height > 0 &&
|
|
78
|
+
data?.length === width * height * 4
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uploadImage(native, image) {
|
|
83
|
+
const held = imageUploads.get(image);
|
|
84
|
+
if (held?.native === native) return held.handle;
|
|
85
|
+
const { width, height } = image;
|
|
86
|
+
const handle = native.createSurface(width, height, 1);
|
|
87
|
+
native.ctxPutImageData(handle, toBuffer(image.data), width, height, 0, 0);
|
|
88
|
+
imageUploads.set(image, { native, handle });
|
|
89
|
+
return handle;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Free an Image's bitmap now, if it has one; drawing it again uploads it
|
|
93
|
+
* again, as ntk's `destroy()` promises for its own copies. */
|
|
94
|
+
export function releaseImageUpload(image) {
|
|
95
|
+
const held = image != null && imageUploads.get(image);
|
|
96
|
+
if (!held) return;
|
|
97
|
+
imageUploads.delete(image);
|
|
98
|
+
if (typeof held.native.releaseSurface === 'function') {
|
|
99
|
+
held.native.releaseSurface(held.handle);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const warnedSources = new Set();
|
|
104
|
+
|
|
105
|
+
/** A source this backend has no pixels for, said once per kind in
|
|
106
|
+
* development — the alternative is an empty box and no reason. */
|
|
107
|
+
function warnUndrawable(image) {
|
|
108
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
109
|
+
const kind =
|
|
110
|
+
typeof image === 'object'
|
|
111
|
+
? (image.constructor?.name ?? 'object')
|
|
112
|
+
: typeof image;
|
|
113
|
+
if (warnedSources.has(kind)) return;
|
|
114
|
+
warnedSources.add(kind);
|
|
115
|
+
const serverSide = typeof image === 'object' && 'id' in image;
|
|
116
|
+
const article = /^[aeiou]/i.test(kind) ? 'an' : 'a';
|
|
117
|
+
console.warn(
|
|
118
|
+
`react-x11: drawImage on the cocoa backend has no pixels for ${article} ${kind}, ` +
|
|
119
|
+
'and draws nothing. ' +
|
|
120
|
+
(serverSide
|
|
121
|
+
? 'It names an X server-side Picture or Drawable — <image picture>, ' +
|
|
122
|
+
'<image drawable>, an ntk Picture — and this backend has no X ' +
|
|
123
|
+
'server to composite from, so those are X11-only. Hand <image src> ' +
|
|
124
|
+
'the pixels instead: encoded PNG/JPEG bytes, raw RGBA, or an ntk Image.'
|
|
125
|
+
: 'This backend draws a Surface (react-x11/ntk) or an ntk Image; wrap ' +
|
|
126
|
+
'raw RGBA as new Image({ width, height, data }).'),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
36
130
|
class LinearGradient {
|
|
37
131
|
constructor(x0, y0, x1, y1) {
|
|
38
132
|
this._coords = [x0, y0, x1, y1];
|
|
@@ -952,9 +1046,32 @@ export class CocoaContext2D {
|
|
|
952
1046
|
this._dirty();
|
|
953
1047
|
}
|
|
954
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* An SF Symbol by name, fitted into the rect and centred, in the fill
|
|
1051
|
+
* colour — `ctxDrawSymbol`, `@windowkit/appkit` 0.12.0. Answers false,
|
|
1052
|
+
* drawing nothing, for a name the system does not know and on a bridge
|
|
1053
|
+
* without the verb. `options` are the bridge's: `pointSize`, `weight`,
|
|
1054
|
+
* `scale`, `variableValue`.
|
|
1055
|
+
*/
|
|
1056
|
+
drawSymbol(name, x, y, width, height, options) {
|
|
1057
|
+
if (typeof this._native.ctxDrawSymbol !== 'function') return false;
|
|
1058
|
+
this._applyFill();
|
|
1059
|
+
const drawn = this._native.ctxDrawSymbol(
|
|
1060
|
+
this._s(),
|
|
1061
|
+
name,
|
|
1062
|
+
x,
|
|
1063
|
+
y,
|
|
1064
|
+
width,
|
|
1065
|
+
height,
|
|
1066
|
+
options,
|
|
1067
|
+
);
|
|
1068
|
+
if (drawn) this._dirty();
|
|
1069
|
+
return drawn === true;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
955
1072
|
drawImage(image, ...args) {
|
|
956
|
-
const src = image
|
|
957
|
-
if (!src) return;
|
|
1073
|
+
const src = this._sourceHandle(image);
|
|
1074
|
+
if (!src) return;
|
|
958
1075
|
const size = this._native.surfaceSize(src);
|
|
959
1076
|
let sx = 0;
|
|
960
1077
|
let sy = 0;
|
|
@@ -990,6 +1107,25 @@ export class CocoaContext2D {
|
|
|
990
1107
|
this._dirty();
|
|
991
1108
|
}
|
|
992
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* The bitmap a `drawImage` source composites from: a surface's own, an
|
|
1112
|
+
* ntk Image's upload (made on its first draw, see `uploadImage`), or
|
|
1113
|
+
* none. A destroyed surface is none, silently — it had pixels once; any
|
|
1114
|
+
* other source is one this backend cannot draw at all, and development
|
|
1115
|
+
* says so once per kind.
|
|
1116
|
+
*/
|
|
1117
|
+
_sourceHandle(image) {
|
|
1118
|
+
if (image == null) return null;
|
|
1119
|
+
if (typeof image === 'object') {
|
|
1120
|
+
if ('_surfaceHandle' in image || image._surface) {
|
|
1121
|
+
return image._surfaceHandle ?? image._surface?._surfaceHandle ?? null;
|
|
1122
|
+
}
|
|
1123
|
+
if (isImagePixels(image)) return uploadImage(this._native, image);
|
|
1124
|
+
}
|
|
1125
|
+
warnUndrawable(image);
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
993
1129
|
/**
|
|
994
1130
|
* `drawImage` as a row memcpy, for the one shape where a copy is all it
|
|
995
1131
|
* ever was: a surface composited into another at a translate, whole
|
|
@@ -1081,12 +1217,9 @@ export class CocoaContext2D {
|
|
|
1081
1217
|
|
|
1082
1218
|
putImageData(data, x, y) {
|
|
1083
1219
|
if (!data?.data) return;
|
|
1084
|
-
const buf = Buffer.isBuffer(data.data)
|
|
1085
|
-
? data.data
|
|
1086
|
-
: Buffer.from(data.data.buffer ?? data.data);
|
|
1087
1220
|
this._native.ctxPutImageData(
|
|
1088
1221
|
this._s(),
|
|
1089
|
-
|
|
1222
|
+
toBuffer(data.data),
|
|
1090
1223
|
data.width,
|
|
1091
1224
|
data.height,
|
|
1092
1225
|
Math.round(x),
|