react-x11 2.6.0 → 2.7.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 +358 -20
- package/src/cocoa/bezels.js +51 -1
- package/src/cocoa/context2d.js +271 -25
- package/src/cocoa/dnd.js +347 -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 +190 -2
- package/src/cocoa/statusitem.js +112 -0
- package/src/cocoa/window.js +85 -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 +92 -3
- package/src/errors.js +6 -3
- package/src/filedialog.js +81 -16
- package/src/index.d.ts +17 -1
- package/src/index.js +17 -0
- package/src/launcher.js +170 -0
- package/src/launcherhooks.js +81 -0
- package/src/nodes.js +553 -35
- 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/screens.js +39 -4
- 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,21 @@
|
|
|
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';
|
|
18
19
|
import { setCompositingForTests } from '../compositing.js';
|
|
19
20
|
import { setScreensForTests } from '../screens.js';
|
|
20
21
|
import { setScaleForTests } from '../scale.js';
|
|
21
22
|
import { BezelStore } from './bezels.js';
|
|
22
23
|
import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
|
|
24
|
+
import { CocoaDockMenu } from './dock.js';
|
|
23
25
|
import { CocoaGlobalMenuExport } from './globalmenu.js';
|
|
26
|
+
import { CocoaStatusItem } from './statusitem.js';
|
|
27
|
+
import { CocoaNotifications } from './notifications.js';
|
|
24
28
|
import { CocoaPaneHost } from './panehost.js';
|
|
29
|
+
import { CocoaPermissions } from './permissions.js';
|
|
25
30
|
import { CocoaPaneWindow } from './panewindow.js';
|
|
31
|
+
import { CocoaFilePanels } from './filepanels.js';
|
|
26
32
|
import { CocoaFontManager } from './fonts.js';
|
|
27
33
|
import { CocoaSurface } from './surface.js';
|
|
28
34
|
import { CocoaWindow } from './window.js';
|
|
@@ -55,6 +61,12 @@ export class CocoaApp {
|
|
|
55
61
|
// window paces itself on its own display (`frameIntervalFor`)
|
|
56
62
|
this._frameInterval = options.cocoa?.frameInterval ?? null;
|
|
57
63
|
this._pumpInterval = PUMP_INTERVAL_MS;
|
|
64
|
+
// src/desktopsettings.js's subscribers to the accessibility display
|
|
65
|
+
// options — "reduce motion" — which arrive as a backend event
|
|
66
|
+
this._a11yListeners = new Set();
|
|
67
|
+
// the layer presenter's animations, by the id the bridge reports their
|
|
68
|
+
// end under (`animation-end`, src/cocoa/presenter.js)
|
|
69
|
+
this._animationEnds = new Map();
|
|
58
70
|
// the one-shot that runs a frame due between two pump ticks, and when
|
|
59
71
|
// it is due (`_armFrameTimer`)
|
|
60
72
|
this._frameTimer = null;
|
|
@@ -63,11 +75,43 @@ export class CocoaApp {
|
|
|
63
75
|
this._shadowStale = new Set();
|
|
64
76
|
this._pump = null;
|
|
65
77
|
this._closed = false;
|
|
78
|
+
// the DragSession whose gesture an NSDraggingSession is tracking; a drop
|
|
79
|
+
// on one of our own windows is routed to its live payload
|
|
80
|
+
this._activeDrag = null;
|
|
81
|
+
// set by a quit request; read by close() to end the process
|
|
82
|
+
this._quitting = false;
|
|
83
|
+
|
|
84
|
+
// The activation policy has to be fixed before the app finishes
|
|
85
|
+
// launching — a Regular launch registers a Dock tile, so an agent app
|
|
86
|
+
// that switched afterwards would already have flashed its icon — and
|
|
87
|
+
// `listScreens` below is the first native call that launches it. So the
|
|
88
|
+
// policy goes first, through `initApp`'s option (bridge >= 0.5), and
|
|
89
|
+
// only when the root asked for one: a bridge without the option, or a
|
|
90
|
+
// fake, is left alone.
|
|
91
|
+
const policy = options.cocoa?.activationPolicy;
|
|
92
|
+
if (policy != null && typeof native.initApp === 'function') {
|
|
93
|
+
native.initApp({ activationPolicy: policy });
|
|
94
|
+
}
|
|
66
95
|
|
|
67
96
|
const screens = native.listScreens();
|
|
68
97
|
this.scale = screens[0]?.scale ?? 1;
|
|
69
98
|
this._screens = screens;
|
|
70
99
|
|
|
100
|
+
// The name the Dock, ⌘-Tab and the menu bar print. An unbundled
|
|
101
|
+
// process is registered with LaunchServices under its executable —
|
|
102
|
+
// `node` — and this renames that record; a bundle's Info.plist wins
|
|
103
|
+
// (the bridge answers false and leaves it).
|
|
104
|
+
const appName = options.cocoa?.appName;
|
|
105
|
+
if (appName != null && typeof native.setAppName === 'function') {
|
|
106
|
+
native.setAppName(String(appName));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The Dock menu (src/cocoa/dock.js), installed by `useDockMenu`.
|
|
110
|
+
this._dockMenu = new CocoaDockMenu(this);
|
|
111
|
+
// The tray items (src/cocoa/statusitem.js), by the bridge's handle —
|
|
112
|
+
// which is what a click event names them by.
|
|
113
|
+
this._statusItems = new Map();
|
|
114
|
+
|
|
71
115
|
// 'surface' (the measured default) or 'layers' — the retained CALayer
|
|
72
116
|
// presenter, opt-in while docs/macos.md's measure-first gate is open.
|
|
73
117
|
this._presenterMode =
|
|
@@ -80,6 +124,16 @@ export class CocoaApp {
|
|
|
80
124
|
// it is built standalone
|
|
81
125
|
this.fonts = new CocoaFontManager(native);
|
|
82
126
|
|
|
127
|
+
// Native open/save panels (src/cocoa/filepanels.js). Present exactly
|
|
128
|
+
// when the bridge has them (>= 0.5), and its presence is what puts the
|
|
129
|
+
// top rung on src/filedialog.js's ladder for this app — a fake bridge
|
|
130
|
+
// without `openPanel`, or an older one, leaves the ladder as it was.
|
|
131
|
+
this.filePanels =
|
|
132
|
+
typeof native.openPanel === 'function' &&
|
|
133
|
+
typeof native.savePanel === 'function'
|
|
134
|
+
? new CocoaFilePanels(this)
|
|
135
|
+
: null;
|
|
136
|
+
|
|
83
137
|
// AppKit-rendered control bezels. Its *presence* is the capability:
|
|
84
138
|
// `useSupports('nativeControls')` and the widget set's `controls:
|
|
85
139
|
// 'auto'` policy both test for this property, so a backend without it
|
|
@@ -87,6 +141,24 @@ export class CocoaApp {
|
|
|
87
141
|
// branching.
|
|
88
142
|
this.nativeBezels = new BezelStore(native);
|
|
89
143
|
|
|
144
|
+
// macOS privacy authorizations (src/cocoa/permissions.js). Present
|
|
145
|
+
// exactly when the bridge has them (>= 0.5), and its presence is the
|
|
146
|
+
// rung src/permissions.js finds for this app.
|
|
147
|
+
this.permissions =
|
|
148
|
+
typeof native.authorizationStatus === 'function' &&
|
|
149
|
+
typeof native.requestAuthorization === 'function'
|
|
150
|
+
? new CocoaPermissions(native)
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
// The notification centre (src/cocoa/notifications.js). Present exactly
|
|
154
|
+
// when the bridge has it (>= 0.5); whether it can *deliver* — a bundle
|
|
155
|
+
// id — is its own `available()`, the second gate the ladder asks.
|
|
156
|
+
this.notifications =
|
|
157
|
+
typeof native.postNotification === 'function' &&
|
|
158
|
+
typeof native.notificationSettings === 'function'
|
|
159
|
+
? new CocoaNotifications(this)
|
|
160
|
+
: null;
|
|
161
|
+
|
|
90
162
|
// The GL policy, glbackend.js's shape. No GLX exists here, so the
|
|
91
163
|
// default is 'auto' (the direct backend where the runtime loads);
|
|
92
164
|
// useSupports('shaders') stays false until the first <glarea> resolves
|
|
@@ -293,6 +365,87 @@ export class CocoaApp {
|
|
|
293
365
|
return fps > 0 ? 1000 / fps : RAF_INTERVAL_MS;
|
|
294
366
|
}
|
|
295
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Bring this app and one of its windows to the front — the cocoa answer to
|
|
370
|
+
* `activateWindow()` (src/activate.js), the raise a deep link's
|
|
371
|
+
* `useAppOpen`/`useAppActivate` handler performs.
|
|
372
|
+
*
|
|
373
|
+
* There is no window manager here and no `_NET_ACTIVE_WINDOW` to send:
|
|
374
|
+
* `activateApp()` is `NSApp.activate` and `showWindow(h, true)` re-issues
|
|
375
|
+
* `makeKeyAndOrderFront:`, which is the whole of a raise on this backend.
|
|
376
|
+
* The X11 path weighs a launch timestamp against focus-stealing
|
|
377
|
+
* prevention; macOS has no such negotiation from a client, so the
|
|
378
|
+
* timestamp is not a parameter here.
|
|
379
|
+
*
|
|
380
|
+
* `target` is whatever `activate.js` resolved — a `CocoaWindow`, the
|
|
381
|
+
* `WindowNode` that owns one, or nothing when there is no window to raise.
|
|
382
|
+
* Returns whether a window was actually raised, matching the X path's
|
|
383
|
+
* "was the request issued" contract (and so *not* the false success the
|
|
384
|
+
* no-op `X.SendClientMessage` stub would report).
|
|
385
|
+
*/
|
|
386
|
+
raiseWindow(target) {
|
|
387
|
+
if (this._closed) return false;
|
|
388
|
+
const wnd = target && target._h != null ? target : (target?.window ?? null);
|
|
389
|
+
if (!wnd || wnd.destroyed || wnd._h == null) return false;
|
|
390
|
+
this._native.activateApp();
|
|
391
|
+
this._native.showWindow(wnd._h, true);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --- the Dock tile ---------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The badge on the Dock tile — `setBadge()`'s first rung (src/launcher.js),
|
|
399
|
+
* which finds it by this method's presence. `null` clears.
|
|
400
|
+
*/
|
|
401
|
+
setDockBadge(label) {
|
|
402
|
+
this._native.setDockBadge(label == null ? null : String(label));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** The menu behind a right-click on the Dock icon — `useDockMenu()`. */
|
|
406
|
+
setDockMenu(items) {
|
|
407
|
+
this._dockMenu.update(items);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Bounce the Dock icon until the app is activated — the cocoa answer to a
|
|
412
|
+
* window's `demands_attention` state (src/cocoa/window.js `setWmState`).
|
|
413
|
+
* AppKit ignores the request while the app is already active, which is
|
|
414
|
+
* the same outcome a window manager gives a focused window's urgency.
|
|
415
|
+
*/
|
|
416
|
+
requestAttention() {
|
|
417
|
+
return this._native.requestUserAttention('critical');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
cancelAttention(requestId) {
|
|
421
|
+
this._native.cancelUserAttention(requestId);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// --- the tray --------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* An `NSStatusItem` — `useTray()`'s mechanism, found by this method's
|
|
428
|
+
* presence (the X11 backend has none, and the hook says so). Returns the
|
|
429
|
+
* item; `remove()` on it is the whole of its teardown.
|
|
430
|
+
*/
|
|
431
|
+
createStatusItem(options) {
|
|
432
|
+
const item = new CocoaStatusItem(this, options);
|
|
433
|
+
this._statusItems.set(item.handle, item);
|
|
434
|
+
const remove = item.remove.bind(item);
|
|
435
|
+
item.remove = () => {
|
|
436
|
+
this._statusItems.delete(item.handle);
|
|
437
|
+
remove();
|
|
438
|
+
};
|
|
439
|
+
return item;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** A `menu-activate` tagged `status`: the item whose menu owns the id. */
|
|
443
|
+
_routeStatusMenu(id) {
|
|
444
|
+
for (const item of this._statusItems.values()) {
|
|
445
|
+
if (item.owns(id)) return item.activate(id);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
296
449
|
/**
|
|
297
450
|
* The pane's end of the frame channel (childmain hands it over,
|
|
298
451
|
* feature-detected so the X11 pane path never notices): geometry and
|
|
@@ -523,8 +676,45 @@ export class CocoaApp {
|
|
|
523
676
|
case 'window-occlusion':
|
|
524
677
|
return this._routeOcclusion(ev);
|
|
525
678
|
case 'menu-activate':
|
|
526
|
-
|
|
679
|
+
// `menu` says which tree the id belongs to (bridge >= 0.5): the
|
|
680
|
+
// Dock menu and the menu bar allocate ids independently. Absent on
|
|
681
|
+
// an older bridge, where only the bar exists.
|
|
682
|
+
if (ev.menu === 'dock') this._dockMenu.activate(ev.id);
|
|
683
|
+
else if (ev.menu === 'status') this._routeStatusMenu(ev.id);
|
|
684
|
+
else this._activeGlobalMenu?.activate(ev.id);
|
|
685
|
+
return this._afterInput();
|
|
686
|
+
case 'status-item-click':
|
|
687
|
+
this._statusItems.get(ev.statusItem)?.click(ev);
|
|
527
688
|
return this._afterInput();
|
|
689
|
+
case 'accessibility-display-changed':
|
|
690
|
+
return this._routeAccessibility(ev);
|
|
691
|
+
case 'drag-enter':
|
|
692
|
+
case 'drag-over':
|
|
693
|
+
case 'drag-exit':
|
|
694
|
+
case 'drag-perform':
|
|
695
|
+
return this._routeDrop(ev);
|
|
696
|
+
case 'drag-session-began':
|
|
697
|
+
return undefined;
|
|
698
|
+
case 'drag-session-moved':
|
|
699
|
+
this._activeDrag?.nativeMoved(ev);
|
|
700
|
+
return undefined;
|
|
701
|
+
case 'drag-session-ended':
|
|
702
|
+
return this._routeDragEnded(ev);
|
|
703
|
+
case 'app-open-urls':
|
|
704
|
+
return this._routeAppOpen(ev);
|
|
705
|
+
case 'app-reopen':
|
|
706
|
+
return this._routeAppReopen(ev);
|
|
707
|
+
case 'app-quit-request':
|
|
708
|
+
return this.requestQuit();
|
|
709
|
+
case 'notification-action':
|
|
710
|
+
case 'notification-dismissed':
|
|
711
|
+
this.notifications?.route(ev);
|
|
712
|
+
return this._afterInput();
|
|
713
|
+
case 'animation-end':
|
|
714
|
+
// the presenter that added the animation registered for its id; an
|
|
715
|
+
// id nobody knows is an animation already forgotten (cancelled, or
|
|
716
|
+
// its layer dropped), and the bridge's report is just late
|
|
717
|
+
return this._animationEnds.get(ev.id)?.(ev);
|
|
528
718
|
default:
|
|
529
719
|
return undefined;
|
|
530
720
|
}
|
|
@@ -689,6 +879,48 @@ export class CocoaApp {
|
|
|
689
879
|
wnd._occluded = ev.visible === false;
|
|
690
880
|
}
|
|
691
881
|
|
|
882
|
+
_routeAccessibility(ev) {
|
|
883
|
+
for (const fn of [...this._a11yListeners]) fn(ev);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* System Settings › Accessibility › Display, as the bridge reports it —
|
|
888
|
+
* `{ reduceMotion, reduceTransparency, increaseContrast,
|
|
889
|
+
* differentiateWithoutColor, invertColors }` — or null over a bridge (or
|
|
890
|
+
* a test fake) that does not answer. The seam src/desktopsettings.js reads
|
|
891
|
+
* "reduce motion" through; `watchAccessibilityDisplay` is its other half.
|
|
892
|
+
*/
|
|
893
|
+
accessibilityDisplayOptions() {
|
|
894
|
+
return this._native.accessibilityDisplayOptions?.() ?? null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Subscribe to the options changing while the app runs. */
|
|
898
|
+
watchAccessibilityDisplay(fn) {
|
|
899
|
+
this._a11yListeners.add(fn);
|
|
900
|
+
return () => this._a11yListeners.delete(fn);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* AppKit's four destination questions, answered inside the callback: the
|
|
905
|
+
* window's transport (src/cocoa/dnd.js) drives its DropSession and sets
|
|
906
|
+
* the response before this returns. A frame is flushed on the way out,
|
|
907
|
+
* like a click's — `:drag-over` is a repaint.
|
|
908
|
+
*/
|
|
909
|
+
_routeDrop(ev) {
|
|
910
|
+
const wnd = this._window(ev);
|
|
911
|
+
if (!wnd || wnd.destroyed) return;
|
|
912
|
+
wnd._dropTransport?.handle(ev);
|
|
913
|
+
this._afterInput();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** The release of a drag AppKit was tracking for us. */
|
|
917
|
+
_routeDragEnded(ev) {
|
|
918
|
+
const drag = this._activeDrag;
|
|
919
|
+
this._activeDrag = null;
|
|
920
|
+
drag?.nativeEnded(ev);
|
|
921
|
+
this._afterInput();
|
|
922
|
+
}
|
|
923
|
+
|
|
692
924
|
_routeClose(ev) {
|
|
693
925
|
const wnd = this._window(ev);
|
|
694
926
|
if (!wnd) return;
|
|
@@ -706,6 +938,82 @@ export class CocoaApp {
|
|
|
706
938
|
this._afterInput();
|
|
707
939
|
}
|
|
708
940
|
|
|
941
|
+
// --- the application as a whole ------------------------------------------
|
|
942
|
+
//
|
|
943
|
+
// What the OS asks the app rather than one of its windows, through the
|
|
944
|
+
// NSApplicationDelegate the bridge installs (>= 0.5). The bridge decides
|
|
945
|
+
// nothing — it answers `applicationShouldHandleReopen:` NO and
|
|
946
|
+
// `applicationShouldTerminate:` Cancel and hands the question over — so
|
|
947
|
+
// each of these is the renderer's decision, and the decisions are the ones
|
|
948
|
+
// the freedesktop transport already made (src/application.js): a URL is an
|
|
949
|
+
// `Open`, a second launch is an `Activate`, and quitting is what closing
|
|
950
|
+
// the app's primary window means. Anything that arrived before the
|
|
951
|
+
// callback was installed — the launching Apple Event lands inside
|
|
952
|
+
// `initApp()` — the bridge holds and replays on the first pump.
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* `application:openURLs:` — a URL for a scheme the bundle's `Info.plist`
|
|
956
|
+
* registers, or a document the Finder handed over as `file://`. Delivered
|
|
957
|
+
* through the same filter, buffer and replay a D-Bus `Open` takes, so
|
|
958
|
+
* `useAppOpen` fires the same way here.
|
|
959
|
+
*/
|
|
960
|
+
_routeAppOpen(ev) {
|
|
961
|
+
const urls = Array.isArray(ev.urls)
|
|
962
|
+
? ev.urls.filter((u) => typeof u === 'string')
|
|
963
|
+
: [];
|
|
964
|
+
deliverOpen(urls, {});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* `applicationShouldHandleReopen:` — the user launched the app again while
|
|
969
|
+
* it was running (a Dock click, `open -a`). AppKit has already brought
|
|
970
|
+
* the windows forward by the time this arrives; what a second launch
|
|
971
|
+
* *means* is the app's, through `useAppActivate`, and `has-visible-windows`
|
|
972
|
+
* on the context is the one fact the OS adds.
|
|
973
|
+
*/
|
|
974
|
+
_routeAppReopen(ev) {
|
|
975
|
+
deliverActivate({ 'has-visible-windows': ev.hasVisibleWindows === true });
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Quit, from wherever macOS says it: the Dock's Quit, ⌘Q on the app menu, a
|
|
980
|
+
* logout. One route for all of them, and it is the **primary window's
|
|
981
|
+
* close request** — `onCloseRequest` where the app wrote one (a "save your
|
|
982
|
+
* work?" dialog is a veto until it decides), the default otherwise, which
|
|
983
|
+
* unmounts the tree. Quit means what closing the app's window means on
|
|
984
|
+
* every desktop, rather than a second, faster exit that skips the same
|
|
985
|
+
* question.
|
|
986
|
+
*
|
|
987
|
+
* With no window to ask — an accessory app between windows — the tree
|
|
988
|
+
* unmounts directly.
|
|
989
|
+
*
|
|
990
|
+
* The bridge has already answered `applicationShouldTerminate:` with
|
|
991
|
+
* Cancel, so the process outliving the tree is this side's to end: when a
|
|
992
|
+
* quit request leads to the app closing, `close()` exits the process
|
|
993
|
+
* (`cocoa.exitOnQuit`, on by default — a Regular-policy process that is
|
|
994
|
+
* still alive is still in the Dock, and a Dock tile for an app whose
|
|
995
|
+
* windows are all gone is what a user calls a hang).
|
|
996
|
+
*/
|
|
997
|
+
requestQuit() {
|
|
998
|
+
if (this._closed) return;
|
|
999
|
+
this._quitting = true;
|
|
1000
|
+
const windows = [...this._windows.values()].filter(
|
|
1001
|
+
(w) => !w.destroyed && !w._popup,
|
|
1002
|
+
);
|
|
1003
|
+
const primary =
|
|
1004
|
+
windows.find((w) => w._reactX11Node?._isPrimaryWindow?.()) ??
|
|
1005
|
+
windows[0] ??
|
|
1006
|
+
null;
|
|
1007
|
+
if (primary) {
|
|
1008
|
+
primary.emit('close', { preventDefault() {} });
|
|
1009
|
+
} else {
|
|
1010
|
+
Promise.resolve(this._reactX11Root?.unmount?.()).catch((err) => {
|
|
1011
|
+
this.options?.onXError?.(err);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
this._afterInput();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
709
1017
|
// --- teardown ------------------------------------------------------------
|
|
710
1018
|
|
|
711
1019
|
close() {
|
|
@@ -719,10 +1027,58 @@ export class CocoaApp {
|
|
|
719
1027
|
this._cocoaGL = null;
|
|
720
1028
|
this._native.setBackendEventCallback(null);
|
|
721
1029
|
for (const wnd of [...this._windows.values()]) wnd.destroy();
|
|
1030
|
+
// A quit the app accepted: the tree is down and the connection closed,
|
|
1031
|
+
// and macOS is still waiting on the Cancel the bridge answered with. End
|
|
1032
|
+
// the process on the next turn — after the unmount's own microtasks —
|
|
1033
|
+
// rather than leaving a Dock tile with nothing behind it. `exitOnQuit:
|
|
1034
|
+
// false` is the seam for an embedder that owns the process's lifetime.
|
|
1035
|
+
if (this._quitting && this.options?.cocoa?.exitOnQuit !== false) {
|
|
1036
|
+
setImmediate(() => process.exit(0));
|
|
1037
|
+
}
|
|
722
1038
|
return Promise.resolve();
|
|
723
1039
|
}
|
|
724
1040
|
}
|
|
725
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* `listScreens()` turned into the screen layout `src/screens.js` publishes.
|
|
1044
|
+
*
|
|
1045
|
+
* **One scale for every screen, and it is the app's.** macOS lays all the
|
|
1046
|
+
* displays out in a single global point space, and `app.scale` is this
|
|
1047
|
+
* app's points-to-device-pixels factor for the whole of it — window
|
|
1048
|
+
* origins, event coordinates, these rects. Converting a 1x external
|
|
1049
|
+
* display by *its own* 1 while windows on it still report `points * 2`
|
|
1050
|
+
* would put the monitor somewhere no window ever is, and `monitorAt()`
|
|
1051
|
+
* would answer with the wrong head. (What backing scale a window on a
|
|
1052
|
+
* mixed-DPI desk should raster at is a real and separate question; the
|
|
1053
|
+
* layout is not where it is answered.)
|
|
1054
|
+
*
|
|
1055
|
+
* **A usable rect per monitor.** `NSScreen.visibleFrame` is per screen —
|
|
1056
|
+
* that display's own menu bar and Dock taken off — so each monitor carries
|
|
1057
|
+
* its `visible` and `usable()` takes it as a rect. Publishing only the
|
|
1058
|
+
* primary's, the way `_NET_WORKAREA` forces on X11, applied the primary's
|
|
1059
|
+
* *width* as a bound to every other head: a second display wider than the
|
|
1060
|
+
* built-in had its right edge pulled in by the difference, and every
|
|
1061
|
+
* anchored popup that reached past it was clamped back (issue #453).
|
|
1062
|
+
*/
|
|
1063
|
+
export function screenLayout(screens, scale) {
|
|
1064
|
+
const rect = (r) => ({
|
|
1065
|
+
x: Math.round(r.x * scale),
|
|
1066
|
+
y: Math.round(r.y * scale),
|
|
1067
|
+
width: Math.round(r.width * scale),
|
|
1068
|
+
height: Math.round(r.height * scale),
|
|
1069
|
+
});
|
|
1070
|
+
const primary = screens?.[0];
|
|
1071
|
+
return {
|
|
1072
|
+
monitors: (screens ?? []).map((screen) => ({
|
|
1073
|
+
...rect(screen),
|
|
1074
|
+
...(screen.visible ? { visible: rect(screen.visible) } : null),
|
|
1075
|
+
})),
|
|
1076
|
+
// Still published for `useScreens().workArea`, which is one rect for
|
|
1077
|
+
// the desktop by definition; the primary's is the closest macOS has.
|
|
1078
|
+
workArea: primary?.visible ? rect(primary.visible) : null,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
726
1082
|
/**
|
|
727
1083
|
* Build the app and seed the platform stores the way the mock seeds them —
|
|
728
1084
|
* `beginScale`/`beginScreens`/`beginCompositing` find a session already
|
|
@@ -733,25 +1089,7 @@ export async function createCocoaApp(options = {}) {
|
|
|
733
1089
|
const app = new CocoaApp(native, options);
|
|
734
1090
|
|
|
735
1091
|
setScaleForTests(app, app.scale, 'cocoa');
|
|
736
|
-
|
|
737
|
-
const monitors = app._screens.map((screen) => ({
|
|
738
|
-
x: Math.round(screen.x * s),
|
|
739
|
-
y: Math.round(screen.y * s),
|
|
740
|
-
width: Math.round(screen.width * s),
|
|
741
|
-
height: Math.round(screen.height * s),
|
|
742
|
-
}));
|
|
743
|
-
const primary = app._screens[0];
|
|
744
|
-
setScreensForTests(app, {
|
|
745
|
-
monitors,
|
|
746
|
-
workArea: primary
|
|
747
|
-
? {
|
|
748
|
-
x: Math.round(primary.visible.x * s),
|
|
749
|
-
y: Math.round(primary.visible.y * s),
|
|
750
|
-
width: Math.round(primary.visible.width * s),
|
|
751
|
-
height: Math.round(primary.visible.height * s),
|
|
752
|
-
}
|
|
753
|
-
: null,
|
|
754
|
-
});
|
|
1092
|
+
setScreensForTests(app, screenLayout(app._screens, app.scale));
|
|
755
1093
|
setCompositingForTests(app, true);
|
|
756
1094
|
|
|
757
1095
|
app.start(options.cocoa ?? {});
|
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;
|