react-x11 2.10.2 → 2.12.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 +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +304 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/screencolor.js +62 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/screencolor.js +212 -38
- package/src/screencolorhooks.js +6 -2
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +33 -5
- package/src/types/screencolor.d.ts +20 -14
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- package/src/nodes.js +0 -13120
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// What the window manager is told: the window's size props and their clamps,
|
|
2
|
+
// the CreateWindow attributes, _NET_WM_STATE, Motif decorations,
|
|
3
|
+
// WM_NORMAL_HINTS and transientFor.
|
|
4
|
+
|
|
5
|
+
import { flattenStyle, isEventProp } from '../../styles.js';
|
|
6
|
+
import { windowIdOf } from '../../windowid.js';
|
|
7
|
+
import { DEV, shallowEqual } from '../util.js';
|
|
8
|
+
|
|
9
|
+
// X window geometry is CARD16 and coordinates are INT16, so a window wider
|
|
10
|
+
// than this cannot be positioned or damaged coherently even where the server
|
|
11
|
+
// accepts it. Nothing sized from content should get near it; it is the
|
|
12
|
+
// backstop for a measure function that answered Infinity.
|
|
13
|
+
export const MAX_WINDOW_EXTENT = 32767;
|
|
14
|
+
|
|
15
|
+
/** One axis of an auto size, bounded the way CSS bounds `width: auto`. */
|
|
16
|
+
export function clampExtent(value, min, max) {
|
|
17
|
+
const v = Math.ceil(Number.isFinite(value) ? value : 0);
|
|
18
|
+
// CSS's resolution order: the max bound applies first and the min wins
|
|
19
|
+
// over it, so `minWidth` beats `maxWidth` where an app sets both and they
|
|
20
|
+
// disagree.
|
|
21
|
+
const bounded = Math.max(min ?? 0, Math.min(v, max ?? Infinity));
|
|
22
|
+
// A zero-dimension window is a BadValue outright, so a `<window>` with
|
|
23
|
+
// nothing in it is 1x1 rather than a protocol error.
|
|
24
|
+
return Math.max(1, Math.min(bounded, MAX_WINDOW_EXTENT));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A bound measured from the content, held inside the space there is. Unlike
|
|
29
|
+
* a size it may legitimately be 0 — a window whose every part can give has
|
|
30
|
+
* no floor to speak of — and a bound the window cannot satisfy is worse than
|
|
31
|
+
* none: a `minWidth` past the screen is a window that cannot be put on it.
|
|
32
|
+
*/
|
|
33
|
+
export function clampBound(value, max) {
|
|
34
|
+
return Math.max(
|
|
35
|
+
0,
|
|
36
|
+
Math.min(Math.ceil(value), max ?? Infinity, MAX_WINDOW_EXTENT),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Where a `transientFor` owner sits on screen, for picking the monitor a
|
|
42
|
+
* dialog should be sized against. Accepts everything `windowIdOf` does — a
|
|
43
|
+
* window ref, a node, a drawn node's ref — and answers null for a raw XID,
|
|
44
|
+
* which carries no geometry with it.
|
|
45
|
+
*/
|
|
46
|
+
export function screenOriginOf(target) {
|
|
47
|
+
if (target == null || typeof target !== 'object') return null;
|
|
48
|
+
if ('current' in target && !target.isWindow) {
|
|
49
|
+
return screenOriginOf(target.current);
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
target._screenOrigin ??
|
|
53
|
+
target.window?._screenOrigin ??
|
|
54
|
+
target.root?.window?._screenOrigin ??
|
|
55
|
+
null
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Everything a <window> owns: the real geometry, and the WM size hints that
|
|
60
|
+
// constrain it. On a <window> these are never style.
|
|
61
|
+
export const WINDOW_HINT_PROPS = [
|
|
62
|
+
'minWidth',
|
|
63
|
+
'minHeight',
|
|
64
|
+
'maxWidth',
|
|
65
|
+
'maxHeight',
|
|
66
|
+
'widthInc',
|
|
67
|
+
'heightInc',
|
|
68
|
+
'baseWidth',
|
|
69
|
+
'baseHeight',
|
|
70
|
+
'minAspect',
|
|
71
|
+
'maxAspect',
|
|
72
|
+
'gravity',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The bounds that can be spelled `'auto'` — asked of the content rather than
|
|
77
|
+
* named as a number. The increments and the aspect ratios cannot: there is
|
|
78
|
+
* no content answer to what a resize step is.
|
|
79
|
+
*/
|
|
80
|
+
export const CONTENT_BOUND_PROPS = [
|
|
81
|
+
'minWidth',
|
|
82
|
+
'minHeight',
|
|
83
|
+
'maxWidth',
|
|
84
|
+
'maxHeight',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/** A bound that asks the content instead of naming a number. */
|
|
88
|
+
export const isContentBound = (value) => value === 'auto';
|
|
89
|
+
|
|
90
|
+
/** A bound as a number, or nothing where it is the content's to answer. */
|
|
91
|
+
export const numericBound = (value) =>
|
|
92
|
+
isContentBound(value) ? undefined : value;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A `<window width>`/`<height>` that is not a number: sized from its own
|
|
96
|
+
* content instead. CSS's initial value for `width`, and the same meaning —
|
|
97
|
+
* for a box whose containing block is the viewport but which is not in flow
|
|
98
|
+
* (a float, an abspos, an inline-block) `auto` is shrink-to-fit, and a
|
|
99
|
+
* top-level window is exactly that shape. It has no container to stretch
|
|
100
|
+
* into; stretching into the screen is what `fullscreen` means.
|
|
101
|
+
*
|
|
102
|
+
* Omitting the prop is the same thing, which is why this is a `??` rather
|
|
103
|
+
* than an `===`: leaving a size out cannot sensibly mean "some number
|
|
104
|
+
* somebody picked", and it used to mean ntk's 800x800.
|
|
105
|
+
*/
|
|
106
|
+
export const isAutoSize = (value) => (value ?? 'auto') === 'auto';
|
|
107
|
+
|
|
108
|
+
/** A size prop reduced to what it means, so the two spellings of auto — the
|
|
109
|
+
* keyword and the missing prop — compare equal. */
|
|
110
|
+
export const canonicalSize = (value) => (isAutoSize(value) ? 'auto' : value);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A `<window>` size is a number of pixels or `'auto'`, and nothing else.
|
|
114
|
+
*
|
|
115
|
+
* Worth its own error because the near misses all come from CSS and all look
|
|
116
|
+
* reasonable: `'100%'` has no containing block to be a percentage of,
|
|
117
|
+
* `'fit-content'` is what `'auto'` already means here, and `'600px'` is the
|
|
118
|
+
* unit X11 works in anyway. Left to itself each of them reaches ntk as a
|
|
119
|
+
* string and comes back as a `BadValue` on CreateWindow with a sequence
|
|
120
|
+
* number and nothing else — an X protocol error for what is a typo in JSX.
|
|
121
|
+
*/
|
|
122
|
+
export function assertWindowSize(props, kind) {
|
|
123
|
+
if (!DEV) return;
|
|
124
|
+
for (const axis of ['width', 'height']) {
|
|
125
|
+
const value = props[axis];
|
|
126
|
+
if (value === undefined || value === 'auto') continue;
|
|
127
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
throw new Error(
|
|
131
|
+
`react-x11: <${kind} ${axis}={${JSON.stringify(value)}}> — a window ` +
|
|
132
|
+
`size is a number of pixels or 'auto' (sized to its content, ` +
|
|
133
|
+
`capped at the screen), which is also what leaving ${axis} out ` +
|
|
134
|
+
'means. See docs/elements.md, "Natural size".',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
for (const bound of CONTENT_BOUND_PROPS) {
|
|
138
|
+
const value = props[bound];
|
|
139
|
+
if (value === undefined || value === 'auto') continue;
|
|
140
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
throw new Error(
|
|
144
|
+
`react-x11: <${kind} ${bound}={${JSON.stringify(value)}}> — a window ` +
|
|
145
|
+
`bound is a number of pixels or 'auto' (measured from the content), ` +
|
|
146
|
+
'and leaving it out means no bound at all. ' +
|
|
147
|
+
'See docs/elements.md, "A floor the content decides".',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* ntk's Window constructor takes every creation attribute up front. The
|
|
154
|
+
* user-facing shape and ntk's differ in three places: size hints are flat
|
|
155
|
+
* props here and a `sizeHints` object there, the window background is a
|
|
156
|
+
* style property here and a creation attribute there, and an `'auto'`
|
|
157
|
+
* width or height is resolved to a number by `realize()` — ntk is handed
|
|
158
|
+
* pixels or nothing, never the keyword.
|
|
159
|
+
*
|
|
160
|
+
* Event props never travel this way. ntk reads `onKeyDown` & co. off its
|
|
161
|
+
* creation args and registers them as raw listeners (events_map.toSnake),
|
|
162
|
+
* which would hand the application the native X event instead of the
|
|
163
|
+
* synthetic one the EventManager dispatches — and hold the first render's
|
|
164
|
+
* closure forever. Handlers are read from current props at dispatch time
|
|
165
|
+
* instead, so they can never go stale. `children` is the tree's,
|
|
166
|
+
* `transientFor` holds a React ref that only the commit phase can resolve
|
|
167
|
+
* (WindowNode._applyTransientFor), `anchor` is a position `realize()` works
|
|
168
|
+
* out from the size it just measured (WindowNode._anchorPlacement), and
|
|
169
|
+
* `transparent` names a visual that has to be looked up on the connection
|
|
170
|
+
* (WindowNode._argbAttributes) rather than a value ntk takes.
|
|
171
|
+
*/
|
|
172
|
+
// The WM hints that are distances. The aspect pair are ratios — the same in
|
|
173
|
+
// any unit — and `gravity` is an enum; scaling either would be wrong.
|
|
174
|
+
const LENGTH_HINT_PROPS = new Set([
|
|
175
|
+
'minWidth',
|
|
176
|
+
'minHeight',
|
|
177
|
+
'maxWidth',
|
|
178
|
+
'maxHeight',
|
|
179
|
+
'widthInc',
|
|
180
|
+
'heightInc',
|
|
181
|
+
'baseWidth',
|
|
182
|
+
'baseHeight',
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A window's geometry props, converted to the device pixels every consumer
|
|
187
|
+
* — `_measure`'s yoga math, `setState`, the WM hints — works in. Numbers
|
|
188
|
+
* multiply; `'auto'` and the content bounds pass through; the identity
|
|
189
|
+
* fast path keeps the 1x world allocation-free.
|
|
190
|
+
*/
|
|
191
|
+
export function scaleWindowGeometry(props, scale) {
|
|
192
|
+
if (scale === 1) return props;
|
|
193
|
+
const out = { ...props };
|
|
194
|
+
for (const key of ['width', 'height', 'x', 'y']) {
|
|
195
|
+
if (typeof out[key] === 'number') out[key] = Math.round(out[key] * scale);
|
|
196
|
+
}
|
|
197
|
+
for (const key of LENGTH_HINT_PROPS) {
|
|
198
|
+
if (typeof out[key] === 'number') out[key] = Math.round(out[key] * scale);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function windowAttributes(props, scale = 1) {
|
|
204
|
+
const attributes = {};
|
|
205
|
+
const hints = {};
|
|
206
|
+
// Geometry props are logical pixels like everything an app writes, and
|
|
207
|
+
// this is their one door into device pixels: X windows are device-pixel
|
|
208
|
+
// rectangles, so the multiply happens where CreateWindow's numbers are
|
|
209
|
+
// assembled, and `abs`/`_requestedSize`/ConfigureNotify all stay in one
|
|
210
|
+
// unit downstream (src/scale.js). Rounded because the wire is integers.
|
|
211
|
+
const device = (v) => (typeof v === 'number' ? Math.round(v * scale) : v);
|
|
212
|
+
for (const key of Object.keys(props)) {
|
|
213
|
+
if (key === 'children' || key === 'style' || isEventProp(key)) continue;
|
|
214
|
+
if (key === 'transientFor' || key === 'transparent') continue;
|
|
215
|
+
if (key === 'anchor' || key === 'hidden') continue;
|
|
216
|
+
if ((key === 'width' || key === 'height') && isAutoSize(props[key])) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (WINDOW_HINT_PROPS.includes(key)) {
|
|
220
|
+
// An `'auto'` bound is not a number ntk can be given; `realize()`
|
|
221
|
+
// measures it and merges the answer in before CreateWindow.
|
|
222
|
+
if (!isContentBound(props[key])) {
|
|
223
|
+
hints[key] = LENGTH_HINT_PROPS.has(key)
|
|
224
|
+
? device(props[key])
|
|
225
|
+
: props[key];
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
attributes[key] =
|
|
230
|
+
key === 'width' || key === 'height' || key === 'x' || key === 'y'
|
|
231
|
+
? device(props[key])
|
|
232
|
+
: props[key];
|
|
233
|
+
}
|
|
234
|
+
if (Object.keys(hints).length > 0) attributes.sizeHints = hints;
|
|
235
|
+
if (props.style !== undefined) {
|
|
236
|
+
const style = flattenStyle(props.style);
|
|
237
|
+
if (style.backgroundColor !== undefined) {
|
|
238
|
+
attributes.backgroundColor = style.backgroundColor;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return attributes;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The `_NET_WM_STATE` names these props ask for. `states` is the general
|
|
245
|
+
* mechanism; `fullscreen` and `alwaysOnTop` are sugar for the two everyone
|
|
246
|
+
* reaches for, and they union rather than compete with `states`.
|
|
247
|
+
*/
|
|
248
|
+
export function windowStates(props) {
|
|
249
|
+
const states = new Set(props.states ?? []);
|
|
250
|
+
if (props.fullscreen) states.add('fullscreen');
|
|
251
|
+
if (props.alwaysOnTop) states.add('above');
|
|
252
|
+
return states;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* One state per message. EWMH gives a `_NET_WM_STATE` ClientMessage two
|
|
257
|
+
* state slots — which is what `'maximized'` uses, expanding to the
|
|
258
|
+
* vert/horz pair — so anything longer has to be several messages, and
|
|
259
|
+
* splitting by name is the only chunking that cannot land a pair across a
|
|
260
|
+
* boundary. These are rare, deliberate calls; the round trips do not matter.
|
|
261
|
+
*/
|
|
262
|
+
export function applyWindowStates(wnd, names, action) {
|
|
263
|
+
if (typeof wnd?.setWmState !== 'function') return;
|
|
264
|
+
for (const name of names) {
|
|
265
|
+
// an unsupported state resolves false rather than throwing; a window
|
|
266
|
+
// that went away mid-flight is not worth an unhandled rejection
|
|
267
|
+
Promise.resolve(wnd.setWmState(name, action)).catch(() => {});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// _MOTIF_WM_HINTS: flags, functions, decorations, input_mode, status.
|
|
272
|
+
// flags = 2 is MWM_HINTS_DECORATIONS, i.e. "only the decorations field
|
|
273
|
+
// here means anything". The property's type atom is the property's own
|
|
274
|
+
// name, not CARDINAL — the one thing that is easy to get wrong, and a WM
|
|
275
|
+
// that reads the type will ignore the hint if it is.
|
|
276
|
+
const MOTIF_HINTS = '_MOTIF_WM_HINTS';
|
|
277
|
+
const MOTIF_DECORATIONS = (on) => [2, 0, on ? 1 : 0, 0, 0];
|
|
278
|
+
|
|
279
|
+
export function applyDecorations(wnd, on) {
|
|
280
|
+
if (typeof wnd?.setProperty !== 'function') return;
|
|
281
|
+
Promise.resolve(
|
|
282
|
+
wnd.setProperty(MOTIF_HINTS, MOTIF_DECORATIONS(on), {
|
|
283
|
+
type: MOTIF_HINTS,
|
|
284
|
+
format: 32,
|
|
285
|
+
}),
|
|
286
|
+
).catch(() => {});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const WINDOW_SEMANTIC_NAMES = new Set([
|
|
290
|
+
'width',
|
|
291
|
+
'height',
|
|
292
|
+
...WINDOW_HINT_PROPS,
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
/** Window manager hints, installed onto `WindowNode.prototype` by window.js. */
|
|
296
|
+
export class WindowHints {
|
|
297
|
+
/**
|
|
298
|
+
* Window-manager hints that changed since the last render (ntk >= 3.5.0).
|
|
299
|
+
* Creation is handled by ntk's Window constructor — every non-event prop
|
|
300
|
+
* is forwarded there as a creation attribute — so this only has to cover
|
|
301
|
+
* updates.
|
|
302
|
+
*
|
|
303
|
+
* Size hints are flat props — `minWidth`, `maxHeight`, `widthInc`… — and
|
|
304
|
+
* so is the geometry they constrain. They only had to hide inside a
|
|
305
|
+
* `sizeHints` object while yoga style shared this namespace; with style
|
|
306
|
+
* in its own channel the names are free, and `<window minWidth={360}>`
|
|
307
|
+
* means the one thing it can mean.
|
|
308
|
+
*/
|
|
309
|
+
_applyWindowHints(next, prev) {
|
|
310
|
+
const wnd = this.window;
|
|
311
|
+
|
|
312
|
+
const hints = this._sizeHints(next);
|
|
313
|
+
if (
|
|
314
|
+
next.resizable !== prev.resizable ||
|
|
315
|
+
!shallowEqual(hints, this._sizeHints(prev))
|
|
316
|
+
) {
|
|
317
|
+
if (CONTENT_BOUND_PROPS.some((key) => isContentBound(next[key]))) {
|
|
318
|
+
// A bound this commit cannot resolve: `'auto'` is a measurement, and
|
|
319
|
+
// measuring leaves the tree laid out at a size that is nobody's
|
|
320
|
+
// arrangement. Asking for the layout this frame owes anyway is what
|
|
321
|
+
// makes it safe — `flush()` measures, sends the hints and lays the
|
|
322
|
+
// tree back out, in that order.
|
|
323
|
+
this.invalidate(true, null, 'props');
|
|
324
|
+
} else {
|
|
325
|
+
this._sendSizeHints(next);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!shallowEqual(next.wmClass, prev.wmClass) && next.wmClass) {
|
|
329
|
+
const c = next.wmClass;
|
|
330
|
+
if (Array.isArray(c)) wnd.setClass?.(c[0], c[1]);
|
|
331
|
+
else if (typeof c === 'object') wnd.setClass?.(c.instance, c.class);
|
|
332
|
+
else wnd.setClass?.(c);
|
|
333
|
+
}
|
|
334
|
+
if (!shallowEqual(next.windowType, prev.windowType) && next.windowType) {
|
|
335
|
+
wnd.setWindowType?.(next.windowType);
|
|
336
|
+
}
|
|
337
|
+
// Diffed against the *previous props*, never against what the window
|
|
338
|
+
// manager currently has. That is what makes these controlled: on X the
|
|
339
|
+
// WM changes state behind the app's back all the time — the user hits
|
|
340
|
+
// maximize, a hotkey leaves fullscreen — and a prop re-asserted every
|
|
341
|
+
// commit would fight it. React only hears about reality through
|
|
342
|
+
// `onStatesChange`, and only re-asks when the app itself changes its
|
|
343
|
+
// mind.
|
|
344
|
+
const before = windowStates(prev);
|
|
345
|
+
const now = windowStates(next);
|
|
346
|
+
applyWindowStates(
|
|
347
|
+
wnd,
|
|
348
|
+
[...now].filter((s) => !before.has(s)),
|
|
349
|
+
'add',
|
|
350
|
+
);
|
|
351
|
+
applyWindowStates(
|
|
352
|
+
wnd,
|
|
353
|
+
[...before].filter((s) => !now.has(s)),
|
|
354
|
+
'remove',
|
|
355
|
+
);
|
|
356
|
+
if (next.decorations !== prev.decorations) {
|
|
357
|
+
applyDecorations(wnd, next.decorations !== false);
|
|
358
|
+
}
|
|
359
|
+
if (next.transientFor !== prev.transientFor) {
|
|
360
|
+
this._pendingTransientFor = undefined;
|
|
361
|
+
this._applyTransientFor(next.transientFor);
|
|
362
|
+
} else if (this._pendingTransientFor !== undefined) {
|
|
363
|
+
// the owner was not realized last time round; every commit is another
|
|
364
|
+
// chance, and a sibling window earlier in the tree is realized by now
|
|
365
|
+
this._applyTransientFor(this._pendingTransientFor);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Write `WM_TRANSIENT_FOR`, resolving whatever the prop holds — a ref to a
|
|
371
|
+
* `<window>`/`<popup>`, a ref to any drawn node (resolved to the window
|
|
372
|
+
* that owns it), a raw XID, `'root'` for the client's whole window group,
|
|
373
|
+
* or `null` to clear.
|
|
374
|
+
*
|
|
375
|
+
* Resolution has to happen here rather than in `windowAttributes`, which
|
|
376
|
+
* copies every non-event prop straight into ntk's creation attributes: a
|
|
377
|
+
* React ref is not something ntk should be asked to understand.
|
|
378
|
+
*
|
|
379
|
+
* **Refs attach in the layout phase, after every mutation.** So on the
|
|
380
|
+
* commit that mounts two sibling `<window>`s, the second one realizes
|
|
381
|
+
* while the first one's ref is still null — the owner is unresolvable
|
|
382
|
+
* exactly when a single-tree multi-window app needs it. That is what
|
|
383
|
+
* `_pendingTransientFor` is for: an unresolved owner is retried on the
|
|
384
|
+
* next commit rather than dropped, and the frame this window schedules on
|
|
385
|
+
* mount gives it one without waiting for an unrelated re-render.
|
|
386
|
+
*/
|
|
387
|
+
_applyTransientFor(owner) {
|
|
388
|
+
const wnd = this.window;
|
|
389
|
+
if (!wnd || typeof wnd.setTransientFor !== 'function') return;
|
|
390
|
+
if (owner == null) {
|
|
391
|
+
this._pendingTransientFor = undefined;
|
|
392
|
+
// only clear a property we actually wrote; a bare `undefined` on mount
|
|
393
|
+
// must not cost a DeleteProperty on every window in the app
|
|
394
|
+
if (this._transientForId != null) {
|
|
395
|
+
this._transientForId = null;
|
|
396
|
+
wnd.setTransientFor(null);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const id = owner === 'root' ? 'root' : windowIdOf(owner);
|
|
401
|
+
if (id == null) {
|
|
402
|
+
this._pendingTransientFor = owner;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this._pendingTransientFor = undefined;
|
|
406
|
+
if (id === this._transientForId) return;
|
|
407
|
+
if (id === wnd.id) {
|
|
408
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
409
|
+
console.warn(
|
|
410
|
+
'react-x11: transientFor points at the window itself. A window ' +
|
|
411
|
+
'cannot own itself; the property is ignored.',
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this._transientForId = id;
|
|
417
|
+
wnd.setTransientFor(id);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The WM size hints among these props, as the author wrote them. */
|
|
421
|
+
_sizeHints(props) {
|
|
422
|
+
// WM_NORMAL_HINTS reach the window manager, which measures the real
|
|
423
|
+
// window — device pixels, like every geometry prop's destination.
|
|
424
|
+
const scaled = scaleWindowGeometry(props, this.scale);
|
|
425
|
+
const hints = {};
|
|
426
|
+
for (const key of WINDOW_HINT_PROPS) {
|
|
427
|
+
if (scaled[key] !== undefined) hints[key] = scaled[key];
|
|
428
|
+
}
|
|
429
|
+
return hints;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The whole `WM_NORMAL_HINTS` struct to write: what the author named, with
|
|
434
|
+
* every `'auto'` replaced by the number `_measure()` resolved for it.
|
|
435
|
+
*
|
|
436
|
+
* Whole, because `setSizeHints` writes the property outright and carries
|
|
437
|
+
* nothing over from the last call — a floor sent on its own would drop the
|
|
438
|
+
* `widthInc` beside it. A bound left unresolved (a torn-down window with
|
|
439
|
+
* nothing to measure) is dropped rather than sent: `'auto'` reaches the
|
|
440
|
+
* wire as a CARD32 of 0, which is a floor of nothing dressed up as a
|
|
441
|
+
* declaration.
|
|
442
|
+
*/
|
|
443
|
+
_hintsToSend(props, resolved) {
|
|
444
|
+
const hints = { ...this._sizeHints(props), ...resolved };
|
|
445
|
+
for (const key of CONTENT_BOUND_PROPS) {
|
|
446
|
+
if (isContentBound(hints[key])) delete hints[key];
|
|
447
|
+
}
|
|
448
|
+
if (props.resizable === false) hints.resizable = false;
|
|
449
|
+
return hints;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Write the hints, if they are not the ones already written.
|
|
454
|
+
*
|
|
455
|
+
* Diffed against what actually went out rather than against props: a
|
|
456
|
+
* content-measured bound is recomputed on every frame that lays out, and
|
|
457
|
+
* most frames move nothing. Without the check, a window with
|
|
458
|
+
* `minWidth="auto"` would spend a `ChangeProperty` per frame restating a
|
|
459
|
+
* number the window manager already has.
|
|
460
|
+
*
|
|
461
|
+
* `resizable: false` is the one hint whose meaning is not in its keys — it
|
|
462
|
+
* pins min and max to the size the window has *at the call* — so the size
|
|
463
|
+
* is part of what is compared, and a window that pins itself and then
|
|
464
|
+
* grows re-pins at the size it grew to.
|
|
465
|
+
*/
|
|
466
|
+
_sendSizeHints(props = this.props, resolved = null) {
|
|
467
|
+
const wnd = this.window;
|
|
468
|
+
if (!wnd || typeof wnd.setSizeHints !== 'function') return;
|
|
469
|
+
const hints = this._hintsToSend(props, resolved);
|
|
470
|
+
if (Object.keys(hints).length === 0) return;
|
|
471
|
+
const at = `${wnd.width}x${wnd.height}`;
|
|
472
|
+
if (
|
|
473
|
+
shallowEqual(hints, this._sentHints) &&
|
|
474
|
+
(hints.resizable !== false || at === this._sentHintsAt)
|
|
475
|
+
) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
this._sentHints = hints;
|
|
479
|
+
this._sentHintsAt = at;
|
|
480
|
+
wnd.setSizeHints(hints);
|
|
481
|
+
}
|
|
482
|
+
}
|