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
package/src/cocoa/app.js
CHANGED
|
@@ -22,7 +22,7 @@ import { deliverActivate, deliverOpen } from '../application.js';
|
|
|
22
22
|
import { flushPendingFrames } from '../frames.js';
|
|
23
23
|
import { flushSyncWork } from '../priority.js';
|
|
24
24
|
import { setCompositingForTests } from '../compositing.js';
|
|
25
|
-
import { setScreensForTests } from '../screens.js';
|
|
25
|
+
import { setScreenPolling, setScreensForTests } from '../screens.js';
|
|
26
26
|
import { setScaleForTests } from '../scale.js';
|
|
27
27
|
import { BezelStore } from './bezels.js';
|
|
28
28
|
import { CocoaGLArea, cocoaGLConfig, resolveCocoaGLRuntime } from './glarea.js';
|
|
@@ -40,6 +40,7 @@ import { CocoaFilePanels } from './filepanels.js';
|
|
|
40
40
|
import { CocoaFontManager } from './fonts.js';
|
|
41
41
|
import { releaseImageUpload } from './context2d.js';
|
|
42
42
|
import { CocoaSurface } from './surface.js';
|
|
43
|
+
import { CocoaSymbols } from './symbols.js';
|
|
43
44
|
import { CocoaWindow } from './window.js';
|
|
44
45
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
45
46
|
import { loadNative } from './native.js';
|
|
@@ -69,6 +70,23 @@ const FRAME_SLACK_MS = 1;
|
|
|
69
70
|
// to 100, docs/windows.md §"Resize"). `createRoot({ cocoa: { resizeWait } })`
|
|
70
71
|
// sets it; 0 is no handshake at all.
|
|
71
72
|
const RESIZE_WAIT_MS = 50;
|
|
73
|
+
// How often the screen layout is re-read while something is watching it, in
|
|
74
|
+
// ms. There is no event to wait for — the bridge keeps its `NSScreen` copy
|
|
75
|
+
// current on macOS's notification and emits nothing (#617) — so
|
|
76
|
+
// `useScreens()`'s promise to re-render when a monitor is plugged in is
|
|
77
|
+
// kept by asking. 500 is half a second behind a replug, which is under the
|
|
78
|
+
// time it takes to look at the screen, against one `listScreens()` read a
|
|
79
|
+
// second (a lock and a few doubles on a worker, an `NSScreen.screens` walk
|
|
80
|
+
// on the main thread). Only while a component is subscribed:
|
|
81
|
+
// `createRoot({ cocoa: { screenPoll } })` sets it, 0 turns it off, and
|
|
82
|
+
// nothing polls in an app that never calls `useScreens()`.
|
|
83
|
+
const SCREEN_POLL_MS = 500;
|
|
84
|
+
// The floor under a re-read driven by something that is *not* a clock: a
|
|
85
|
+
// window reporting where it is, which a drag reports every frame and a
|
|
86
|
+
// display change reports once. A second is far too long to matter to a
|
|
87
|
+
// person and short enough that a rearrangement is noticed by the time they
|
|
88
|
+
// have finished moving the window (`_recheckScreens`).
|
|
89
|
+
const SCREEN_RECHECK_MS = 1000;
|
|
72
90
|
|
|
73
91
|
export class CocoaApp {
|
|
74
92
|
constructor(native, options = {}) {
|
|
@@ -156,6 +174,26 @@ export class CocoaApp {
|
|
|
156
174
|
const screens = native.listScreens();
|
|
157
175
|
this.scale = screens[0]?.scale ?? 1;
|
|
158
176
|
this._screens = screens;
|
|
177
|
+
// …and how the layout stays current, because nothing pushes it: the
|
|
178
|
+
// bridge republishes its copy on macOS's own notification but emits no
|
|
179
|
+
// event, so this side asks — before a placement reads the layout, when
|
|
180
|
+
// a window reports a move, and on a clock while `useScreens()` has a
|
|
181
|
+
// subscriber (`refreshScreens`, #617). `_screensAt` is when the last
|
|
182
|
+
// read happened, which is what throttles the ones that are not clocks.
|
|
183
|
+
this._screensAt = performance.now();
|
|
184
|
+
this._screenTimer = null;
|
|
185
|
+
const screenPoll = options.cocoa?.screenPoll ?? SCREEN_POLL_MS;
|
|
186
|
+
if (!(screenPoll >= 0) || !Number.isFinite(screenPoll)) {
|
|
187
|
+
throw new TypeError(
|
|
188
|
+
'react-x11: cocoa.screenPoll is a number of milliseconds, 0 for ' +
|
|
189
|
+
`none — got ${screenPoll}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
this._screenPoll = screenPoll;
|
|
193
|
+
setScreenPolling(this, {
|
|
194
|
+
revalidate: () => this.refreshScreens(),
|
|
195
|
+
watched: (on) => this._pollScreens(on),
|
|
196
|
+
});
|
|
159
197
|
|
|
160
198
|
// The name the Dock, ⌘-Tab and the menu bar print. An unbundled
|
|
161
199
|
// process is registered with LaunchServices under its executable —
|
|
@@ -200,6 +238,8 @@ export class CocoaApp {
|
|
|
200
238
|
// real bridge on the machine — the manager's default loads it only when
|
|
201
239
|
// it is built standalone
|
|
202
240
|
this.fonts = new CocoaFontManager(native);
|
|
241
|
+
// `<image src={{ symbol }}>`'s names are SF Symbols here (src/symbols.js)
|
|
242
|
+
this.symbols = new CocoaSymbols(native);
|
|
203
243
|
|
|
204
244
|
// Native open/save panels (src/cocoa/filepanels.js). Present exactly
|
|
205
245
|
// when the bridge has them (>= 0.5), and its presence is what puts the
|
|
@@ -501,6 +541,102 @@ export class CocoaApp {
|
|
|
501
541
|
this._windows.set(wnd._key, wnd);
|
|
502
542
|
}
|
|
503
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Re-read the screen layout and publish it if the desk has changed — the
|
|
546
|
+
* answer to a monitor plugged in, unplugged, rearranged, woken or made
|
|
547
|
+
* primary (#617). Returns whether anything moved.
|
|
548
|
+
*
|
|
549
|
+
* **`listScreens()` is always current; this side's copy was not.** The
|
|
550
|
+
* bridge republishes its `NSScreen` snapshot on
|
|
551
|
+
* `NSApplicationDidChangeScreenParametersNotification` and answers a
|
|
552
|
+
* pump-mode call from AppKit live — what was read once and kept forever
|
|
553
|
+
* is `_screens`, taken in the constructor. So the whole fix is to ask
|
|
554
|
+
* again, and the read is cheap enough to ask on demand: before a
|
|
555
|
+
* placement clamps a popup to a monitor (`availableArea` through
|
|
556
|
+
* `setScreenPolling`), when a window reports a move (`_recheckScreens`),
|
|
557
|
+
* and on a clock while `useScreens()` is mounted (`_pollScreens`).
|
|
558
|
+
*
|
|
559
|
+
* Public, and the seam for an app that knows before any of those do —
|
|
560
|
+
* `app.refreshScreens()` publishes whatever the OS says right now.
|
|
561
|
+
*
|
|
562
|
+
* **The app's scale is not re-derived.** Every rect this backend speaks
|
|
563
|
+
* in is points × `app.scale`, fixed at startup, and a window's origin,
|
|
564
|
+
* an event's coordinates and a surface's pixels all already exist in
|
|
565
|
+
* that space; moving it under them is a different and much larger change
|
|
566
|
+
* than re-reading a layout. The layout is converted at the scale the rest
|
|
567
|
+
* of the app uses, which is what keeps `monitorAt()` answering with the
|
|
568
|
+
* head a window is actually on (see `screenLayout`).
|
|
569
|
+
*/
|
|
570
|
+
refreshScreens() {
|
|
571
|
+
if (this._closed) return false;
|
|
572
|
+
this._screensAt = performance.now();
|
|
573
|
+
let screens = null;
|
|
574
|
+
try {
|
|
575
|
+
screens = this._native.listScreens?.();
|
|
576
|
+
} catch {
|
|
577
|
+
// a bridge going away, or a fake with nothing to say
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
// An empty answer is "could not tell", never "no displays": a Mac with
|
|
581
|
+
// the lid shut and no panel attached still has the desk it had, and
|
|
582
|
+
// publishing nothing would take every monitor out from under
|
|
583
|
+
// `availableArea` and size the next window against the void.
|
|
584
|
+
if (!screens?.length) return false;
|
|
585
|
+
if (sameScreens(screens, this._screens)) return false;
|
|
586
|
+
this._screens = screens;
|
|
587
|
+
setScreensForTests(this, screenLayout(screens, this.scale));
|
|
588
|
+
// A monitor's refresh rate can change under a window that never moved —
|
|
589
|
+
// a mode switch, a panel woken at 60Hz — and a window's clock is read
|
|
590
|
+
// from this list rather than kept by the display (`frameIntervalFor`).
|
|
591
|
+
for (const wnd of this._windows.values()) {
|
|
592
|
+
if (!wnd.destroyed) wnd._refreshFrameInterval?.();
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* The layout, re-read at most once every `SCREEN_RECHECK_MS`.
|
|
599
|
+
*
|
|
600
|
+
* For the signals that mean "something about the desk may have moved"
|
|
601
|
+
* rather than "it did": a window reporting its position, which a drag
|
|
602
|
+
* reports every frame and a display change reports once. Plugging a
|
|
603
|
+
* monitor in moves the windows that were on it, so this is the one place
|
|
604
|
+
* the OS does tell us something — it just does not say what.
|
|
605
|
+
*/
|
|
606
|
+
_recheckScreens() {
|
|
607
|
+
if (performance.now() - this._screensAt < SCREEN_RECHECK_MS) return false;
|
|
608
|
+
return this.refreshScreens();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* The layout on a clock, while something is subscribed to it.
|
|
613
|
+
*
|
|
614
|
+
* `useScreens()` says it re-renders when a monitor is plugged in or
|
|
615
|
+
* unplugged and when the arrangement changes. On X11 that is a RandR
|
|
616
|
+
* event; here there is nothing to wait for, so while a component is
|
|
617
|
+
* watching, this asks every `cocoa.screenPoll` ms — on an unref'd timer,
|
|
618
|
+
* which never holds the process open and never wakes an app that is
|
|
619
|
+
* waiting on nothing else. An app that never calls `useScreens()` pays
|
|
620
|
+
* nothing at all: the paths where a stale layout is visible ask for
|
|
621
|
+
* themselves.
|
|
622
|
+
*
|
|
623
|
+
* Started and stopped by the session as the first subscriber arrives and
|
|
624
|
+
* the last one leaves (`setScreenPolling`), so a hook that unmounts takes
|
|
625
|
+
* the clock with it.
|
|
626
|
+
*/
|
|
627
|
+
_pollScreens(on) {
|
|
628
|
+
if (this._screenTimer) {
|
|
629
|
+
clearInterval(this._screenTimer);
|
|
630
|
+
this._screenTimer = null;
|
|
631
|
+
}
|
|
632
|
+
if (!on || this._closed || !(this._screenPoll > 0)) return;
|
|
633
|
+
this._screenTimer = setInterval(
|
|
634
|
+
() => this.refreshScreens(),
|
|
635
|
+
this._screenPoll,
|
|
636
|
+
);
|
|
637
|
+
this._screenTimer.unref?.();
|
|
638
|
+
}
|
|
639
|
+
|
|
504
640
|
/**
|
|
505
641
|
* How often `wnd` may paint, in ms: the explicit `frameInterval` when the
|
|
506
642
|
* root was given one, else the period of the screen under the window's
|
|
@@ -641,6 +777,8 @@ export class CocoaApp {
|
|
|
641
777
|
_unregisterWindow(wnd) {
|
|
642
778
|
this._windows.delete(wnd._key);
|
|
643
779
|
if (this._grabWindow === wnd) this._grabWindow = null;
|
|
780
|
+
// a popup that took the keyboard and closed is not where keys go next
|
|
781
|
+
if (this._lastKeyWindow === wnd) this._lastKeyWindow = null;
|
|
644
782
|
}
|
|
645
783
|
|
|
646
784
|
// --- the pump ------------------------------------------------------------
|
|
@@ -802,8 +940,8 @@ export class CocoaApp {
|
|
|
802
940
|
this._rafQueue = [];
|
|
803
941
|
let soonest = Infinity;
|
|
804
942
|
for (const entry of queue) {
|
|
805
|
-
// A window whose last flip has not
|
|
806
|
-
//
|
|
943
|
+
// A window whose last flip has not reached its layer yet (threaded
|
|
944
|
+
// mode's fence, `CocoaWindow.frameInFlight`) waits before
|
|
807
945
|
// its clock is asked, or the clock would count a frame that did not
|
|
808
946
|
// run. The release is an event, and the batch it arrives in ticks
|
|
809
947
|
// again.
|
|
@@ -874,7 +1012,7 @@ export class CocoaApp {
|
|
|
874
1012
|
* paid once, when the batch is done: ten moves and a click that crossed
|
|
875
1013
|
* while this thread was busy are one frame, not eleven. Then what a pump
|
|
876
1014
|
* tick does: the frames that are due, and the ones a window's visibility
|
|
877
|
-
* or its
|
|
1015
|
+
* or its last flip was holding, which an occlusion change or a
|
|
878
1016
|
* `surface-released` in this very batch may just have freed.
|
|
879
1017
|
*/
|
|
880
1018
|
_routeBatch(batch) {
|
|
@@ -1090,9 +1228,9 @@ export class CocoaApp {
|
|
|
1090
1228
|
// the notch and the scroll, most of a refresh period of nothing.
|
|
1091
1229
|
//
|
|
1092
1230
|
// Mostly: a trackpad's momentum lands two in one tick often, and a
|
|
1093
|
-
// second flip inside the refresh
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1231
|
+
// second flip inside the refresh is a frame the display never shows
|
|
1232
|
+
// (`CocoaWindow._flippedRecently` says what it cost). So the rest of a
|
|
1233
|
+
// burst lands React's half and leaves the
|
|
1096
1234
|
// paint to the paced frame the scroll already asked for — the model
|
|
1097
1235
|
// has scrolled, and the next refresh shows all of it.
|
|
1098
1236
|
if (wnd._flippedRecently()) {
|
|
@@ -1135,6 +1273,10 @@ export class CocoaApp {
|
|
|
1135
1273
|
_routeGeometry(ev) {
|
|
1136
1274
|
const wnd = this._window(ev);
|
|
1137
1275
|
if (!wnd || wnd.destroyed) return;
|
|
1276
|
+
// Before the window re-paces itself against the screen list below: a
|
|
1277
|
+
// display plugged in or removed moves the windows that were on it, and
|
|
1278
|
+
// this is the only thing the bridge says about it (`_recheckScreens`).
|
|
1279
|
+
this._recheckScreens();
|
|
1138
1280
|
wnd._nativeResized(ev);
|
|
1139
1281
|
wnd.emit('resize', {
|
|
1140
1282
|
width: wnd.width,
|
|
@@ -1201,9 +1343,10 @@ export class CocoaApp {
|
|
|
1201
1343
|
/**
|
|
1202
1344
|
* `surface-released`: a worker's frame took an IOSurface off a layer and
|
|
1203
1345
|
* the frame that replaced it has committed (windowkit/appkit#52). The
|
|
1204
|
-
* window whose
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1346
|
+
* window whose buffer that was stops waiting on it, and may draw into it
|
|
1347
|
+
* again once the WindowServer lets go of it too
|
|
1348
|
+
* (`CocoaWindow._surfaceReleased`); the tick at the end of this batch
|
|
1349
|
+
* runs the frame it was holding.
|
|
1207
1350
|
*/
|
|
1208
1351
|
_routeSurfaceReleased(ev) {
|
|
1209
1352
|
for (const wnd of this._windows.values()) {
|
|
@@ -1369,6 +1512,7 @@ export class CocoaApp {
|
|
|
1369
1512
|
this._pump = null;
|
|
1370
1513
|
if (this._frameTimer) clearTimeout(this._frameTimer);
|
|
1371
1514
|
this._frameTimer = null;
|
|
1515
|
+
this._pollScreens(false);
|
|
1372
1516
|
this._cocoaGL?.destroy();
|
|
1373
1517
|
this._cocoaGL = null;
|
|
1374
1518
|
this._unsubscribe?.();
|
|
@@ -1407,6 +1551,12 @@ export class CocoaApp {
|
|
|
1407
1551
|
* *width* as a bound to every other head: a second display wider than the
|
|
1408
1552
|
* built-in had its right edge pulled in by the difference, and every
|
|
1409
1553
|
* anchored popup that reached past it was clamped back (issue #453).
|
|
1554
|
+
*
|
|
1555
|
+
* **Everything the bridge says about a screen comes through**, not only its
|
|
1556
|
+
* rects. `primary` and the panel's refresh rate were dropped here, so
|
|
1557
|
+
* `useScreens().primary` read null on macOS — every entry `primary: false`
|
|
1558
|
+
* — and `refreshRate` null beside a `frameIntervalFor` that was pacing
|
|
1559
|
+
* windows on that very number (#617).
|
|
1410
1560
|
*/
|
|
1411
1561
|
export function screenLayout(screens, scale) {
|
|
1412
1562
|
const rect = (r) => ({
|
|
@@ -1417,9 +1567,17 @@ export function screenLayout(screens, scale) {
|
|
|
1417
1567
|
});
|
|
1418
1568
|
const primary = screens?.[0];
|
|
1419
1569
|
return {
|
|
1420
|
-
monitors: (screens ?? []).map((screen) => ({
|
|
1570
|
+
monitors: (screens ?? []).map((screen, i) => ({
|
|
1421
1571
|
...rect(screen),
|
|
1422
1572
|
...(screen.visible ? { visible: rect(screen.visible) } : null),
|
|
1573
|
+
// `NSScreen.screens[0]` **is** the primary — the screen with the menu
|
|
1574
|
+
// bar, which is where macOS puts a window that names no position —
|
|
1575
|
+
// and the bridge flags it as well; the index is the same fact for a
|
|
1576
|
+
// bridge that does not.
|
|
1577
|
+
primary: screen.primary ?? i === 0,
|
|
1578
|
+
// `NSScreen.maximumFramesPerSecond`, as `fps`. 0 is the OS declining
|
|
1579
|
+
// to say (before macOS 12), which is `useScreens()`'s null.
|
|
1580
|
+
refreshRate: screen.fps > 0 ? screen.fps : null,
|
|
1423
1581
|
})),
|
|
1424
1582
|
// Still published for `useScreens().workArea`, which is one rect for
|
|
1425
1583
|
// the desktop by definition; the primary's is the closest macOS has.
|
|
@@ -1427,6 +1585,48 @@ export function screenLayout(screens, scale) {
|
|
|
1427
1585
|
};
|
|
1428
1586
|
}
|
|
1429
1587
|
|
|
1588
|
+
/**
|
|
1589
|
+
* Whether two `listScreens()` answers describe the same desk.
|
|
1590
|
+
*
|
|
1591
|
+
* Every field the layout is built from, in the order they arrived — which
|
|
1592
|
+
* is `NSScreen.screens`, so the order is the arrangement and the primary,
|
|
1593
|
+
* and a change in it is a change. `fps` counts because a window's frame
|
|
1594
|
+
* clock is read from it, and `scale` because a screen that switched mode
|
|
1595
|
+
* is not the screen it was even at the same size.
|
|
1596
|
+
*
|
|
1597
|
+
* Pure, and exported for that reason: it decides whether a re-read
|
|
1598
|
+
* re-renders every `useScreens()` subscriber, and a poll that publishes an
|
|
1599
|
+
* unchanged layout twice a second is a render loop rather than a fix.
|
|
1600
|
+
*/
|
|
1601
|
+
export function sameScreens(a, b) {
|
|
1602
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
1603
|
+
for (let i = 0; i < a.length; i++) {
|
|
1604
|
+
const x = a[i];
|
|
1605
|
+
const y = b[i];
|
|
1606
|
+
if (
|
|
1607
|
+
x.x !== y.x ||
|
|
1608
|
+
x.y !== y.y ||
|
|
1609
|
+
x.width !== y.width ||
|
|
1610
|
+
x.height !== y.height ||
|
|
1611
|
+
x.scale !== y.scale ||
|
|
1612
|
+
x.fps !== y.fps ||
|
|
1613
|
+
x.primary !== y.primary ||
|
|
1614
|
+
!sameRect(x.visible, y.visible)
|
|
1615
|
+
) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** Two `visible` rects, either of which a bridge may not have reported. */
|
|
1623
|
+
function sameRect(a, b) {
|
|
1624
|
+
if (!a || !b) return !a === !b;
|
|
1625
|
+
return (
|
|
1626
|
+
a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1430
1630
|
/**
|
|
1431
1631
|
* Build the app and seed the platform stores the way the mock seeds them —
|
|
1432
1632
|
* `beginScale`/`beginScreens`/`beginCompositing` find a session already
|
package/src/cocoa/context2d.js
CHANGED
|
@@ -1046,6 +1046,29 @@ export class CocoaContext2D {
|
|
|
1046
1046
|
this._dirty();
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* An SF Symbol by name, fitted into the rect and centred, in the fill
|
|
1051
|
+
* colour — `ctxDrawSymbol`, `@windowkit/appkit` 0.12.0. Answers false,
|
|
1052
|
+
* drawing nothing, for a name the system does not know and on a bridge
|
|
1053
|
+
* without the verb. `options` are the bridge's: `pointSize`, `weight`,
|
|
1054
|
+
* `scale`, `variableValue`.
|
|
1055
|
+
*/
|
|
1056
|
+
drawSymbol(name, x, y, width, height, options) {
|
|
1057
|
+
if (typeof this._native.ctxDrawSymbol !== 'function') return false;
|
|
1058
|
+
this._applyFill();
|
|
1059
|
+
const drawn = this._native.ctxDrawSymbol(
|
|
1060
|
+
this._s(),
|
|
1061
|
+
name,
|
|
1062
|
+
x,
|
|
1063
|
+
y,
|
|
1064
|
+
width,
|
|
1065
|
+
height,
|
|
1066
|
+
options,
|
|
1067
|
+
);
|
|
1068
|
+
if (drawn) this._dirty();
|
|
1069
|
+
return drawn === true;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1049
1072
|
drawImage(image, ...args) {
|
|
1050
1073
|
const src = this._sourceHandle(image);
|
|
1051
1074
|
if (!src) return;
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -462,11 +462,31 @@ export class CocoaFace {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
/**
|
|
466
|
+
* The features a letter-spaced span is set with: CSS's rule, and ntk's, that
|
|
467
|
+
* the optional ligatures come off where letters are spaced — an `fi` drawn as
|
|
468
|
+
* one glyph cannot open in its middle — unless the style names them.
|
|
469
|
+
*/
|
|
470
|
+
const OPTIONAL_LIGATURES_OFF = Object.freeze({
|
|
471
|
+
liga: 0,
|
|
472
|
+
clig: 0,
|
|
473
|
+
dlig: 0,
|
|
474
|
+
hlig: 0,
|
|
475
|
+
});
|
|
476
|
+
function spacedFeatures(features, spacing) {
|
|
477
|
+
if (!spacing) return features;
|
|
478
|
+
return features
|
|
479
|
+
? { ...OPTIONAL_LIGATURES_OFF, ...features }
|
|
480
|
+
: OPTIONAL_LIGATURES_OFF;
|
|
481
|
+
}
|
|
482
|
+
|
|
465
483
|
export class CocoaFontManager {
|
|
466
484
|
/** @param native the @windowkit/appkit module; the tests hand in a fake */
|
|
467
485
|
constructor(native = loadNative()) {
|
|
468
486
|
this._native = native;
|
|
469
487
|
this._fonts = new Map(); // family|weight|italic|size -> handle
|
|
488
|
+
// handle -> features key -> the handle with those features set
|
|
489
|
+
this._featured = new WeakMap();
|
|
470
490
|
this._faces = new Map(); // family|weight|italic -> face wrapper
|
|
471
491
|
this._registered = new Map(); // lowercase family -> [{cg, weight, italic}]
|
|
472
492
|
this._byKey = new Map(); // ntk Font key -> { cg } | { ps }
|
|
@@ -585,6 +605,52 @@ export class CocoaFontManager {
|
|
|
585
605
|
return handle;
|
|
586
606
|
}
|
|
587
607
|
|
|
608
|
+
/**
|
|
609
|
+
* `handle` with OpenType features set — `fontApplyFeatures`, which
|
|
610
|
+
* `@windowkit/appkit` has from 0.11.0 — for the resolved `features` a
|
|
611
|
+
* style hands the engines, tag → value. Kept per handle, so a paragraph
|
|
612
|
+
* laid out every repaint asks the bridge for a featured font once. A
|
|
613
|
+
* bridge without the verb gets the handle back as it was
|
|
614
|
+
* (`_textAdjustments`).
|
|
615
|
+
*/
|
|
616
|
+
_withFeatures(handle, features) {
|
|
617
|
+
if (!handle || !features || !this._textAdjustments()) return handle;
|
|
618
|
+
let perHandle = this._featured.get(handle);
|
|
619
|
+
if (!perHandle) this._featured.set(handle, (perHandle = new Map()));
|
|
620
|
+
const key = JSON.stringify(features);
|
|
621
|
+
let featured = perHandle.get(key);
|
|
622
|
+
if (!featured) {
|
|
623
|
+
featured = this._native.fontApplyFeatures(handle, features);
|
|
624
|
+
perHandle.set(key, featured);
|
|
625
|
+
}
|
|
626
|
+
return featured;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Whether the bridge sets features and letter spacing at all. Both arrived
|
|
631
|
+
* in one `@windowkit/appkit` release, `fontApplyFeatures` with the span's
|
|
632
|
+
* `letterSpacing`; an older bridge takes the span option and ignores it,
|
|
633
|
+
* so the verb is what answers for both.
|
|
634
|
+
*
|
|
635
|
+
* Without them the text draws as the face has it — digits proportional,
|
|
636
|
+
* letters unspaced — and nothing else goes wrong. But a style that asked
|
|
637
|
+
* for either and got nothing is a question with no thread to pull, so
|
|
638
|
+
* development says so, once.
|
|
639
|
+
*/
|
|
640
|
+
_textAdjustments() {
|
|
641
|
+
if (typeof this._native.fontApplyFeatures === 'function') return true;
|
|
642
|
+
if (process.env.NODE_ENV !== 'production' && !this._warnedAdjustments) {
|
|
643
|
+
this._warnedAdjustments = true;
|
|
644
|
+
console.warn(
|
|
645
|
+
'react-x11: this @windowkit/appkit has no fontApplyFeatures, so ' +
|
|
646
|
+
'letterSpacing, fontVariantNumeric and fontFeatureSettings do ' +
|
|
647
|
+
'nothing on the Cocoa backend. Update @windowkit/appkit to the ' +
|
|
648
|
+
'version react-x11 lists in its optionalDependencies.',
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
588
654
|
_withVariations(handle, variations) {
|
|
589
655
|
if (
|
|
590
656
|
variations &&
|
|
@@ -882,6 +948,8 @@ export class CocoaFontManager {
|
|
|
882
948
|
sp.color ?? base.color ?? null,
|
|
883
949
|
sp.variations ?? base.variations ?? null,
|
|
884
950
|
(sp.font ?? base.font)?.key ?? null,
|
|
951
|
+
sp.features ?? base.features ?? null,
|
|
952
|
+
sp.letterSpacing ?? base.letterSpacing ?? 0,
|
|
885
953
|
]),
|
|
886
954
|
options.maxWidth,
|
|
887
955
|
options.align,
|
|
@@ -926,12 +994,22 @@ export class CocoaFontManager {
|
|
|
926
994
|
),
|
|
927
995
|
variations,
|
|
928
996
|
);
|
|
997
|
+
// Letter spacing is the typesetter's kerning attribute, and it turns
|
|
998
|
+
// the optional ligatures off the way ntk does, so the two engines
|
|
999
|
+
// agree on what a spaced word is made of (#588).
|
|
1000
|
+
let spacing = span.letterSpacing ?? base.letterSpacing ?? 0;
|
|
1001
|
+
if (spacing && !this._textAdjustments()) spacing = 0;
|
|
1002
|
+
handle = this._withFeatures(
|
|
1003
|
+
handle,
|
|
1004
|
+
spacedFeatures(span.features ?? base.features, spacing),
|
|
1005
|
+
);
|
|
929
1006
|
const color = span.color ?? base.color;
|
|
930
1007
|
if (color == null) contextInk = true;
|
|
931
1008
|
nativeSpans.push({
|
|
932
1009
|
text: t,
|
|
933
1010
|
font: handle,
|
|
934
1011
|
...(color == null ? {} : { color: parseColor(color) }),
|
|
1012
|
+
...(spacing ? { letterSpacing: spacing } : {}),
|
|
935
1013
|
});
|
|
936
1014
|
}
|
|
937
1015
|
// A width offer of zero is a question, not a degenerate layout: yoga
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -480,9 +480,21 @@ export function propBoxProps(node, app, scale, parentOrigin, order) {
|
|
|
480
480
|
backgroundColor: colour(style.backgroundColor),
|
|
481
481
|
borderWidth: border / scale,
|
|
482
482
|
borderColor: colour(style.borderColor),
|
|
483
|
+
opacity: layerOpacity(style),
|
|
483
484
|
};
|
|
484
485
|
}
|
|
485
486
|
|
|
487
|
+
/**
|
|
488
|
+
* `opacity` as a layer's own. On the layer presenter a node's children are
|
|
489
|
+
* its sublayers, so this fades the subtree with it; the bitmap paths draw
|
|
490
|
+
* the group themselves and never promote a faded box.
|
|
491
|
+
*/
|
|
492
|
+
export function layerOpacity(style) {
|
|
493
|
+
const value = style?.opacity;
|
|
494
|
+
if (typeof value !== 'number' || Number.isNaN(value)) return 1;
|
|
495
|
+
return Math.min(1, Math.max(0, value));
|
|
496
|
+
}
|
|
497
|
+
|
|
486
498
|
// --- animations the render server runs ---------------------------------------
|
|
487
499
|
//
|
|
488
500
|
// The node model keeps deciding what is animating and when it ends
|
|
@@ -586,6 +598,10 @@ export class LayerAnimations {
|
|
|
586
598
|
opts.timing = EASING_CONTROL_POINTS[entry.easing];
|
|
587
599
|
opts.repeat = Infinity;
|
|
588
600
|
opts.autoreverse = entry.alternate;
|
|
601
|
+
// CA's two halves of one CSS delay: a begin time the layer waits
|
|
602
|
+
// for showing `from`, or a time offset that starts it that far in
|
|
603
|
+
if (entry.delay > 0) opts.delay = entry.delay / 1000;
|
|
604
|
+
else if (entry.delay < 0) opts.timeOffset = -entry.delay / 1000;
|
|
589
605
|
} else if (map.colour) {
|
|
590
606
|
// From where the pixels are — which is what "an interrupted
|
|
591
607
|
// transition reverses from where it got to" means here. A colour
|
|
@@ -1112,6 +1128,7 @@ export class CocoaLayerPresenter {
|
|
|
1112
1128
|
],
|
|
1113
1129
|
zPosition: order,
|
|
1114
1130
|
hidden: Boolean(node.hidden),
|
|
1131
|
+
opacity: layerOpacity(node.style),
|
|
1115
1132
|
// the layer covers the ink bounds; clipping (if any) belongs to the
|
|
1116
1133
|
// CONTENT box, which a raster self cannot express — scrolling
|
|
1117
1134
|
// containers that also raster keep clipping via a child guard below
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -154,6 +154,25 @@ function insideGlArea(node) {
|
|
|
154
154
|
return false;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
const faded = (style) => style?.opacity !== undefined && !(style.opacity >= 1);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A box whose `opacity` is below 1, now or on its way there, is a group the
|
|
161
|
+
* bitmap composites (`NodePaint._paintGroup`) — the box and everything in it
|
|
162
|
+
* drawn once and faded as one. On a layer of its own the box and its content
|
|
163
|
+
* would be separate layers under the one opacity, fading one over the other,
|
|
164
|
+
* and an opacity the frame clock is still animating would never reach the
|
|
165
|
+
* layer at all.
|
|
166
|
+
*/
|
|
167
|
+
function fadesAsGroup(node) {
|
|
168
|
+
return (
|
|
169
|
+
faded(node.style) ||
|
|
170
|
+
faded(node._targetStyle) ||
|
|
171
|
+
Boolean(node._anim?.has('opacity')) ||
|
|
172
|
+
Boolean(node._loops?.some((loop) => loop.prop === 'opacity'))
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
157
176
|
/**
|
|
158
177
|
* Can this node be a property box on a layer at all — the static half of
|
|
159
178
|
* the answer, the same whatever the scene around it does: a plain box by
|
|
@@ -163,6 +182,7 @@ function insideGlArea(node) {
|
|
|
163
182
|
function promotableNode(node) {
|
|
164
183
|
if (node.destroyed || !plainBox(node)) return false;
|
|
165
184
|
if (insideGlArea(node)) return false;
|
|
185
|
+
if (fadesAsGroup(node)) return false;
|
|
166
186
|
if (!stylePaintsPlain(node, node._targetStyle ?? node.style)) return false;
|
|
167
187
|
if (node.isScroller?.()) return false;
|
|
168
188
|
return !paintsOutline(node);
|
package/src/cocoa/relaunch.js
CHANGED
|
@@ -181,11 +181,16 @@ export function relaunch(entry, native) {
|
|
|
181
181
|
// the package's own would be a second one the bundler never emits. The
|
|
182
182
|
// worker sets itself up when it imports react-x11 (`bootstrapWorker`,
|
|
183
183
|
// src/bootstrap.js), recognising the shared state in its workerData.
|
|
184
|
-
//
|
|
185
|
-
//
|
|
184
|
+
//
|
|
185
|
+
// No `execArgv`: a Worker given none inherits the node flags the process
|
|
186
|
+
// started with, and among them a loader like `--import tsx`, which is what
|
|
187
|
+
// lets the worker load a .jsx entry at all. Handed `process.execArgv`
|
|
188
|
+
// instead, it refuses the whole list over one V8 or process-wide flag —
|
|
189
|
+
// `--expose-gc`, `--max-old-space-size`, `--title` — which the worker
|
|
190
|
+
// would have had anyway, since those hold for every thread: it gets `gc`
|
|
191
|
+
// either way (measured, Node 20–26; Bun treats the two the same).
|
|
186
192
|
new Worker(entry, {
|
|
187
193
|
argv: process.argv.slice(2),
|
|
188
|
-
execArgv: process.execArgv,
|
|
189
194
|
env: SHARE_ENV,
|
|
190
195
|
workerData: { reactX11State: state.buffer },
|
|
191
196
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SF Symbols for the Cocoa backend (#591): `app.symbols`, the provider
|
|
2
|
+
// `<image src={{ symbol }}>` asks (src/symbols.js), over `@windowkit/appkit`'s
|
|
3
|
+
// `symbolSize` and `ctxDrawSymbol` (0.12.0).
|
|
4
|
+
//
|
|
5
|
+
// A symbol is a template — its shape, in whatever colour it is drawn with —
|
|
6
|
+
// so it is drawn the way a glyph run is: in the context's fill colour, through
|
|
7
|
+
// its transform and clip. Sizes are points, which on this backend are logical
|
|
8
|
+
// pixels.
|
|
9
|
+
|
|
10
|
+
import { warnOnce } from '../symbols.js';
|
|
11
|
+
|
|
12
|
+
export class CocoaSymbols {
|
|
13
|
+
constructor(native) {
|
|
14
|
+
this.native = native;
|
|
15
|
+
this.sizes = new Map(); // name and options -> { width, height } | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whether the bridge draws symbols at all. Without the verbs nothing is
|
|
19
|
+
* shown and nothing else goes wrong, so development says so, once. */
|
|
20
|
+
_available() {
|
|
21
|
+
if (typeof this.native?.ctxDrawSymbol === 'function') return true;
|
|
22
|
+
warnOnce(
|
|
23
|
+
'react-x11: this @windowkit/appkit has no ctxDrawSymbol, so <image ' +
|
|
24
|
+
'src={{ symbol }}> shows nothing on the Cocoa backend. Update ' +
|
|
25
|
+
'@windowkit/appkit to the version react-x11 lists in its ' +
|
|
26
|
+
'optionalDependencies.',
|
|
27
|
+
);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
size(name, options) {
|
|
32
|
+
if (!this._available()) return null;
|
|
33
|
+
const bridge = bridgeOptions(options);
|
|
34
|
+
const key = JSON.stringify([name, bridge]);
|
|
35
|
+
if (!this.sizes.has(key)) {
|
|
36
|
+
if (this.sizes.size > 512) this.sizes.clear();
|
|
37
|
+
this.sizes.set(key, this.native.symbolSize(name, bridge));
|
|
38
|
+
}
|
|
39
|
+
return this.sizes.get(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
draw(ctx, name, rect, options) {
|
|
43
|
+
if (typeof ctx.drawSymbol !== 'function' || !this._available()) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
ctx.fillStyle = options.color;
|
|
47
|
+
return ctx.drawSymbol(
|
|
48
|
+
name,
|
|
49
|
+
rect.x,
|
|
50
|
+
rect.y,
|
|
51
|
+
rect.width,
|
|
52
|
+
rect.height,
|
|
53
|
+
bridgeOptions(options),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What the bridge is handed: the options it knows, and only those set. */
|
|
59
|
+
const bridgeOptions = ({ pointSize, weight, scale, variableValue }) => ({
|
|
60
|
+
pointSize,
|
|
61
|
+
weight,
|
|
62
|
+
...(scale === undefined ? {} : { scale }),
|
|
63
|
+
...(variableValue === undefined ? {} : { variableValue }),
|
|
64
|
+
});
|