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/README.md
CHANGED
|
@@ -18,9 +18,10 @@ Build GUI programs for a Linux desktop, for a display at the other end of an
|
|
|
18
18
|
ssh connection, or as a native Mac app, with your React / React Native
|
|
19
19
|
experience — flexbox layout, components, hooks, synthetic events.
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### Four backends, one tree
|
|
22
22
|
|
|
23
|
-
The same components, the same hooks and the same `style` objects run on
|
|
23
|
+
The same components, the same hooks and the same `style` objects run on all
|
|
24
|
+
of them. The two that are mature:
|
|
24
25
|
|
|
25
26
|
- **X11** ([the flagship](docs/remote.md)) — a Linux desktop, a display
|
|
26
27
|
forwarded over `ssh -X`, `Xvfb` in CI, a thin client, or macOS through
|
|
@@ -35,20 +36,25 @@ The same components, the same hooks and the same `style` objects run on both:
|
|
|
35
36
|
thin mechanism-only Objective-C++ bridge — an optional dependency shipping
|
|
36
37
|
prebuilds, absent on Linux installs.
|
|
37
38
|
|
|
38
|
-
`createRoot()` picks for you: **Cocoa on macOS**
|
|
39
|
-
installed, X11 via `$DISPLAY` everywhere else
|
|
40
|
-
bridge so an XQuartz
|
|
41
|
-
|
|
39
|
+
`createRoot()` picks for you: **Cocoa on macOS** and **win32 on Windows**
|
|
40
|
+
when the matching bridge is installed, X11 via `$DISPLAY` everywhere else —
|
|
41
|
+
and X11 on either of those without its bridge, so an XQuartz or WSLg setup
|
|
42
|
+
keeps working. `createRoot({ backend: 'x11' })` or `REACT_X11_BACKEND=x11`
|
|
43
|
+
pins it. On macOS the app's JS runs on a worker
|
|
42
44
|
while AppKit keeps the main thread, so a menu, a drag or a live resize never
|
|
43
45
|
stops it — `node app.js` is enough, and `REACT_X11_THREADED=0` keeps the
|
|
44
46
|
main thread ([docs/macos.md](docs/macos.md#js-on-a-worker-a-ui-thread-of-the-bridges-own)).
|
|
45
47
|
|
|
46
|
-
Two more backends
|
|
47
|
-
cross-platform support
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
Two more backends exist beside these, serving the goal of **full
|
|
49
|
+
cross-platform support**, and both are younger than the two above.
|
|
50
|
+
**native Wayland** ([docs/wayland.md](docs/wayland.md)) is opt-in rather than
|
|
51
|
+
automatic, because X11 stays the default on Linux — the remote case is the
|
|
52
|
+
flagship reason this project exists and Wayland has no network transparency.
|
|
53
|
+
**Windows** ([docs/windows.md](docs/windows.md)) — Win32 windows, Direct2D
|
|
54
|
+
and DirectWrite, DWM compositing through DirectComposition — renders and
|
|
55
|
+
takes mouse input, with the desktop integrations still to come. Each is a
|
|
56
|
+
backend beside the others rather than a replacement for any of them: the
|
|
57
|
+
same tree, the same components, the same `style` objects.
|
|
52
58
|
|
|
53
59
|
Layout is [yoga-layout](https://www.npmjs.com/package/yoga-layout) (WASM) on
|
|
54
60
|
both, and text shaping is [fontkit](https://github.com/foliojs/fontkit) on
|
|
@@ -125,17 +131,26 @@ That is the shape of the problem this is good at:
|
|
|
125
131
|
And the shape it is not good at, so you can stop here rather than in week
|
|
126
132
|
three:
|
|
127
133
|
|
|
128
|
-
- **Windows — today.**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
- **Windows — today.** The Windows backend is young, and further along than
|
|
135
|
+
that sounds. Win32 windows composited by DWM through DirectComposition,
|
|
136
|
+
Direct2D and DirectWrite behind the same contracts the Cocoa backend
|
|
137
|
+
proved, OpenGL, the keyboard and all five mouse buttons, popups, the
|
|
138
|
+
clipboard, file dialogs, the tray, the taskbar, global hotkeys,
|
|
139
|
+
notifications and window states — on a mechanism-only bridge shaped like
|
|
140
|
+
the Cocoa one, with a test suite and CI of its own. The examples run.
|
|
141
|
+
Drag and drop works both ways with the desktop, and the taskbar's own
|
|
142
|
+
surfaces — a thumbnail toolbar, a jump list, recent documents — are there
|
|
143
|
+
behind a capability, so an app that also runs elsewhere asks
|
|
144
|
+
`useSupports()` rather than the platform. What it does not have is **IME
|
|
145
|
+
and screen-reader support**: an app is unusable with a screen reader and
|
|
146
|
+
in CJK input. So an app that needs either on Windows _this month_ still
|
|
147
|
+
wants Electron or Tauri.
|
|
148
|
+
[docs/windows-integrations.md](docs/windows-integrations.md) is the
|
|
149
|
+
measured status and [docs/windows.md](docs/windows.md) the design. The three
|
|
150
|
+
targets are not the same app: the desktop-shell half of X11 (`<foreign>`
|
|
151
|
+
embedding, panel struts, substructure redirect, the window-manager example
|
|
152
|
+
below) has no macOS or Windows equivalent, and `react-x11/test` drives the
|
|
153
|
+
X11 backend only. [docs/macos.md](docs/macos.md) says which is which.
|
|
139
154
|
- **native Wayland — today.** There is no Wayland backend yet. Ordinary
|
|
140
155
|
application windows work fine on a Wayland desktop through Xwayland, which
|
|
141
156
|
is not going away — but the desktop-shell half of X11 (panel struts,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"examples:attention": "tsx examples/attention.jsx",
|
|
34
34
|
"examples:badge": "tsx examples/badge.jsx",
|
|
35
35
|
"examples:tray": "tsx examples/tray.jsx",
|
|
36
|
+
"examples:taskbar": "tsx examples/taskbar.jsx",
|
|
36
37
|
"examples:desktop": "tsx examples/desktop.jsx",
|
|
37
38
|
"examples:tooltips": "tsx examples/tooltips.jsx",
|
|
38
39
|
"examples:chat": "tsx examples/chat.jsx",
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
"optionalDependencies": {
|
|
110
111
|
"@windowkit/appkit": "^0.12.0",
|
|
111
112
|
"@windowkit/wayland": "^3.1.1",
|
|
113
|
+
"@windowkit/win32": "^0.0.1",
|
|
112
114
|
"dbus-native": "^0.15.1",
|
|
113
115
|
"x11-dri": "^0.9.0"
|
|
114
116
|
},
|
package/src/Reconciler.js
CHANGED
|
@@ -116,6 +116,9 @@ const ROOT_CONTEXT = Object.freeze({
|
|
|
116
116
|
isInsideText: false,
|
|
117
117
|
isInsideSvg: false,
|
|
118
118
|
atRoot: true,
|
|
119
|
+
// Nothing is above the root, so a window written straight into it waits for
|
|
120
|
+
// no palette — unlike one under a `<ThemeProvider>` there.
|
|
121
|
+
underRootScope: false,
|
|
119
122
|
inWindow: false,
|
|
120
123
|
});
|
|
121
124
|
|
|
@@ -191,6 +194,11 @@ const HostConfig = {
|
|
|
191
194
|
// Still at the root under a `<ThemeProvider>` written there, which
|
|
192
195
|
// draws nothing; under anything else, inside a window.
|
|
193
196
|
atRoot: parentHostContext.atRoot && type === THEME_SCOPE,
|
|
197
|
+
// *Below* one of those providers, which is a different fact: a window
|
|
198
|
+
// here is handed its palette when the scope inserts it, after it was
|
|
199
|
+
// built. `atRoot` is true at the root itself, where no provider is
|
|
200
|
+
// waiting to hand anything over, so the two cannot be one flag.
|
|
201
|
+
underRootScope: parentHostContext.atRoot && type === THEME_SCOPE,
|
|
194
202
|
// Directly inside a window, where a provider can hand a nested window
|
|
195
203
|
// on to it.
|
|
196
204
|
inWindow: type === 'window' || type === 'popup',
|
|
@@ -255,10 +263,19 @@ const HostConfig = {
|
|
|
255
263
|
case 'window':
|
|
256
264
|
// No X11 calls here: the render phase may be discarded. The real
|
|
257
265
|
// window is created top-down in the commit phase (realize).
|
|
266
|
+
// `underRootScope` goes in at construction, not after it: this
|
|
267
|
+
// constructor resolves the window's own style, and a window under a
|
|
268
|
+
// root `<ThemeProvider>` is handed that palette only when the scope
|
|
269
|
+
// inserts it (nodes/scope.js). Until then its ancestry is incomplete
|
|
270
|
+
// and every `$token` the provider defines would be reported as
|
|
271
|
+
// unknown — a warning for a style that resolves correctly a moment
|
|
272
|
+
// later, and under `REACT_X11_STRICT_TOKENS=1` a throw that killed an
|
|
273
|
+
// app whose palette was fine.
|
|
258
274
|
node = new WindowNode(
|
|
259
275
|
rootContainer,
|
|
260
276
|
windowAttributes(props, scaleOf(rootContainer)),
|
|
261
277
|
props,
|
|
278
|
+
{ awaitsRootScope: hostContext.underRootScope },
|
|
262
279
|
);
|
|
263
280
|
break;
|
|
264
281
|
case 'popup':
|
|
@@ -641,11 +658,18 @@ const isNtkApp = (v) =>
|
|
|
641
658
|
*/
|
|
642
659
|
function resolveBackend(options) {
|
|
643
660
|
const asked = options.backend ?? process.env.REACT_X11_BACKEND ?? 'auto';
|
|
644
|
-
if (
|
|
661
|
+
if (
|
|
662
|
+
asked === 'x11' ||
|
|
663
|
+
asked === 'cocoa' ||
|
|
664
|
+
asked === 'wayland' ||
|
|
665
|
+
asked === 'win32'
|
|
666
|
+
) {
|
|
667
|
+
return asked;
|
|
668
|
+
}
|
|
645
669
|
if (asked !== 'auto') {
|
|
646
670
|
throw new Error(
|
|
647
671
|
`react-x11: unknown backend ${JSON.stringify(asked)} — expected ` +
|
|
648
|
-
"'x11', 'cocoa', 'wayland' or 'auto'.",
|
|
672
|
+
"'x11', 'cocoa', 'wayland', 'win32' or 'auto'.",
|
|
649
673
|
);
|
|
650
674
|
}
|
|
651
675
|
// Naming an X endpoint is choosing X11: a `display` or a `stream` (the
|
|
@@ -655,7 +679,13 @@ function resolveBackend(options) {
|
|
|
655
679
|
if (options.display !== undefined || options.stream !== undefined) {
|
|
656
680
|
return 'x11';
|
|
657
681
|
}
|
|
658
|
-
|
|
682
|
+
if (process.platform === 'darwin') return 'cocoa';
|
|
683
|
+
// Windows has no X server to fall back to in the ordinary case, so 'auto'
|
|
684
|
+
// reaches for the native backend the way it does on a mac — and falls back
|
|
685
|
+
// to X11 below if the bridge is not installed, which is what keeps a Cygwin
|
|
686
|
+
// or WSLg setup with DISPLAY set working.
|
|
687
|
+
if (process.platform === 'win32') return 'win32';
|
|
688
|
+
return 'x11';
|
|
659
689
|
}
|
|
660
690
|
|
|
661
691
|
// What a root that opens its own connection forwards to ntk. `stream` is
|
|
@@ -769,17 +799,27 @@ export async function createRoot(options = {}) {
|
|
|
769
799
|
// synchronous block, and only where the handshake takes longer than it. It
|
|
770
800
|
// is not measurable on a Unix socket, and it is lost in the noise on a link
|
|
771
801
|
// slow enough to matter. This is the right order, not a fast one.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
802
|
+
// `REACT_X11_FONT_SOURCE` is the companion to `REACT_X11_BACKEND` above:
|
|
803
|
+
// an A/B run without touching code. ntk finds its fonts through fontconfig,
|
|
804
|
+
// and a machine with no `fc-match` — a slim container, or Windows, where
|
|
805
|
+
// the X11 backend is only ever run to compare it against the native one —
|
|
806
|
+
// has to be told where the fonts are instead. An explicit `fontSource`
|
|
807
|
+
// wins; this only fills the gap.
|
|
808
|
+
const connectX11 = () => {
|
|
809
|
+
const options = Object.fromEntries(
|
|
810
|
+
CONNECT_OPTIONS.filter((k) => rest[k] !== undefined).map((k) => [
|
|
811
|
+
k,
|
|
812
|
+
rest[k],
|
|
813
|
+
]),
|
|
780
814
|
);
|
|
815
|
+
options.fontSource ??= process.env.REACT_X11_FONT_SOURCE || undefined;
|
|
816
|
+
if (options.fontSource === undefined) delete options.fontSource;
|
|
817
|
+
return connect(options);
|
|
818
|
+
};
|
|
781
819
|
const cocoaAsked =
|
|
782
820
|
rest.backend === 'cocoa' || process.env.REACT_X11_BACKEND === 'cocoa';
|
|
821
|
+
const win32Asked =
|
|
822
|
+
rest.backend === 'win32' || process.env.REACT_X11_BACKEND === 'win32';
|
|
783
823
|
const connecting = !owned
|
|
784
824
|
? Promise.resolve(borrowed)
|
|
785
825
|
: backend === 'wayland'
|
|
@@ -791,25 +831,44 @@ export async function createRoot(options = {}) {
|
|
|
791
831
|
import('./wayland/app.js').then(({ createWaylandApp }) =>
|
|
792
832
|
createWaylandApp(rest),
|
|
793
833
|
)
|
|
794
|
-
: backend === '
|
|
795
|
-
? import('./
|
|
796
|
-
.then(({
|
|
834
|
+
: backend === 'win32'
|
|
835
|
+
? import('./win32/app.js')
|
|
836
|
+
.then(({ createWin32App }) => createWin32App(rest))
|
|
797
837
|
.catch((err) => {
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
// without it falls back to X11 so
|
|
801
|
-
// working — said once, because a silent
|
|
802
|
-
//
|
|
803
|
-
|
|
838
|
+
// Same bargain as cocoa's below: asked for by name the bridge is
|
|
839
|
+
// required and its absence is the error, reached by 'auto' a
|
|
840
|
+
// machine without it falls back to X11 so a WSLg or Cygwin setup
|
|
841
|
+
// with DISPLAY set keeps working — said once, because a silent
|
|
842
|
+
// fallback reads as the native backend being broken rather than
|
|
843
|
+
// absent.
|
|
844
|
+
if (win32Asked) throw err;
|
|
804
845
|
if (process.env.NODE_ENV !== 'production') {
|
|
805
846
|
console.warn(
|
|
806
|
-
'react-x11: no @windowkit/
|
|
847
|
+
'react-x11: no @windowkit/win32 bridge — falling back to the ' +
|
|
807
848
|
`X11 backend. (${err.message.split('\n')[0]})`,
|
|
808
849
|
);
|
|
809
850
|
}
|
|
810
851
|
return connectX11();
|
|
811
852
|
})
|
|
812
|
-
:
|
|
853
|
+
: backend === 'cocoa'
|
|
854
|
+
? import('./cocoa/app.js')
|
|
855
|
+
.then(({ createCocoaApp }) => createCocoaApp(rest))
|
|
856
|
+
.catch((err) => {
|
|
857
|
+
// Asked for by name, the bridge is required and its absence is
|
|
858
|
+
// the error (it says how to install). Reached by 'auto', a mac
|
|
859
|
+
// without it falls back to X11 so an XQuartz setup keeps
|
|
860
|
+
// working — said once, because a silent fallback would look
|
|
861
|
+
// like the native backend being broken rather than absent.
|
|
862
|
+
if (cocoaAsked) throw err;
|
|
863
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
864
|
+
console.warn(
|
|
865
|
+
'react-x11: no @windowkit/appkit bridge — falling back to the ' +
|
|
866
|
+
`X11 backend. (${err.message.split('\n')[0]})`,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return connectX11();
|
|
870
|
+
})
|
|
871
|
+
: connectX11();
|
|
813
872
|
const layout = loadLayout();
|
|
814
873
|
const integrations = loadIntegrations(); // null when there is nothing to install
|
|
815
874
|
const [app] = await Promise.all([connecting, layout, integrations]);
|
|
@@ -843,7 +902,7 @@ export async function createRoot(options = {}) {
|
|
|
843
902
|
// critical path — every rung that fails is a normal, silent "off"
|
|
844
903
|
// (docs/accessibility.md). Deliberately not awaited: a root must not
|
|
845
904
|
// wait on a bus that is not there.
|
|
846
|
-
startA11y();
|
|
905
|
+
startA11y(app);
|
|
847
906
|
|
|
848
907
|
// Before anything renders: the launch id has to be on the first toplevel
|
|
849
908
|
// before it maps, and the environment variable has to be consumed whether
|
package/src/a11y.js
CHANGED
|
@@ -1275,12 +1275,29 @@ function a11yEnabled() {
|
|
|
1275
1275
|
* it stopped — which is the debugging story for "why does Orca not see my
|
|
1276
1276
|
* app".
|
|
1277
1277
|
*/
|
|
1278
|
-
export function startA11y() {
|
|
1278
|
+
export function startA11y(app) {
|
|
1279
1279
|
if (startPromise) return startPromise;
|
|
1280
1280
|
if (!a11yEnabled()) {
|
|
1281
1281
|
startPromise = Promise.resolve(null);
|
|
1282
1282
|
return startPromise;
|
|
1283
1283
|
}
|
|
1284
|
+
// A backend with an accessibility bridge of its own answers first, and its
|
|
1285
|
+
// presence *is* the capability — the same rule the rest of the library
|
|
1286
|
+
// uses. The Windows backend installs one, because UI Automation asks
|
|
1287
|
+
// synchronously and has to be answered from a pushed copy rather than from
|
|
1288
|
+
// the live tree (docs/windows.md §Accessibility); AT-SPI's asynchronous
|
|
1289
|
+
// questions are answered from the tree itself, below.
|
|
1290
|
+
if (typeof app?.startAccessibility === 'function') {
|
|
1291
|
+
startPromise = Promise.resolve()
|
|
1292
|
+
.then(() => app.startAccessibility())
|
|
1293
|
+
.catch((err) => {
|
|
1294
|
+
if (process.env.REACT_X11_A11Y) {
|
|
1295
|
+
console.warn('react-x11: accessibility bridge failed to start:', err);
|
|
1296
|
+
}
|
|
1297
|
+
return null;
|
|
1298
|
+
});
|
|
1299
|
+
return startPromise;
|
|
1300
|
+
}
|
|
1284
1301
|
startPromise = import('./atspi.js')
|
|
1285
1302
|
.then((atspi) => atspi.start())
|
|
1286
1303
|
.catch((err) => {
|
package/src/acceleratorhooks.js
CHANGED
|
@@ -11,6 +11,35 @@ import { useEffect, useMemo, useRef } from 'react';
|
|
|
11
11
|
import { matchesShortcut } from './accelerators.js';
|
|
12
12
|
import { useTopLevelWindow } from './windowid.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A binding whose anchor resolved to nothing binds nothing — there is no
|
|
16
|
+
* event manager to register it with, and the effect runs once, so it will
|
|
17
|
+
* not come back. Silent, that is a shortcut that simply never fires, with
|
|
18
|
+
* the tree, the chord and the handler all looking correct (issue #616).
|
|
19
|
+
*
|
|
20
|
+
* Once per process, in development: the mistake is structural, and an app
|
|
21
|
+
* that made it once made it for every chord it declares.
|
|
22
|
+
*/
|
|
23
|
+
let warnedAboutAnchor = false;
|
|
24
|
+
export function resetAcceleratorWarningForTests() {
|
|
25
|
+
warnedAboutAnchor = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function warnUnanchored() {
|
|
29
|
+
if (process.env.NODE_ENV === 'production' || warnedAboutAnchor) return;
|
|
30
|
+
warnedAboutAnchor = true;
|
|
31
|
+
console.warn(
|
|
32
|
+
'react-x11: a shortcut was bound in a component with no window to hang ' +
|
|
33
|
+
'it off, so it is bound to nothing and will never fire. By default a ' +
|
|
34
|
+
"binding belongs to the tree's top-level <window> or, in an app with " +
|
|
35
|
+
'none, the root-level <popup> holding the keyboard. Anchor it ' +
|
|
36
|
+
'explicitly with `scope`:\n' +
|
|
37
|
+
' const here = useRef(null);\n' +
|
|
38
|
+
" useAccelerator([['space']], onToggle, { scope: here });\n" +
|
|
39
|
+
' return <box ref={here}>…</box>;',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
/**
|
|
15
44
|
* Bind a chord for as long as this component is mounted, anchored at
|
|
16
45
|
* `anchorRef` — the node the binding belongs to, which is what decides
|
|
@@ -30,7 +59,10 @@ export function useAcceleratorEntry(anchorRef, handle, enabled = true) {
|
|
|
30
59
|
useEffect(() => {
|
|
31
60
|
if (!enabled) return undefined;
|
|
32
61
|
const manager = anchorRef.current?.root?.events;
|
|
33
|
-
if (!manager)
|
|
62
|
+
if (!manager) {
|
|
63
|
+
warnUnanchored();
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
34
66
|
return manager.registerAccelerator({
|
|
35
67
|
anchor: () => anchorRef.current ?? null,
|
|
36
68
|
handle: (ev) => live.current?.(ev) ?? false,
|
|
@@ -52,11 +84,13 @@ export function useAcceleratorEntry(anchorRef, handle, enabled = true) {
|
|
|
52
84
|
* The handler is called with the key event, and the key is consumed.
|
|
53
85
|
*
|
|
54
86
|
* By default the binding belongs to the window the component is in, which is
|
|
55
|
-
* what an application-wide shortcut wants
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
87
|
+
* what an application-wide shortcut wants — or, in an app with no `<window>`
|
|
88
|
+
* at all, to the root-level `<popup>` that took the keyboard, which is the
|
|
89
|
+
* whole of a tray popover's UI (`useTopLevelWindow`). Two options for when
|
|
90
|
+
* that is not it: `enabled: false` unbinds it without unmounting anything,
|
|
91
|
+
* and `scope` takes a ref to a node the binding hangs off instead — the way
|
|
92
|
+
* to give a modal `<Dialog>` a shortcut of its own, since a binding on the
|
|
93
|
+
* window behind it is one the modal has taken the keyboard from.
|
|
60
94
|
*/
|
|
61
95
|
export function useAccelerator(shortcut, handler, options = {}) {
|
|
62
96
|
const { enabled = true, scope } = options;
|
package/src/anchor.js
CHANGED
|
@@ -148,7 +148,20 @@ export function deviceAnchorArea(node) {
|
|
|
148
148
|
if (!app) return null;
|
|
149
149
|
const at = screenRect(node);
|
|
150
150
|
const s = node.scale ?? 1;
|
|
151
|
-
|
|
151
|
+
// The whole rect, not its corner: a window straddling two monitors has
|
|
152
|
+
// nodes on each, and the monitor a node is on is the one it covers most of
|
|
153
|
+
// (`monitorAt`, src/screens.js).
|
|
154
|
+
return availableArea(
|
|
155
|
+
app,
|
|
156
|
+
at
|
|
157
|
+
? {
|
|
158
|
+
x: at.x * s,
|
|
159
|
+
y: at.y * s,
|
|
160
|
+
width: at.width * s,
|
|
161
|
+
height: at.height * s,
|
|
162
|
+
}
|
|
163
|
+
: null,
|
|
164
|
+
);
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
/**
|
|
@@ -248,7 +261,12 @@ export function anchorScreenRect(app, rect, options = {}) {
|
|
|
248
261
|
width: (rect.width ?? 0) * s,
|
|
249
262
|
height: (rect.height ?? 0) * s,
|
|
250
263
|
};
|
|
251
|
-
|
|
264
|
+
// The monitor is picked from the whole rect rather than its top-left
|
|
265
|
+
// corner, because a tray item's frame is not inside its own display: a
|
|
266
|
+
// menu-bar button reports a rect that starts a few points above the top
|
|
267
|
+
// edge, and the corner alone lands on whichever display happens to reach
|
|
268
|
+
// up past it — a different one, on a desk with a taller head (#618).
|
|
269
|
+
const area = app ? availableArea(app, anchor) : null;
|
|
252
270
|
return placeAgainst(anchor, anchor, area, s, options);
|
|
253
271
|
}
|
|
254
272
|
|
package/src/appcontext.js
CHANGED
|
@@ -80,6 +80,14 @@ const FEATURES = {
|
|
|
80
80
|
},
|
|
81
81
|
embedding: { watch: NEVER_CHANGES, read: canEmbed },
|
|
82
82
|
glOverlay: { watch: NEVER_CHANGES, read: canOverlay },
|
|
83
|
+
// Everything here is a property of the **display**: what the server, the
|
|
84
|
+
// compositor and the drawing pipeline can do. A desktop's own surfaces --
|
|
85
|
+
// a tray, a launcher icon and what hangs off it, a notification daemon --
|
|
86
|
+
// are a different question with a different shape, because they can appear
|
|
87
|
+
// and vanish while the process runs and because "available" alone is a lie
|
|
88
|
+
// about them. They go through `desktopCapability()` (src/capabilities.js),
|
|
89
|
+
// which answers `{ available, backend, features }`. Adding one here would
|
|
90
|
+
// flatten that to a boolean and lose the mechanism with it.
|
|
83
91
|
};
|
|
84
92
|
|
|
85
93
|
/**
|
package/src/appearance.js
CHANGED
|
@@ -955,6 +955,38 @@ async function macosRung() {
|
|
|
955
955
|
return runWatcher();
|
|
956
956
|
}
|
|
957
957
|
|
|
958
|
+
// --------------------------------------------------------------------------
|
|
959
|
+
// Rung 4: Windows
|
|
960
|
+
// --------------------------------------------------------------------------
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* The backend answers this one rather than this file reaching for a bridge:
|
|
964
|
+
* the values live behind a native addon that exists only on Windows, and
|
|
965
|
+
* asking the app for them is the capability test every other ladder here
|
|
966
|
+
* makes (AGENTS.md, "Find the mechanism, not the platform").
|
|
967
|
+
*
|
|
968
|
+
* All four values come from one source — the Personalize key for light or
|
|
969
|
+
* dark, DWM for the accent, and the two accessibility flags from
|
|
970
|
+
* `SystemParametersInfo` — so this rung owns every field, which is the rule
|
|
971
|
+
* the ladder is ordered for.
|
|
972
|
+
*
|
|
973
|
+
* Live rather than polled: Windows broadcasts `WM_SETTINGCHANGE`,
|
|
974
|
+
* `WM_THEMECHANGED` and `WM_DWMCOLORIZATIONCOLORCHANGED` to every top-level
|
|
975
|
+
* window, and the bridge's UI thread has one.
|
|
976
|
+
*/
|
|
977
|
+
async function windowsRung(app) {
|
|
978
|
+
if (typeof app?.systemAppearance !== 'function') return false;
|
|
979
|
+
const values = app.systemAppearance();
|
|
980
|
+
if (!values) return false;
|
|
981
|
+
publish(values, 'windows');
|
|
982
|
+
app.onAppearanceChange?.(() => {
|
|
983
|
+
if (owner !== 'windows') return;
|
|
984
|
+
const next = app.systemAppearance();
|
|
985
|
+
if (next) publish(next, 'windows');
|
|
986
|
+
});
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
|
|
958
990
|
// Killed rather than left behind: `unref()` keeps it from holding *this*
|
|
959
991
|
// process open, and nothing keeps it from outliving it.
|
|
960
992
|
process.on('exit', () => {
|
|
@@ -975,6 +1007,10 @@ async function runLadder(app) {
|
|
|
975
1007
|
// an XQuartz display never reaches this at all, which is correct — it
|
|
976
1008
|
// cannot read that Mac's defaults.
|
|
977
1009
|
['macos', macosRung],
|
|
1010
|
+
// Before XSETTINGS for the same reason macOS is: where the process is
|
|
1011
|
+
// Windows, the desktop's own preference is the one the user set, and an
|
|
1012
|
+
// XSETTINGS daemon there would be something they installed by hand.
|
|
1013
|
+
['windows', () => windowsRung(app)],
|
|
978
1014
|
['xsettings', () => xsettingsRung(app)],
|
|
979
1015
|
]) {
|
|
980
1016
|
let answered = false;
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
// A canvas-shaped 2d context over a
|
|
1
|
+
// A canvas-shaped 2d context over a **verb table** — the drawing dialect
|
|
2
|
+
// every native backend answers, and the one place it is written down.
|
|
2
3
|
//
|
|
3
|
-
// This is
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// This is macos.md §"The split" step 4, taken when a second native backend
|
|
5
|
+
// started: the class takes its `native` as a constructor argument and calls
|
|
6
|
+
// nothing else, so one wrapper drives @windowkit/appkit's CoreGraphics verbs
|
|
7
|
+
// and @windowkit/win32's Direct2D verbs alike. A bridge that lacks an
|
|
8
|
+
// optional verb is feature-detected (`ctxSetBlendMode`, `blitSurface`,
|
|
9
|
+
// `ctxDrawSymbol`) and degrades rather than throwing.
|
|
10
|
+
//
|
|
11
|
+
// **Wayland is deliberately not here.** src/wayland/context2d.js is not
|
|
12
|
+
// another copy of this file — it is a GLES rasterizer, wayland.md's Tier D,
|
|
13
|
+
// which implements the same dialect by drawing it rather than by forwarding
|
|
14
|
+
// it. The two are different layers, not duplicates, and folding one into the
|
|
15
|
+
// other would lose that.
|
|
16
|
+
//
|
|
17
|
+
// On the surface presenter this is the whole drawing path; on the layer
|
|
18
|
+
// presenter it stays as the fallback every painted-code node (<canvas>,
|
|
19
|
+
// <svg>, registered elements) rasters through — docs/macos.md §"Custom
|
|
20
|
+
// drawing on a layer tree".
|
|
7
21
|
//
|
|
8
22
|
// The native surface holds the real graphics state (paths, CTM, clip); this
|
|
9
23
|
// class keeps the JS-visible state (fillStyle strings, gradient objects,
|
|
@@ -265,7 +279,7 @@ class SolidPicture {
|
|
|
265
279
|
}
|
|
266
280
|
}
|
|
267
281
|
|
|
268
|
-
export class
|
|
282
|
+
export class BackendContext2D {
|
|
269
283
|
/**
|
|
270
284
|
* @param native the @windowkit/appkit module
|
|
271
285
|
* @param surfaceOf () => current surface handle — the owner replaces the
|
|
@@ -1425,9 +1439,15 @@ export class CocoaContext2D {
|
|
|
1425
1439
|
const family = m ? m[4] : 'sans-serif';
|
|
1426
1440
|
const weight = m?.[2] === 'bold' ? 700 : m?.[2] ? Number(m[2]) : 400;
|
|
1427
1441
|
const style = m?.[1] ? 'italic' : 'normal';
|
|
1442
|
+
// No colour unless the caller named one, which is what `_contextInk`
|
|
1443
|
+
// above is waiting for: a layout with no ink of its own is drawn with the
|
|
1444
|
+
// context's fill, the way `fillText` is defined to be. Defaulting the
|
|
1445
|
+
// base to black instead made every layout carry an ink, so `fillStyle`
|
|
1446
|
+
// was read, found to be irrelevant, and never applied — a `fillText`
|
|
1447
|
+
// under a white fill came out black on both backends.
|
|
1428
1448
|
return fonts.layout(
|
|
1429
1449
|
[{ text: String(text), family, size, weight, style, color }],
|
|
1430
|
-
{ family, size, weight, style, color
|
|
1450
|
+
{ family, size, weight, style, color },
|
|
1431
1451
|
{},
|
|
1432
1452
|
);
|
|
1433
1453
|
}
|