react-x11 2.6.1 → 2.8.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 +5 -3
- package/package.json +10 -3
- package/src/activate.js +12 -0
- package/src/anchor.js +6 -0
- package/src/appearance.js +351 -28
- package/src/appearancehooks.js +5 -2
- package/src/application.js +41 -0
- package/src/cocoa/app.js +367 -3
- package/src/cocoa/bezels.js +51 -1
- package/src/cocoa/dnd.js +358 -0
- package/src/cocoa/dock.js +39 -0
- package/src/cocoa/filepanels.js +155 -0
- package/src/cocoa/fonts.js +93 -2
- package/src/cocoa/globalmenu.js +41 -33
- package/src/cocoa/notifications.js +244 -0
- package/src/cocoa/permissions.js +74 -0
- package/src/cocoa/presenter.js +274 -33
- package/src/cocoa/promotion.js +708 -0
- package/src/cocoa/statusitem.js +112 -0
- package/src/cocoa/window.js +113 -4
- package/src/components/Button.js +20 -1
- package/src/components/Checkbox.js +17 -2
- package/src/components/Menu.js +108 -38
- package/src/components/Radio.js +17 -2
- package/src/components/Select.js +159 -27
- package/src/components/Switch.js +8 -1
- package/src/components/native.js +99 -0
- package/src/components/theme.js +37 -20
- package/src/desktopsettings.js +34 -2
- package/src/dnd.js +137 -11
- package/src/errors.js +6 -3
- package/src/filedialog.js +81 -16
- package/src/index.d.ts +29 -2
- package/src/index.js +17 -0
- package/src/launcher.js +170 -0
- package/src/launcherhooks.js +81 -0
- package/src/nodes.js +604 -37
- package/src/notificationhooks.js +56 -0
- package/src/notifications.js +558 -0
- package/src/palette.js +144 -8
- package/src/permissionhooks.js +89 -0
- package/src/permissions.js +196 -0
- package/src/style.d.ts +10 -4
- package/src/style.js +1 -0
- package/src/styles.js +161 -15
- package/src/textselection.js +1 -4
- package/src/trayhooks.js +90 -0
- package/src/types/appearance.d.ts +24 -0
- package/src/types/components.d.ts +10 -0
- package/src/types/elements.d.ts +14 -0
- package/src/types/events.d.ts +14 -0
- package/src/types/filedialog.d.ts +18 -7
- package/src/types/launcher.d.ts +43 -0
- package/src/types/notifications.d.ts +113 -0
- package/src/types/permissions.d.ts +100 -0
- package/src/types/style.d.ts +30 -2
- package/src/types/system.d.ts +5 -3
- package/src/types/tray.d.ts +54 -0
- package/src/windowid.js +23 -0
package/src/application.js
CHANGED
|
@@ -347,6 +347,15 @@ function environmentContext() {
|
|
|
347
347
|
*/
|
|
348
348
|
let current = null;
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The schemes the registration declared, kept beside it for the transports
|
|
352
|
+
* that are not the bus: `deliverOpen` filters an Apple Event's URLs by the
|
|
353
|
+
* same list an `Open` call is filtered by. Recorded on the no-bus path too —
|
|
354
|
+
* a Mac with no D-Bus still has a registration's *intent*, and that is the
|
|
355
|
+
* path the cocoa backend takes.
|
|
356
|
+
*/
|
|
357
|
+
let currentSchemes = null;
|
|
358
|
+
|
|
350
359
|
const openHandlers = new Set();
|
|
351
360
|
const activateHandlers = new Set();
|
|
352
361
|
|
|
@@ -406,6 +415,36 @@ function drainTo(kind, handler) {
|
|
|
406
415
|
}
|
|
407
416
|
}
|
|
408
417
|
|
|
418
|
+
// --------------------------------------------------------------------------
|
|
419
|
+
// The other transports
|
|
420
|
+
// --------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* A launch that arrived over a transport other than the bus: the cocoa
|
|
424
|
+
* backend's Apple Events (src/cocoa/app.js), where the OS hands the app its
|
|
425
|
+
* URLs through `application:openURLs:` and a Dock click on a running app
|
|
426
|
+
* through `applicationShouldHandleReopen:`. Same scheme filter, same buffer,
|
|
427
|
+
* same replay to the first handler — one code path in the app whichever
|
|
428
|
+
* desktop launched it.
|
|
429
|
+
*
|
|
430
|
+
* Not public: a transport calls this, an app subscribes with `onAppOpen`.
|
|
431
|
+
* `platformData` is whatever that transport knows about the launch. Apple
|
|
432
|
+
* Events carry no startup id and no timestamp, and the context says so with
|
|
433
|
+
* nulls rather than inventing them — on that backend the raise needs neither
|
|
434
|
+
* (`activateWindow` is `NSApp.activate` there).
|
|
435
|
+
*/
|
|
436
|
+
export function deliverOpen(uris, platformData = {}) {
|
|
437
|
+
const accepted = acceptUris(uris, currentSchemes);
|
|
438
|
+
if (accepted.length === 0) return;
|
|
439
|
+
debug(`open from the platform: ${accepted.map(redactUri).join(', ')}`);
|
|
440
|
+
deliver({ kind: 'open', uris: accepted, ctx: launchContext(platformData) });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The platform asked this app to come forward with nothing to open. */
|
|
444
|
+
export function deliverActivate(platformData = {}) {
|
|
445
|
+
deliver({ kind: 'activate', ctx: launchContext(platformData) });
|
|
446
|
+
}
|
|
447
|
+
|
|
409
448
|
/**
|
|
410
449
|
* Be told when the desktop hands this app URIs to open.
|
|
411
450
|
*
|
|
@@ -541,6 +580,7 @@ export async function registerApplication(options = {}) {
|
|
|
541
580
|
const appId = checkAppId(options.appId);
|
|
542
581
|
const schemes = checkSchemes(options.schemes);
|
|
543
582
|
const objectPath = objectPathForAppId(appId);
|
|
583
|
+
currentSchemes = schemes;
|
|
544
584
|
|
|
545
585
|
if (current) {
|
|
546
586
|
// An app has one identity; two registrations would be two RequestNames and
|
|
@@ -742,6 +782,7 @@ export function currentRegistration() {
|
|
|
742
782
|
/** Test seam, not public: forget every handler, buffer and registration. */
|
|
743
783
|
export function _resetApplicationState() {
|
|
744
784
|
current = null;
|
|
785
|
+
currentSchemes = null;
|
|
745
786
|
openHandlers.clear();
|
|
746
787
|
activateHandlers.clear();
|
|
747
788
|
buffered = [];
|
package/src/cocoa/app.js
CHANGED
|
@@ -14,15 +14,22 @@
|
|
|
14
14
|
// measured upgrade, not the first version.
|
|
15
15
|
import { cssColorStraight } from 'ntk';
|
|
16
16
|
|
|
17
|
+
import { deliverActivate, deliverOpen } from '../application.js';
|
|
17
18
|
import { flushPendingFrames } from '../frames.js';
|
|
19
|
+
import { flushSyncWork } from '../priority.js';
|
|
18
20
|
import { setCompositingForTests } from '../compositing.js';
|
|
19
21
|
import { setScreensForTests } from '../screens.js';
|
|
20
22
|
import { setScaleForTests } from '../scale.js';
|
|
21
23
|
import { BezelStore } from './bezels.js';
|
|
22
24
|
import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
|
|
25
|
+
import { CocoaDockMenu } from './dock.js';
|
|
23
26
|
import { CocoaGlobalMenuExport } from './globalmenu.js';
|
|
27
|
+
import { CocoaStatusItem } from './statusitem.js';
|
|
28
|
+
import { CocoaNotifications } from './notifications.js';
|
|
24
29
|
import { CocoaPaneHost } from './panehost.js';
|
|
30
|
+
import { CocoaPermissions } from './permissions.js';
|
|
25
31
|
import { CocoaPaneWindow } from './panewindow.js';
|
|
32
|
+
import { CocoaFilePanels } from './filepanels.js';
|
|
26
33
|
import { CocoaFontManager } from './fonts.js';
|
|
27
34
|
import { CocoaSurface } from './surface.js';
|
|
28
35
|
import { CocoaWindow } from './window.js';
|
|
@@ -55,6 +62,12 @@ export class CocoaApp {
|
|
|
55
62
|
// window paces itself on its own display (`frameIntervalFor`)
|
|
56
63
|
this._frameInterval = options.cocoa?.frameInterval ?? null;
|
|
57
64
|
this._pumpInterval = PUMP_INTERVAL_MS;
|
|
65
|
+
// src/desktopsettings.js's subscribers to the accessibility display
|
|
66
|
+
// options — "reduce motion" — which arrive as a backend event
|
|
67
|
+
this._a11yListeners = new Set();
|
|
68
|
+
// the layer presenter's animations, by the id the bridge reports their
|
|
69
|
+
// end under (`animation-end`, src/cocoa/presenter.js)
|
|
70
|
+
this._animationEnds = new Map();
|
|
58
71
|
// the one-shot that runs a frame due between two pump ticks, and when
|
|
59
72
|
// it is due (`_armFrameTimer`)
|
|
60
73
|
this._frameTimer = null;
|
|
@@ -63,23 +76,82 @@ export class CocoaApp {
|
|
|
63
76
|
this._shadowStale = new Set();
|
|
64
77
|
this._pump = null;
|
|
65
78
|
this._closed = false;
|
|
79
|
+
// the DragSession whose gesture an NSDraggingSession is tracking; a drop
|
|
80
|
+
// on one of our own windows is routed to its live payload
|
|
81
|
+
this._activeDrag = null;
|
|
82
|
+
// set by a quit request; read by close() to end the process
|
|
83
|
+
this._quitting = false;
|
|
84
|
+
|
|
85
|
+
// The activation policy has to be fixed before the app finishes
|
|
86
|
+
// launching — a Regular launch registers a Dock tile, so an agent app
|
|
87
|
+
// that switched afterwards would already have flashed its icon — and
|
|
88
|
+
// `listScreens` below is the first native call that launches it. So the
|
|
89
|
+
// policy goes first, through `initApp`'s option (bridge >= 0.5), and
|
|
90
|
+
// only when the root asked for one: a bridge without the option, or a
|
|
91
|
+
// fake, is left alone.
|
|
92
|
+
const policy = options.cocoa?.activationPolicy;
|
|
93
|
+
if (policy != null && typeof native.initApp === 'function') {
|
|
94
|
+
native.initApp({ activationPolicy: policy });
|
|
95
|
+
}
|
|
66
96
|
|
|
67
97
|
const screens = native.listScreens();
|
|
68
98
|
this.scale = screens[0]?.scale ?? 1;
|
|
69
99
|
this._screens = screens;
|
|
70
100
|
|
|
101
|
+
// The name the Dock, ⌘-Tab and the menu bar print. An unbundled
|
|
102
|
+
// process is registered with LaunchServices under its executable —
|
|
103
|
+
// `node` — and this renames that record; a bundle's Info.plist wins
|
|
104
|
+
// (the bridge answers false and leaves it).
|
|
105
|
+
const appName = options.cocoa?.appName;
|
|
106
|
+
if (appName != null && typeof native.setAppName === 'function') {
|
|
107
|
+
native.setAppName(String(appName));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The Dock menu (src/cocoa/dock.js), installed by `useDockMenu`.
|
|
111
|
+
this._dockMenu = new CocoaDockMenu(this);
|
|
112
|
+
// The tray items (src/cocoa/statusitem.js), by the bridge's handle —
|
|
113
|
+
// which is what a click event names them by.
|
|
114
|
+
this._statusItems = new Map();
|
|
115
|
+
|
|
71
116
|
// 'surface' (the measured default) or 'layers' — the retained CALayer
|
|
72
117
|
// presenter, opt-in while docs/macos.md's measure-first gate is open.
|
|
73
118
|
this._presenterMode =
|
|
74
119
|
options.cocoa?.presenter ??
|
|
75
120
|
process.env.REACT_X11_COCOA_PRESENTER ??
|
|
76
121
|
'surface';
|
|
122
|
+
// With the surface presenter, whether the nodes that animate get a
|
|
123
|
+
// layer of their own above the bitmap (src/cocoa/promotion.js). On by
|
|
124
|
+
// default — an animation the render server runs survives a busy JS
|
|
125
|
+
// thread, and the bitmap keeps the frame for everything else — where
|
|
126
|
+
// the bridge draws a layer's colour and a rastered one the same, which
|
|
127
|
+
// it says with `colorSpace()` (@windowkit/appkit >= 0.5.1, the range
|
|
128
|
+
// package.json asks for): before it a layer's colour was Generic RGB
|
|
129
|
+
// against sRGB surfaces, and a node moving between its layer and the
|
|
130
|
+
// bitmap changed shade on the way.
|
|
131
|
+
// Off (`cocoa.promote: false`, REACT_X11_COCOA_PROMOTE=0) keeps every
|
|
132
|
+
// animation on the frame clock, which is what the presenter bench's
|
|
133
|
+
// `surface` column measures; on (`true`, `=1`) is on regardless.
|
|
134
|
+
const promoteEnv = process.env.REACT_X11_COCOA_PROMOTE;
|
|
135
|
+
this._promote =
|
|
136
|
+
options.cocoa?.promote ??
|
|
137
|
+
(promoteEnv === '1' ||
|
|
138
|
+
(promoteEnv !== '0' && native.colorSpace?.() === 'sRGB'));
|
|
77
139
|
|
|
78
140
|
// the app's own bridge, so an app over a fake one (the tests) needs no
|
|
79
141
|
// real bridge on the machine — the manager's default loads it only when
|
|
80
142
|
// it is built standalone
|
|
81
143
|
this.fonts = new CocoaFontManager(native);
|
|
82
144
|
|
|
145
|
+
// Native open/save panels (src/cocoa/filepanels.js). Present exactly
|
|
146
|
+
// when the bridge has them (>= 0.5), and its presence is what puts the
|
|
147
|
+
// top rung on src/filedialog.js's ladder for this app — a fake bridge
|
|
148
|
+
// without `openPanel`, or an older one, leaves the ladder as it was.
|
|
149
|
+
this.filePanels =
|
|
150
|
+
typeof native.openPanel === 'function' &&
|
|
151
|
+
typeof native.savePanel === 'function'
|
|
152
|
+
? new CocoaFilePanels(this)
|
|
153
|
+
: null;
|
|
154
|
+
|
|
83
155
|
// AppKit-rendered control bezels. Its *presence* is the capability:
|
|
84
156
|
// `useSupports('nativeControls')` and the widget set's `controls:
|
|
85
157
|
// 'auto'` policy both test for this property, so a backend without it
|
|
@@ -87,6 +159,24 @@ export class CocoaApp {
|
|
|
87
159
|
// branching.
|
|
88
160
|
this.nativeBezels = new BezelStore(native);
|
|
89
161
|
|
|
162
|
+
// macOS privacy authorizations (src/cocoa/permissions.js). Present
|
|
163
|
+
// exactly when the bridge has them (>= 0.5), and its presence is the
|
|
164
|
+
// rung src/permissions.js finds for this app.
|
|
165
|
+
this.permissions =
|
|
166
|
+
typeof native.authorizationStatus === 'function' &&
|
|
167
|
+
typeof native.requestAuthorization === 'function'
|
|
168
|
+
? new CocoaPermissions(native)
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
// The notification centre (src/cocoa/notifications.js). Present exactly
|
|
172
|
+
// when the bridge has it (>= 0.5); whether it can *deliver* — a bundle
|
|
173
|
+
// id — is its own `available()`, the second gate the ladder asks.
|
|
174
|
+
this.notifications =
|
|
175
|
+
typeof native.postNotification === 'function' &&
|
|
176
|
+
typeof native.notificationSettings === 'function'
|
|
177
|
+
? new CocoaNotifications(this)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
90
180
|
// The GL policy, glbackend.js's shape. No GLX exists here, so the
|
|
91
181
|
// default is 'auto' (the direct backend where the runtime loads);
|
|
92
182
|
// useSupports('shaders') stays false until the first <glarea> resolves
|
|
@@ -293,6 +383,87 @@ export class CocoaApp {
|
|
|
293
383
|
return fps > 0 ? 1000 / fps : RAF_INTERVAL_MS;
|
|
294
384
|
}
|
|
295
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Bring this app and one of its windows to the front — the cocoa answer to
|
|
388
|
+
* `activateWindow()` (src/activate.js), the raise a deep link's
|
|
389
|
+
* `useAppOpen`/`useAppActivate` handler performs.
|
|
390
|
+
*
|
|
391
|
+
* There is no window manager here and no `_NET_ACTIVE_WINDOW` to send:
|
|
392
|
+
* `activateApp()` is `NSApp.activate` and `showWindow(h, true)` re-issues
|
|
393
|
+
* `makeKeyAndOrderFront:`, which is the whole of a raise on this backend.
|
|
394
|
+
* The X11 path weighs a launch timestamp against focus-stealing
|
|
395
|
+
* prevention; macOS has no such negotiation from a client, so the
|
|
396
|
+
* timestamp is not a parameter here.
|
|
397
|
+
*
|
|
398
|
+
* `target` is whatever `activate.js` resolved — a `CocoaWindow`, the
|
|
399
|
+
* `WindowNode` that owns one, or nothing when there is no window to raise.
|
|
400
|
+
* Returns whether a window was actually raised, matching the X path's
|
|
401
|
+
* "was the request issued" contract (and so *not* the false success the
|
|
402
|
+
* no-op `X.SendClientMessage` stub would report).
|
|
403
|
+
*/
|
|
404
|
+
raiseWindow(target) {
|
|
405
|
+
if (this._closed) return false;
|
|
406
|
+
const wnd = target && target._h != null ? target : (target?.window ?? null);
|
|
407
|
+
if (!wnd || wnd.destroyed || wnd._h == null) return false;
|
|
408
|
+
this._native.activateApp();
|
|
409
|
+
this._native.showWindow(wnd._h, true);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- the Dock tile ---------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The badge on the Dock tile — `setBadge()`'s first rung (src/launcher.js),
|
|
417
|
+
* which finds it by this method's presence. `null` clears.
|
|
418
|
+
*/
|
|
419
|
+
setDockBadge(label) {
|
|
420
|
+
this._native.setDockBadge(label == null ? null : String(label));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The menu behind a right-click on the Dock icon — `useDockMenu()`. */
|
|
424
|
+
setDockMenu(items) {
|
|
425
|
+
this._dockMenu.update(items);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Bounce the Dock icon until the app is activated — the cocoa answer to a
|
|
430
|
+
* window's `demands_attention` state (src/cocoa/window.js `setWmState`).
|
|
431
|
+
* AppKit ignores the request while the app is already active, which is
|
|
432
|
+
* the same outcome a window manager gives a focused window's urgency.
|
|
433
|
+
*/
|
|
434
|
+
requestAttention() {
|
|
435
|
+
return this._native.requestUserAttention('critical');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
cancelAttention(requestId) {
|
|
439
|
+
this._native.cancelUserAttention(requestId);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// --- the tray --------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* An `NSStatusItem` — `useTray()`'s mechanism, found by this method's
|
|
446
|
+
* presence (the X11 backend has none, and the hook says so). Returns the
|
|
447
|
+
* item; `remove()` on it is the whole of its teardown.
|
|
448
|
+
*/
|
|
449
|
+
createStatusItem(options) {
|
|
450
|
+
const item = new CocoaStatusItem(this, options);
|
|
451
|
+
this._statusItems.set(item.handle, item);
|
|
452
|
+
const remove = item.remove.bind(item);
|
|
453
|
+
item.remove = () => {
|
|
454
|
+
this._statusItems.delete(item.handle);
|
|
455
|
+
remove();
|
|
456
|
+
};
|
|
457
|
+
return item;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** A `menu-activate` tagged `status`: the item whose menu owns the id. */
|
|
461
|
+
_routeStatusMenu(id) {
|
|
462
|
+
for (const item of this._statusItems.values()) {
|
|
463
|
+
if (item.owns(id)) return item.activate(id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
296
467
|
/**
|
|
297
468
|
* The pane's end of the frame channel (childmain hands it over,
|
|
298
469
|
* feature-detected so the X11 pane path never notices): geometry and
|
|
@@ -485,9 +656,21 @@ export class CocoaApp {
|
|
|
485
656
|
for (const wnd of this._windows.values()) wnd.present();
|
|
486
657
|
}
|
|
487
658
|
|
|
488
|
-
/**
|
|
489
|
-
*
|
|
659
|
+
/**
|
|
660
|
+
* After any synchronously dispatched input: land React's half of the
|
|
661
|
+
* response, paint it (the same early flush a discrete event gets on X11 —
|
|
662
|
+
* `discrete` in events.js) and put it on glass.
|
|
663
|
+
*
|
|
664
|
+
* `flushSyncWork` is not belt and braces. A discrete-priority update is
|
|
665
|
+
* committed on a **microtask**, and a microtask does not run until the
|
|
666
|
+
* call that dispatched the event returns to Node — which, inside one of
|
|
667
|
+
* AppKit's modal loops (a live resize, menu tracking, a dragging
|
|
668
|
+
* session), is after the whole gesture. Without it a handler's own
|
|
669
|
+
* `setState` reaches the screen when the gesture ends, and the gesture is
|
|
670
|
+
* exactly when it was worth showing.
|
|
671
|
+
*/
|
|
490
672
|
_afterInput() {
|
|
673
|
+
flushSyncWork();
|
|
491
674
|
flushPendingFrames();
|
|
492
675
|
this._presentAll();
|
|
493
676
|
}
|
|
@@ -523,8 +706,49 @@ export class CocoaApp {
|
|
|
523
706
|
case 'window-occlusion':
|
|
524
707
|
return this._routeOcclusion(ev);
|
|
525
708
|
case 'menu-activate':
|
|
526
|
-
|
|
709
|
+
// `menu` says which tree the id belongs to (bridge >= 0.5): the
|
|
710
|
+
// Dock menu and the menu bar allocate ids independently. Absent on
|
|
711
|
+
// an older bridge, where only the bar exists.
|
|
712
|
+
if (ev.menu === 'dock') this._dockMenu.activate(ev.id);
|
|
713
|
+
else if (ev.menu === 'status') this._routeStatusMenu(ev.id);
|
|
714
|
+
else this._activeGlobalMenu?.activate(ev.id);
|
|
715
|
+
return this._afterInput();
|
|
716
|
+
case 'status-item-click':
|
|
717
|
+
this._statusItems.get(ev.statusItem)?.click(ev);
|
|
527
718
|
return this._afterInput();
|
|
719
|
+
case 'accessibility-display-changed':
|
|
720
|
+
return this._routeAccessibility(ev);
|
|
721
|
+
case 'drag-enter':
|
|
722
|
+
case 'drag-over':
|
|
723
|
+
case 'drag-exit':
|
|
724
|
+
case 'drag-perform':
|
|
725
|
+
return this._routeDrop(ev);
|
|
726
|
+
case 'drag-session-began':
|
|
727
|
+
// `draggingSession:willBeginAtPoint:`, dispatched from inside
|
|
728
|
+
// `beginDrag` — before the `onDragStart` handler's own render has
|
|
729
|
+
// been asked for, so there is nothing to flush yet. The frame that
|
|
730
|
+
// shows the drag beginning goes out from `CocoaWindow.beginDrag`,
|
|
731
|
+
// on the way back past it.
|
|
732
|
+
return undefined;
|
|
733
|
+
case 'drag-session-moved':
|
|
734
|
+
return this._routeDragMoved(ev);
|
|
735
|
+
case 'drag-session-ended':
|
|
736
|
+
return this._routeDragEnded(ev);
|
|
737
|
+
case 'app-open-urls':
|
|
738
|
+
return this._routeAppOpen(ev);
|
|
739
|
+
case 'app-reopen':
|
|
740
|
+
return this._routeAppReopen(ev);
|
|
741
|
+
case 'app-quit-request':
|
|
742
|
+
return this.requestQuit();
|
|
743
|
+
case 'notification-action':
|
|
744
|
+
case 'notification-dismissed':
|
|
745
|
+
this.notifications?.route(ev);
|
|
746
|
+
return this._afterInput();
|
|
747
|
+
case 'animation-end':
|
|
748
|
+
// the presenter that added the animation registered for its id; an
|
|
749
|
+
// id nobody knows is an animation already forgotten (cancelled, or
|
|
750
|
+
// its layer dropped), and the bridge's report is just late
|
|
751
|
+
return this._animationEnds.get(ev.id)?.(ev);
|
|
528
752
|
default:
|
|
529
753
|
return undefined;
|
|
530
754
|
}
|
|
@@ -689,6 +913,62 @@ export class CocoaApp {
|
|
|
689
913
|
wnd._occluded = ev.visible === false;
|
|
690
914
|
}
|
|
691
915
|
|
|
916
|
+
_routeAccessibility(ev) {
|
|
917
|
+
for (const fn of [...this._a11yListeners]) fn(ev);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* System Settings › Accessibility › Display, as the bridge reports it —
|
|
922
|
+
* `{ reduceMotion, reduceTransparency, increaseContrast,
|
|
923
|
+
* differentiateWithoutColor, invertColors }` — or null over a bridge (or
|
|
924
|
+
* a test fake) that does not answer. The seam src/desktopsettings.js reads
|
|
925
|
+
* "reduce motion" through; `watchAccessibilityDisplay` is its other half.
|
|
926
|
+
*/
|
|
927
|
+
accessibilityDisplayOptions() {
|
|
928
|
+
return this._native.accessibilityDisplayOptions?.() ?? null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Subscribe to the options changing while the app runs. */
|
|
932
|
+
watchAccessibilityDisplay(fn) {
|
|
933
|
+
this._a11yListeners.add(fn);
|
|
934
|
+
return () => this._a11yListeners.delete(fn);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* AppKit's four destination questions, answered inside the callback: the
|
|
939
|
+
* window's transport (src/cocoa/dnd.js) drives its DropSession and sets
|
|
940
|
+
* the response before this returns. A frame is flushed on the way out,
|
|
941
|
+
* like a click's — `:drag-over` is a repaint.
|
|
942
|
+
*/
|
|
943
|
+
_routeDrop(ev) {
|
|
944
|
+
const wnd = this._window(ev);
|
|
945
|
+
if (!wnd || wnd.destroyed) return;
|
|
946
|
+
wnd._dropTransport?.handle(ev);
|
|
947
|
+
this._afterInput();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* `draggingSession:movedToPoint:` — the source's own view of the gesture,
|
|
952
|
+
* and the only thing that arrives while AppKit tracks it: the pointer's
|
|
953
|
+
* `mousemove` stops, `pump2` does not return until the drop, and no timer
|
|
954
|
+
* or microtask of ours runs in between. So the frame belongs to the
|
|
955
|
+
* callback, like the destination's (`_routeDrop`) — that is what lets a
|
|
956
|
+
* `<popup dragPreview>` follow the pointer, and an `onDrag` that moves an
|
|
957
|
+
* insertion marker move it during the drag rather than on the release.
|
|
958
|
+
*/
|
|
959
|
+
_routeDragMoved(ev) {
|
|
960
|
+
this._activeDrag?.nativeMoved(ev);
|
|
961
|
+
this._afterInput();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** The release of a drag AppKit was tracking for us. */
|
|
965
|
+
_routeDragEnded(ev) {
|
|
966
|
+
const drag = this._activeDrag;
|
|
967
|
+
this._activeDrag = null;
|
|
968
|
+
drag?.nativeEnded(ev);
|
|
969
|
+
this._afterInput();
|
|
970
|
+
}
|
|
971
|
+
|
|
692
972
|
_routeClose(ev) {
|
|
693
973
|
const wnd = this._window(ev);
|
|
694
974
|
if (!wnd) return;
|
|
@@ -706,6 +986,82 @@ export class CocoaApp {
|
|
|
706
986
|
this._afterInput();
|
|
707
987
|
}
|
|
708
988
|
|
|
989
|
+
// --- the application as a whole ------------------------------------------
|
|
990
|
+
//
|
|
991
|
+
// What the OS asks the app rather than one of its windows, through the
|
|
992
|
+
// NSApplicationDelegate the bridge installs (>= 0.5). The bridge decides
|
|
993
|
+
// nothing — it answers `applicationShouldHandleReopen:` NO and
|
|
994
|
+
// `applicationShouldTerminate:` Cancel and hands the question over — so
|
|
995
|
+
// each of these is the renderer's decision, and the decisions are the ones
|
|
996
|
+
// the freedesktop transport already made (src/application.js): a URL is an
|
|
997
|
+
// `Open`, a second launch is an `Activate`, and quitting is what closing
|
|
998
|
+
// the app's primary window means. Anything that arrived before the
|
|
999
|
+
// callback was installed — the launching Apple Event lands inside
|
|
1000
|
+
// `initApp()` — the bridge holds and replays on the first pump.
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* `application:openURLs:` — a URL for a scheme the bundle's `Info.plist`
|
|
1004
|
+
* registers, or a document the Finder handed over as `file://`. Delivered
|
|
1005
|
+
* through the same filter, buffer and replay a D-Bus `Open` takes, so
|
|
1006
|
+
* `useAppOpen` fires the same way here.
|
|
1007
|
+
*/
|
|
1008
|
+
_routeAppOpen(ev) {
|
|
1009
|
+
const urls = Array.isArray(ev.urls)
|
|
1010
|
+
? ev.urls.filter((u) => typeof u === 'string')
|
|
1011
|
+
: [];
|
|
1012
|
+
deliverOpen(urls, {});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* `applicationShouldHandleReopen:` — the user launched the app again while
|
|
1017
|
+
* it was running (a Dock click, `open -a`). AppKit has already brought
|
|
1018
|
+
* the windows forward by the time this arrives; what a second launch
|
|
1019
|
+
* *means* is the app's, through `useAppActivate`, and `has-visible-windows`
|
|
1020
|
+
* on the context is the one fact the OS adds.
|
|
1021
|
+
*/
|
|
1022
|
+
_routeAppReopen(ev) {
|
|
1023
|
+
deliverActivate({ 'has-visible-windows': ev.hasVisibleWindows === true });
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Quit, from wherever macOS says it: the Dock's Quit, ⌘Q on the app menu, a
|
|
1028
|
+
* logout. One route for all of them, and it is the **primary window's
|
|
1029
|
+
* close request** — `onCloseRequest` where the app wrote one (a "save your
|
|
1030
|
+
* work?" dialog is a veto until it decides), the default otherwise, which
|
|
1031
|
+
* unmounts the tree. Quit means what closing the app's window means on
|
|
1032
|
+
* every desktop, rather than a second, faster exit that skips the same
|
|
1033
|
+
* question.
|
|
1034
|
+
*
|
|
1035
|
+
* With no window to ask — an accessory app between windows — the tree
|
|
1036
|
+
* unmounts directly.
|
|
1037
|
+
*
|
|
1038
|
+
* The bridge has already answered `applicationShouldTerminate:` with
|
|
1039
|
+
* Cancel, so the process outliving the tree is this side's to end: when a
|
|
1040
|
+
* quit request leads to the app closing, `close()` exits the process
|
|
1041
|
+
* (`cocoa.exitOnQuit`, on by default — a Regular-policy process that is
|
|
1042
|
+
* still alive is still in the Dock, and a Dock tile for an app whose
|
|
1043
|
+
* windows are all gone is what a user calls a hang).
|
|
1044
|
+
*/
|
|
1045
|
+
requestQuit() {
|
|
1046
|
+
if (this._closed) return;
|
|
1047
|
+
this._quitting = true;
|
|
1048
|
+
const windows = [...this._windows.values()].filter(
|
|
1049
|
+
(w) => !w.destroyed && !w._popup,
|
|
1050
|
+
);
|
|
1051
|
+
const primary =
|
|
1052
|
+
windows.find((w) => w._reactX11Node?._isPrimaryWindow?.()) ??
|
|
1053
|
+
windows[0] ??
|
|
1054
|
+
null;
|
|
1055
|
+
if (primary) {
|
|
1056
|
+
primary.emit('close', { preventDefault() {} });
|
|
1057
|
+
} else {
|
|
1058
|
+
Promise.resolve(this._reactX11Root?.unmount?.()).catch((err) => {
|
|
1059
|
+
this.options?.onXError?.(err);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
this._afterInput();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
709
1065
|
// --- teardown ------------------------------------------------------------
|
|
710
1066
|
|
|
711
1067
|
close() {
|
|
@@ -719,6 +1075,14 @@ export class CocoaApp {
|
|
|
719
1075
|
this._cocoaGL = null;
|
|
720
1076
|
this._native.setBackendEventCallback(null);
|
|
721
1077
|
for (const wnd of [...this._windows.values()]) wnd.destroy();
|
|
1078
|
+
// A quit the app accepted: the tree is down and the connection closed,
|
|
1079
|
+
// and macOS is still waiting on the Cancel the bridge answered with. End
|
|
1080
|
+
// the process on the next turn — after the unmount's own microtasks —
|
|
1081
|
+
// rather than leaving a Dock tile with nothing behind it. `exitOnQuit:
|
|
1082
|
+
// false` is the seam for an embedder that owns the process's lifetime.
|
|
1083
|
+
if (this._quitting && this.options?.cocoa?.exitOnQuit !== false) {
|
|
1084
|
+
setImmediate(() => process.exit(0));
|
|
1085
|
+
}
|
|
722
1086
|
return Promise.resolve();
|
|
723
1087
|
}
|
|
724
1088
|
}
|
package/src/cocoa/bezels.js
CHANGED
|
@@ -26,6 +26,22 @@ export class BezelStore {
|
|
|
26
26
|
this._MAX = 160;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Forget every rendered bezel. The pixels depend on one thing that is not
|
|
31
|
+
* a parameter of `get`: the desktop's accent, which AppKit reads for
|
|
32
|
+
* itself when it draws the cell. After the user picks another accent a
|
|
33
|
+
* cached bezel is the old colour, and its key still matches — so a bezel
|
|
34
|
+
* whose state happened to change came up in the new accent while the
|
|
35
|
+
* ones beside it kept the old, until something else redrew them. The
|
|
36
|
+
* appearance change forgets them all; the next paint renders each again.
|
|
37
|
+
*
|
|
38
|
+
* The measured insets and natural sizes stay: geometry is not coloured.
|
|
39
|
+
*/
|
|
40
|
+
clear() {
|
|
41
|
+
// the surfaces are freed by their External finalizer
|
|
42
|
+
this._cache.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
/**
|
|
30
46
|
* The control's natural size in logical px — the size the bezel is
|
|
31
47
|
* designed at, which layout adopts for the kinds that must not stretch
|
|
@@ -40,6 +56,21 @@ export class BezelStore {
|
|
|
40
56
|
};
|
|
41
57
|
}
|
|
42
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The translucent rows above and below the bezel's solid body, in logical
|
|
61
|
+
* px — a push button's drop shadow, mostly. They are part of the natural
|
|
62
|
+
* box (`natural` measures every inked pixel, so the box is the footprint)
|
|
63
|
+
* and not part of the control: AppKit centres a title in the cell's body,
|
|
64
|
+
* and a label centred in the footprint sits half the shadow too low.
|
|
65
|
+
*/
|
|
66
|
+
shadow(kind, controlSize = 'regular') {
|
|
67
|
+
const c = this._scan(kind, controlSize, 2);
|
|
68
|
+
return {
|
|
69
|
+
top: Math.round(c.body.top),
|
|
70
|
+
bottom: Math.round(c.body.bottom),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
/**
|
|
44
75
|
* The bezel for one laid-out box: `w`/`h` in device px, blit-ready.
|
|
45
76
|
* Returns `{ surface, sx, sy, sw, sh }` — draw with the 9-arg
|
|
@@ -126,14 +157,23 @@ export class BezelStore {
|
|
|
126
157
|
let y0 = ph;
|
|
127
158
|
let x1 = -1;
|
|
128
159
|
let y1 = -1;
|
|
160
|
+
// and the solid body inside the footprint: where the bezel is opaque,
|
|
161
|
+
// which is the control itself rather than its shadow
|
|
162
|
+
let by0 = ph;
|
|
163
|
+
let by1 = -1;
|
|
129
164
|
for (let y = 0; y < ph; y++) {
|
|
130
165
|
for (let x = 0; x < pw; x++) {
|
|
131
|
-
|
|
166
|
+
const alpha = buf[(y * pw + x) * 4 + 3];
|
|
167
|
+
if (alpha > 8) {
|
|
132
168
|
if (x < x0) x0 = x;
|
|
133
169
|
if (x > x1) x1 = x;
|
|
134
170
|
if (y < y0) y0 = y;
|
|
135
171
|
if (y > y1) y1 = y;
|
|
136
172
|
}
|
|
173
|
+
if (alpha >= 250) {
|
|
174
|
+
if (y < by0) by0 = y;
|
|
175
|
+
if (y > by1) by1 = y;
|
|
176
|
+
}
|
|
137
177
|
}
|
|
138
178
|
}
|
|
139
179
|
if (x1 < 0) {
|
|
@@ -143,6 +183,12 @@ export class BezelStore {
|
|
|
143
183
|
x1 = pw - 1;
|
|
144
184
|
y1 = ph - 1;
|
|
145
185
|
}
|
|
186
|
+
if (by1 < 0) {
|
|
187
|
+
// no solid pixel at all (a fully translucent bezel): the body is the
|
|
188
|
+
// footprint
|
|
189
|
+
by0 = y0;
|
|
190
|
+
by1 = y1;
|
|
191
|
+
}
|
|
146
192
|
c = {
|
|
147
193
|
insets: {
|
|
148
194
|
left: x0 / scale,
|
|
@@ -154,6 +200,10 @@ export class BezelStore {
|
|
|
154
200
|
width: (x1 - x0 + 1) / scale,
|
|
155
201
|
height: (y1 - y0 + 1) / scale,
|
|
156
202
|
},
|
|
203
|
+
body: {
|
|
204
|
+
top: (by0 - y0) / scale,
|
|
205
|
+
bottom: (y1 - by1) / scale,
|
|
206
|
+
},
|
|
157
207
|
};
|
|
158
208
|
this._canonical.set(key, c);
|
|
159
209
|
return c;
|