react-x11 2.51.0 → 2.52.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 +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
package/docs/gl.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# 3D: `<glarea>` and the three ways underneath it
|
|
2
|
+
|
|
3
|
+
react-x11 gives you a **GL surface in the layout** and nothing above it. A
|
|
4
|
+
scene graph — meshes, materials, lights, post-processing — is
|
|
5
|
+
[`@react-x11/components/three`](ecosystem.md), which brings its own
|
|
6
|
+
reconciler and renders through the surface described here.
|
|
7
|
+
|
|
8
|
+
```jsx
|
|
9
|
+
<glarea
|
|
10
|
+
style={{ flexGrow: 1 }}
|
|
11
|
+
clearColor="#0b1021"
|
|
12
|
+
frameLoop="always"
|
|
13
|
+
onCreated={(gl) => gl.Enable(gl.DEPTH_TEST)}
|
|
14
|
+
onDraw={(gl, { width, height }) => {
|
|
15
|
+
/* one frame */
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`examples/viewer3d.jsx` is the worked example: a model viewer that orbits,
|
|
21
|
+
and the display-list discipline the indirect backend demands.
|
|
22
|
+
|
|
23
|
+
## The three ways
|
|
24
|
+
|
|
25
|
+
`<glarea>` and `onDraw` are the same everywhere. What is underneath is not:
|
|
26
|
+
these are different APIs, not three spellings of one, and which one a
|
|
27
|
+
connection has decides what `onDraw` can do.
|
|
28
|
+
|
|
29
|
+
| | **direct** (X11) | **indirect** (X11) | **Cocoa** |
|
|
30
|
+
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
31
|
+
| how it draws | OpenGL ES 2 on the GPU, in two flavors: on Linux frames reach the server as dma-buf descriptors over DRI3 + Present, and on macOS/XQuartz the server exports the window's surface over Apple-DRI and CGL draws into it | GL commands encoded into the X connection | CGL — OpenGL 4.1 core on Metal — into IOSurface-backed framebuffers, presented as a sublayer |
|
|
32
|
+
| shaders | **yes**, GLSL ES 1.00 | none; the protocol encodes no shader objects | **yes** |
|
|
33
|
+
| render targets | **yes** — framebuffer objects | none; the protocol encodes no framebuffer objects | **yes** — it draws into one |
|
|
34
|
+
| geometry | vertex buffers on the GPU | immediate mode compiled into display lists | vertex buffers on the GPU |
|
|
35
|
+
| lighting | per fragment | per vertex | per fragment |
|
|
36
|
+
| cost per frame | one Present request | matrices, material state and one `CallList` per mesh | a swap between two targets; the WindowServer composites |
|
|
37
|
+
| where it runs | a local connection, plus ntk's optional `x11-dri` addon: a Linux server with DRI3, or macOS/XQuartz with Apple-DRI (ntk 8.4.0) | any server that allows indirect contexts, including over a network | any Mac, on the Cocoa backend — it needs the same optional `x11-dri` addon and nothing else |
|
|
38
|
+
|
|
39
|
+
The Cocoa column is the X11 direct path with the X-specific half swapped
|
|
40
|
+
out: the same `x11-dri` CGL context and the same WebGL-shaped `gl` table,
|
|
41
|
+
but instead of attaching to a surface an X server exported, frames render
|
|
42
|
+
into IOSurfaces and present by handing the surface's id to the area's own
|
|
43
|
+
`CALayer`. `glPolicy: 'indirect'` is the one setting that is an error there
|
|
44
|
+
and says so — GLX is a protocol, and there is no server to speak it to.
|
|
45
|
+
|
|
46
|
+
The two other backends have no column because they hand `onDraw` the same
|
|
47
|
+
direct API the Cocoa one does, and report `gl.backend === 'direct'` too. On
|
|
48
|
+
**win32** a `<glarea>` is a WGL core context drawing into a Direct3D texture
|
|
49
|
+
that is copied into a composition swap chain under the window's own visual
|
|
50
|
+
([windows-gl.md](windows-gl.md)), so it composites with the window's alpha
|
|
51
|
+
where an X11 surface does not. On **Wayland** it is not a surface of its own
|
|
52
|
+
at all: it is a rectangle of the window's backing target, drawn by the
|
|
53
|
+
window's own GLES context between the tree's 2D paint and its children's
|
|
54
|
+
panes (`src/wayland/glarea.js`).
|
|
55
|
+
|
|
56
|
+
On X11 the default is indirect, because it is what react-x11 has always
|
|
57
|
+
used. Turn the other on per app:
|
|
58
|
+
|
|
59
|
+
```jsx
|
|
60
|
+
const root = await createRoot({ glPolicy: 'auto' });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`'auto'` uses direct where it is available and indirect otherwise, which is
|
|
64
|
+
usually what you want: most modern desktops **refuse** indirect GLX — Xorg
|
|
65
|
+
1.17 and later, and Xwayland, ship with it off — and those are exactly the
|
|
66
|
+
machines where direct works. `'direct'` and `'off'` are the strict forms, and
|
|
67
|
+
`'indirect'` is the default. One run can be switched without touching code:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
NTK_GL_POLICY=direct npm start
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Which flavor a connection got is `app.glCapabilities().flavor` — `'dri3'` or
|
|
74
|
+
`'appledri'`. Both spell the context the same way, so nothing above the
|
|
75
|
+
policy has to branch on it; it is worth reading when a machine that should
|
|
76
|
+
have direct does not.
|
|
77
|
+
|
|
78
|
+
Everything about how the backend is chosen and why it might be unavailable —
|
|
79
|
+
the `GLError` codes, `app.glCapabilities()`, the addon — is ntk's, and is
|
|
80
|
+
documented in [ntk's context-gles.md](https://github.com/sidorares/ntk/blob/master/docs/context-gles.md).
|
|
81
|
+
|
|
82
|
+
`npm run labs:direct-gl` is the shader path on a real display, and reports
|
|
83
|
+
both the flavor and which backend actually drew.
|
|
84
|
+
|
|
85
|
+
## Raw GL through `onDraw`
|
|
86
|
+
|
|
87
|
+
`onDraw(gl, { width, height, node })` hands you the context itself, and
|
|
88
|
+
there are two spellings of GL to write against — camelCase ES 2 against
|
|
89
|
+
PascalCase OpenGL 1.x. Nothing translates between them. Branch on
|
|
90
|
+
`gl.backend`, which is `'direct'` or `'indirect'`; the Cocoa path reports
|
|
91
|
+
`'direct'`, because that is the API it hands you:
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
<glarea
|
|
95
|
+
onDraw={(gl) => {
|
|
96
|
+
if (gl.backend === 'direct') gl.clear(gl.COLOR_BUFFER_BIT);
|
|
97
|
+
else gl.Clear(gl.COLOR_BUFFER_BIT);
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Code written against one spelling will not run on the other, which is why
|
|
103
|
+
the default X11 policy does not switch under an app that never asked for it
|
|
104
|
+
— and why `examples/viewer3d.jsx` reports which one it got rather than
|
|
105
|
+
pretending it can draw on both. It also means a scene written for the direct
|
|
106
|
+
API runs unchanged on the Cocoa backend, and one written for indirect GLX
|
|
107
|
+
does not run there at all.
|
|
108
|
+
|
|
109
|
+
**On the indirect backend, geometry belongs in a display list.** Every
|
|
110
|
+
immediate-mode vertex is a command on the wire, so a mesh re-sent per frame
|
|
111
|
+
costs kilobytes per frame while a compiled list costs one `CallList`. Names
|
|
112
|
+
are yours to choose — `GenLists` is a round trip, and this is the backend
|
|
113
|
+
where round trips are the thing to avoid.
|
|
114
|
+
|
|
115
|
+
## Input
|
|
116
|
+
|
|
117
|
+
The pointer over a surface is the tree's, on every backend: `onMouseDown`,
|
|
118
|
+
`onMouseMove`, `onMouseUp`, `onClick`, `onWheel` and the rest fire at the
|
|
119
|
+
`<glarea>` — or at a child drawn over it (below) — and bubble, with
|
|
120
|
+
`ev.localX`/`localY` measured from the target's corner, which is what a
|
|
121
|
+
scene picks with. Nothing needs to listen on the surface's own window, and
|
|
122
|
+
nothing should: on X11 a listener there takes the event away from the tree
|
|
123
|
+
([elements.md](elements.md#glarea) says why).
|
|
124
|
+
|
|
125
|
+
## 2D over the surface
|
|
126
|
+
|
|
127
|
+
A `<glarea>`'s children are drawn above it — a legend, a toolbar, a label
|
|
128
|
+
over the scene — laid out in its box and hit before it. They are ordinary
|
|
129
|
+
elements. Core paints them on panes stacked over the surface, from the
|
|
130
|
+
window's own frame. On the Cocoa backend Core Animation composites the
|
|
131
|
+
panes with the GL frame, translucency and all. On X11 a pane is an opaque
|
|
132
|
+
child window, so what a child leaves unpainted shows the surface's
|
|
133
|
+
`clearColor` ([elements.md](elements.md#glarea) has the table).
|
|
134
|
+
`useSupports('glOverlay')` asks whether a connection draws them;
|
|
135
|
+
`examples/labs/gl-overlay.jsx` runs one on both backends.
|
|
136
|
+
|
|
137
|
+
The answer is no on XQuartz. The macOS window server composites every GL
|
|
138
|
+
surface there above everything the X server draws, so nothing can be drawn
|
|
139
|
+
over one. The children are laid out and not drawn, and a development build
|
|
140
|
+
warns once. On macOS the Cocoa backend is the one that composites a HUD
|
|
141
|
+
over GL.
|
|
142
|
+
|
|
143
|
+
So a HUD is not GL's business: its text is set by the app's own text
|
|
144
|
+
engine, its controls take their own input, and nothing is rasterized into a
|
|
145
|
+
texture.
|
|
146
|
+
|
|
147
|
+
## When there is no surface at all
|
|
148
|
+
|
|
149
|
+
`onError(err)` fires when no GL context could be made: no GLX, indirect
|
|
150
|
+
disabled, no matching visual. `err.code` is one of ntk's `GLXError` values,
|
|
151
|
+
`GLX_INDIRECT_DISABLED` being the usual one. Without a handler the failure is
|
|
152
|
+
a console warning and the element draws nothing, which looks like a bug in
|
|
153
|
+
your scene rather than a fact about the machine — so handle it and say what
|
|
154
|
+
the reader can do.
|
|
155
|
+
|
|
156
|
+
## Testing
|
|
157
|
+
|
|
158
|
+
A GL app is tested on **what it emits**, not on pixels — which is also the
|
|
159
|
+
only option, because GL renders where `GetImage` cannot read it (see
|
|
160
|
+
[glx.md](glx.md)):
|
|
161
|
+
|
|
162
|
+
- `test/glarea.test.js` drives node-x11's in-process X server with its GLX
|
|
163
|
+
emulator registered, so a frame's GL calls land on a `RecordingBackend`.
|
|
164
|
+
No display, no GPU, no addon.
|
|
165
|
+
- `test/viewer3d.test.js` does the same for the example, and says in its
|
|
166
|
+
header which claim that harness cannot see and where it is measured
|
|
167
|
+
instead.
|
|
168
|
+
|
|
169
|
+
See also [glx.md](glx.md) for the indirect backend's design, and what its
|
|
170
|
+
transport can never do.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# The global menu
|
|
2
|
+
|
|
3
|
+
Some desktops put an application's menu bar in their own panel rather than in
|
|
4
|
+
its window — Unity's design, still shipped by KDE Plasma's Application Menu
|
|
5
|
+
widget, by `vala-panel-appmenu`, and by several GNOME extensions. On one of
|
|
6
|
+
those, an app that draws its own menu bar is drawing a second one.
|
|
7
|
+
|
|
8
|
+
react-x11 notices and hands the menu over. There is **nothing to configure and
|
|
9
|
+
nothing to call**:
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
import { MenuBar } from 'react-x11';
|
|
13
|
+
|
|
14
|
+
<MenuBar
|
|
15
|
+
menus={[
|
|
16
|
+
{
|
|
17
|
+
label: 'File',
|
|
18
|
+
items: [
|
|
19
|
+
{ label: 'New', shortcut: [['Control', 'N']], onSelect: newFile },
|
|
20
|
+
{ type: 'separator' },
|
|
21
|
+
{ label: 'Save As…', enabled: false },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
]}
|
|
25
|
+
/>;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That is the whole of it, and it is the same line every time. Where the
|
|
29
|
+
platform has somewhere to put a menu, `MenuBar` renders nothing and the
|
|
30
|
+
platform shows this one; where it does not, `MenuBar` draws the bar itself,
|
|
31
|
+
exactly as before.
|
|
32
|
+
|
|
33
|
+
| where the menu goes | when |
|
|
34
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
35
|
+
| **the macOS menu bar**, at the top of the screen | the Cocoa backend, always |
|
|
36
|
+
| **the desktop's panel**, over `com.canonical.dbusmenu` | a panel is running one of the applets above |
|
|
37
|
+
| **the window**, drawn by `MenuBar` | everything else — a stock GNOME session, XQuartz, an ssh session, a bare `startx`, CI |
|
|
38
|
+
|
|
39
|
+
The rest of this page is about the middle row, which is the one with a
|
|
40
|
+
protocol in it. The macOS row is
|
|
41
|
+
[macos.md §Menus](macos.md#menus-the-global-menu-finally-at-home): the
|
|
42
|
+
adapter is `src/cocoa/globalmenu.js`, it consumes the same `menus` array
|
|
43
|
+
through the same snapshot machinery, and because a menu bar is a platform
|
|
44
|
+
constant rather than a service that may or may not be running, it takes the
|
|
45
|
+
menu the moment it is installed — the drawn fallback never renders there.
|
|
46
|
+
|
|
47
|
+
## The item vocabulary is the protocol's
|
|
48
|
+
|
|
49
|
+
`menus` is a plain data prop rather than JSX children, and its item shape is
|
|
50
|
+
`com.canonical.dbusmenu`'s. That is what makes the rest work: **the identical
|
|
51
|
+
array serialises**, so there is one authoring model rather than a drawn one and
|
|
52
|
+
an exported one that drift apart.
|
|
53
|
+
|
|
54
|
+
| property | meaning |
|
|
55
|
+
| -------------------------- | ----------------------------------------------------------- |
|
|
56
|
+
| `label` | the row's text |
|
|
57
|
+
| `type` | `'separator'` draws a rule; absent means an ordinary row |
|
|
58
|
+
| `items` | a submenu |
|
|
59
|
+
| `enabled` | defaults to `true`; `false` dims the row and makes it inert |
|
|
60
|
+
| `visible` | defaults to `true`; `false` removes it |
|
|
61
|
+
| `shortcut` | `[['Control', 'S']]` — drawn, exported **and bound** |
|
|
62
|
+
| `toggleType`/`toggleState` | `'checkmark'` or `'radio'`, and `0` / `1` / `-1` |
|
|
63
|
+
| `iconName` | an icon-theme name, for the panel's menu |
|
|
64
|
+
| `iconData` | raw PNG bytes, for an icon that is not in a theme |
|
|
65
|
+
| `disposition` | `'normal'`, `'informative'`, `'warning'`, `'alert'` |
|
|
66
|
+
| `onSelect(item)` | runs whichever menu the click came from |
|
|
67
|
+
| `key` | identity, when the label is not stable |
|
|
68
|
+
|
|
69
|
+
`toggleState` has **three** values, not two: `-1` is _indeterminate_ — a "Bold"
|
|
70
|
+
that is on for part of the selection — and it draws as a dash rather than as
|
|
71
|
+
unchecked, because "off" is a different claim from "it depends".
|
|
72
|
+
|
|
73
|
+
`icon` is the one thing that does not cross the bus. It is a react-x11 drawing
|
|
74
|
+
callback ([components.md](components.md#menubar--contextmenu)) and the desktop
|
|
75
|
+
cannot call a function, so an item that wants an icon in both menus carries
|
|
76
|
+
`icon` for ours and `iconName` for the panel's.
|
|
77
|
+
|
|
78
|
+
### Shortcuts are a list, not a string
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
shortcut: [['Control', 'S']];
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The outer array is a list of _alternatives_; each inner one is modifier tokens
|
|
85
|
+
(`Control`, `Alt`, `Shift`, `Super`) ending in the key. Only the first is
|
|
86
|
+
drawn — a row has one shortcut column.
|
|
87
|
+
|
|
88
|
+
**On macOS, `Control` means ⌘.** A cross-platform app writes its shortcuts
|
|
89
|
+
in the primary modifier, and the Mac's primary modifier is Command, not its
|
|
90
|
+
own ⌃ — so `[['Control', 'S']]` becomes ⌘S in the menu bar, which is what a
|
|
91
|
+
Mac user expects Save to be. `Super` maps there too; `Shift` and `Alt` are
|
|
92
|
+
themselves. A chord the `NSMenu` spelling cannot express is simply not
|
|
93
|
+
printed beside the item — the binding still fires, because the app's own
|
|
94
|
+
window answers it (below). The text controls' own chords follow the same rule
|
|
95
|
+
there, and so does the edit menu that prints them
|
|
96
|
+
([events.md](events.md#primary-modifier)), and so do the window's own
|
|
97
|
+
bindings: `useAccelerator`, and a drawn `MenuBar` or `ContextMenu`, which
|
|
98
|
+
prints `⌘S` beside the row ([events.md](events.md#the-chord)).
|
|
99
|
+
|
|
100
|
+
The key is named the way GDK names it, so `plus` rather than `+` and `Prior`
|
|
101
|
+
rather than `PgUp`, because that is what a panel's importer parses. Menus print
|
|
102
|
+
the friendly form: `[['Control', 'plus']]` draws as `Ctrl++`.
|
|
103
|
+
|
|
104
|
+
**It is a binding**, and the app writes it once. A mounted `MenuBar` answers
|
|
105
|
+
its own items' chords: Ctrl+S fires the item's `onSelect` without the menu
|
|
106
|
+
being opened, gated by that item's own `enabled` and `visible`
|
|
107
|
+
([events.md](events.md#accelerators)).
|
|
108
|
+
|
|
109
|
+
That matters most in exactly this case. When the panel draws the menu, the
|
|
110
|
+
**panel is not going to deliver the key** — it has no idea the window's
|
|
111
|
+
keyboard is where the user is — so the app's own window still has to, and
|
|
112
|
+
`MenuBar` keeps its bindings when it stops drawing. `accelerators={false}`
|
|
113
|
+
turns them off for an application with a dispatcher of its own.
|
|
114
|
+
|
|
115
|
+
The macOS menu bar is the one delegate that **does** deliver keys: it runs an
|
|
116
|
+
item on the ⌘ chord it installed as its key equivalent, and it does that after
|
|
117
|
+
the window has already seen the key. So there `MenuBar` reserves those chords
|
|
118
|
+
— no binding in the window answers them, its own included, and the item runs
|
|
119
|
+
once — and keeps answering the rest, the chords no `NSMenu` key equivalent can
|
|
120
|
+
spell ([events.md](events.md#the-macos-menu-bar)).
|
|
121
|
+
|
|
122
|
+
## What happens on the wire
|
|
123
|
+
|
|
124
|
+
The D-Bus path, in enough detail to recognise it in `dbus-monitor` and to
|
|
125
|
+
know what to look at when a menu does not appear. None of this applies on
|
|
126
|
+
the Cocoa backend, where there is no wire and no registrar — installing an
|
|
127
|
+
`NSMenu` is the whole of it.
|
|
128
|
+
|
|
129
|
+
1. **Detection.** Does something own `com.canonical.AppMenu.Registrar` right
|
|
130
|
+
now? Only ownership counts — see the trap below.
|
|
131
|
+
2. **Export.** The menu is served at `/com/react_x11/menus/<xid>` on the
|
|
132
|
+
process's existing bus connection ([dbus.md](dbus.md)), so the desktop sees
|
|
133
|
+
one application rather than several.
|
|
134
|
+
3. **Registration.** `RegisterWindow(xid, path)`. The service name is inferred
|
|
135
|
+
from the sender, so only the path is supplied.
|
|
136
|
+
4. **The KDE properties**, in addition: `_KDE_NET_WM_APPMENU_SERVICE_NAME` and
|
|
137
|
+
`_KDE_NET_WM_APPMENU_OBJECT_PATH`, written as `STRING`/format 8. Plasma
|
|
138
|
+
reads these off the window instead of asking a registrar, and runs a
|
|
139
|
+
registrar _as well_ — which of the two a given version prefers is not
|
|
140
|
+
something to bet a menu on.
|
|
141
|
+
5. **Only then** does `MenuBar` stop drawing. `RegisterWindow` returning is the
|
|
142
|
+
evidence; a registrar that refuses the call leaves the menu where the user
|
|
143
|
+
can still reach it.
|
|
144
|
+
|
|
145
|
+
It is followed for the life of the window, not sampled once: `NameOwnerChanged`
|
|
146
|
+
moves the menu into the panel when one starts and back into the window when one
|
|
147
|
+
exits, with no re-render on the app's part.
|
|
148
|
+
|
|
149
|
+
### The trap: activatable is not running
|
|
150
|
+
|
|
151
|
+
Ubuntu ships `com.canonical.AppMenu.Registrar` as a D-Bus **activatable**
|
|
152
|
+
service, so the name resolves even with no panel installed. This is the exact
|
|
153
|
+
opposite of the rule `hasService()` follows for portals, and getting it wrong
|
|
154
|
+
is silent and total — an app with no menu anywhere.
|
|
155
|
+
|
|
156
|
+
A registrar is not the feature; it is a directory the panel reads. Nothing
|
|
157
|
+
starts one except a panel starting up. So react-x11 asks `ListNames` for a live
|
|
158
|
+
owner, and every call it then makes carries D-Bus's `NO_AUTO_START` flag —
|
|
159
|
+
without which the tidy-up `UnregisterWindow` sent after a panel dies would
|
|
160
|
+
_launch a fresh registrar_, which would own the name with nothing drawing the
|
|
161
|
+
menus it collects.
|
|
162
|
+
|
|
163
|
+
## Seeing it work, without a desktop that has one
|
|
164
|
+
|
|
165
|
+
`com.canonical.dbusmenu` has no reference client you can install on its own —
|
|
166
|
+
the consumers are panel applets welded into Plasma, into `vala-panel`, or into
|
|
167
|
+
a GNOME extension. So there is one in this repo:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run globalmenu:host
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It owns the registrar name — it _is_ the panel — and prints every menu handed
|
|
174
|
+
to it. In another terminal:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm run examples:menu
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The window's own menu bar disappears, which is the feature, and the menu
|
|
181
|
+
appears in the host. Type an item's id and press Enter to activate it; the
|
|
182
|
+
example's own state changes, and the property update comes back the other way.
|
|
183
|
+
Ctrl-C the host and the bar returns to the window.
|
|
184
|
+
|
|
185
|
+
On a machine that really does have a panel, `npm run globalmenu:host -- --watch`
|
|
186
|
+
attaches to the live registrar rather than competing for the name.
|
|
187
|
+
|
|
188
|
+
## Turning it off
|
|
189
|
+
|
|
190
|
+
```jsx
|
|
191
|
+
<MenuBar menus={menus} globalMenu={false} />
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
REACT_X11_NO_GLOBAL_MENU=1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
await createRoot({ desktop: { globalMenu: false } });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The prop is for one bar. The other two are process-wide: the environment
|
|
203
|
+
variable needs no application change, which is what a user working around a
|
|
204
|
+
panel that renders something badly actually has, and the `createRoot` option
|
|
205
|
+
is the one an embedder can reach without also setting it for every child
|
|
206
|
+
process it spawns — see [desktop.md](desktop.md#turning-the-desktop-off),
|
|
207
|
+
where `desktop: false` turns this off along with the other two integrations
|
|
208
|
+
that talk to the session bus.
|
|
209
|
+
|
|
210
|
+
## Drawing your own bar
|
|
211
|
+
|
|
212
|
+
`MenuBar` does all of this itself. A component that draws its own can have the
|
|
213
|
+
same behaviour:
|
|
214
|
+
|
|
215
|
+
```jsx
|
|
216
|
+
import { useGlobalMenu } from 'react-x11';
|
|
217
|
+
|
|
218
|
+
function Bar({ menus }) {
|
|
219
|
+
const exported = useGlobalMenu(menus, { onSelect });
|
|
220
|
+
if (exported) return null; // the panel has it
|
|
221
|
+
return; /* … your bar … */
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Known limits, so they are not rediscovered as bugs
|
|
226
|
+
|
|
227
|
+
- **Stock GNOME has no global menu bar at all**, so nothing owns the registrar
|
|
228
|
+
name and the in-window bar stays. That is correct rather than broken. The
|
|
229
|
+
extensions that add one do own it, and then this works.
|
|
230
|
+
- **`ContextMenu` is not exported**, and should not be. The registrar's unit is
|
|
231
|
+
a window's menu _bar_; a right-click menu belongs where the pointer is.
|
|
232
|
+
- **One menu per window.** The registrar's entry, the object path and the KDE
|
|
233
|
+
properties all belong to the window, so a second `MenuBar` in a window that
|
|
234
|
+
already has one stays drawn in the window until the first unmounts or turns
|
|
235
|
+
`globalMenu` off, and only then takes the panel. A bar remounted with a new
|
|
236
|
+
`key`, or switched off and on again, waits the same way for the one it
|
|
237
|
+
replaces to finish leaving, which takes a round trip or two.
|
|
238
|
+
- **`AboutToShow` cannot be answered honestly.** dbusmenu wants a synchronous
|
|
239
|
+
"do you need to rebuild this submenu", and a React `setState` has not
|
|
240
|
+
rendered by the time the reply must go out. react-x11 answers `false`, runs
|
|
241
|
+
`onAboutToShow`, and emits `LayoutUpdated` when the new items actually
|
|
242
|
+
serialise — shells listen for that unconditionally, so a lazily-built submenu
|
|
243
|
+
fills in one round trip later than a synchronous toolkit manages. Answering
|
|
244
|
+
`true` instead would be worse: the shell would immediately fetch a subtree
|
|
245
|
+
that has not been built and cache the old one.
|
|
246
|
+
- **`org.gtk.Menus`/`org.gtk.Actions` is deliberately not exported.** Handling
|
|
247
|
+
both serialisations is right for a panel and wrong for an app — traffic flows
|
|
248
|
+
_towards_ dbusmenu, not away from it. Plasma ships `gmenu-dbusmenu-proxy` to
|
|
249
|
+
convert GTK's menus into dbusmenu, and the GNOME extensions that render
|
|
250
|
+
third-party menus consume dbusmenu. A second exporter would be roughly double
|
|
251
|
+
the work for no consumer the first does not already reach.
|
|
252
|
+
- **No bus is a first-class configuration.** Everything here degrades to
|
|
253
|
+
"draw the bar" without logging: no `dbus-native` (Node 20), no
|
|
254
|
+
`$DBUS_SESSION_BUS_ADDRESS`, a connection that fails. See
|
|
255
|
+
[dbus.md](dbus.md).
|
|
256
|
+
|
|
257
|
+
## Performance, and the one thing worth knowing
|
|
258
|
+
|
|
259
|
+
A menu is re-serialised whenever the `menus` array changes identity, which for
|
|
260
|
+
an app that builds it inline is every render. That part is cheap. What is not
|
|
261
|
+
cheap is the signal it produces, and the difference is the reason this feature
|
|
262
|
+
has a diff at all:
|
|
263
|
+
|
|
264
|
+
- ids and children identical, only properties differ → `ItemsPropertiesUpdated`,
|
|
265
|
+
**revision unchanged**;
|
|
266
|
+
- structure changed → `revision++` and `LayoutUpdated(revision, parent)`, with
|
|
267
|
+
`parent` the nearest common ancestor of what changed rather than `0`.
|
|
268
|
+
|
|
269
|
+
Plasma re-fetches a whole subtree on `LayoutUpdated` and only patches on
|
|
270
|
+
`ItemsPropertiesUpdated`. A menu whose one check mark flipped must therefore
|
|
271
|
+
not bump the revision — which is exactly what a naive
|
|
272
|
+
`setState` → `LayoutUpdated(++rev, 0)` does on every keystroke, and it turns one
|
|
273
|
+
property write into a re-walk of every menu in the application.
|
|
274
|
+
|
|
275
|
+
Item ids are keyed on the path of `key ?? label` down the tree — the same
|
|
276
|
+
identity `MenuBar` already uses for its React keys — so they survive a
|
|
277
|
+
re-render, and they are never reused, because a shell has properties cached
|
|
278
|
+
against them.
|
package/docs/glx.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# 3D over indirect GLX
|
|
2
|
+
|
|
3
|
+
How a 3D scene works over this transport, and what the transport can never
|
|
4
|
+
do. `<glarea>` and the three ways underneath it are [gl.md](gl.md); the
|
|
5
|
+
scene graph that rides on this one is
|
|
6
|
+
[`@react-x11/components/three`](ecosystem.md). This page is the design
|
|
7
|
+
underneath both, kept because the constraints explain most of the API's
|
|
8
|
+
shape — and because they are permanent: they are properties of a wire
|
|
9
|
+
protocol frozen in the 1990s, not of any implementation.
|
|
10
|
+
|
|
11
|
+
On X11 `<glarea>` is a real X child window on a GLX visual, and under the
|
|
12
|
+
default `glPolicy` everything drawn into it travels as **indirect GLX**: the
|
|
13
|
+
GL protocol encoded into the same X connection as everything else. No direct
|
|
14
|
+
rendering, no native bindings, no GPU driver bindings — the same pure-JS
|
|
15
|
+
story as the rest of the stack. (The direct path, which does take a native
|
|
16
|
+
addon, is [gl.md](gl.md)'s.)
|
|
17
|
+
|
|
18
|
+
The cost of that reach is a **fixed-function OpenGL 1.x pipeline**, and that
|
|
19
|
+
single fact decides the rest.
|
|
20
|
+
|
|
21
|
+
## What the protocol encodes
|
|
22
|
+
|
|
23
|
+
node-x11's `glxrender.js` encodes 70 GL commands:
|
|
24
|
+
|
|
25
|
+
| available | |
|
|
26
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
+
| immediate mode | `Begin`/`End`, `Vertex3f`, `Normal3f`, `Color3f/4f`, `TexCoord2f` |
|
|
28
|
+
| matrices | `MatrixMode`, `LoadIdentity`, `LoadMatrixf`, `MultMatrixf`, `Push/PopMatrix`, `Translatef`, `Rotatef`, `Scalef`, `Frustum`, `Ortho` |
|
|
29
|
+
| lighting | `Lightfv`, `LightModelf`, `Materialfv`, `Materialf`, `ColorMaterial`, `ShadeModel` |
|
|
30
|
+
| texturing | `TexImage2D`, `BindTexture`, `TexParameter*`, `TexEnv*`, `TexGen*` |
|
|
31
|
+
| state | `Enable`/`Disable`, `Clear*`, `DepthFunc`, `DepthMask`, `BlendFunc`, `AlphaFunc`, `CullFace`, `FrontFace`, `PolygonMode`, `Viewport`, `Scissor`, `Fog*` |
|
|
32
|
+
| display lists | `CallList`, `ListBase` (+ `NewList`/`EndList`/`DeleteLists` in `glx.js`) |
|
|
33
|
+
|
|
34
|
+
There are **no GLSL shaders, no VBOs, no vertex arrays** (`DrawArrays` and
|
|
35
|
+
`VertexPointer` are not encoded), **no framebuffer objects and no
|
|
36
|
+
instancing**. `ProgramString`/`BindProgram` expose ARB _assembly_ programs,
|
|
37
|
+
which are out of scope.
|
|
38
|
+
|
|
39
|
+
That rules out, permanently and by protocol: shader materials and
|
|
40
|
+
post-processing. Both are implemented on the direct backend, and asking for
|
|
41
|
+
one here throws an error naming the reason rather than rendering something
|
|
42
|
+
that only looks right — the scene graph keeps the list, as `DIRECT_ONLY_KINDS`.
|
|
43
|
+
(`<instancedMesh>` works on both, but as a loop over transforms rather than
|
|
44
|
+
as hardware instancing, which GLX also encodes no way to ask for.)
|
|
45
|
+
|
|
46
|
+
## Why display lists are mandatory
|
|
47
|
+
|
|
48
|
+
With no vertex arrays, geometry can only be sent as immediate-mode commands.
|
|
49
|
+
A 1 000-triangle mesh is ~3 000 `Vertex3f` + 3 000 `Normal3f` commands —
|
|
50
|
+
about **96 KB per frame** if re-sent every frame, or 5.7 MB/s at 60fps for
|
|
51
|
+
one modest mesh.
|
|
52
|
+
|
|
53
|
+
So each geometry is compiled into a **server-side display list** once, and a
|
|
54
|
+
frame sends only matrices, material state and one `CallList` per mesh. A
|
|
55
|
+
static scene costs O(meshes) requests per frame instead of O(vertices).
|
|
56
|
+
|
|
57
|
+
This is the AGENTS.md "Protocol efficiency" rule set applied literally — use
|
|
58
|
+
server-side primitives, batch, never re-send what the server already has —
|
|
59
|
+
and because it is a protocol property rather than an intention, the scene
|
|
60
|
+
graph's own tests assert it on the encoded command stream: a 6 000-vertex
|
|
61
|
+
sphere compiles once, and the steady-state frame is under 30 GL commands
|
|
62
|
+
with no vertices in it.
|
|
63
|
+
|
|
64
|
+
The cache is keyed by geometry identity. A geometry prop change recompiles
|
|
65
|
+
that one list; a transform or material change is per-frame state only, so the
|
|
66
|
+
same list is shared by meshes that differ in material.
|
|
67
|
+
|
|
68
|
+
## The shape of the tree
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
<glarea> host element -> real X child window with a
|
|
72
|
+
GLX visual + its own colormap
|
|
73
|
+
|
|
|
74
|
+
GLContext (ntk) MakeCurrent -> context tag
|
|
75
|
+
|
|
|
76
|
+
Scene node tree (parallel to the drawn-node tree)
|
|
77
|
+
|
|
|
78
|
+
commit: compile dirty geometries -> display lists
|
|
79
|
+
frame: Clear -> camera matrices -> per mesh: PushMatrix,
|
|
80
|
+
MultMatrixf, material state, CallList, PopMatrix -> SwapBuffers
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`<glarea>` is one of the four elements that legitimately need an X window of
|
|
84
|
+
their own — with `<window>`, `<popup>` and `<foreign>` (NEXT_STEPS §4): GLX needs its own visual and cannot share the
|
|
85
|
+
XRender pipeline the 2D elements draw through.
|
|
86
|
+
|
|
87
|
+
A scene tree over this surface is **separate from the drawn-node tree** — no
|
|
88
|
+
yoga, no hit testing against the 2D tree, no painting into the parent's 2d
|
|
89
|
+
context. To layout, `<glarea>` is a leaf sized by ordinary layout props.
|
|
90
|
+
|
|
91
|
+
## The scene graph moved
|
|
92
|
+
|
|
93
|
+
The r3f-shaped names this page used to map — `<mesh>`, the geometries, the
|
|
94
|
+
materials, the lights, the camera prop — belong to
|
|
95
|
+
[`@react-x11/components/three`](ecosystem.md) now, together with the display-
|
|
96
|
+
list cache described above: it keeps its own reconciler and its own indirect
|
|
97
|
+
renderer, built on exactly the constraints this page documents. What stays
|
|
98
|
+
here is the transport, which is what those constraints come from.
|
|
99
|
+
|
|
100
|
+
## Testing
|
|
101
|
+
|
|
102
|
+
The primary tests are **hermetic and assert the encoded GLX command
|
|
103
|
+
stream**, not pixels: that a geometry compiles to one display list, that a
|
|
104
|
+
frame emits matrices plus `CallList` rather than thousands of `Vertex3f`,
|
|
105
|
+
and that a transform change re-sends no geometry. That is the property that
|
|
106
|
+
actually matters, and checking it needs no GL at all. Core's half is
|
|
107
|
+
`test/glarea.test.js` and `test/viewer3d.test.js`; the scene graph tests its
|
|
108
|
+
own in its own repo.
|
|
109
|
+
|
|
110
|
+
Pixels are the awkward part. On XQuartz, GL renders into a Metal surface the
|
|
111
|
+
compositor owns rather than into the X drawable, so `GetImage` reads back
|
|
112
|
+
white and a 3D screenshot has to be captured by hand from
|
|
113
|
+
a scene on a real server with indirect GLX — which is why
|
|
114
|
+
`npm run screenshots` skips it. The documentation site's playground is the
|
|
115
|
+
exception that renders GL headlessly: node-x11's browser GLX emulator
|
|
116
|
+
replays the same protocol onto WebGL2.
|
|
117
|
+
|
|
118
|
+
## Open questions
|
|
119
|
+
|
|
120
|
+
- Is there a depth-buffer-capable visual reachable via `GetVisualConfigs` on
|
|
121
|
+
both XQuartz and Xvfb/llvmpipe?
|
|
122
|
+
- Can a `<glarea>` be composited with 2D content drawn over it, or does the
|
|
123
|
+
GL child window always sit on top? Answered: the GL window is always on
|
|
124
|
+
top of the parent's drawing, so the surface's own children are drawn on
|
|
125
|
+
windows of their own stacked above it — opaque ones, one per region they
|
|
126
|
+
reach, since a child window cannot be translucent without a compositor
|
|
127
|
+
(`src/gloverlay.js`, [elements.md](elements.md#glarea)). Except on
|
|
128
|
+
XQuartz, where the window server composites the surface above every X
|
|
129
|
+
window, those included, and the children are not drawn at all.
|
|
130
|
+
|
|
131
|
+
## Scope discipline
|
|
132
|
+
|
|
133
|
+
Indirect GLX is a 1990s fixed-function pipeline reached over a network
|
|
134
|
+
protocol. The value is _reach_ — 3D in a pure-JS X11 client with no native
|
|
135
|
+
dependencies — not performance or fidelity. A plausible-looking API that
|
|
136
|
+
quietly fails on anything modern would be worse than a small one with honest
|
|
137
|
+
edges, which is why unsupported r3f names throw with the reason.
|