react-x11 2.16.1 → 2.17.1
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/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 +17 -9
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/glarea.js +48 -10
- 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 +23 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +21 -7
- package/src/idle.js +59 -1
- package/src/index.d.ts +41 -0
- package/src/index.js +30 -3
- 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/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 +16 -0
- 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/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 +66 -0
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.1",
|
|
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/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
|
}
|
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
|
|