react-x11 2.15.3 → 2.16.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 +37 -0
- package/package.json +3 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/capabilities.js +29 -4
- package/src/cocoa/app.js +15 -9
- 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.js +3 -0
- 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/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/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +14 -2
package/README.md
CHANGED
|
@@ -609,6 +609,43 @@ The full threat model, including what react-x11 does and does not defend
|
|
|
609
609
|
against, is [docs/security.md](docs/security.md). To report something,
|
|
610
610
|
[SECURITY.md](SECURITY.md).
|
|
611
611
|
|
|
612
|
+
## Similar projects
|
|
613
|
+
|
|
614
|
+
- [GPUIX](https://github.com/remorses/gpuix) — React for
|
|
615
|
+
[GPUI](https://github.com/zed-industries/zed/tree/main/crates/gpui), Zed's
|
|
616
|
+
GPU UI framework. Each commit crosses a napi-rs addon as one batch of
|
|
617
|
+
mutations to a retained Rust tree, which GPUI lays out with Taffy's flexbox
|
|
618
|
+
and paints through Metal, DirectX or Vulkan, on macOS, Windows and Linux.
|
|
619
|
+
- [react-native-gpui](https://github.com/natew/react-native-gpui) — React
|
|
620
|
+
Native's component API (`View`, `Text`, `Pressable`, `TextInput`,
|
|
621
|
+
`FlatList`) on GPUI as well, hosted the other way round: a Rust binary
|
|
622
|
+
embeds the system's JavaScriptCore and runs the bundled app, and commits
|
|
623
|
+
reach GPUI in memory. macOS on Apple silicon only, for now.
|
|
624
|
+
- [GTKX](https://github.com/gtkx-org/gtkx) — React for GNOME: every GObject
|
|
625
|
+
is a JSX element, so the tree is the platform's own GTK4 and Adwaita
|
|
626
|
+
widgets. The TypeScript bindings are generated from GObject-Introspection
|
|
627
|
+
and call into GTK through a Rust core on Node.js. Linux-only by design.
|
|
628
|
+
- React Native for [Windows](https://github.com/microsoft/react-native-windows)
|
|
629
|
+
and [macOS](https://github.com/microsoft/react-native-macos) — Microsoft's
|
|
630
|
+
desktop platforms for React Native. On Windows the new architecture's
|
|
631
|
+
renderer targets Composition from the start, in Windows App SDK apps by
|
|
632
|
+
default, with XAML islands for advanced controls; on macOS a fork of React
|
|
633
|
+
Native maps its UIKit views onto AppKit. [docs/windows.md](docs/windows.md)
|
|
634
|
+
cites its threading as prior art: JS on a thread of its own, and every
|
|
635
|
+
mount posted to the UI thread that alone touches the visuals.
|
|
636
|
+
- [React Native Skia](https://github.com/Shopify/react-native-skia) — 2D
|
|
637
|
+
graphics for React Native, with a React renderer of its own: the
|
|
638
|
+
components inside a `<Canvas>` become a Skia display list, cheap to
|
|
639
|
+
animate with Reanimated. On iOS, Android, macOS and tvOS, in the browser
|
|
640
|
+
through CanvasKit, and headless on Node. Its experimental Graphite builds
|
|
641
|
+
render through Dawn and can share a GPU device with React Native WebGPU.
|
|
642
|
+
- [React Native WebGPU](https://github.com/wcandillon/react-native-webgpu) —
|
|
643
|
+
not a renderer but a GPU surface for React Native: the WebGPU API
|
|
644
|
+
(`navigator.gpu` and a `<Canvas>` view) on iOS, Android, macOS and
|
|
645
|
+
visionOS, implemented over Dawn, Chrome's WebGPU implementation. It runs
|
|
646
|
+
three.js's WebGPU renderer and React Three Fiber. The nearest thing in
|
|
647
|
+
react-x11 is [`<glarea>`](#3d), which speaks GL instead.
|
|
648
|
+
|
|
612
649
|
## See also
|
|
613
650
|
|
|
614
651
|
- [awesome-react-renderer](https://github.com/chentsulin/awesome-react-renderer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -102,12 +102,12 @@
|
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"ical.js": "^2.2.1",
|
|
104
104
|
"linebreak": "^1.1.0",
|
|
105
|
-
"ntk": "^8.
|
|
105
|
+
"ntk": "^8.9.0",
|
|
106
106
|
"react-reconciler": "^0.34.0",
|
|
107
107
|
"yoga-layout": "^3.2.1"
|
|
108
108
|
},
|
|
109
109
|
"optionalDependencies": {
|
|
110
|
-
"@windowkit/appkit": "^0.
|
|
110
|
+
"@windowkit/appkit": "^0.12.0",
|
|
111
111
|
"@windowkit/wayland": "^3.1.1",
|
|
112
112
|
"dbus-native": "^0.15.1",
|
|
113
113
|
"x11-dri": "^0.9.0"
|
package/src/Reconciler.js
CHANGED
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
} from './nodes/window/window.js';
|
|
25
25
|
import { PopupNode } from './nodes/window/popup.js';
|
|
26
26
|
import { BoxNode } from './nodes/box.js';
|
|
27
|
+
import { THEME_SCOPE } from './nodes/kinds.js';
|
|
28
|
+
import {
|
|
29
|
+
ThemeBoxNode,
|
|
30
|
+
ThemeScopeNode,
|
|
31
|
+
attachTopLevel,
|
|
32
|
+
detachTopLevel,
|
|
33
|
+
} from './nodes/scope.js';
|
|
27
34
|
import { TextNode, TextChunkNode, setTextStripBelow } from './nodes/text.js';
|
|
28
35
|
import { ImageNode } from './nodes/image.js';
|
|
29
36
|
import { CanvasNode } from './nodes/canvas.js';
|
|
@@ -60,7 +67,11 @@ import { watchAppearance } from './appearance.js';
|
|
|
60
67
|
import { setDesktopIntegration } from './desktopintegration.js';
|
|
61
68
|
import { ForeignNode } from './foreignnodes.js';
|
|
62
69
|
import { GlAreaNode } from './glnodes.js';
|
|
63
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
createRegisteredNode,
|
|
72
|
+
elementDefinition,
|
|
73
|
+
registeredElements,
|
|
74
|
+
} from './registry.js';
|
|
64
75
|
import { SvgNode, SvgChildNode } from './svgnodes.js';
|
|
65
76
|
import { loadLayout } from './yoga.js';
|
|
66
77
|
|
|
@@ -97,6 +108,36 @@ export const HOST_TYPES = [
|
|
|
97
108
|
'foreign',
|
|
98
109
|
];
|
|
99
110
|
|
|
111
|
+
// The host context of the root's own children. `atRoot` is what lets
|
|
112
|
+
// `createInstance` see that an element has no window above it — there is no
|
|
113
|
+
// other moment to know it: a node is built bottom-up, before it has a parent
|
|
114
|
+
// to ask, and appended to the container only in the commit.
|
|
115
|
+
const ROOT_CONTEXT = Object.freeze({
|
|
116
|
+
isInsideText: false,
|
|
117
|
+
isInsideSvg: false,
|
|
118
|
+
atRoot: true,
|
|
119
|
+
inWindow: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// What the root of the tree can hold: the elements that are windows of their
|
|
123
|
+
// own, and the provider's node, which holds more of them. Everything else
|
|
124
|
+
// draws into a window, so it has to be inside one.
|
|
125
|
+
const ROOT_TYPES = new Set(['window', 'popup', THEME_SCOPE]);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A drawn element with no window above it. Without this it reached
|
|
129
|
+
* `appendChildToContainer` and failed there as `child.realize is not a
|
|
130
|
+
* function` — or, under a `<ThemeProvider>`, as a `<window>` nested in a
|
|
131
|
+
* `<box>` the app never wrote (#584).
|
|
132
|
+
*/
|
|
133
|
+
function notAtRoot(type) {
|
|
134
|
+
return new Error(
|
|
135
|
+
`react-x11: <${type}> must be inside a <window> — it is at the root of ` +
|
|
136
|
+
'the tree, which holds only <window> and <popup> elements. Render it ' +
|
|
137
|
+
`inside one: <window><${type} …/></window>.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
100
141
|
const HostConfig = {
|
|
101
142
|
supportsMutation: true,
|
|
102
143
|
supportsPersistence: false,
|
|
@@ -135,10 +176,7 @@ const HostConfig = {
|
|
|
135
176
|
scheduleMicrotask: queueMicrotask,
|
|
136
177
|
|
|
137
178
|
getRootHostContext() {
|
|
138
|
-
return
|
|
139
|
-
isInsideText: false,
|
|
140
|
-
isInsideSvg: false,
|
|
141
|
-
};
|
|
179
|
+
return ROOT_CONTEXT;
|
|
142
180
|
},
|
|
143
181
|
|
|
144
182
|
getChildHostContext(parentHostContext, type) {
|
|
@@ -150,6 +188,12 @@ const HostConfig = {
|
|
|
150
188
|
isInsideText: parentHostContext.isInsideText || type === 'text',
|
|
151
189
|
// <svg> children are declarative SVG elements, not react-x11 nodes
|
|
152
190
|
isInsideSvg: parentHostContext.isInsideSvg || type === 'svg',
|
|
191
|
+
// Still at the root under a `<ThemeProvider>` written there, which
|
|
192
|
+
// draws nothing; under anything else, inside a window.
|
|
193
|
+
atRoot: parentHostContext.atRoot && type === THEME_SCOPE,
|
|
194
|
+
// Directly inside a window, where a provider can hand a nested window
|
|
195
|
+
// on to it.
|
|
196
|
+
inWindow: type === 'window' || type === 'popup',
|
|
153
197
|
};
|
|
154
198
|
},
|
|
155
199
|
|
|
@@ -195,6 +239,17 @@ const HostConfig = {
|
|
|
195
239
|
'<text> spans and strings are.',
|
|
196
240
|
);
|
|
197
241
|
}
|
|
242
|
+
// Asked here, in the render phase, rather than when the node reaches the
|
|
243
|
+
// container: an error boundary above can catch it, and nothing has been
|
|
244
|
+
// built yet. A name nobody knows gets the unknown-element error below,
|
|
245
|
+
// which lists what there is.
|
|
246
|
+
if (
|
|
247
|
+
hostContext.atRoot &&
|
|
248
|
+
!ROOT_TYPES.has(type) &&
|
|
249
|
+
(HOST_TYPES.includes(type) || elementDefinition(type))
|
|
250
|
+
) {
|
|
251
|
+
throw notAtRoot(type);
|
|
252
|
+
}
|
|
198
253
|
let node;
|
|
199
254
|
switch (type) {
|
|
200
255
|
case 'window':
|
|
@@ -216,6 +271,18 @@ const HostConfig = {
|
|
|
216
271
|
case 'box':
|
|
217
272
|
node = new BoxNode(props, rootContainer);
|
|
218
273
|
break;
|
|
274
|
+
case THEME_SCOPE:
|
|
275
|
+
// `<ThemeProvider>`'s node (nodes/kinds.js). Above the windows it
|
|
276
|
+
// draws nothing and hands its palette to the windows under it; inside
|
|
277
|
+
// a window it is the box that fills its parent it has always been, so
|
|
278
|
+
// `style` means what it says and a `$token` beneath resolves — and
|
|
279
|
+
// directly inside one, a box that passes nested windows on to it.
|
|
280
|
+
node = hostContext.atRoot
|
|
281
|
+
? new ThemeScopeNode(props, rootContainer)
|
|
282
|
+
: hostContext.inWindow
|
|
283
|
+
? new ThemeBoxNode(props, rootContainer)
|
|
284
|
+
: new BoxNode(props, rootContainer);
|
|
285
|
+
break;
|
|
219
286
|
case 'textinput':
|
|
220
287
|
node = new TextInputNode(props, rootContainer);
|
|
221
288
|
break;
|
|
@@ -278,6 +345,13 @@ const HostConfig = {
|
|
|
278
345
|
},
|
|
279
346
|
|
|
280
347
|
createTextInstance(text, rootContainer, hostContext) {
|
|
348
|
+
if (hostContext.atRoot) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`react-x11: raw text ${JSON.stringify(text)} must be inside a ` +
|
|
351
|
+
'<window>, in a <text> element — it is at the root of the tree, ' +
|
|
352
|
+
'which holds only <window> and <popup> elements.',
|
|
353
|
+
);
|
|
354
|
+
}
|
|
281
355
|
if (!hostContext.isInsideText && !hostContext.isInsideSvg) {
|
|
282
356
|
throw new Error(
|
|
283
357
|
`react-x11: raw text ${JSON.stringify(text)} must be wrapped in a ` +
|
|
@@ -338,19 +412,11 @@ const HostConfig = {
|
|
|
338
412
|
parentInstance.insertBefore(child, null);
|
|
339
413
|
},
|
|
340
414
|
|
|
415
|
+
// The root holds windows, popups and theme scopes (`createInstance` turned
|
|
416
|
+
// everything else away), and nodes/scope.js is what knows how to put each
|
|
417
|
+
// of them there — a scope's windows can arrive after the scope did.
|
|
341
418
|
appendChildToContainer(container, child) {
|
|
342
|
-
|
|
343
|
-
// Top-level window: realize the whole subtree top-down against the
|
|
344
|
-
// screen root.
|
|
345
|
-
child.realize(null);
|
|
346
|
-
}
|
|
347
|
-
// React's getPublicRootInstance answers from the root fiber's first
|
|
348
|
-
// child, and only when that child is a host component. `render()` wraps
|
|
349
|
-
// the tree in a context provider, which is not one, so it would answer
|
|
350
|
-
// null — the container keeps the list instead. Same answer as before:
|
|
351
|
-
// the first top-level node the tree put here.
|
|
352
|
-
(container._rootChildren ??= []).push(child);
|
|
353
|
-
a11yHooks.rootMounted?.(child);
|
|
419
|
+
attachTopLevel(container, child);
|
|
354
420
|
},
|
|
355
421
|
|
|
356
422
|
insertBefore(parentInstance, child, beforeChild) {
|
|
@@ -358,7 +424,7 @@ const HostConfig = {
|
|
|
358
424
|
},
|
|
359
425
|
|
|
360
426
|
insertInContainerBefore(container, child) {
|
|
361
|
-
|
|
427
|
+
attachTopLevel(container, child);
|
|
362
428
|
},
|
|
363
429
|
|
|
364
430
|
removeChild(parentInstance, child) {
|
|
@@ -366,11 +432,8 @@ const HostConfig = {
|
|
|
366
432
|
},
|
|
367
433
|
|
|
368
434
|
removeChildFromContainer(container, child) {
|
|
369
|
-
const roots = container._rootChildren;
|
|
370
|
-
const at = roots ? roots.indexOf(child) : -1;
|
|
371
|
-
if (at !== -1) roots.splice(at, 1);
|
|
372
435
|
// before the destroy, while the subtree is still walkable
|
|
373
|
-
|
|
436
|
+
detachTopLevel(container, child);
|
|
374
437
|
child.destroySubtree();
|
|
375
438
|
},
|
|
376
439
|
|
package/src/anchor.js
CHANGED
|
@@ -204,44 +204,86 @@ export function anchorRect(node, options = {}) {
|
|
|
204
204
|
// popup's `x`/`y` props. The math between runs in device pixels, because
|
|
205
205
|
// `abs`, the window origin and the monitor area are (src/scale.js).
|
|
206
206
|
const s = node.scale ?? 1;
|
|
207
|
+
// The anchor is the sub-rect where there is one, all the way through:
|
|
208
|
+
// the side that flips, the edge that aligns, and — since a popup with no
|
|
209
|
+
// size of its own is as wide as the thing it hangs off — the default
|
|
210
|
+
// width.
|
|
211
|
+
const anchor = subRect(node, deviceAt(options.at, s));
|
|
212
|
+
// the rect the *alignment* reads, which is the anchor's own unless the
|
|
213
|
+
// caller split the two axes — one origin serves both, since both nodes are
|
|
214
|
+
// in the same window
|
|
215
|
+
const cross = options.alignTo ? (subRect(options.alignTo) ?? anchor) : anchor;
|
|
216
|
+
const origin = windowOrigin(node);
|
|
217
|
+
const onScreen = (r) => ({
|
|
218
|
+
x: origin.x + r.x,
|
|
219
|
+
y: origin.y + r.y,
|
|
220
|
+
width: r.width,
|
|
221
|
+
height: r.height,
|
|
222
|
+
});
|
|
223
|
+
return placeAgainst(
|
|
224
|
+
onScreen(anchor),
|
|
225
|
+
onScreen(cross),
|
|
226
|
+
deviceAnchorArea(node),
|
|
227
|
+
s,
|
|
228
|
+
{ ...options, direction: options.direction ?? node.direction },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The same placement against a rect **on the screen**, with no node behind
|
|
234
|
+
* it: the frame of the tray item a click reports, or a point where the
|
|
235
|
+
* pointer was. `rect` is logical screen pixels, the unit a popup's `x`/`y`
|
|
236
|
+
* are in, and `{ x, y }` alone is a point; the result is logical too.
|
|
237
|
+
*
|
|
238
|
+
* `scale` is the display's, since there is no node to ask, and `direction`
|
|
239
|
+
* decides what `'start'` and `'end'` mean — a node's anchor takes both from
|
|
240
|
+
* the node. The popup is kept on the monitor the rect is on.
|
|
241
|
+
*/
|
|
242
|
+
export function anchorScreenRect(app, rect, options = {}) {
|
|
243
|
+
if (typeof rect?.x !== 'number' || typeof rect?.y !== 'number') return null;
|
|
244
|
+
const s = options.scale ?? 1;
|
|
245
|
+
const anchor = {
|
|
246
|
+
x: rect.x * s,
|
|
247
|
+
y: rect.y * s,
|
|
248
|
+
width: (rect.width ?? 0) * s,
|
|
249
|
+
height: (rect.height ?? 0) * s,
|
|
250
|
+
};
|
|
251
|
+
const area = app ? availableArea(app, { x: anchor.x, y: anchor.y }) : null;
|
|
252
|
+
return placeAgainst(anchor, anchor, area, s, options);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The placement both anchors share, in device pixels: `anchor` is the rect
|
|
257
|
+
* the popup hangs off, `cross` the one it aligns to, both on the screen, and
|
|
258
|
+
* `area` what it is kept inside — null where there is nothing to ask, and it
|
|
259
|
+
* then neither flips nor clamps.
|
|
260
|
+
*/
|
|
261
|
+
function placeAgainst(anchor, cross, area, s, options) {
|
|
207
262
|
const {
|
|
208
263
|
placement = 'bottom',
|
|
209
264
|
align = 'start',
|
|
210
265
|
alignOffset: logicalAlignOffset = 0,
|
|
211
266
|
offset: logicalOffset = 2,
|
|
212
|
-
at,
|
|
213
|
-
alignTo,
|
|
214
267
|
// A popup that covers its own anchor — a native popup button's menu,
|
|
215
268
|
// which opens with the chosen row over the control — has no other side
|
|
216
269
|
// to flip to: it is clamped into the screen instead, as AppKit clamps.
|
|
217
270
|
flip = true,
|
|
218
|
-
direction
|
|
271
|
+
direction,
|
|
219
272
|
} = options;
|
|
220
273
|
const alignOffset = logicalAlignOffset * s;
|
|
221
274
|
const offset = logicalOffset * s;
|
|
222
275
|
const rtl = direction === 'rtl';
|
|
223
276
|
|
|
224
|
-
// The anchor is the sub-rect where there is one, all the way through:
|
|
225
|
-
// the side that flips, the edge that aligns, and — since a popup with no
|
|
226
|
-
// size of its own is as wide as the thing it hangs off — the default
|
|
227
|
-
// width.
|
|
228
|
-
const anchor = subRect(node, deviceAt(at, s));
|
|
229
277
|
const width = options.width !== undefined ? options.width * s : anchor.width;
|
|
230
278
|
const height = options.height !== undefined ? options.height * s : 0;
|
|
231
279
|
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const ay = origin.y + anchor.y;
|
|
280
|
+
const ax = anchor.x;
|
|
281
|
+
const ay = anchor.y;
|
|
235
282
|
const aw = anchor.width;
|
|
236
283
|
const ah = anchor.height;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// in the same window
|
|
240
|
-
const cross = alignTo ? (subRect(alignTo) ?? anchor) : anchor;
|
|
241
|
-
const cx = origin.x + cross.x;
|
|
242
|
-
const cy = origin.y + cross.y;
|
|
284
|
+
const cx = cross.x;
|
|
285
|
+
const cy = cross.y;
|
|
243
286
|
|
|
244
|
-
const area = deviceAnchorArea(node);
|
|
245
287
|
const left = area?.x ?? 0;
|
|
246
288
|
const top = area?.y ?? 0;
|
|
247
289
|
const right = area ? area.x + area.width : null;
|
package/src/capabilities.js
CHANGED
|
@@ -176,8 +176,8 @@ async function probeNotifications({ app } = {}) {
|
|
|
176
176
|
// tray
|
|
177
177
|
// ---------------------------------------------------------------------------
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
/** The tray on an app that has one of its own, which needs nothing asked. */
|
|
180
|
+
function trayNow(target) {
|
|
181
181
|
if (typeof target?.createStatusItem === 'function') {
|
|
182
182
|
return frozen('cocoa', {
|
|
183
183
|
menu: true,
|
|
@@ -194,6 +194,12 @@ async function probeTray({ app } = {}) {
|
|
|
194
194
|
scroll: false,
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function probeTray({ app } = {}) {
|
|
201
|
+
const now = trayNow(app ?? soleApp());
|
|
202
|
+
if (now) return now;
|
|
197
203
|
|
|
198
204
|
const ref = await sessionBus();
|
|
199
205
|
if (!ref) return NONE;
|
|
@@ -226,8 +232,8 @@ async function probeTray({ app } = {}) {
|
|
|
226
232
|
// launcher
|
|
227
233
|
// ---------------------------------------------------------------------------
|
|
228
234
|
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
/** The Dock tile on an app that has one, which needs nothing asked. */
|
|
236
|
+
function launcherNow(target) {
|
|
231
237
|
if (typeof target?.setDockBadge === 'function') {
|
|
232
238
|
return frozen('cocoa', {
|
|
233
239
|
badge: true,
|
|
@@ -239,6 +245,12 @@ async function probeLauncher({ app } = {}) {
|
|
|
239
245
|
needsDesktopFile: false,
|
|
240
246
|
});
|
|
241
247
|
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function probeLauncher({ app } = {}) {
|
|
252
|
+
const now = launcherNow(app ?? soleApp());
|
|
253
|
+
if (now) return now;
|
|
242
254
|
|
|
243
255
|
// The Linux rung needs two things that are not the bus: an app id to
|
|
244
256
|
// attribute the entry to, and a `.desktop` file of that name for the
|
|
@@ -322,3 +334,16 @@ export async function desktopCapability(name, options = {}) {
|
|
|
322
334
|
/** The "nothing here" answer, exported so a caller can compare against it and
|
|
323
335
|
* so the hook has something stable to return on the first frame. */
|
|
324
336
|
export const NO_CAPABILITY = NONE;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The answer for a capability where it is known without asking anything —
|
|
340
|
+
* the Cocoa app's own tray and Dock tile — or null where finding out takes a
|
|
341
|
+
* round trip. What lets `useDesktopCapability` be settled on its first frame
|
|
342
|
+
* where the answer never needed waiting for.
|
|
343
|
+
*/
|
|
344
|
+
export function capabilityNow(name, { app } = {}) {
|
|
345
|
+
const target = app ?? soleApp();
|
|
346
|
+
if (name === 'tray') return trayNow(target);
|
|
347
|
+
if (name === 'launcher') return launcherNow(target);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
package/src/cocoa/app.js
CHANGED
|
@@ -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';
|
|
@@ -200,6 +201,8 @@ export class CocoaApp {
|
|
|
200
201
|
// real bridge on the machine — the manager's default loads it only when
|
|
201
202
|
// it is built standalone
|
|
202
203
|
this.fonts = new CocoaFontManager(native);
|
|
204
|
+
// `<image src={{ symbol }}>`'s names are SF Symbols here (src/symbols.js)
|
|
205
|
+
this.symbols = new CocoaSymbols(native);
|
|
203
206
|
|
|
204
207
|
// Native open/save panels (src/cocoa/filepanels.js). Present exactly
|
|
205
208
|
// when the bridge has them (>= 0.5), and its presence is what puts the
|
|
@@ -641,6 +644,8 @@ export class CocoaApp {
|
|
|
641
644
|
_unregisterWindow(wnd) {
|
|
642
645
|
this._windows.delete(wnd._key);
|
|
643
646
|
if (this._grabWindow === wnd) this._grabWindow = null;
|
|
647
|
+
// a popup that took the keyboard and closed is not where keys go next
|
|
648
|
+
if (this._lastKeyWindow === wnd) this._lastKeyWindow = null;
|
|
644
649
|
}
|
|
645
650
|
|
|
646
651
|
// --- the pump ------------------------------------------------------------
|
|
@@ -802,8 +807,8 @@ export class CocoaApp {
|
|
|
802
807
|
this._rafQueue = [];
|
|
803
808
|
let soonest = Infinity;
|
|
804
809
|
for (const entry of queue) {
|
|
805
|
-
// A window whose last flip has not
|
|
806
|
-
//
|
|
810
|
+
// A window whose last flip has not reached its layer yet (threaded
|
|
811
|
+
// mode's fence, `CocoaWindow.frameInFlight`) waits before
|
|
807
812
|
// its clock is asked, or the clock would count a frame that did not
|
|
808
813
|
// run. The release is an event, and the batch it arrives in ticks
|
|
809
814
|
// again.
|
|
@@ -874,7 +879,7 @@ export class CocoaApp {
|
|
|
874
879
|
* paid once, when the batch is done: ten moves and a click that crossed
|
|
875
880
|
* while this thread was busy are one frame, not eleven. Then what a pump
|
|
876
881
|
* tick does: the frames that are due, and the ones a window's visibility
|
|
877
|
-
* or its
|
|
882
|
+
* or its last flip was holding, which an occlusion change or a
|
|
878
883
|
* `surface-released` in this very batch may just have freed.
|
|
879
884
|
*/
|
|
880
885
|
_routeBatch(batch) {
|
|
@@ -1090,9 +1095,9 @@ export class CocoaApp {
|
|
|
1090
1095
|
// the notch and the scroll, most of a refresh period of nothing.
|
|
1091
1096
|
//
|
|
1092
1097
|
// Mostly: a trackpad's momentum lands two in one tick often, and a
|
|
1093
|
-
// second flip inside the refresh
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1098
|
+
// second flip inside the refresh is a frame the display never shows
|
|
1099
|
+
// (`CocoaWindow._flippedRecently` says what it cost). So the rest of a
|
|
1100
|
+
// burst lands React's half and leaves the
|
|
1096
1101
|
// paint to the paced frame the scroll already asked for — the model
|
|
1097
1102
|
// has scrolled, and the next refresh shows all of it.
|
|
1098
1103
|
if (wnd._flippedRecently()) {
|
|
@@ -1201,9 +1206,10 @@ export class CocoaApp {
|
|
|
1201
1206
|
/**
|
|
1202
1207
|
* `surface-released`: a worker's frame took an IOSurface off a layer and
|
|
1203
1208
|
* the frame that replaced it has committed (windowkit/appkit#52). The
|
|
1204
|
-
* window whose
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1209
|
+
* window whose buffer that was stops waiting on it, and may draw into it
|
|
1210
|
+
* again once the WindowServer lets go of it too
|
|
1211
|
+
* (`CocoaWindow._surfaceReleased`); the tick at the end of this batch
|
|
1212
|
+
* runs the frame it was holding.
|
|
1207
1213
|
*/
|
|
1208
1214
|
_routeSurfaceReleased(ev) {
|
|
1209
1215
|
for (const wnd of this._windows.values()) {
|
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
|