react-x11 2.16.0 → 2.17.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 +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/acceleratorhooks.js +40 -6
- package/src/anchor.js +20 -2
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +204 -6
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +2 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +12 -1
- package/src/idle.js +59 -1
- package/src/index.d.ts +51 -1
- package/src/index.js +30 -3
- package/src/keysymchars.js +47 -0
- package/src/keysyms.d.ts +19 -1
- package/src/keysyms.js +107 -8
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/screens.js +159 -24
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +21 -0
- package/src/types/filedialog.d.ts +3 -1
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/wayland/xkb.js +170 -59
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +128 -20
package/src/capabilities.js
CHANGED
|
@@ -79,6 +79,21 @@ function soleApp() {
|
|
|
79
79
|
return showing.length === 1 ? showing[0] : null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The mechanism a backend says it built one rung on, or null.
|
|
84
|
+
*
|
|
85
|
+
* Backends install the *same method names* for the same rung on purpose --
|
|
86
|
+
* `createStatusItem` is how `useTray` stays one hook -- so a method name
|
|
87
|
+
* cannot tell two mechanisms apart, and a probe that tried reported
|
|
88
|
+
* Shell_NotifyIcon as `cocoa`. A backend with more than one mechanism to
|
|
89
|
+
* distinguish declares them (`app.shellMechanisms`); one with nothing to
|
|
90
|
+
* disambiguate says nothing and the method checks below still answer.
|
|
91
|
+
*/
|
|
92
|
+
function shellMechanism(target, rung) {
|
|
93
|
+
const declared = target?.shellMechanisms;
|
|
94
|
+
return typeof declared?.[rung] === 'string' ? declared[rung] : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
// ---------------------------------------------------------------------------
|
|
83
98
|
// notifications
|
|
84
99
|
// ---------------------------------------------------------------------------
|
|
@@ -154,6 +169,27 @@ async function probeNotifications({ app } = {}) {
|
|
|
154
169
|
});
|
|
155
170
|
}
|
|
156
171
|
|
|
172
|
+
if (backend === 'win32') {
|
|
173
|
+
// A Shell_NotifyIcon balloon: text with a severity, shown by the tray
|
|
174
|
+
// icon and kept by the Action Center afterwards. Everything richer is a
|
|
175
|
+
// toast, which is WinRT and a different mechanism entirely.
|
|
176
|
+
return frozen('win32', {
|
|
177
|
+
actions: false,
|
|
178
|
+
events: false, // NIN_BALLOONUSERCLICK is not routed
|
|
179
|
+
update: false,
|
|
180
|
+
close: false,
|
|
181
|
+
body: true,
|
|
182
|
+
bodyMarkup: false,
|
|
183
|
+
bodyImage: false,
|
|
184
|
+
// The balloon shows the severity's own glyph. NIIF_USER would put the
|
|
185
|
+
// caller's icon there and is not wired up, so an icon cannot be chosen.
|
|
186
|
+
icon: false,
|
|
187
|
+
sound: true, // the shell plays one; NIIF_NOSOUND is not set
|
|
188
|
+
persistence: true, // it lands in the Action Center
|
|
189
|
+
urgency: true, // low/normal/critical -> NIIF_NONE/INFO/ERROR
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
157
193
|
// `osascript` and `notify-send`: one way, and that is the whole of it.
|
|
158
194
|
// `notify-send -p` prints an id on newer libnotify, which is why `update`
|
|
159
195
|
// is not flatly false there — see notifications.js.
|
|
@@ -178,6 +214,33 @@ async function probeNotifications({ app } = {}) {
|
|
|
178
214
|
|
|
179
215
|
/** The tray on an app that has one of its own, which needs nothing asked. */
|
|
180
216
|
function trayNow(target) {
|
|
217
|
+
// The mechanism is *declared* by the backend, not guessed from a method
|
|
218
|
+
// name, because two backends deliberately install the same names -- that
|
|
219
|
+
// sharing is what lets `useTray` be one hook -- and a probe that read
|
|
220
|
+
// `createStatusItem` as "this is AppKit" reported Shell_NotifyIcon as
|
|
221
|
+
// `cocoa`, with SF Symbols and click modifiers it does not have.
|
|
222
|
+
if (shellMechanism(target, 'tray') === 'shellnotifyicon') {
|
|
223
|
+
return frozen('shellnotifyicon', {
|
|
224
|
+
menu: true,
|
|
225
|
+
// No name vocabulary: the shell has no icon theme to look a name up
|
|
226
|
+
// in, so an icon here is always pixels or a file.
|
|
227
|
+
iconName: false,
|
|
228
|
+
iconBytes: true,
|
|
229
|
+
attention: false,
|
|
230
|
+
overlay: false,
|
|
231
|
+
tooltip: true,
|
|
232
|
+
// An NSStatusItem can show a label beside its icon; a notify icon is
|
|
233
|
+
// an icon. `title` is accepted and used as the tooltip, which is a
|
|
234
|
+
// fallback rather than the feature.
|
|
235
|
+
title: false,
|
|
236
|
+
click: true,
|
|
237
|
+
clickPosition: true,
|
|
238
|
+
clickRect: false,
|
|
239
|
+
clickModifiers: false,
|
|
240
|
+
// WM_MOUSEWHEEL is not delivered to a notify icon.
|
|
241
|
+
scroll: false,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
181
244
|
if (typeof target?.createStatusItem === 'function') {
|
|
182
245
|
return frozen('cocoa', {
|
|
183
246
|
menu: true,
|
|
@@ -234,15 +297,45 @@ async function probeTray({ app } = {}) {
|
|
|
234
297
|
|
|
235
298
|
/** The Dock tile on an app that has one, which needs nothing asked. */
|
|
236
299
|
function launcherNow(target) {
|
|
300
|
+
// Declared, not guessed -- see `trayNow`. This backend installs
|
|
301
|
+
// `setDockBadge` too, so the cocoa branch below used to answer for it and
|
|
302
|
+
// say `progress: false` with a working taskbar progress bar right there.
|
|
303
|
+
if (shellMechanism(target, 'launcher') === 'taskbar') {
|
|
304
|
+
return frozen('taskbar', {
|
|
305
|
+
badge: true,
|
|
306
|
+
// Drawn into the overlay icon, so it is text rather than a count --
|
|
307
|
+
// but a 16x16 overlay holds about three glyphs and longer labels come
|
|
308
|
+
// out as `99+`.
|
|
309
|
+
badgeText: true,
|
|
310
|
+
progress: true, // ITaskbarList3::SetProgressValue
|
|
311
|
+
urgent: true, // FlashWindowEx
|
|
312
|
+
// The taskbar button's menu is the jump list, whose entries start a
|
|
313
|
+
// *new* process; nothing on it can call back into this one. That is a
|
|
314
|
+
// different feature from the Dock menu, and `tasks` is its name --
|
|
315
|
+
// reporting `menu: true` here would promise a callback that never
|
|
316
|
+
// comes.
|
|
317
|
+
menu: false,
|
|
318
|
+
needsDesktopFile: false,
|
|
319
|
+
tasks: typeof target.jumpList === 'function',
|
|
320
|
+
thumbnailToolbar: typeof target.thumbnailToolbar === 'function',
|
|
321
|
+
recentDocuments: typeof target.noteRecentDocument === 'function',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
237
324
|
if (typeof target?.setDockBadge === 'function') {
|
|
238
325
|
return frozen('cocoa', {
|
|
239
326
|
badge: true,
|
|
240
327
|
badgeText: true, // the tile takes any label
|
|
241
328
|
progress: false, // NSDockTile has no progress bar
|
|
242
329
|
urgent: true, // requestUserAttention, via window states
|
|
243
|
-
menu: typeof target.
|
|
330
|
+
menu: typeof target.setLauncherMenu === 'function',
|
|
244
331
|
// The Dock always shows the app; nothing has to be installed for it.
|
|
245
332
|
needsDesktopFile: false,
|
|
333
|
+
// The Dock has no static-task menu and no hover toolbar. macOS does
|
|
334
|
+
// keep a Recent list (`noteNewRecentDocumentURL:`), but this backend
|
|
335
|
+
// has nothing wired to it, and the map says what the backend can do.
|
|
336
|
+
tasks: false,
|
|
337
|
+
thumbnailToolbar: false,
|
|
338
|
+
recentDocuments: typeof target.noteRecentDocument === 'function',
|
|
246
339
|
});
|
|
247
340
|
}
|
|
248
341
|
return null;
|
|
@@ -283,6 +376,11 @@ async function probeLauncher({ app } = {}) {
|
|
|
283
376
|
urgent: true,
|
|
284
377
|
menu: true, // the quicklist
|
|
285
378
|
needsDesktopFile: true,
|
|
379
|
+
// The quicklist is the menu; `Actions=` in the .desktop file are the
|
|
380
|
+
// closest thing to static tasks and are not driven from here.
|
|
381
|
+
tasks: false,
|
|
382
|
+
thumbnailToolbar: false,
|
|
383
|
+
recentDocuments: false,
|
|
286
384
|
});
|
|
287
385
|
}
|
|
288
386
|
|
package/src/cocoa/app.js
CHANGED
|
@@ -22,7 +22,7 @@ import { deliverActivate, deliverOpen } from '../application.js';
|
|
|
22
22
|
import { flushPendingFrames } from '../frames.js';
|
|
23
23
|
import { flushSyncWork } from '../priority.js';
|
|
24
24
|
import { setCompositingForTests } from '../compositing.js';
|
|
25
|
-
import { setScreensForTests } from '../screens.js';
|
|
25
|
+
import { setScreenPolling, setScreensForTests } from '../screens.js';
|
|
26
26
|
import { setScaleForTests } from '../scale.js';
|
|
27
27
|
import { BezelStore } from './bezels.js';
|
|
28
28
|
import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
|
|
@@ -38,7 +38,7 @@ 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 '
|
|
41
|
+
import { releaseImageUpload } from '../backend/context2d.js';
|
|
42
42
|
import { CocoaSurface } from './surface.js';
|
|
43
43
|
import { CocoaSymbols } from './symbols.js';
|
|
44
44
|
import { CocoaWindow } from './window.js';
|
|
@@ -70,6 +70,23 @@ const FRAME_SLACK_MS = 1;
|
|
|
70
70
|
// to 100, docs/windows.md §"Resize"). `createRoot({ cocoa: { resizeWait } })`
|
|
71
71
|
// sets it; 0 is no handshake at all.
|
|
72
72
|
const RESIZE_WAIT_MS = 50;
|
|
73
|
+
// How often the screen layout is re-read while something is watching it, in
|
|
74
|
+
// ms. There is no event to wait for — the bridge keeps its `NSScreen` copy
|
|
75
|
+
// current on macOS's notification and emits nothing (#617) — so
|
|
76
|
+
// `useScreens()`'s promise to re-render when a monitor is plugged in is
|
|
77
|
+
// kept by asking. 500 is half a second behind a replug, which is under the
|
|
78
|
+
// time it takes to look at the screen, against one `listScreens()` read a
|
|
79
|
+
// second (a lock and a few doubles on a worker, an `NSScreen.screens` walk
|
|
80
|
+
// on the main thread). Only while a component is subscribed:
|
|
81
|
+
// `createRoot({ cocoa: { screenPoll } })` sets it, 0 turns it off, and
|
|
82
|
+
// nothing polls in an app that never calls `useScreens()`.
|
|
83
|
+
const SCREEN_POLL_MS = 500;
|
|
84
|
+
// The floor under a re-read driven by something that is *not* a clock: a
|
|
85
|
+
// window reporting where it is, which a drag reports every frame and a
|
|
86
|
+
// display change reports once. A second is far too long to matter to a
|
|
87
|
+
// person and short enough that a rearrangement is noticed by the time they
|
|
88
|
+
// have finished moving the window (`_recheckScreens`).
|
|
89
|
+
const SCREEN_RECHECK_MS = 1000;
|
|
73
90
|
|
|
74
91
|
export class CocoaApp {
|
|
75
92
|
constructor(native, options = {}) {
|
|
@@ -157,6 +174,26 @@ export class CocoaApp {
|
|
|
157
174
|
const screens = native.listScreens();
|
|
158
175
|
this.scale = screens[0]?.scale ?? 1;
|
|
159
176
|
this._screens = screens;
|
|
177
|
+
// …and how the layout stays current, because nothing pushes it: the
|
|
178
|
+
// bridge republishes its copy on macOS's own notification but emits no
|
|
179
|
+
// event, so this side asks — before a placement reads the layout, when
|
|
180
|
+
// a window reports a move, and on a clock while `useScreens()` has a
|
|
181
|
+
// subscriber (`refreshScreens`, #617). `_screensAt` is when the last
|
|
182
|
+
// read happened, which is what throttles the ones that are not clocks.
|
|
183
|
+
this._screensAt = performance.now();
|
|
184
|
+
this._screenTimer = null;
|
|
185
|
+
const screenPoll = options.cocoa?.screenPoll ?? SCREEN_POLL_MS;
|
|
186
|
+
if (!(screenPoll >= 0) || !Number.isFinite(screenPoll)) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
'react-x11: cocoa.screenPoll is a number of milliseconds, 0 for ' +
|
|
189
|
+
`none — got ${screenPoll}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
this._screenPoll = screenPoll;
|
|
193
|
+
setScreenPolling(this, {
|
|
194
|
+
revalidate: () => this.refreshScreens(),
|
|
195
|
+
watched: (on) => this._pollScreens(on),
|
|
196
|
+
});
|
|
160
197
|
|
|
161
198
|
// The name the Dock, ⌘-Tab and the menu bar print. An unbundled
|
|
162
199
|
// process is registered with LaunchServices under its executable —
|
|
@@ -167,7 +204,7 @@ export class CocoaApp {
|
|
|
167
204
|
native.setAppName(String(appName));
|
|
168
205
|
}
|
|
169
206
|
|
|
170
|
-
// The Dock menu (src/cocoa/dock.js), installed by `
|
|
207
|
+
// The Dock menu (src/cocoa/dock.js), installed by `useLauncherMenu`.
|
|
171
208
|
this._dockMenu = new CocoaDockMenu(this);
|
|
172
209
|
// The tray items (src/cocoa/statusitem.js), by the bridge's handle —
|
|
173
210
|
// which is what a click event names them by.
|
|
@@ -504,6 +541,102 @@ export class CocoaApp {
|
|
|
504
541
|
this._windows.set(wnd._key, wnd);
|
|
505
542
|
}
|
|
506
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Re-read the screen layout and publish it if the desk has changed — the
|
|
546
|
+
* answer to a monitor plugged in, unplugged, rearranged, woken or made
|
|
547
|
+
* primary (#617). Returns whether anything moved.
|
|
548
|
+
*
|
|
549
|
+
* **`listScreens()` is always current; this side's copy was not.** The
|
|
550
|
+
* bridge republishes its `NSScreen` snapshot on
|
|
551
|
+
* `NSApplicationDidChangeScreenParametersNotification` and answers a
|
|
552
|
+
* pump-mode call from AppKit live — what was read once and kept forever
|
|
553
|
+
* is `_screens`, taken in the constructor. So the whole fix is to ask
|
|
554
|
+
* again, and the read is cheap enough to ask on demand: before a
|
|
555
|
+
* placement clamps a popup to a monitor (`availableArea` through
|
|
556
|
+
* `setScreenPolling`), when a window reports a move (`_recheckScreens`),
|
|
557
|
+
* and on a clock while `useScreens()` is mounted (`_pollScreens`).
|
|
558
|
+
*
|
|
559
|
+
* Public, and the seam for an app that knows before any of those do —
|
|
560
|
+
* `app.refreshScreens()` publishes whatever the OS says right now.
|
|
561
|
+
*
|
|
562
|
+
* **The app's scale is not re-derived.** Every rect this backend speaks
|
|
563
|
+
* in is points × `app.scale`, fixed at startup, and a window's origin,
|
|
564
|
+
* an event's coordinates and a surface's pixels all already exist in
|
|
565
|
+
* that space; moving it under them is a different and much larger change
|
|
566
|
+
* than re-reading a layout. The layout is converted at the scale the rest
|
|
567
|
+
* of the app uses, which is what keeps `monitorAt()` answering with the
|
|
568
|
+
* head a window is actually on (see `screenLayout`).
|
|
569
|
+
*/
|
|
570
|
+
refreshScreens() {
|
|
571
|
+
if (this._closed) return false;
|
|
572
|
+
this._screensAt = performance.now();
|
|
573
|
+
let screens = null;
|
|
574
|
+
try {
|
|
575
|
+
screens = this._native.listScreens?.();
|
|
576
|
+
} catch {
|
|
577
|
+
// a bridge going away, or a fake with nothing to say
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
// An empty answer is "could not tell", never "no displays": a Mac with
|
|
581
|
+
// the lid shut and no panel attached still has the desk it had, and
|
|
582
|
+
// publishing nothing would take every monitor out from under
|
|
583
|
+
// `availableArea` and size the next window against the void.
|
|
584
|
+
if (!screens?.length) return false;
|
|
585
|
+
if (sameScreens(screens, this._screens)) return false;
|
|
586
|
+
this._screens = screens;
|
|
587
|
+
setScreensForTests(this, screenLayout(screens, this.scale));
|
|
588
|
+
// A monitor's refresh rate can change under a window that never moved —
|
|
589
|
+
// a mode switch, a panel woken at 60Hz — and a window's clock is read
|
|
590
|
+
// from this list rather than kept by the display (`frameIntervalFor`).
|
|
591
|
+
for (const wnd of this._windows.values()) {
|
|
592
|
+
if (!wnd.destroyed) wnd._refreshFrameInterval?.();
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The layout, re-read at most once every `SCREEN_RECHECK_MS`.
|
|
599
|
+
*
|
|
600
|
+
* For the signals that mean "something about the desk may have moved"
|
|
601
|
+
* rather than "it did": a window reporting its position, which a drag
|
|
602
|
+
* reports every frame and a display change reports once. Plugging a
|
|
603
|
+
* monitor in moves the windows that were on it, so this is the one place
|
|
604
|
+
* the OS does tell us something — it just does not say what.
|
|
605
|
+
*/
|
|
606
|
+
_recheckScreens() {
|
|
607
|
+
if (performance.now() - this._screensAt < SCREEN_RECHECK_MS) return false;
|
|
608
|
+
return this.refreshScreens();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The layout on a clock, while something is subscribed to it.
|
|
613
|
+
*
|
|
614
|
+
* `useScreens()` says it re-renders when a monitor is plugged in or
|
|
615
|
+
* unplugged and when the arrangement changes. On X11 that is a RandR
|
|
616
|
+
* event; here there is nothing to wait for, so while a component is
|
|
617
|
+
* watching, this asks every `cocoa.screenPoll` ms — on an unref'd timer,
|
|
618
|
+
* which never holds the process open and never wakes an app that is
|
|
619
|
+
* waiting on nothing else. An app that never calls `useScreens()` pays
|
|
620
|
+
* nothing at all: the paths where a stale layout is visible ask for
|
|
621
|
+
* themselves.
|
|
622
|
+
*
|
|
623
|
+
* Started and stopped by the session as the first subscriber arrives and
|
|
624
|
+
* the last one leaves (`setScreenPolling`), so a hook that unmounts takes
|
|
625
|
+
* the clock with it.
|
|
626
|
+
*/
|
|
627
|
+
_pollScreens(on) {
|
|
628
|
+
if (this._screenTimer) {
|
|
629
|
+
clearInterval(this._screenTimer);
|
|
630
|
+
this._screenTimer = null;
|
|
631
|
+
}
|
|
632
|
+
if (!on || this._closed || !(this._screenPoll > 0)) return;
|
|
633
|
+
this._screenTimer = setInterval(
|
|
634
|
+
() => this.refreshScreens(),
|
|
635
|
+
this._screenPoll,
|
|
636
|
+
);
|
|
637
|
+
this._screenTimer.unref?.();
|
|
638
|
+
}
|
|
639
|
+
|
|
507
640
|
/**
|
|
508
641
|
* How often `wnd` may paint, in ms: the explicit `frameInterval` when the
|
|
509
642
|
* root was given one, else the period of the screen under the window's
|
|
@@ -570,8 +703,12 @@ export class CocoaApp {
|
|
|
570
703
|
this._native.setDockBadge(label == null ? null : String(label));
|
|
571
704
|
}
|
|
572
705
|
|
|
573
|
-
/**
|
|
574
|
-
|
|
706
|
+
/**
|
|
707
|
+
* The menu behind a right-click on the launcher icon — `useLauncherMenu()`.
|
|
708
|
+
* Here the launcher is the Dock, which is why the AppKit call below keeps
|
|
709
|
+
* its own name: `_native.setDockMenu` *is* what Apple calls it.
|
|
710
|
+
*/
|
|
711
|
+
setLauncherMenu(items) {
|
|
575
712
|
this._dockMenu.update(items);
|
|
576
713
|
}
|
|
577
714
|
|
|
@@ -1140,6 +1277,10 @@ export class CocoaApp {
|
|
|
1140
1277
|
_routeGeometry(ev) {
|
|
1141
1278
|
const wnd = this._window(ev);
|
|
1142
1279
|
if (!wnd || wnd.destroyed) return;
|
|
1280
|
+
// Before the window re-paces itself against the screen list below: a
|
|
1281
|
+
// display plugged in or removed moves the windows that were on it, and
|
|
1282
|
+
// this is the only thing the bridge says about it (`_recheckScreens`).
|
|
1283
|
+
this._recheckScreens();
|
|
1143
1284
|
wnd._nativeResized(ev);
|
|
1144
1285
|
wnd.emit('resize', {
|
|
1145
1286
|
width: wnd.width,
|
|
@@ -1375,6 +1516,7 @@ export class CocoaApp {
|
|
|
1375
1516
|
this._pump = null;
|
|
1376
1517
|
if (this._frameTimer) clearTimeout(this._frameTimer);
|
|
1377
1518
|
this._frameTimer = null;
|
|
1519
|
+
this._pollScreens(false);
|
|
1378
1520
|
this._cocoaGL?.destroy();
|
|
1379
1521
|
this._cocoaGL = null;
|
|
1380
1522
|
this._unsubscribe?.();
|
|
@@ -1413,6 +1555,12 @@ export class CocoaApp {
|
|
|
1413
1555
|
* *width* as a bound to every other head: a second display wider than the
|
|
1414
1556
|
* built-in had its right edge pulled in by the difference, and every
|
|
1415
1557
|
* anchored popup that reached past it was clamped back (issue #453).
|
|
1558
|
+
*
|
|
1559
|
+
* **Everything the bridge says about a screen comes through**, not only its
|
|
1560
|
+
* rects. `primary` and the panel's refresh rate were dropped here, so
|
|
1561
|
+
* `useScreens().primary` read null on macOS — every entry `primary: false`
|
|
1562
|
+
* — and `refreshRate` null beside a `frameIntervalFor` that was pacing
|
|
1563
|
+
* windows on that very number (#617).
|
|
1416
1564
|
*/
|
|
1417
1565
|
export function screenLayout(screens, scale) {
|
|
1418
1566
|
const rect = (r) => ({
|
|
@@ -1423,9 +1571,17 @@ export function screenLayout(screens, scale) {
|
|
|
1423
1571
|
});
|
|
1424
1572
|
const primary = screens?.[0];
|
|
1425
1573
|
return {
|
|
1426
|
-
monitors: (screens ?? []).map((screen) => ({
|
|
1574
|
+
monitors: (screens ?? []).map((screen, i) => ({
|
|
1427
1575
|
...rect(screen),
|
|
1428
1576
|
...(screen.visible ? { visible: rect(screen.visible) } : null),
|
|
1577
|
+
// `NSScreen.screens[0]` **is** the primary — the screen with the menu
|
|
1578
|
+
// bar, which is where macOS puts a window that names no position —
|
|
1579
|
+
// and the bridge flags it as well; the index is the same fact for a
|
|
1580
|
+
// bridge that does not.
|
|
1581
|
+
primary: screen.primary ?? i === 0,
|
|
1582
|
+
// `NSScreen.maximumFramesPerSecond`, as `fps`. 0 is the OS declining
|
|
1583
|
+
// to say (before macOS 12), which is `useScreens()`'s null.
|
|
1584
|
+
refreshRate: screen.fps > 0 ? screen.fps : null,
|
|
1429
1585
|
})),
|
|
1430
1586
|
// Still published for `useScreens().workArea`, which is one rect for
|
|
1431
1587
|
// the desktop by definition; the primary's is the closest macOS has.
|
|
@@ -1433,6 +1589,48 @@ export function screenLayout(screens, scale) {
|
|
|
1433
1589
|
};
|
|
1434
1590
|
}
|
|
1435
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Whether two `listScreens()` answers describe the same desk.
|
|
1594
|
+
*
|
|
1595
|
+
* Every field the layout is built from, in the order they arrived — which
|
|
1596
|
+
* is `NSScreen.screens`, so the order is the arrangement and the primary,
|
|
1597
|
+
* and a change in it is a change. `fps` counts because a window's frame
|
|
1598
|
+
* clock is read from it, and `scale` because a screen that switched mode
|
|
1599
|
+
* is not the screen it was even at the same size.
|
|
1600
|
+
*
|
|
1601
|
+
* Pure, and exported for that reason: it decides whether a re-read
|
|
1602
|
+
* re-renders every `useScreens()` subscriber, and a poll that publishes an
|
|
1603
|
+
* unchanged layout twice a second is a render loop rather than a fix.
|
|
1604
|
+
*/
|
|
1605
|
+
export function sameScreens(a, b) {
|
|
1606
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
1607
|
+
for (let i = 0; i < a.length; i++) {
|
|
1608
|
+
const x = a[i];
|
|
1609
|
+
const y = b[i];
|
|
1610
|
+
if (
|
|
1611
|
+
x.x !== y.x ||
|
|
1612
|
+
x.y !== y.y ||
|
|
1613
|
+
x.width !== y.width ||
|
|
1614
|
+
x.height !== y.height ||
|
|
1615
|
+
x.scale !== y.scale ||
|
|
1616
|
+
x.fps !== y.fps ||
|
|
1617
|
+
x.primary !== y.primary ||
|
|
1618
|
+
!sameRect(x.visible, y.visible)
|
|
1619
|
+
) {
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/** Two `visible` rects, either of which a bridge may not have reported. */
|
|
1627
|
+
function sameRect(a, b) {
|
|
1628
|
+
if (!a || !b) return !a === !b;
|
|
1629
|
+
return (
|
|
1630
|
+
a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1436
1634
|
/**
|
|
1437
1635
|
* Build the app and seed the platform stores the way the mock seeds them —
|
|
1438
1636
|
* `beginScale`/`beginScreens`/`beginCompositing` find a session already
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -846,7 +846,7 @@ export class CocoaFontManager {
|
|
|
846
846
|
}
|
|
847
847
|
|
|
848
848
|
/**
|
|
849
|
-
* The CTFont a glyph run draws with (`
|
|
849
|
+
* The CTFont a glyph run draws with (`BackendContext2D.drawGlyphs`): a face
|
|
850
850
|
* of this engine's at the run's size, or an ntk `Font` — `openFont()`'s —
|
|
851
851
|
* resolved to CoreText from the same bytes, so the glyph ids it shaped
|
|
852
852
|
* with hold. Null for anything else, and the run is skipped.
|
package/src/cocoa/overlay.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// `map`, `unmap`, `getContext`, `destroy` — plus `present`, which puts what
|
|
15
15
|
// was painted on the layer: ntk blits an X window's backing store on its
|
|
16
16
|
// own, where a layer's contents are a copy the bitmap has to be pushed to.
|
|
17
|
-
import {
|
|
17
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
18
|
|
|
19
19
|
export const OVERLAY_Z = 1e7 + 1;
|
|
20
20
|
|
|
@@ -115,7 +115,7 @@ export class CocoaOverlayPane {
|
|
|
115
115
|
|
|
116
116
|
getContext() {
|
|
117
117
|
if (!this._ctx) {
|
|
118
|
-
this._ctx = new
|
|
118
|
+
this._ctx = new BackendContext2D(
|
|
119
119
|
this._native,
|
|
120
120
|
() => this._ensureSurface(),
|
|
121
121
|
() => {
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// via emit. Geometry and input arrive as channel messages (the host owns
|
|
15
15
|
// layout and hit-testing — this is CPU offloading, not isolation), and the
|
|
16
16
|
// only outbound traffic is pane-present.
|
|
17
|
-
import {
|
|
17
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
18
18
|
|
|
19
19
|
let nextPaneId = 1;
|
|
20
20
|
|
|
@@ -183,7 +183,7 @@ export class CocoaPaneWindow {
|
|
|
183
183
|
|
|
184
184
|
getContext() {
|
|
185
185
|
if (!this._ctx) {
|
|
186
|
-
this._ctx = new
|
|
186
|
+
this._ctx = new BackendContext2D(
|
|
187
187
|
this._native,
|
|
188
188
|
() => this._ensureSurface(),
|
|
189
189
|
() => {
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -30,7 +30,7 @@ import { Node } from '../nodes/node.js';
|
|
|
30
30
|
import { addDamageRect, damageToPaint } from '../nodes/damage.js';
|
|
31
31
|
import { intersectRects } from '../nodes/rects.js';
|
|
32
32
|
import { EASING_CONTROL_POINTS, TRANSITION_CONTROL_POINTS } from '../styles.js';
|
|
33
|
-
import {
|
|
33
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
34
34
|
|
|
35
35
|
export const RASTER_PAD = 2; // antialiasing/italic overhang outside the ink bounds
|
|
36
36
|
|
|
@@ -431,7 +431,7 @@ export class RasterState {
|
|
|
431
431
|
this.height = height;
|
|
432
432
|
this.gen++;
|
|
433
433
|
if (!this.ctx) {
|
|
434
|
-
this.ctx = new
|
|
434
|
+
this.ctx = new BackendContext2D(
|
|
435
435
|
presenter.native,
|
|
436
436
|
() => this.surface,
|
|
437
437
|
() => this.gen,
|
package/src/cocoa/surface.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// buffer the same way on both backends and names neither:
|
|
7
7
|
//
|
|
8
8
|
// const surface = new Surface(app, { width, height }); // device pixels
|
|
9
|
-
// const ctx = surface.getContext('2d'); // a
|
|
9
|
+
// const ctx = surface.getContext('2d'); // a BackendContext2D
|
|
10
10
|
// ctx.fillRect(0, 0, width, height);
|
|
11
11
|
// surface.copyWithin({ x: 0, y: 0, width, height }, 0, -rowHeight);
|
|
12
12
|
// windowCtx.drawImage(surface, x, y); // one composite
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// one from `contentBox()` numbers, which are device pixels already
|
|
44
44
|
// (docs/scale.md). The bridge is told the app's scale so the bitmap carries
|
|
45
45
|
// it — inert for a `drawImage` source, right for a layer's contents.
|
|
46
|
-
import {
|
|
46
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
47
47
|
|
|
48
48
|
export class CocoaSurface {
|
|
49
49
|
constructor(app, { width, height, format = 'argb32' } = {}) {
|
|
@@ -113,7 +113,7 @@ export class CocoaSurface {
|
|
|
113
113
|
|
|
114
114
|
_context() {
|
|
115
115
|
if (!this._ctx) {
|
|
116
|
-
this._ctx = new
|
|
116
|
+
this._ctx = new BackendContext2D(
|
|
117
117
|
this._native,
|
|
118
118
|
() => this._handle(),
|
|
119
119
|
() => 1,
|
package/src/cocoa/window.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// an X window — attributes, reported width/height, event coordinates,
|
|
7
7
|
// _screenOrigin. The divide-by-scale into Cocoa points happens against the
|
|
8
8
|
// native layer and nowhere above it.
|
|
9
|
-
import {
|
|
9
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
10
10
|
import { CocoaDropTransport, dragSpec } from './dnd.js';
|
|
11
11
|
import { CocoaLayerPresenter } from './presenter.js';
|
|
12
12
|
import { CocoaPromotion } from './promotion.js';
|
|
@@ -794,7 +794,7 @@ export class CocoaWindow {
|
|
|
794
794
|
|
|
795
795
|
getContext() {
|
|
796
796
|
if (!this._ctx) {
|
|
797
|
-
this._ctx = new
|
|
797
|
+
this._ctx = new BackendContext2D(
|
|
798
798
|
this._native,
|
|
799
799
|
() => this._ensureSurface(),
|
|
800
800
|
() => {
|
package/src/events.js
CHANGED
|
@@ -113,6 +113,27 @@ class SyntheticEvent {
|
|
|
113
113
|
this.ctrlKey = Boolean(native?.buttons & MOD.Control);
|
|
114
114
|
this.altKey = Boolean(native?.buttons & MOD.Alt);
|
|
115
115
|
this.metaKey = Boolean(native?.buttons & MOD.Super);
|
|
116
|
+
// Where the pointer is on the **virtual screen**, in the same logical
|
|
117
|
+
// pixels `x`/`y` are in — the DOM's name for the DOM's quantity, and the
|
|
118
|
+
// one an app should reach for when it places something outside the
|
|
119
|
+
// window (a context menu at the pointer, a drag preview).
|
|
120
|
+
//
|
|
121
|
+
// `nativeEvent.rootx`/`rooty` is X11's name for it, is still there, and
|
|
122
|
+
// is still in *device* pixels: the same split as `ev.x` against
|
|
123
|
+
// `nativeEvent.x`. Defined exactly where the backend reported a position
|
|
124
|
+
// — an X11 KeyPress carries one too, so this is not pointer-events-only —
|
|
125
|
+
// and absent otherwise, because a made-up 0 would read as the screen's
|
|
126
|
+
// top-left corner rather than as "no answer".
|
|
127
|
+
if (native?.rootx !== undefined && native?.rootx !== null) {
|
|
128
|
+
// The **window's** scale, not the target's. `x`/`y` are in the target's
|
|
129
|
+
// unit on purpose, so a subtree zoomed by a `scale` prop reads its own
|
|
130
|
+
// — but a screen coordinate is not in that subtree's space at all, and
|
|
131
|
+
// dividing it by a zoom factor would put it somewhere nobody is. The
|
|
132
|
+
// drag events have always computed it this way (src/dnd.js).
|
|
133
|
+
const screen = manager.scale;
|
|
134
|
+
this.screenX = native.rootx / screen;
|
|
135
|
+
this.screenY = (native.rooty ?? 0) / screen;
|
|
136
|
+
}
|
|
116
137
|
this.defaultPrevented = false;
|
|
117
138
|
this.propagationStopped = false;
|
|
118
139
|
if (extra) Object.assign(this, extra);
|
package/src/foreignnodes.js
CHANGED
|
@@ -192,10 +192,15 @@ export class ForeignNode extends Node {
|
|
|
192
192
|
*/
|
|
193
193
|
_refuse() {
|
|
194
194
|
this._refused = true;
|
|
195
|
+
// Named by the capability, not by the backend that happens to have it.
|
|
196
|
+
// An app cannot act on "use X11", and a second backend growing embedding
|
|
197
|
+
// would make that wording wrong as well as unhelpful; what an app *can*
|
|
198
|
+
// act on is the question the last sentence names (AGENTS.md,
|
|
199
|
+
// "Vocabulary").
|
|
195
200
|
const err = new Error(
|
|
196
|
-
'react-x11: <foreign> needs
|
|
197
|
-
'
|
|
198
|
-
"useSupports('embedding') before rendering one.",
|
|
201
|
+
'react-x11: <foreign> needs a backend with cross-process window ' +
|
|
202
|
+
'embedding, and this one has none — so nothing can be put in it. ' +
|
|
203
|
+
"Ask useSupports('embedding') before rendering one.",
|
|
199
204
|
);
|
|
200
205
|
this.error = err;
|
|
201
206
|
// The client is the whole reason this node is a Tab stop by default, and
|
package/src/frame/index.js
CHANGED
|
@@ -44,6 +44,7 @@ import React, {
|
|
|
44
44
|
} from 'react';
|
|
45
45
|
|
|
46
46
|
import { useAppOrNull } from '../appcontext.js';
|
|
47
|
+
import { canEmbed } from '../embedding.js';
|
|
47
48
|
import { FrameEnv } from './env.js';
|
|
48
49
|
import { CallbackTable, PROTOCOL } from './protocol.js';
|
|
49
50
|
|
|
@@ -221,11 +222,14 @@ export function Frame({
|
|
|
221
222
|
ref,
|
|
222
223
|
}) {
|
|
223
224
|
const env = useContext(FrameEnv);
|
|
224
|
-
// A backend that composites panes from shared
|
|
225
|
-
// itself with createPaneHost; everything else embeds the pane's
|
|
226
|
-
// window through <foreign>, exactly as before.
|
|
225
|
+
// A backend that composites panes from a shared buffer (Cocoa, Windows)
|
|
226
|
+
// declares itself with createPaneHost; everything else embeds the pane's
|
|
227
|
+
// real window through <foreign>, exactly as before.
|
|
227
228
|
const appOrNull = useAppOrNull();
|
|
228
229
|
const paneApp = appOrNull?.createPaneHost ? appOrNull : null;
|
|
230
|
+
// Two mechanisms, one question: is there any way to *show* a pane here?
|
|
231
|
+
// Asked before the fork rather than after it — see the session effect.
|
|
232
|
+
const canShowPane = Boolean(paneApp) || canEmbed(appOrNull);
|
|
229
233
|
const [state, setState] = useState({
|
|
230
234
|
phase: 'starting',
|
|
231
235
|
windowId: null,
|
|
@@ -262,6 +266,28 @@ export function Frame({
|
|
|
262
266
|
setState({ phase: 'failed', windowId: null, error });
|
|
263
267
|
};
|
|
264
268
|
|
|
269
|
+
// Before the fork, not after it. A backend with neither mechanism used
|
|
270
|
+
// to start the pane, let it load its module and mount, and only then
|
|
271
|
+
// discover at the embed that there was nowhere to put it — a whole
|
|
272
|
+
// process spawned and killed to reach a conclusion the app object had
|
|
273
|
+
// all along. The fallback it renders is the same one either way; what
|
|
274
|
+
// changes is that it renders at once and costs nothing.
|
|
275
|
+
if (!canShowPane) {
|
|
276
|
+
fail(
|
|
277
|
+
Object.assign(
|
|
278
|
+
new Error(
|
|
279
|
+
'react-x11: <Frame> needs a backend that can show a pane — one ' +
|
|
280
|
+
'that composites panes from a shared buffer, or one with ' +
|
|
281
|
+
'cross-process window embedding — and this one has neither, ' +
|
|
282
|
+
'so no pane was started. Ask ' +
|
|
283
|
+
"useSupports('embedding') before rendering one.",
|
|
284
|
+
),
|
|
285
|
+
{ phase: 'embed' },
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
265
291
|
let t;
|
|
266
292
|
try {
|
|
267
293
|
t = makeTransport({ src: source, display });
|
|
@@ -396,7 +422,7 @@ export function Frame({
|
|
|
396
422
|
// ask, and everything is unref'd so nothing holds the host open
|
|
397
423
|
if (session.current === s) session.current = null;
|
|
398
424
|
};
|
|
399
|
-
}, [source, display, generation, makeTransport]);
|
|
425
|
+
}, [source, display, generation, makeTransport, canShowPane]);
|
|
400
426
|
|
|
401
427
|
// One update per commit that changed the pane's inputs, props and env in
|
|
402
428
|
// the same message — so a theme flip and the state change that caused it
|