react-x11 2.15.3 → 2.16.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 +37 -0
- package/package.json +3 -3
- package/src/Reconciler.js +85 -22
- package/src/acceleratorhooks.js +40 -6
- package/src/anchor.js +79 -19
- package/src/capabilities.js +29 -4
- package/src/cocoa/app.js +211 -11
- package/src/cocoa/context2d.js +23 -0
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/desktopcapabilityhooks.js +29 -6
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +83 -1
- package/src/index.d.ts +10 -1
- package/src/index.js +3 -0
- package/src/keysymchars.js +47 -0
- package/src/keysyms.d.ts +19 -1
- package/src/keysyms.js +107 -8
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +63 -1
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/screens.js +159 -24
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +164 -17
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +21 -5
- package/src/types/capabilities.d.ts +13 -1
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/events.d.ts +5 -0
- package/src/types/filedialog.d.ts +3 -1
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +14 -2
- package/src/wayland/xkb.js +170 -59
- package/src/windowid.js +62 -20
|
@@ -11,7 +11,6 @@ import { forgetTopLevel, hasDropProps } from '../../dnd.js';
|
|
|
11
11
|
import { clearPendingFrame } from '../../frames.js';
|
|
12
12
|
import { FramePacer } from '../../pacing.js';
|
|
13
13
|
import { endWindowState } from '../../windowstate.js';
|
|
14
|
-
import { anchorOffscreen } from '../../anchor.js';
|
|
15
14
|
import { topLevelWindows } from '../../windowid.js';
|
|
16
15
|
import { WindowAnimation } from '../animation.js';
|
|
17
16
|
import { WindowCascade } from '../cascade.js';
|
|
@@ -153,6 +152,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
153
152
|
// one flag everything reads (`_mapNow`, painting, a11y, anchoring).
|
|
154
153
|
this._reactHidden = false;
|
|
155
154
|
this.hidden = Boolean(props.hidden);
|
|
155
|
+
// A `<ThemeProvider>` this window takes its palette from other than its
|
|
156
|
+
// parent — one written above it at the root, or one directly inside the
|
|
157
|
+
// window it is nested in, which handed it on — and a third writer of
|
|
158
|
+
// `hidden`, since React hides the provider's node rather than the window
|
|
159
|
+
// under it (nodes/scope.js). Null everywhere else.
|
|
160
|
+
this._scope = null;
|
|
156
161
|
// whether this is the tree's own top-level window rather than a nested
|
|
157
162
|
// one or a popup — decided by realize(), read when it maps
|
|
158
163
|
this._topLevel = false;
|
|
@@ -360,6 +365,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
360
365
|
attributes.eventMask = (attributes.eventMask ?? 0) | WINDOW_EVENT_MASK;
|
|
361
366
|
const wnd = this.app.createWindow(attributes);
|
|
362
367
|
this.window = wnd;
|
|
368
|
+
// What the window actually took, which is not always what was asked
|
|
369
|
+
// for: Cocoa puts a window on the point grid. The record is what the
|
|
370
|
+
// resize echo is compared against, so it has to be the size the echo
|
|
371
|
+
// will carry (#586, `_snapSize`).
|
|
372
|
+
this._requestedSize = { width: wnd.width, height: wnd.height };
|
|
363
373
|
// Now that the visual is known: settle the capabilities, re-resolve any
|
|
364
374
|
// `@supports` block against them, and start following the compositor.
|
|
365
375
|
// Before the first paint, and before children realize against it.
|
|
@@ -422,8 +432,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
422
432
|
// and vanish.
|
|
423
433
|
if (this.props.anchor) {
|
|
424
434
|
this._watchAnchor();
|
|
425
|
-
|
|
426
|
-
this._anchorLost = !node || anchorOffscreen(node, this.props.anchor.at);
|
|
435
|
+
this._anchorLost = this._anchorGone();
|
|
427
436
|
}
|
|
428
437
|
// Queued rather than mapped, when there is a commit to queue behind:
|
|
429
438
|
// React hides a subtree only once it has inserted it (beginWindowMaps).
|
|
@@ -747,7 +756,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
747
756
|
// The flag `realize()`'s map will read — set directly, since there is
|
|
748
757
|
// nothing on screen yet for the notification half of `_applyHidden`
|
|
749
758
|
// to be about.
|
|
750
|
-
this.hidden = this.
|
|
759
|
+
this.hidden = this._hiddenByReact() || Boolean(newProps.hidden);
|
|
751
760
|
return;
|
|
752
761
|
}
|
|
753
762
|
|
|
@@ -804,10 +813,10 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
804
813
|
const geo = scaleWindowGeometry(newProps, this.scale);
|
|
805
814
|
if (sizeChanged) {
|
|
806
815
|
this._userSized = false;
|
|
807
|
-
this._requestedSize = {
|
|
816
|
+
this._requestedSize = this._snapSize({
|
|
808
817
|
width: isAutoSize(geo.width) ? wnd.width : geo.width,
|
|
809
818
|
height: isAutoSize(geo.height) ? wnd.height : geo.height,
|
|
810
|
-
};
|
|
819
|
+
});
|
|
811
820
|
}
|
|
812
821
|
if (geometryChanged) {
|
|
813
822
|
if (typeof wnd.setState === 'function') {
|
|
@@ -821,8 +830,15 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
821
830
|
height: isAutoSize(geo.height) ? undefined : geo.height,
|
|
822
831
|
});
|
|
823
832
|
} else {
|
|
824
|
-
|
|
825
|
-
|
|
833
|
+
// A window with no `setState` — Cocoa's — is resized whole or not at
|
|
834
|
+
// all, so an axis handed back to `'auto'` goes as the size recorded
|
|
835
|
+
// for it above: the one the window has, which this change leaves
|
|
836
|
+
// alone. Skipping the call for one `'auto'` axis dropped the other
|
|
837
|
+
// axis's change on the floor: `_refit()` then found the record
|
|
838
|
+
// already matching and sent nothing either (#585). With both axes
|
|
839
|
+
// `'auto'` there is no size to send, and `_refit()` works one out.
|
|
840
|
+
if (sizeChanged && !(isAutoSize(geo.width) && isAutoSize(geo.height))) {
|
|
841
|
+
wnd.resize?.(this._requestedSize.width, this._requestedSize.height);
|
|
826
842
|
}
|
|
827
843
|
if (movedByProps) {
|
|
828
844
|
wnd.move?.(geo.x, geo.y);
|
|
@@ -887,14 +903,25 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
887
903
|
}
|
|
888
904
|
|
|
889
905
|
/**
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
906
|
+
* Whether React is hiding this window: its own flag, or one on the theme
|
|
907
|
+
* scope it is written under — a `<Suspense>` around a `<ThemeProvider>` at
|
|
908
|
+
* the root hides the provider's node, the topmost host instance there.
|
|
909
|
+
* Kept apart from the window's own flag so an inner boundary that still
|
|
910
|
+
* hides the window is not overruled when an outer one reveals the scope.
|
|
911
|
+
*/
|
|
912
|
+
_hiddenByReact() {
|
|
913
|
+
return this._reactHidden || (this._scope?._hiddenByReact() ?? false);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Re-derive `this.hidden` from its writers — React, through
|
|
918
|
+
* `_hiddenByReact`, and the `hidden` prop — and make the window agree.
|
|
919
|
+
* Either saying "hidden" wins, so a `<Suspense>` revealing its content does
|
|
920
|
+
* not map a window the app is holding off screen, and clearing the prop
|
|
921
|
+
* does not map one React still hides.
|
|
895
922
|
*/
|
|
896
923
|
_applyHidden() {
|
|
897
|
-
const hidden = this.
|
|
924
|
+
const hidden = this._hiddenByReact() || Boolean(this.props.hidden);
|
|
898
925
|
if (hidden === this.hidden) return;
|
|
899
926
|
this.hidden = hidden;
|
|
900
927
|
// An unmapped window draws nothing, so a loop inside one is frames
|
package/src/registry.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
DRAWN_KINDS,
|
|
29
29
|
CUSTOM_SEMANTIC_NAMES,
|
|
30
30
|
CUSTOM_SELF_DAMAGED,
|
|
31
|
+
THEME_SCOPE,
|
|
31
32
|
} from './nodes/kinds.js';
|
|
32
33
|
import { Node } from './nodes/node.js';
|
|
33
34
|
import { markLayoutsHotReloadSession } from './layouts.js';
|
|
@@ -35,7 +36,7 @@ import { markLayoutsHotReloadSession } from './layouts.js';
|
|
|
35
36
|
/** kind -> definition. Insertion-ordered, which is the order errors list. */
|
|
36
37
|
const registry = new Map();
|
|
37
38
|
|
|
38
|
-
const RESERVED = new Set(['textchunk', 'svgchild']);
|
|
39
|
+
const RESERVED = new Set(['textchunk', 'svgchild', THEME_SCOPE]);
|
|
39
40
|
|
|
40
41
|
// The re-registration policy for hot reload (issue #318). Module-scope
|
|
41
42
|
// registration is the pattern the docs recommend and tree-shaking forces on
|
package/src/screens.js
CHANGED
|
@@ -119,6 +119,11 @@ class ScreenSession {
|
|
|
119
119
|
this._desktopAtom = null;
|
|
120
120
|
this._snapshot = null;
|
|
121
121
|
this._listeners = new Set();
|
|
122
|
+
/** A backend that has to be *asked* for its layout rather than told —
|
|
123
|
+
* see `setScreenPolling`. `_revalidate` re-reads it now; `_watched` is
|
|
124
|
+
* told whether anything is subscribed. */
|
|
125
|
+
this._revalidate = null;
|
|
126
|
+
this._watched = null;
|
|
122
127
|
/** Every `X.on('event')` handler installed here, so `stop()` can take
|
|
123
128
|
* them off again rather than leaving one per root on a shared client. */
|
|
124
129
|
this._handlers = [];
|
|
@@ -138,7 +143,11 @@ class ScreenSession {
|
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
this._handlers.length = 0;
|
|
146
|
+
const watched = this._listeners.size > 0;
|
|
141
147
|
this._listeners.clear();
|
|
148
|
+
if (watched) this._watch(false);
|
|
149
|
+
this._revalidate = null;
|
|
150
|
+
this._watched = null;
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
/** Install an X event handler that this session owns. */
|
|
@@ -183,32 +192,35 @@ class ScreenSession {
|
|
|
183
192
|
|
|
184
193
|
subscribe(fn) {
|
|
185
194
|
this._listeners.add(fn);
|
|
186
|
-
|
|
195
|
+
if (this._listeners.size === 1) this._watch(true);
|
|
196
|
+
return () => {
|
|
197
|
+
if (!this._listeners.delete(fn)) return;
|
|
198
|
+
if (!this._listeners.size) this._watch(false);
|
|
199
|
+
};
|
|
187
200
|
}
|
|
188
|
-
}
|
|
189
201
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
point.x < m.x + m.width &&
|
|
200
|
-
point.y >= m.y &&
|
|
201
|
-
point.y < m.y + m.height
|
|
202
|
-
) {
|
|
203
|
-
return m;
|
|
204
|
-
}
|
|
202
|
+
/** Ask a pulled backend to re-read the layout, now. Synchronous: the
|
|
203
|
+
* callers are placement paths with no round trip available to them. */
|
|
204
|
+
revalidate() {
|
|
205
|
+
if (!this._revalidate || this.stopped) return;
|
|
206
|
+
try {
|
|
207
|
+
this._revalidate();
|
|
208
|
+
} catch {
|
|
209
|
+
// a backend that cannot answer leaves the layout it published
|
|
210
|
+
// standing, which is a better answer than none
|
|
205
211
|
}
|
|
206
212
|
}
|
|
207
|
-
|
|
208
|
-
for
|
|
209
|
-
|
|
213
|
+
|
|
214
|
+
/** Whether anything is subscribed, for a backend that only has to keep
|
|
215
|
+
* asking while someone is listening. */
|
|
216
|
+
_watch(on) {
|
|
217
|
+
if (!this._watched) return;
|
|
218
|
+
try {
|
|
219
|
+
this._watched(on);
|
|
220
|
+
} catch {
|
|
221
|
+
// as above: its clock, its problem
|
|
222
|
+
}
|
|
210
223
|
}
|
|
211
|
-
return best;
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
/** The overlap of two rects, or `null` where they do not touch. */
|
|
@@ -221,6 +233,82 @@ function intersect(a, b) {
|
|
|
221
233
|
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
222
234
|
}
|
|
223
235
|
|
|
236
|
+
/** How far apart two rects are, squared: zero where they meet, and the gap
|
|
237
|
+
* between their nearest edges otherwise. Squared because nothing compares
|
|
238
|
+
* it against a length — only against another of these. */
|
|
239
|
+
function gapSquared(a, b) {
|
|
240
|
+
const dx = Math.max(a.x - (b.x + b.width), b.x - (a.x + a.width), 0);
|
|
241
|
+
const dy = Math.max(a.y - (b.y + b.height), b.y - (a.y + a.height), 0);
|
|
242
|
+
return dx * dx + dy * dy;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The biggest monitor there is — the stand-in for "the one you look at",
|
|
246
|
+
* for a question with no position in it at all. */
|
|
247
|
+
function largestMonitor(monitors) {
|
|
248
|
+
let best = monitors[0];
|
|
249
|
+
for (const m of monitors) {
|
|
250
|
+
if (m.width * m.height > best.width * best.height) best = m;
|
|
251
|
+
}
|
|
252
|
+
return best;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The monitor `near` is on. `near` is a rect in screen coordinates, and a
|
|
257
|
+
* point is the 1x1 rect at it — the same containment a point used to get,
|
|
258
|
+
* since a 1x1 rect overlaps exactly the monitor that contains its corner.
|
|
259
|
+
*
|
|
260
|
+
* **The one it overlaps most**, because one corner of a rect does not say
|
|
261
|
+
* which monitor the rect is on. A menu-bar item's frame starts a few points
|
|
262
|
+
* *above* the top of its own display, and on a desk where another display
|
|
263
|
+
* reaches down past that edge, the corner alone is inside the *other*
|
|
264
|
+
* monitor — or inside none — and the popup opens there (#618). Every rect
|
|
265
|
+
* that has a size knows better than its corner does.
|
|
266
|
+
*
|
|
267
|
+
* **The nearest one**, by the gap between the rects, when it overlaps none.
|
|
268
|
+
* A rect that is off every monitor is nearly always just outside one of
|
|
269
|
+
* them — that same menu-bar furniture, a pointer at the very edge, a window
|
|
270
|
+
* the WM has not placed yet — and the nearest monitor is the only answer
|
|
271
|
+
* that has anything to do with where it was. The largest was the old answer
|
|
272
|
+
* and it can be anywhere on the desk.
|
|
273
|
+
*
|
|
274
|
+
* With no position at all (`near` null — an auto-sized window with no owner
|
|
275
|
+
* to open beside), the largest monitor, which is all there is to go on.
|
|
276
|
+
*/
|
|
277
|
+
function monitorAt(monitors, near) {
|
|
278
|
+
if (!monitors?.length) return null;
|
|
279
|
+
if (!near) return largestMonitor(monitors);
|
|
280
|
+
// A degenerate rect counts as its own thinnest real version, the way
|
|
281
|
+
// `anchorOffscreen` reads a caret: a point is 1x1, and so is a rect whose
|
|
282
|
+
// size nobody filled in.
|
|
283
|
+
const rect = {
|
|
284
|
+
x: near.x,
|
|
285
|
+
y: near.y,
|
|
286
|
+
width: near.width > 1 ? near.width : 1,
|
|
287
|
+
height: near.height > 1 ? near.height : 1,
|
|
288
|
+
};
|
|
289
|
+
let best = null;
|
|
290
|
+
let most = 0;
|
|
291
|
+
for (const m of monitors) {
|
|
292
|
+
const over = intersect(m, rect);
|
|
293
|
+
const area = over ? over.width * over.height : 0;
|
|
294
|
+
if (area > most) {
|
|
295
|
+
best = m;
|
|
296
|
+
most = area;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (best) return best;
|
|
300
|
+
let nearest = monitors[0];
|
|
301
|
+
let least = Infinity;
|
|
302
|
+
for (const m of monitors) {
|
|
303
|
+
const gap = gapSquared(m, rect);
|
|
304
|
+
if (gap < least) {
|
|
305
|
+
nearest = m;
|
|
306
|
+
least = gap;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return nearest;
|
|
310
|
+
}
|
|
311
|
+
|
|
224
312
|
/**
|
|
225
313
|
* The usable part of one monitor.
|
|
226
314
|
*
|
|
@@ -262,12 +350,19 @@ function usable(monitor, work) {
|
|
|
262
350
|
|
|
263
351
|
/**
|
|
264
352
|
* The rect an auto-sized window may grow into, or `null` where there is
|
|
265
|
-
* nothing to ask. `near` is a screen-coordinate
|
|
266
|
-
*
|
|
267
|
-
* monitor when there
|
|
353
|
+
* nothing to ask. `near` is a screen-coordinate **rect** the window will
|
|
354
|
+
* open against — a `transientFor` owner's origin, the node a popup hangs
|
|
355
|
+
* off, the tray item a click reported — and picks the monitor when there
|
|
356
|
+
* are several (`monitorAt`); `{x, y}` alone is a point.
|
|
268
357
|
*/
|
|
269
358
|
export function availableArea(app, near = null) {
|
|
270
359
|
const session = sessions.get(app);
|
|
360
|
+
// The monitor a popup is flipped and clamped into is picked here, so a
|
|
361
|
+
// backend whose layout is pulled rather than pushed is asked *now*
|
|
362
|
+
// rather than answered from whatever it last read (`setScreenPolling`).
|
|
363
|
+
// A rect the desk has since moved lands inside another monitor's stale
|
|
364
|
+
// one, and the popup opens at that monitor's edge (#617).
|
|
365
|
+
session?.revalidate();
|
|
271
366
|
const screen = session?.screenRect ?? null;
|
|
272
367
|
if (!session) return screen;
|
|
273
368
|
const monitor = monitorAt(session.monitors, near) ?? screen;
|
|
@@ -364,6 +459,46 @@ export function watchScreens(app, fn) {
|
|
|
364
459
|
return session.subscribe(fn);
|
|
365
460
|
}
|
|
366
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Register a backend whose layout has to be **pulled**.
|
|
464
|
+
*
|
|
465
|
+
* X11 and Wayland are told: RandR sends an event, a `wl_output` announces
|
|
466
|
+
* itself, and a `publish` lands from the handler. The cocoa bridge keeps
|
|
467
|
+
* its `NSScreen` copy current on macOS's own
|
|
468
|
+
* `NSApplicationDidChangeScreenParametersNotification` but emits no event
|
|
469
|
+
* for it, so there a display plugged in, rearranged or made primary is a
|
|
470
|
+
* question nobody asked (#617). This is where the asking is wired up:
|
|
471
|
+
*
|
|
472
|
+
* - `revalidate()` re-reads the layout and publishes any change. Called
|
|
473
|
+
* before `availableArea()` picks the monitor a window is sized against
|
|
474
|
+
* or a popup is clamped into, which is where a stale rect does visible
|
|
475
|
+
* damage, and synchronous for that reason.
|
|
476
|
+
* - `watched(on)` is told when the *first* subscriber arrives and when the
|
|
477
|
+
* last one leaves. A change nobody asked about still has to reach
|
|
478
|
+
* `useScreens()`, which needs a clock where there is no event — and a
|
|
479
|
+
* clock that only runs while a component is watching costs an app that
|
|
480
|
+
* never asks nothing at all.
|
|
481
|
+
*
|
|
482
|
+
* Both are optional, and a session with neither behaves exactly as it did:
|
|
483
|
+
* this adds no work to the X11 path.
|
|
484
|
+
*/
|
|
485
|
+
export function setScreenPolling(
|
|
486
|
+
app,
|
|
487
|
+
{ revalidate = null, watched = null } = {},
|
|
488
|
+
) {
|
|
489
|
+
let session = sessions.get(app);
|
|
490
|
+
if (!session) {
|
|
491
|
+
session = new ScreenSession(app);
|
|
492
|
+
sessions.set(app, session);
|
|
493
|
+
}
|
|
494
|
+
session._revalidate = revalidate;
|
|
495
|
+
session._watched = watched;
|
|
496
|
+
// Registered after a `useScreens()` already mounted — the backend still
|
|
497
|
+
// has to hear that it is being watched.
|
|
498
|
+
if (watched && session._listeners.size) session._watch(true);
|
|
499
|
+
return session;
|
|
500
|
+
}
|
|
501
|
+
|
|
367
502
|
// --------------------------------------------------------------------------
|
|
368
503
|
// Starting up
|
|
369
504
|
// --------------------------------------------------------------------------
|