react-x11 2.11.0 → 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.
Files changed (96) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +292 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/threaded.js +246 -0
  20. package/src/cocoa/window.js +256 -42
  21. package/src/components/Select.js +2 -2
  22. package/src/components/anchor.js +3 -3
  23. package/src/components/native.js +12 -7
  24. package/src/components/theme.js +2 -2
  25. package/src/debug.js +1 -1
  26. package/src/decorations.js +1 -1
  27. package/src/editmenu.js +2 -2
  28. package/src/errors.js +46 -0
  29. package/src/events.js +6 -6
  30. package/src/foreignnodes.js +3 -2
  31. package/src/frames.js +2 -2
  32. package/src/glnodes.js +1 -1
  33. package/src/grid.js +1653 -0
  34. package/src/host.d.ts +230 -0
  35. package/src/host.js +11 -3
  36. package/src/imagesource.js +1 -1
  37. package/src/index.d.ts +21 -4
  38. package/src/index.js +9 -1
  39. package/src/layouts.js +721 -0
  40. package/src/node.d.ts +4 -2
  41. package/src/node.js +19 -21
  42. package/src/nodes/animation.js +644 -0
  43. package/src/nodes/box.js +21 -0
  44. package/src/nodes/boxpaint.js +473 -0
  45. package/src/nodes/canvas.js +269 -0
  46. package/src/nodes/cascade.js +600 -0
  47. package/src/nodes/damage.js +183 -0
  48. package/src/nodes/edithistory.js +124 -0
  49. package/src/nodes/editmenupopup.js +260 -0
  50. package/src/nodes/hittest.js +185 -0
  51. package/src/nodes/image.js +266 -0
  52. package/src/nodes/install.js +75 -0
  53. package/src/nodes/invalidate.js +465 -0
  54. package/src/nodes/kinds.js +31 -0
  55. package/src/nodes/layout.js +439 -0
  56. package/src/nodes/layouthost.js +949 -0
  57. package/src/nodes/node.js +868 -0
  58. package/src/nodes/paint.js +466 -0
  59. package/src/nodes/position.js +366 -0
  60. package/src/nodes/preedit.js +127 -0
  61. package/src/nodes/queries.js +330 -0
  62. package/src/nodes/rects.js +102 -0
  63. package/src/nodes/scrollable.js +891 -0
  64. package/src/nodes/scrollbars.js +138 -0
  65. package/src/nodes/scrollblit.js +1034 -0
  66. package/src/nodes/selectable.js +142 -0
  67. package/src/nodes/styling.js +225 -0
  68. package/src/nodes/text.js +649 -0
  69. package/src/nodes/textarea.js +391 -0
  70. package/src/nodes/textinput.js +1146 -0
  71. package/src/nodes/util.js +17 -0
  72. package/src/nodes/window/anchoring.js +161 -0
  73. package/src/nodes/window/capabilities.js +190 -0
  74. package/src/nodes/window/debugpaint.js +83 -0
  75. package/src/nodes/window/droptarget.js +145 -0
  76. package/src/nodes/window/floors.js +577 -0
  77. package/src/nodes/window/flush.js +334 -0
  78. package/src/nodes/window/hints.js +482 -0
  79. package/src/nodes/window/listeners.js +222 -0
  80. package/src/nodes/window/popup.js +71 -0
  81. package/src/nodes/window/size.js +591 -0
  82. package/src/nodes/window/window.js +945 -0
  83. package/src/palette.js +1 -1
  84. package/src/registry.js +7 -3
  85. package/src/styles.js +137 -15
  86. package/src/svgnodes.js +2 -1
  87. package/src/testing/harness.js +2 -2
  88. package/src/textselection.js +5 -3
  89. package/src/trace-registry.js +1 -1
  90. package/src/types/components.d.ts +38 -6
  91. package/src/types/elements.d.ts +11 -1
  92. package/src/types/nodes.d.ts +17 -2
  93. package/src/types/style.d.ts +94 -3
  94. package/src/windowstate.js +1 -1
  95. package/src/yoga.js +1 -1
  96. 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
+ }