react-x11 2.16.1 → 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/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 +8 -4
- 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 +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/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
|
package/src/glnodes.js
CHANGED
|
@@ -58,9 +58,20 @@ export function glxConfig(app, spec) {
|
|
|
58
58
|
.chooseGLXConfig(spec)
|
|
59
59
|
.then((config) => ({ backend: 'indirect', ...config }));
|
|
60
60
|
} else {
|
|
61
|
+
// Which backend is asking decides what the honest answer is. Telling a
|
|
62
|
+
// Windows user to upgrade ntk sends them after a package that backend
|
|
63
|
+
// does not use, and an error a developer cannot act on is worse than
|
|
64
|
+
// the feature simply being absent (AGENTS.md, "An error you hit is an
|
|
65
|
+
// error an app developer will hit").
|
|
61
66
|
promise = Promise.reject(
|
|
62
67
|
new Error(
|
|
63
|
-
|
|
68
|
+
process.platform === 'win32'
|
|
69
|
+
? 'react-x11: <glarea> is not built on the win32 backend yet — it ' +
|
|
70
|
+
'needs ANGLE (EGL and GLES over Direct3D 11), which ' +
|
|
71
|
+
'docs/windows.md plans as an optional dependency the way ' +
|
|
72
|
+
'x11-dri is on X11. Everything else on this backend works ' +
|
|
73
|
+
'without it; for GL content today, use the X11 backend.'
|
|
74
|
+
: 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
|
|
64
75
|
),
|
|
65
76
|
);
|
|
66
77
|
}
|
package/src/idle.js
CHANGED
|
@@ -186,6 +186,17 @@ class IdleWatcher {
|
|
|
186
186
|
*/
|
|
187
187
|
async function armIdle(watcher) {
|
|
188
188
|
const session = watcher.session;
|
|
189
|
+
|
|
190
|
+
// The Windows rung, first because it is the only one there: everything
|
|
191
|
+
// below this reaches into `app.X`, and a tree that is not on an X server
|
|
192
|
+
// has none. `GetLastInputInfo` answers for the whole session, which is what
|
|
193
|
+
// an idle timeout means, so the shape is the polling rung's — with the
|
|
194
|
+
// answer arriving synchronously instead of over a connection.
|
|
195
|
+
if (typeof session.app?.lastInputMs === 'function') {
|
|
196
|
+
pollLastInput(watcher);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
const counter = await session.counter();
|
|
190
201
|
if (watcher.stopped) return;
|
|
191
202
|
|
|
@@ -281,6 +292,31 @@ function poll(watcher, saver) {
|
|
|
281
292
|
});
|
|
282
293
|
}
|
|
283
294
|
|
|
295
|
+
/**
|
|
296
|
+
* The same adaptive wait as {@link poll}, over a counter this process can read
|
|
297
|
+
* without asking anybody: `GetLastInputInfo` is a call, not a round trip.
|
|
298
|
+
*
|
|
299
|
+
* The two directions are still asymmetric for the same reason. Not idle yet:
|
|
300
|
+
* sleep for exactly the remainder. Idle: nothing says when the user will come
|
|
301
|
+
* back, and no input reaches this process while they are typing in another
|
|
302
|
+
* window, so poll on an interval scaled to the timeout.
|
|
303
|
+
*/
|
|
304
|
+
function pollLastInput(watcher) {
|
|
305
|
+
if (watcher.stopped) return;
|
|
306
|
+
const elapsed = watcher.session.app.lastInputMs?.();
|
|
307
|
+
if (typeof elapsed !== 'number') return;
|
|
308
|
+
const idle = elapsed >= watcher.timeout;
|
|
309
|
+
watcher.set(idle);
|
|
310
|
+
clearTimeout(watcher._timer);
|
|
311
|
+
watcher._timer = setTimeout(
|
|
312
|
+
() => pollLastInput(watcher),
|
|
313
|
+
idle
|
|
314
|
+
? Math.min(30_000, Math.max(1_000, watcher.timeout / 4))
|
|
315
|
+
: Math.max(250, watcher.timeout - elapsed),
|
|
316
|
+
);
|
|
317
|
+
watcher._timer.unref?.();
|
|
318
|
+
}
|
|
319
|
+
|
|
284
320
|
function schedule(watcher, saver, delay) {
|
|
285
321
|
clearTimeout(watcher._timer);
|
|
286
322
|
watcher._timer = setTimeout(() => poll(watcher, saver), delay);
|
|
@@ -364,7 +400,12 @@ export function setIdleForTests(app, timeout, idle) {
|
|
|
364
400
|
* code and for tests.
|
|
365
401
|
*/
|
|
366
402
|
export async function keepAwake({ reason = 'Busy', app = null } = {}) {
|
|
367
|
-
for (const rung of [
|
|
403
|
+
for (const rung of [
|
|
404
|
+
windowsInhibit,
|
|
405
|
+
portalInhibit,
|
|
406
|
+
screenSaverInhibit,
|
|
407
|
+
xInhibit,
|
|
408
|
+
]) {
|
|
368
409
|
try {
|
|
369
410
|
const release = await rung(reason, app);
|
|
370
411
|
if (release) return once(release);
|
|
@@ -375,6 +416,23 @@ export async function keepAwake({ reason = 'Busy', app = null } = {}) {
|
|
|
375
416
|
return () => {};
|
|
376
417
|
}
|
|
377
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Rung 0: `SetThreadExecutionState`, on Windows.
|
|
421
|
+
*
|
|
422
|
+
* Above the portal rungs because it is the only one a Windows session has,
|
|
423
|
+
* and below nothing: on a desktop with a portal this returns null on the
|
|
424
|
+
* first line and costs a property read.
|
|
425
|
+
*
|
|
426
|
+
* `reason` is dropped rather than passed. Windows takes no string with the
|
|
427
|
+
* call — `powercfg /requests` names the process, not a reason — and inventing
|
|
428
|
+
* somewhere to put it would be pretending the system shows it.
|
|
429
|
+
*/
|
|
430
|
+
async function windowsInhibit(reason, app) {
|
|
431
|
+
const hold = app?.keepAwake;
|
|
432
|
+
if (typeof hold !== 'function') return null;
|
|
433
|
+
return hold.call(app, true);
|
|
434
|
+
}
|
|
435
|
+
|
|
378
436
|
/** A release that runs once however many times it is called — a double
|
|
379
437
|
* release would drop somebody else's inhibition on the counted X rung. */
|
|
380
438
|
function once(fn) {
|
package/src/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export * from './types/fonts.js';
|
|
|
27
27
|
export * from './types/system.js';
|
|
28
28
|
export * from './types/capabilities.js';
|
|
29
29
|
export * from './types/launcher.js';
|
|
30
|
+
export * from './types/taskbar.js';
|
|
30
31
|
export * from './types/tray.js';
|
|
31
32
|
export * from './types/permissions.js';
|
|
32
33
|
export * from './types/notifications.js';
|
|
@@ -62,6 +63,46 @@ export function useWindowId(
|
|
|
62
63
|
ref: RefObject<NtkWindow | DrawnNode | null>,
|
|
63
64
|
): () => number | null;
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The handle **another process** embeds to show this window, or `null` where
|
|
68
|
+
* this backend cannot hand one out.
|
|
69
|
+
*
|
|
70
|
+
* The companion to `<window embeddable>`, and deliberately not
|
|
71
|
+
* {@link windowIdOf}: on X11 the two are the same number, and everywhere
|
|
72
|
+
* else they are not.
|
|
73
|
+
*
|
|
74
|
+
* - **X11** — the window's XID, which means the same thing in every process
|
|
75
|
+
* on the display.
|
|
76
|
+
* - **Windows** — a composition surface handle, already valid in the host
|
|
77
|
+
* process. A window cannot be embedded here (a composition target stops
|
|
78
|
+
* presenting once its window is a child), so the *buffer* crosses instead
|
|
79
|
+
* and the host binds it to a visual of its own. The host is the parent
|
|
80
|
+
* process unless `createRoot({ win32: { paneHostPid } })` says otherwise.
|
|
81
|
+
* - **Anything else** — `null`, which is the capability rather than a
|
|
82
|
+
* failure.
|
|
83
|
+
*
|
|
84
|
+
* Pass it to the host out of band, as an XID is passed: argv, an environment
|
|
85
|
+
* variable, a message. What the host does with it differs per platform; what
|
|
86
|
+
* an app writes to get it does not.
|
|
87
|
+
*/
|
|
88
|
+
export function windowHandleOf(
|
|
89
|
+
target:
|
|
90
|
+
| NtkWindow
|
|
91
|
+
| DrawnNode
|
|
92
|
+
| RefObject<NtkWindow | DrawnNode | null>
|
|
93
|
+
| null
|
|
94
|
+
| undefined,
|
|
95
|
+
): number | null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `windowHandleOf` bound to a ref. A **getter**, stable across renders, for
|
|
99
|
+
* {@link useWindowId}'s reason: the window is not realized on the render
|
|
100
|
+
* that declares it.
|
|
101
|
+
*/
|
|
102
|
+
export function useWindowHandle(
|
|
103
|
+
ref: RefObject<NtkWindow | DrawnNode | null>,
|
|
104
|
+
): () => number | null;
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* Parse a `text/uri-list` payload (RFC 2483): CRLF-separated,
|
|
67
108
|
* percent-encoded, `#` lines are comments. What `DropEvent.files` is made
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,13 @@ import './bootstrap.js';
|
|
|
4
4
|
|
|
5
5
|
export { createRoot, Renderer } from './Reconciler.js';
|
|
6
6
|
export { createStyles, flattenStyle } from './styles.js';
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
windowIdOf,
|
|
9
|
+
useWindowId,
|
|
10
|
+
windowHandleOf,
|
|
11
|
+
useWindowHandle,
|
|
12
|
+
useTopLevelWindow,
|
|
13
|
+
} from './windowid.js';
|
|
8
14
|
export { launchTimestamp, notifyStartupComplete } from './startup.js';
|
|
9
15
|
export { activateWindow } from './activate.js';
|
|
10
16
|
export { lastInputTime, serverTime } from './inputtime.js';
|
|
@@ -14,9 +20,30 @@ export {
|
|
|
14
20
|
registerApplication,
|
|
15
21
|
} from './application.js';
|
|
16
22
|
export { useAppActivate, useAppOpen } from './apphooks.js';
|
|
17
|
-
export {
|
|
18
|
-
|
|
23
|
+
export {
|
|
24
|
+
setBadge,
|
|
25
|
+
setLauncherMenu,
|
|
26
|
+
setProgress,
|
|
27
|
+
setQuicklist,
|
|
28
|
+
setUrgent,
|
|
29
|
+
} from './launcher.js';
|
|
30
|
+
export {
|
|
31
|
+
useBadge,
|
|
32
|
+
useDockMenu,
|
|
33
|
+
useLauncherMenu,
|
|
34
|
+
useProgress,
|
|
35
|
+
} from './launcherhooks.js';
|
|
19
36
|
export { useTray } from './trayhooks.js';
|
|
37
|
+
// The Windows taskbar's own surfaces. Exported unconditionally and inert
|
|
38
|
+
// where the backend has none — `useDesktopCapability('launcher').features`
|
|
39
|
+
// carries `tasks`, `thumbnailToolbar` and `recentDocuments`, so an app
|
|
40
|
+
// branches on what this desktop has rather than on the platform.
|
|
41
|
+
export {
|
|
42
|
+
noteRecentDocument,
|
|
43
|
+
useJumpList,
|
|
44
|
+
useRecentDocument,
|
|
45
|
+
useThumbnailToolbar,
|
|
46
|
+
} from './taskbarhooks.js';
|
|
20
47
|
export {
|
|
21
48
|
CAPABILITIES,
|
|
22
49
|
NO_CAPABILITY,
|
package/src/launcher.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// desktops, two mechanisms, one call each:
|
|
7
7
|
//
|
|
8
8
|
// 1. **the app's own tile** — the cocoa backend's `NSDockTile`, reached
|
|
9
|
-
// through the app object (`setDockBadge`, `
|
|
9
|
+
// through the app object (`setDockBadge`, `setLauncherMenu`, src/cocoa/
|
|
10
10
|
// app.js).
|
|
11
11
|
// 2. **`com.canonical.Unity.LauncherEntry`** over the session bus — the
|
|
12
12
|
// protocol Unity defined and the KDE, elementary, Cairo-Dock and
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
// is runtime, it follows state, and Dash-to-Dock/ubuntu-dock, Plank and the
|
|
35
35
|
// Unity heritage launchers all render it.
|
|
36
36
|
//
|
|
37
|
-
// So `
|
|
37
|
+
// So `useLauncherMenu()` has a Linux rung after all, and the three menus an app
|
|
38
38
|
// puts on the desktop — panel, tray, launcher — are now one authoring model
|
|
39
39
|
// on both backends. `.desktop` actions are still the right place for entries
|
|
40
40
|
// that must work *while the app is not running*; they are a different feature
|
|
41
41
|
// wearing a similar hat.
|
|
42
42
|
//
|
|
43
|
-
// Nothing here imports react: `useBadge`/`
|
|
43
|
+
// Nothing here imports react: `useBadge`/`useLauncherMenu` (launcherhooks.js) are
|
|
44
44
|
// the hooks, and these are the functions under them, callable from host-side
|
|
45
45
|
// code with no tree. The entry on the bus is held for as long as anything is
|
|
46
46
|
// shown and released when the last of it is cleared — a held bus ref is a
|
|
@@ -300,7 +300,8 @@ export async function setUrgent(urgent, { app } = {}) {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
|
-
* The menu behind a right-click on the app's launcher icon — the
|
|
303
|
+
* The menu behind a right-click on the app's launcher icon — the Dock menu on
|
|
304
|
+
* macOS, the quicklist on Linux.
|
|
304
305
|
*
|
|
305
306
|
* Takes `MenuBar`'s item vocabulary and exports it as a `com.canonical.dbusmenu`
|
|
306
307
|
* tree, exactly as the tray and the global menu do. `null` takes it down.
|
|
@@ -310,12 +311,12 @@ export async function setUrgent(urgent, { app } = {}) {
|
|
|
310
311
|
* launcher that builds its client the instant it sees `quicklist` finds an
|
|
311
312
|
* object there.
|
|
312
313
|
*/
|
|
313
|
-
export async function
|
|
314
|
+
export async function setLauncherMenu(items, { app } = {}) {
|
|
314
315
|
const target = app ?? soleApp();
|
|
315
316
|
|
|
316
|
-
// Rung 1: the app's own
|
|
317
|
-
if (typeof target?.
|
|
318
|
-
target.
|
|
317
|
+
// Rung 1: the app's own icon menu.
|
|
318
|
+
if (typeof target?.setLauncherMenu === 'function') {
|
|
319
|
+
target.setLauncherMenu(items ?? null);
|
|
319
320
|
return true;
|
|
320
321
|
}
|
|
321
322
|
|
|
@@ -367,6 +368,14 @@ export async function setQuicklist(items, { app } = {}) {
|
|
|
367
368
|
return true;
|
|
368
369
|
}
|
|
369
370
|
|
|
371
|
+
/**
|
|
372
|
+
* @deprecated Renamed to {@link setLauncherMenu}. "Quicklist" is the Unity
|
|
373
|
+
* launcher's word for the menu macOS calls the Dock menu; this function
|
|
374
|
+
* always drove both, and the name only ever named one of them. Kept working
|
|
375
|
+
* and kept quiet.
|
|
376
|
+
*/
|
|
377
|
+
export const setQuicklist = setLauncherMenu;
|
|
378
|
+
|
|
370
379
|
/** Test seam, not public: drop the exported entry without emitting. */
|
|
371
380
|
export async function _resetLauncher() {
|
|
372
381
|
await releaseEntry();
|
package/src/launcherhooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `useBadge()`, `useProgress()` and `
|
|
1
|
+
// `useBadge()`, `useProgress()` and `useLauncherMenu()` — the launcher's view of
|
|
2
2
|
// the app, as things a component declares rather than manages.
|
|
3
3
|
//
|
|
4
4
|
// All three now have a rung on both backends except progress, which has one
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// feature an app should branch on, and a development warning for every
|
|
8
8
|
// desktop that lacks one is a warning nobody can act on.
|
|
9
9
|
//
|
|
10
|
-
// The one that changed shape is `
|
|
10
|
+
// The one that changed shape is `useLauncherMenu`. It used to be cocoa-only on
|
|
11
11
|
// the grounds that the freedesktop counterpart was an install step — see
|
|
12
12
|
// `launcher.js`, where that reasoning is corrected: the launcher protocol
|
|
13
13
|
// carries a `quicklist` dbusmenu, so the Dock menu is runtime code on both.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { useEffect, useRef } from 'react';
|
|
16
16
|
|
|
17
17
|
import { useAppOrNull } from './appcontext.js';
|
|
18
|
-
import { setBadge,
|
|
18
|
+
import { setBadge, setLauncherMenu, setProgress } from './launcher.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Show `value` on the app's icon while this component is mounted, and clear
|
|
@@ -63,12 +63,12 @@ export function useProgress(value) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* The menu behind a right-click on the app's icon
|
|
67
|
-
* from the same `items` vocabulary `MenuBar`
|
|
68
|
-
* item's `onSelect` fires when the user picks it.
|
|
66
|
+
* The menu behind a right-click on the app's **launcher icon** — the Dock on
|
|
67
|
+
* macOS, the launcher on Linux — from the same `items` vocabulary `MenuBar`
|
|
68
|
+
* and `ContextMenu` take. An item's `onSelect` fires when the user picks it.
|
|
69
69
|
*
|
|
70
70
|
* ```jsx
|
|
71
|
-
*
|
|
71
|
+
* useLauncherMenu([
|
|
72
72
|
* { label: 'New Window', onSelect: openWindow },
|
|
73
73
|
* { type: 'separator' },
|
|
74
74
|
* { label: 'Recent', items: recent.map(toItem) },
|
|
@@ -80,8 +80,14 @@ export function useProgress(value) {
|
|
|
80
80
|
* backend; the launcher protocol's `quicklist` on Linux, which needs the
|
|
81
81
|
* identity `registerApplication({ appId })` establishes and a `.desktop` file
|
|
82
82
|
* of that name for a launcher to attach it to.
|
|
83
|
+
*
|
|
84
|
+
* It reads `useDesktopCapability('launcher').features.menu`, which is where
|
|
85
|
+
* the name comes from: every desktop has one icon standing for this
|
|
86
|
+
* application and calls it something different — Dock, taskbar, panel, dash —
|
|
87
|
+
* and `launcher` is the one word that is none of their words and all of their
|
|
88
|
+
* meanings (AGENTS.md, "Vocabulary").
|
|
83
89
|
*/
|
|
84
|
-
export function
|
|
90
|
+
export function useLauncherMenu(items) {
|
|
85
91
|
const app = useAppOrNull();
|
|
86
92
|
// read at activation time, so a pick three minutes from now runs the
|
|
87
93
|
// handler from the current render rather than the mounting one
|
|
@@ -89,12 +95,20 @@ export function useDockMenu(items) {
|
|
|
89
95
|
live.current = items;
|
|
90
96
|
|
|
91
97
|
useEffect(() => {
|
|
92
|
-
|
|
98
|
+
setLauncherMenu(items ?? null, { app }).catch(() => {});
|
|
93
99
|
}, [items, app]);
|
|
94
100
|
|
|
95
101
|
useEffect(() => {
|
|
96
102
|
return () => {
|
|
97
|
-
|
|
103
|
+
setLauncherMenu(null, { app }).catch(() => {});
|
|
98
104
|
};
|
|
99
105
|
}, [app]);
|
|
100
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @deprecated Renamed to {@link useLauncherMenu}. "Dock" is one desktop's
|
|
110
|
+
* word for the icon every desktop has; this hook drove the Linux launcher's
|
|
111
|
+
* quicklist long before the name caught up. Kept working, and kept quiet —
|
|
112
|
+
* an alias that warned would punish an app for code that is still correct.
|
|
113
|
+
*/
|
|
114
|
+
export const useDockMenu = useLauncherMenu;
|
package/src/node.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface ImageDataLike {
|
|
|
40
40
|
/**
|
|
41
41
|
* The 2d context a node paints into — the canvas-shaped subset **both**
|
|
42
42
|
* backends implement: ntk's `RenderingContext2D` over XRender on X11, and
|
|
43
|
-
* `
|
|
43
|
+
* `BackendContext2D` over CoreGraphics on macOS. Declared as the contract an
|
|
44
44
|
* element may rely on rather than as either class: what is here is on
|
|
45
45
|
* both, and a member one backend has and the other does not is optional
|
|
46
46
|
* here or absent. Coordinates are device pixels in the owning window's
|
package/src/nodes/cascade.js
CHANGED
|
@@ -422,6 +422,15 @@ export class NodeCascade {
|
|
|
422
422
|
get placed() {
|
|
423
423
|
const owner = this.isWindow ? this : this.root;
|
|
424
424
|
if (!owner) return false;
|
|
425
|
+
// A window written under a root `<ThemeProvider>` is handed that palette
|
|
426
|
+
// when the scope inserts it, which is after it and its subtree were built
|
|
427
|
+
// and first resolved (nodes/scope.js `insertBefore`). Until then its
|
|
428
|
+
// ancestry is as incomplete as a popup's without its parent, and every
|
|
429
|
+
// token the provider defines would be reported as unknown — a warning for
|
|
430
|
+
// a style that then resolves correctly, and under
|
|
431
|
+
// `REACT_X11_STRICT_TOKENS=1` a throw that killed an app whose palette was
|
|
432
|
+
// fine.
|
|
433
|
+
if (owner._awaitsRootScope && owner._scope == null) return false;
|
|
425
434
|
return owner.isPopup ? owner.parent != null : true;
|
|
426
435
|
}
|
|
427
436
|
|
package/src/nodes/node.js
CHANGED
|
@@ -70,7 +70,12 @@ export class Node {
|
|
|
70
70
|
return DEVTOOLS_FAKE_DOCUMENT;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
constructor(kind, props, app, { yoga = true } = {}) {
|
|
73
|
+
constructor(kind, props, app, { yoga = true, awaitsRootScope = false } = {}) {
|
|
74
|
+
// Set before anything resolves a style: a `<window>`'s own style is
|
|
75
|
+
// resolved by this constructor, and whether its ancestry is complete
|
|
76
|
+
// decides whether an unresolved `$token` is reported (cascade.js
|
|
77
|
+
// `placed`). A window under a root `<ThemeProvider>` has no palette yet.
|
|
78
|
+
this._awaitsRootScope = awaitsRootScope;
|
|
74
79
|
this.kind = kind;
|
|
75
80
|
this.props = props;
|
|
76
81
|
this.app = app;
|
|
@@ -232,6 +232,23 @@ export function windowAttributes(props, scale = 1) {
|
|
|
232
232
|
: props[key];
|
|
233
233
|
}
|
|
234
234
|
if (Object.keys(hints).length > 0) attributes.sizeHints = hints;
|
|
235
|
+
// The desktop identity of this window, under the one name every desktop's
|
|
236
|
+
// own word maps onto: `WM_CLASS` on X11, `app_id` on Wayland,
|
|
237
|
+
// AppUserModelID on Windows. `wmClass` was X11's word for it and is still
|
|
238
|
+
// accepted, so both are normalised here and a backend reads one key.
|
|
239
|
+
//
|
|
240
|
+
// X11 carries a *pair* — an instance naming this window and a class naming
|
|
241
|
+
// the application — and everything since carries one string. The class is
|
|
242
|
+
// the application's, so the class is what a single-id backend is given; the
|
|
243
|
+
// pair itself still reaches `setClass` from the props (`applyWindowHints`).
|
|
244
|
+
const identity = attributes.appId ?? attributes.wmClass;
|
|
245
|
+
if (identity !== undefined) {
|
|
246
|
+
attributes.appId = Array.isArray(identity)
|
|
247
|
+
? (identity[1] ?? identity[0])
|
|
248
|
+
: identity && typeof identity === 'object'
|
|
249
|
+
? (identity.class ?? identity.instance)
|
|
250
|
+
: identity;
|
|
251
|
+
}
|
|
235
252
|
if (props.style !== undefined) {
|
|
236
253
|
const style = flattenStyle(props.style);
|
|
237
254
|
if (style.backgroundColor !== undefined) {
|
|
@@ -325,8 +342,10 @@ export class WindowHints {
|
|
|
325
342
|
this._sendSizeHints(next);
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
const identity = next.appId ?? next.wmClass;
|
|
346
|
+
const wasIdentity = prev.appId ?? prev.wmClass;
|
|
347
|
+
if (!shallowEqual(identity, wasIdentity) && identity) {
|
|
348
|
+
const c = identity;
|
|
330
349
|
if (Array.isArray(c)) wnd.setClass?.(c[0], c[1]);
|
|
331
350
|
else if (typeof c === 'object') wnd.setClass?.(c.instance, c.class);
|
|
332
351
|
else wnd.setClass?.(c);
|
|
@@ -138,8 +138,8 @@ export function flushWindowRestacks() {
|
|
|
138
138
|
* issue #4).
|
|
139
139
|
*/
|
|
140
140
|
export class WindowNode extends Scrollable(Node) {
|
|
141
|
-
constructor(app, attributes, props) {
|
|
142
|
-
super('window', props, app, { yoga: true });
|
|
141
|
+
constructor(app, attributes, props, { awaitsRootScope = false } = {}) {
|
|
142
|
+
super('window', props, app, { yoga: true, awaitsRootScope });
|
|
143
143
|
assertWindowSize(props, this.kind);
|
|
144
144
|
this.root = this;
|
|
145
145
|
this.attributes = attributes;
|