react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/anchor.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Popup geometry: where to put a `<popup>` that hangs off something else.
|
|
2
|
+
//
|
|
3
|
+
// Core rather than `src/components/`, because two callers need it and only
|
|
4
|
+
// one of them is a widget. A widget that knows its own size measures the
|
|
5
|
+
// rect itself and passes it as `x`/`y` (`useAnchor`, `useAnchorTracking` —
|
|
6
|
+
// `src/components/anchor.js`, which re-exports everything here). A `<popup>`
|
|
7
|
+
// that sizes itself from its content cannot: its size is settled inside
|
|
8
|
+
// `realize()`, between the measurement and `CreateWindow`, which is after
|
|
9
|
+
// the last moment React could have computed a position for it. So the
|
|
10
|
+
// window places *itself* from the same functions (`WindowNode._followAnchor`,
|
|
11
|
+
// nodes.js), and the two paths agree because they are the same code.
|
|
12
|
+
|
|
13
|
+
import { availableArea } from './screens.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A node's laid-out rect in **screen** coordinates: the owner window's
|
|
17
|
+
* position plus the node's own box.
|
|
18
|
+
*
|
|
19
|
+
* `_screenOrigin` is what the server says the window is at; `x`/`y` are
|
|
20
|
+
* frame-relative under a reparenting window manager and are only a fallback
|
|
21
|
+
* (the headless mock has no server to ask).
|
|
22
|
+
*
|
|
23
|
+
* Exported because a popup sometimes has to know where its *trigger* is and
|
|
24
|
+
* not only where to put itself — a tooltip's arrow points at the middle of
|
|
25
|
+
* the thing it annotates, which stops being the middle of the tooltip as
|
|
26
|
+
* soon as a screen edge slides one of them.
|
|
27
|
+
*/
|
|
28
|
+
export function screenRect(node) {
|
|
29
|
+
if (!node?.abs) return null;
|
|
30
|
+
const origin = windowOrigin(node);
|
|
31
|
+
return {
|
|
32
|
+
x: origin.x + node.abs.x,
|
|
33
|
+
y: origin.y + node.abs.y,
|
|
34
|
+
width: node.abs.width,
|
|
35
|
+
height: node.abs.height,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Where the node's owner window is on the screen. Exported for the one
|
|
40
|
+
* caller that anchors to a *point* rather than to a node's rect — the edit
|
|
41
|
+
* menu opens at the pointer, whose coordinates arrive window-relative on the
|
|
42
|
+
* event (`openEditMenu`, nodes.js). */
|
|
43
|
+
export function windowOrigin(node) {
|
|
44
|
+
const win = node?.root?.window;
|
|
45
|
+
return win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The rect within a node that a popup is anchored to, in the node's own
|
|
50
|
+
* coordinates — a caret, a table cell, a chart datapoint, the span under a
|
|
51
|
+
* text-range tooltip. `width`/`height` default to 0, so `{x, y}` alone is a
|
|
52
|
+
* point.
|
|
53
|
+
*
|
|
54
|
+
* Node-relative rather than screen-relative because the caller measures it
|
|
55
|
+
* against its own content, and because it then survives everything that
|
|
56
|
+
* moves the node: the window being dragged, an ancestor scrolling, the
|
|
57
|
+
* node's own layout shifting. That is what lets `useAnchorTracking` follow a
|
|
58
|
+
* caret with no extra work — it re-reads the options and the node, and the
|
|
59
|
+
* offset between them is still true.
|
|
60
|
+
*/
|
|
61
|
+
export function subRect(node, at) {
|
|
62
|
+
if (!node?.abs) return null;
|
|
63
|
+
if (!at) return node.abs;
|
|
64
|
+
return {
|
|
65
|
+
x: node.abs.x + (at.x ?? 0),
|
|
66
|
+
y: node.abs.y + (at.y ?? 0),
|
|
67
|
+
width: at.width ?? 0,
|
|
68
|
+
height: at.height ?? 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `at` in logical pixels (it comes off an `anchor` prop) → the same rect in
|
|
73
|
+
* the device pixels `node.abs` and everything downstream are in. */
|
|
74
|
+
function deviceAt(at, s) {
|
|
75
|
+
if (!at || s === 1) return at;
|
|
76
|
+
return {
|
|
77
|
+
...at,
|
|
78
|
+
...(typeof at.x === 'number' && { x: at.x * s }),
|
|
79
|
+
...(typeof at.y === 'number' && { y: at.y * s }),
|
|
80
|
+
...(typeof at.width === 'number' && { width: at.width * s }),
|
|
81
|
+
...(typeof at.height === 'number' && { height: at.height * s }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Has the thing this popup points at scrolled out of view? The check paint
|
|
87
|
+
* culling uses (`Node._offscreen`), asked about the sub-rect rather than
|
|
88
|
+
* about the node: an editor scrolls its own text, so the caret leaves the
|
|
89
|
+
* viewport a long time before the editor does.
|
|
90
|
+
*
|
|
91
|
+
* A degenerate rect counts as its own thinnest visible version — a caret is
|
|
92
|
+
* a line with no width and a `{x, y}` anchor is a point with neither, and
|
|
93
|
+
* having no area is not the same as being off screen.
|
|
94
|
+
*/
|
|
95
|
+
export function anchorOffscreen(node, at) {
|
|
96
|
+
const rect = subRect(node, deviceAt(at, node?.scale ?? 1));
|
|
97
|
+
if (!rect || typeof node._offscreen !== 'function') return false;
|
|
98
|
+
return node._offscreen({
|
|
99
|
+
x: rect.x,
|
|
100
|
+
y: rect.y,
|
|
101
|
+
width: Math.max(rect.width, 1),
|
|
102
|
+
height: Math.max(rect.height, 1),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The area a popup anchored to this node may be placed in: the usable part
|
|
108
|
+
* of the monitor the node is on.
|
|
109
|
+
*
|
|
110
|
+
* The same answer `<window width="auto">` is capped by (`src/screens.js`) —
|
|
111
|
+
* per-monitor, minus the panels — rather than `screen.pixel_width`, which is
|
|
112
|
+
* the whole virtual desktop. On a two-head setup that difference is the
|
|
113
|
+
* difference between a menu flipping at the edge of the monitor it is on and
|
|
114
|
+
* a menu that only flips at the far edge of the *other* monitor, having
|
|
115
|
+
* opened halfway across the seam. `null` where there is nothing to ask,
|
|
116
|
+
* which is the headless mock and a server with no Xinerama and no screen:
|
|
117
|
+
* placement then neither flips nor clamps.
|
|
118
|
+
*/
|
|
119
|
+
export function anchorArea(node) {
|
|
120
|
+
const area = deviceAnchorArea(node);
|
|
121
|
+
const s = node?.scale ?? 1;
|
|
122
|
+
if (!area || s === 1) return area;
|
|
123
|
+
// Public callers size popups from this — `maxHeight` styles, widths —
|
|
124
|
+
// and those are logical like every style, so the answer is too.
|
|
125
|
+
return {
|
|
126
|
+
x: area.x / s,
|
|
127
|
+
y: area.y / s,
|
|
128
|
+
width: area.width / s,
|
|
129
|
+
height: area.height / s,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The same area in device pixels, for placement math that runs against
|
|
134
|
+
* `abs` rects and window origins — here and in the edit menu's. */
|
|
135
|
+
export function deviceAnchorArea(node) {
|
|
136
|
+
const app = node?.app;
|
|
137
|
+
if (!app) return null;
|
|
138
|
+
const at = screenRect(node);
|
|
139
|
+
return availableArea(app, at ? { x: at.x, y: at.y } : null);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Where to put a `<popup>` anchored to a drawn node, in **screen**
|
|
144
|
+
* coordinates: the owner window's position plus the node's laid-out rect.
|
|
145
|
+
*
|
|
146
|
+
* `placement` is a preference, not a promise — a menu near the bottom of
|
|
147
|
+
* the screen flips above its trigger rather than opening off-screen, and
|
|
148
|
+
* the result is clamped into the screen either way. The chosen side comes
|
|
149
|
+
* back as `placement` so the caller can style accordingly.
|
|
150
|
+
*
|
|
151
|
+
* `at` anchors to a rect **inside** the node instead of to the node itself
|
|
152
|
+
* (see `subRect`): the caret in an editor, a cell in a table, a point on a
|
|
153
|
+
* chart. Everything else then reads that rect — the side it flips on, the
|
|
154
|
+
* edge it aligns to, the gap `offset` leaves — so a popup anchored to a
|
|
155
|
+
* caret behaves exactly like one anchored to a small widget that happens to
|
|
156
|
+
* be there.
|
|
157
|
+
*
|
|
158
|
+
* `alignTo` takes the two axes from **different** nodes: the placement edge
|
|
159
|
+
* from `node`, the alignment from `alignTo`. A submenu is the case that
|
|
160
|
+
* needs it — it belongs against the outer edge of the menu it comes out of,
|
|
161
|
+
* but lined up with the row that spawned it, and that row is inset by the
|
|
162
|
+
* menu's border and padding. Anchoring both to the row opens the submenu
|
|
163
|
+
* *over* its parent by exactly that inset. Both nodes must be in the same
|
|
164
|
+
* window, which is what lets one origin serve both.
|
|
165
|
+
*
|
|
166
|
+
* `alignOffset` shifts the result along the *alignment* axis, where `offset`
|
|
167
|
+
* moves it along the placement one — and it is applied before the clamp, so
|
|
168
|
+
* a popup nudged towards a screen edge is still brought back from it. What
|
|
169
|
+
* needs it is the difference between lining up a **surface** and lining up
|
|
170
|
+
* what is drawn **in** it: a submenu whose top edge is level with the row
|
|
171
|
+
* that opened it has its first item a border and a padding lower down, and
|
|
172
|
+
* the eye lines up the items, not the boxes.
|
|
173
|
+
*
|
|
174
|
+
* ## Which side is which
|
|
175
|
+
*
|
|
176
|
+
* `placement: 'start'` and `'end'` are the **logical** sides, and they are
|
|
177
|
+
* what a submenu wants: a menu opens away from the edge its rows begin at, so
|
|
178
|
+
* it goes out to the right in an LTR menu and out to the left in an RTL one.
|
|
179
|
+
* `'left'`/`'right'` stay available and stay physical, for the rare placement
|
|
180
|
+
* that really is about the screen. `align: 'start'`/`'end'` mirror the same
|
|
181
|
+
* way — but only when the popup is above or below its trigger, since with a
|
|
182
|
+
* popup beside it the alignment axis is vertical and nothing about a vertical
|
|
183
|
+
* axis mirrors.
|
|
184
|
+
*
|
|
185
|
+
* The direction comes from the anchoring node, so a menu inside a mirrored
|
|
186
|
+
* panel opens the mirrored way without its widget being told.
|
|
187
|
+
*/
|
|
188
|
+
export function anchorRect(node, options = {}) {
|
|
189
|
+
if (!node?.abs) return null;
|
|
190
|
+
// Options are logical pixels — they come from application code, like
|
|
191
|
+
// every length — and so is the returned rect, which is headed for a
|
|
192
|
+
// popup's `x`/`y` props. The math between runs in device pixels, because
|
|
193
|
+
// `abs`, the window origin and the monitor area are (src/scale.js).
|
|
194
|
+
const s = node.scale ?? 1;
|
|
195
|
+
const {
|
|
196
|
+
placement = 'bottom',
|
|
197
|
+
align = 'start',
|
|
198
|
+
alignOffset: logicalAlignOffset = 0,
|
|
199
|
+
offset: logicalOffset = 2,
|
|
200
|
+
at,
|
|
201
|
+
alignTo,
|
|
202
|
+
direction = node.direction,
|
|
203
|
+
} = options;
|
|
204
|
+
const alignOffset = logicalAlignOffset * s;
|
|
205
|
+
const offset = logicalOffset * s;
|
|
206
|
+
const rtl = direction === 'rtl';
|
|
207
|
+
|
|
208
|
+
// The anchor is the sub-rect where there is one, all the way through:
|
|
209
|
+
// the side that flips, the edge that aligns, and — since a popup with no
|
|
210
|
+
// size of its own is as wide as the thing it hangs off — the default
|
|
211
|
+
// width.
|
|
212
|
+
const anchor = subRect(node, deviceAt(at, s));
|
|
213
|
+
const width = options.width !== undefined ? options.width * s : anchor.width;
|
|
214
|
+
const height = options.height !== undefined ? options.height * s : 0;
|
|
215
|
+
|
|
216
|
+
const origin = windowOrigin(node);
|
|
217
|
+
const ax = origin.x + anchor.x;
|
|
218
|
+
const ay = origin.y + anchor.y;
|
|
219
|
+
const aw = anchor.width;
|
|
220
|
+
const ah = anchor.height;
|
|
221
|
+
// the rect the *alignment* reads, which is the anchor's own unless the
|
|
222
|
+
// caller split the two axes — one origin serves both, since both nodes are
|
|
223
|
+
// in the same window
|
|
224
|
+
const cross = alignTo ? (subRect(alignTo) ?? anchor) : anchor;
|
|
225
|
+
const cx = origin.x + cross.x;
|
|
226
|
+
const cy = origin.y + cross.y;
|
|
227
|
+
|
|
228
|
+
const area = deviceAnchorArea(node);
|
|
229
|
+
const left = area?.x ?? 0;
|
|
230
|
+
const top = area?.y ?? 0;
|
|
231
|
+
const right = area ? area.x + area.width : null;
|
|
232
|
+
const bottom = area ? area.y + area.height : null;
|
|
233
|
+
|
|
234
|
+
// `mirrored` is only ever true on the horizontal axis: `alignAlong` serves
|
|
235
|
+
// both, and a vertically-aligned popup's `start` is the top in every
|
|
236
|
+
// direction there is.
|
|
237
|
+
const alignAlong = (start, size, extent, mirrored) => {
|
|
238
|
+
const edge =
|
|
239
|
+
align === 'center'
|
|
240
|
+
? 'center'
|
|
241
|
+
: (align === 'end') !== mirrored
|
|
242
|
+
? 'end'
|
|
243
|
+
: 'start';
|
|
244
|
+
return (
|
|
245
|
+
alignOffset +
|
|
246
|
+
(edge === 'center'
|
|
247
|
+
? start + (size - extent) / 2
|
|
248
|
+
: edge === 'end'
|
|
249
|
+
? start + size - extent
|
|
250
|
+
: start)
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
let side =
|
|
255
|
+
placement === 'start'
|
|
256
|
+
? rtl
|
|
257
|
+
? 'right'
|
|
258
|
+
: 'left'
|
|
259
|
+
: placement === 'end'
|
|
260
|
+
? rtl
|
|
261
|
+
? 'left'
|
|
262
|
+
: 'right'
|
|
263
|
+
: placement;
|
|
264
|
+
let x;
|
|
265
|
+
let y;
|
|
266
|
+
|
|
267
|
+
if (side === 'bottom' || side === 'top') {
|
|
268
|
+
const below = ay + ah + offset;
|
|
269
|
+
const above = ay - height - offset;
|
|
270
|
+
if (
|
|
271
|
+
side === 'bottom' &&
|
|
272
|
+
bottom != null &&
|
|
273
|
+
below + height > bottom &&
|
|
274
|
+
above >= top
|
|
275
|
+
) {
|
|
276
|
+
side = 'top';
|
|
277
|
+
} else if (
|
|
278
|
+
side === 'top' &&
|
|
279
|
+
above < top &&
|
|
280
|
+
(bottom == null || below + height <= bottom)
|
|
281
|
+
) {
|
|
282
|
+
side = 'bottom';
|
|
283
|
+
}
|
|
284
|
+
y = side === 'bottom' ? below : above;
|
|
285
|
+
x = alignAlong(cx, cross.width, width, rtl);
|
|
286
|
+
} else {
|
|
287
|
+
const after = ax + aw + offset;
|
|
288
|
+
const before = ax - width - offset;
|
|
289
|
+
if (
|
|
290
|
+
side === 'right' &&
|
|
291
|
+
right != null &&
|
|
292
|
+
after + width > right &&
|
|
293
|
+
before >= left
|
|
294
|
+
) {
|
|
295
|
+
side = 'left';
|
|
296
|
+
} else if (
|
|
297
|
+
side === 'left' &&
|
|
298
|
+
before < left &&
|
|
299
|
+
(right == null || after + width <= right)
|
|
300
|
+
) {
|
|
301
|
+
side = 'right';
|
|
302
|
+
}
|
|
303
|
+
x = side === 'right' ? after : before;
|
|
304
|
+
y = alignAlong(cy, cross.height, height, false);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (right != null) x = Math.max(left, Math.min(x, right - width));
|
|
308
|
+
if (bottom != null && height) y = Math.max(top, Math.min(y, bottom - height));
|
|
309
|
+
|
|
310
|
+
// Back to logical on the way out. Rounded in *device* pixels first, so
|
|
311
|
+
// the placement still lands on the device grid it was computed on.
|
|
312
|
+
return {
|
|
313
|
+
x: Math.round(x) / s,
|
|
314
|
+
y: Math.round(y) / s,
|
|
315
|
+
width: width / s,
|
|
316
|
+
height: height / s,
|
|
317
|
+
placement: side,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Where to put a `<popup>` of this size **centred over the owner window**,
|
|
323
|
+
* in screen coordinates, clamped into the screen. A dialog is anchored to
|
|
324
|
+
* the window rather than to a widget, which is the one placement
|
|
325
|
+
* `anchorRect` cannot express.
|
|
326
|
+
*/
|
|
327
|
+
export function centerRect(node, { width: logicalW, height: logicalH }) {
|
|
328
|
+
if (!node) return null;
|
|
329
|
+
// Logical in, logical out, device in between — anchorRect's contract,
|
|
330
|
+
// for the same caller: the result becomes a popup's `x`/`y` props.
|
|
331
|
+
const s = node.scale ?? 1;
|
|
332
|
+
const width = logicalW * s;
|
|
333
|
+
const height = logicalH * s;
|
|
334
|
+
const win = node.root?.window;
|
|
335
|
+
const ww = win?.width ?? width;
|
|
336
|
+
const wh = win?.height ?? height;
|
|
337
|
+
// `_screenOrigin` for the same reason `anchorRect` uses it: `x`/`y` come
|
|
338
|
+
// from ConfigureNotify, and under a reparenting window manager those can be
|
|
339
|
+
// relative to the *frame* rather than the root — which would centre the
|
|
340
|
+
// dialog off by the size of the decoration. The server's own answer is
|
|
341
|
+
// right whatever the WM did. (quartz-wm happens to report root-relative
|
|
342
|
+
// coordinates, so the two agree there; that is not something to rely on.)
|
|
343
|
+
// The raw coordinates stay as the fallback, since the headless mock has no
|
|
344
|
+
// server to ask.
|
|
345
|
+
const origin = win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
|
|
346
|
+
let x = origin.x + (ww - width) / 2;
|
|
347
|
+
let y = origin.y + (wh - height) / 2;
|
|
348
|
+
|
|
349
|
+
const area = deviceAnchorArea(node);
|
|
350
|
+
if (area) {
|
|
351
|
+
x = Math.max(area.x, Math.min(x, area.x + area.width - width));
|
|
352
|
+
y = Math.max(area.y, Math.min(y, area.y + area.height - height));
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
x: Math.round(x) / s,
|
|
356
|
+
y: Math.round(y) / s,
|
|
357
|
+
width: logicalW,
|
|
358
|
+
height: logicalH,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Reaching the X connection from a component.
|
|
2
|
+
//
|
|
3
|
+
// `createRoot()` hands the app back to whoever called it, which is fine for
|
|
4
|
+
// an entry point and useless three components down — and a component
|
|
5
|
+
// library has no route to it at all without prop-drilling. So `render()`
|
|
6
|
+
// wraps the tree in a provider and `useApp()` reads it.
|
|
7
|
+
//
|
|
8
|
+
// Context rather than the ref walk `useWindowId`/`useAnchor` use: those
|
|
9
|
+
// answer *per-node* questions, and the ref is the question. The connection
|
|
10
|
+
// is app-scoped, so demanding a host ref to find it would be busywork with
|
|
11
|
+
// no information in it. A module-level global would be simpler still and
|
|
12
|
+
// wrong — one process can drive several roots on several connections, which
|
|
13
|
+
// the test suite does routinely.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createContext,
|
|
17
|
+
useCallback,
|
|
18
|
+
useContext,
|
|
19
|
+
useMemo,
|
|
20
|
+
useSyncExternalStore,
|
|
21
|
+
} from 'react';
|
|
22
|
+
|
|
23
|
+
import { createClipboard } from './clipboard.js';
|
|
24
|
+
import {
|
|
25
|
+
argbVisual,
|
|
26
|
+
compositingActive,
|
|
27
|
+
watchCompositing,
|
|
28
|
+
} from './compositing.js';
|
|
29
|
+
import { hasDirectGL, watchDirectGL } from './glbackend.js';
|
|
30
|
+
|
|
31
|
+
const AppContext = createContext(null);
|
|
32
|
+
|
|
33
|
+
/** Wraps the rendered element; not exported to applications. */
|
|
34
|
+
export const AppProvider = AppContext.Provider;
|
|
35
|
+
|
|
36
|
+
/** `useApp()` without the throw, for components that can still render
|
|
37
|
+
* something useful outside a tree. Not public. */
|
|
38
|
+
export function useAppOrNull() {
|
|
39
|
+
return useContext(AppContext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The ntk connection this tree is rendering onto.
|
|
44
|
+
*
|
|
45
|
+
* Everything ntk exposes hangs off it — `app.fonts`, `app.cursors`,
|
|
46
|
+
* `app.X` for raw protocol — so this is the escape hatch as much as it is
|
|
47
|
+
* an API. Throws outside a react-x11 tree rather than returning null,
|
|
48
|
+
* because every caller would have had to check.
|
|
49
|
+
*/
|
|
50
|
+
export function useApp() {
|
|
51
|
+
const app = useContext(AppContext);
|
|
52
|
+
if (!app) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'react-x11: useApp() must be called inside a tree rendered by createRoot()',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return app;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SUPPORTS_FEATURES = new Set(['transparency', 'shaders']);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Can this **display** do something, as a value a component can branch on?
|
|
64
|
+
*
|
|
65
|
+
* ```jsx
|
|
66
|
+
* const canBlend = useSupports('transparency');
|
|
67
|
+
* // a shadow has to be painted into a margin the popup owns, so the
|
|
68
|
+
* // decision has to be made before the window is sized
|
|
69
|
+
* const margin = canBlend ? 26 : 0;
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* `'transparency'` is true when the server has a 32-bit visual to draw on
|
|
73
|
+
* *and* a compositor is running to blend it. It re-renders when a compositor
|
|
74
|
+
* starts or stops. `REACT_X11_NO_TRANSPARENCY=1` answers false whatever the
|
|
75
|
+
* display can do, so the fallback design can be looked at without stopping
|
|
76
|
+
* the compositor for the whole session.
|
|
77
|
+
*
|
|
78
|
+
* The companion is the `'@supports transparency'` style block, and the two
|
|
79
|
+
* answer deliberately different questions. This one is about the display, so
|
|
80
|
+
* it can be asked before any window exists — which is what a caller sizing a
|
|
81
|
+
* popup needs. The style block is about the window the node is actually in,
|
|
82
|
+
* so a component nested in a plain `<window>` gets the opaque design there
|
|
83
|
+
* and the translucent one inside a `<popup transparent>`, without being
|
|
84
|
+
* told which it is. Reach for the style block first; this is for decisions
|
|
85
|
+
* that are not styling.
|
|
86
|
+
*
|
|
87
|
+
* `'shaders'` is true when 3D can run your own GLSL — that is, when this
|
|
88
|
+
* connection draws through the **direct** rendering backend. It is the
|
|
89
|
+
* question to ask before rendering a `<shaderMaterial>`, which throws where
|
|
90
|
+
* there is no pipeline to compile it:
|
|
91
|
+
*
|
|
92
|
+
* ```jsx
|
|
93
|
+
* const shaders = useSupports('shaders');
|
|
94
|
+
* <mesh>
|
|
95
|
+
* <boxGeometry args={[1, 1, 1]} />
|
|
96
|
+
* {shaders ? <shaderMaterial {...glsl} /> : <meshPhongMaterial color="#e0533d" />}
|
|
97
|
+
* </mesh>
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* It needs `createRoot({ glPolicy: 'auto' })`: the default policy is the
|
|
101
|
+
* indirect backend, which has no shaders at all, so this is false under it
|
|
102
|
+
* whatever the machine could do. That is the line between this and
|
|
103
|
+
* `app.glCapabilities()` — the hook answers which backend *this connection*
|
|
104
|
+
* got, the capabilities answer what the machine could offer, and they
|
|
105
|
+
* disagree exactly when the policy did not ask. The capabilities also say
|
|
106
|
+
* *why* this is false; see docs/gl.md.
|
|
107
|
+
*
|
|
108
|
+
* Unlike `'transparency'`, which comes and goes with the compositor, this
|
|
109
|
+
* settles once and then holds still: under a policy that could pick direct,
|
|
110
|
+
* `createRoot()` waits for ntk's probe before handing the app back, so the
|
|
111
|
+
* first render already reads the final answer. A policy raised after
|
|
112
|
+
* connecting has missed that probe, and re-renders its readers when it
|
|
113
|
+
* settles rather than leaving them with two different answers.
|
|
114
|
+
*/
|
|
115
|
+
export function useSupports(feature) {
|
|
116
|
+
const app = useApp();
|
|
117
|
+
if (!SUPPORTS_FEATURES.has(feature)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`react-x11: useSupports(${JSON.stringify(feature)}) — unknown feature ` +
|
|
120
|
+
`(expected one of ${[...SUPPORTS_FEATURES].join(', ')})`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
// Both features go through the same store, so the hooks below run in the
|
|
124
|
+
// same order whatever is being asked about. Where compositing comes and
|
|
125
|
+
// goes for as long as the app runs, the backend settles at most once — and
|
|
126
|
+
// watching that one moment is what keeps two components rendered either
|
|
127
|
+
// side of it from disagreeing (see watchDirectGL).
|
|
128
|
+
const subscribe = useCallback(
|
|
129
|
+
(onChange) =>
|
|
130
|
+
feature === 'shaders'
|
|
131
|
+
? watchDirectGL(app, onChange)
|
|
132
|
+
: watchCompositing(app, onChange),
|
|
133
|
+
[app, feature],
|
|
134
|
+
);
|
|
135
|
+
// a boolean, so the snapshot is stable for a given state — returning the
|
|
136
|
+
// visual object here would tear on every render
|
|
137
|
+
const snapshot = useCallback(
|
|
138
|
+
() =>
|
|
139
|
+
feature === 'shaders'
|
|
140
|
+
? hasDirectGL(app)
|
|
141
|
+
: compositingActive(app) && Boolean(argbVisual(app)),
|
|
142
|
+
[app, feature],
|
|
143
|
+
);
|
|
144
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The clipboard, scoped to this tree's connection.
|
|
149
|
+
*
|
|
150
|
+
* ```jsx
|
|
151
|
+
* const clipboard = useClipboard();
|
|
152
|
+
* await clipboard.writeText(selection); // stamped with the keystroke
|
|
153
|
+
* const files = await clipboard.readFiles(); // parsed, or []
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* Stable for as long as the app is, so it is safe in a dependency array.
|
|
157
|
+
*/
|
|
158
|
+
export function useClipboard() {
|
|
159
|
+
const app = useApp();
|
|
160
|
+
return useMemo(() => createClipboard(app), [app]);
|
|
161
|
+
}
|