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.
- 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 +292 -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/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/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 +17 -2
- 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,591 @@
|
|
|
1
|
+
// Windows sized by their content: measuring the tree against its content
|
|
2
|
+
// floors, laying the root out, deferring floors during a live resize and
|
|
3
|
+
// catching up after, and refitting the window to what it holds.
|
|
4
|
+
|
|
5
|
+
import { measuringExactly } from '../../styles.js';
|
|
6
|
+
import { availableArea } from '../../screens.js';
|
|
7
|
+
import {
|
|
8
|
+
captureLeafHeights,
|
|
9
|
+
collectFloorStale,
|
|
10
|
+
probeHeightFloors,
|
|
11
|
+
setMeasuringShrink,
|
|
12
|
+
restoreShrink,
|
|
13
|
+
freezeWidths,
|
|
14
|
+
restoreWidths,
|
|
15
|
+
contentSpan,
|
|
16
|
+
writeFloors,
|
|
17
|
+
} from './floors.js';
|
|
18
|
+
import {
|
|
19
|
+
MAX_WINDOW_EXTENT,
|
|
20
|
+
clampExtent,
|
|
21
|
+
clampBound,
|
|
22
|
+
screenOriginOf,
|
|
23
|
+
CONTENT_BOUND_PROPS,
|
|
24
|
+
isContentBound,
|
|
25
|
+
numericBound,
|
|
26
|
+
isAutoSize,
|
|
27
|
+
scaleWindowGeometry,
|
|
28
|
+
} from './hints.js';
|
|
29
|
+
|
|
30
|
+
/** Content-sized windows, installed onto `WindowNode.prototype` by window.js. */
|
|
31
|
+
export class WindowSize {
|
|
32
|
+
/**
|
|
33
|
+
* One measuring pass over the tree, read back into the extents of every
|
|
34
|
+
* node still to be measured (`contentSpan`), and the root's own span
|
|
35
|
+
* returned — the number a `minWidth="auto"` window sends as its hint.
|
|
36
|
+
*
|
|
37
|
+
* `forWidth` is the width the heights are measured for; `probe` says
|
|
38
|
+
* whether the widths that pass settles are still to be checked against
|
|
39
|
+
* the ones the height floors were measured at (`probeHeightFloors`) —
|
|
40
|
+
* they are when nothing has looked yet this frame, and a leaf the probe
|
|
41
|
+
* finds moved has its floor taken off and the pass run again, since a
|
|
42
|
+
* floor still on a node being measured would be read back as content.
|
|
43
|
+
*/
|
|
44
|
+
_measureContentSpans(axis, forWidth, probe = false) {
|
|
45
|
+
this._sweepLayoutHosts();
|
|
46
|
+
const yoga = this.yoga;
|
|
47
|
+
const dir = this._rootDirection;
|
|
48
|
+
// The root carries whatever size the last pass pinned on it, and an
|
|
49
|
+
// available size means nothing to a root that has one of its own.
|
|
50
|
+
yoga.setWidth(undefined);
|
|
51
|
+
yoga.setHeight(undefined);
|
|
52
|
+
if (axis === 'width') {
|
|
53
|
+
// the layout hosts' children first, tree by tree: what they can be
|
|
54
|
+
// squeezed to is what the hosts answer the pass below with
|
|
55
|
+
if (this._layoutHosts.size !== 0) this._measureHostChildWidths();
|
|
56
|
+
const shrunk = [];
|
|
57
|
+
setMeasuringShrink(this, axis, shrunk);
|
|
58
|
+
this._layoutPasses += 1;
|
|
59
|
+
yoga.calculateLayout(0, undefined, dir);
|
|
60
|
+
const span = contentSpan(this, axis, null, this);
|
|
61
|
+
restoreShrink(shrunk);
|
|
62
|
+
return span;
|
|
63
|
+
}
|
|
64
|
+
// Height takes two passes. The first is the tree at its real width with
|
|
65
|
+
// no bound on the height, which is where every leaf reports the height
|
|
66
|
+
// it actually needs there — a wrapped paragraph's is settled by the
|
|
67
|
+
// width, and no leaf can give any of it back. It runs before the shrink
|
|
68
|
+
// is borrowed, since the widths it settles are the real ones. The second
|
|
69
|
+
// is the one that collapses, and it squashes a leaf that a `row`
|
|
70
|
+
// stretches: those are the ones the map above puts back. The widths the
|
|
71
|
+
// first pass settled are held across the second (`freezeWidths`), which
|
|
72
|
+
// is the only thing keeping it a collapse rather than a second opinion.
|
|
73
|
+
this._layoutPasses += 1;
|
|
74
|
+
yoga.calculateLayout(forWidth, undefined, dir);
|
|
75
|
+
if (probe && this._probeHeightFloors().marked) {
|
|
76
|
+
this._writeFloors('height');
|
|
77
|
+
this._layoutPasses += 1;
|
|
78
|
+
yoga.calculateLayout(forWidth, undefined, dir);
|
|
79
|
+
}
|
|
80
|
+
const intrinsic = new Map();
|
|
81
|
+
captureLeafHeights(this, intrinsic);
|
|
82
|
+
const frozen = [];
|
|
83
|
+
freezeWidths(this, frozen);
|
|
84
|
+
const shrunk = [];
|
|
85
|
+
setMeasuringShrink(this, axis, shrunk);
|
|
86
|
+
this._layoutPasses += 1;
|
|
87
|
+
yoga.calculateLayout(forWidth, 0, dir);
|
|
88
|
+
const span = contentSpan(this, axis, intrinsic, this);
|
|
89
|
+
restoreWidths(frozen);
|
|
90
|
+
restoreShrink(shrunk);
|
|
91
|
+
return span;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What the last measurement can no longer answer for, taken off the
|
|
96
|
+
* nodes and listed (`collectFloorStale`). Run ahead of anything that lays
|
|
97
|
+
* the tree out, since a pass clears yoga's record of what changed — and
|
|
98
|
+
* cheap enough to run twice in a frame, because the second walk finds the
|
|
99
|
+
* marks the first one left.
|
|
100
|
+
*/
|
|
101
|
+
_collectFloorStale() {
|
|
102
|
+
this._sweepLayoutHosts();
|
|
103
|
+
const found = { width: false, height: false };
|
|
104
|
+
this._floorsStale.clear();
|
|
105
|
+
// the root's own children are written from here too, and its direction
|
|
106
|
+
// can move like any node's
|
|
107
|
+
this._floorsStale.add(this);
|
|
108
|
+
collectFloorStale(this, this._floorsStale, found, !this._floorsSwept);
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The floors on the children of every node found stale, from the extents
|
|
113
|
+
* they carry — `writeFloors` for each. */
|
|
114
|
+
_writeFloors(axis) {
|
|
115
|
+
for (const node of this._floorsStale) {
|
|
116
|
+
if (!node.destroyed) writeFloors(node, axis);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `probeHeightFloors` over this window's tree. */
|
|
121
|
+
_probeHeightFloors() {
|
|
122
|
+
const hit = { marked: false, owed: false };
|
|
123
|
+
probeHeightFloors(this, this, hit);
|
|
124
|
+
return hit;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Measure the width extents that are stale and write the width floors
|
|
129
|
+
* from them. The floors on the nodes about to be measured come off first
|
|
130
|
+
* (a stale extent writes the style's own minimum), which is what keeps a
|
|
131
|
+
* floor from ratcheting: read back as content, it could only ever grow.
|
|
132
|
+
*/
|
|
133
|
+
_measureWidthFloors() {
|
|
134
|
+
this._writeFloors('width');
|
|
135
|
+
const span = this._measureContentSpans('width');
|
|
136
|
+
this._writeFloors('width');
|
|
137
|
+
return span;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The same for the heights, at `forWidth`. */
|
|
141
|
+
_measureHeightFloors(forWidth, probe) {
|
|
142
|
+
this._writeFloors('height');
|
|
143
|
+
const span = this._measureContentSpans('height', forWidth, probe);
|
|
144
|
+
this._writeFloors('height');
|
|
145
|
+
return span;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_measureMinimum(axis, forWidth) {
|
|
149
|
+
// Measured from the styles alone: the stale nodes' own floors come off
|
|
150
|
+
// in the measurement, and a clean node's extent was measured the same
|
|
151
|
+
// way before it was floored.
|
|
152
|
+
this._collectFloorStale();
|
|
153
|
+
return measuringExactly(() =>
|
|
154
|
+
Math.ceil(
|
|
155
|
+
axis === 'width'
|
|
156
|
+
? this._measureWidthFloors()
|
|
157
|
+
: this._measureHeightFloors(forWidth, true),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The real layout pass: the tree at the window's size, on the pixel grid. */
|
|
163
|
+
_layoutRoot(width, height) {
|
|
164
|
+
this._sweepLayoutHosts();
|
|
165
|
+
this._layoutPasses += 1;
|
|
166
|
+
this.yoga.setWidth(width);
|
|
167
|
+
this.yoga.setHeight(height);
|
|
168
|
+
this.yoga.calculateLayout(width, height, this._rootDirection);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Give every flex item in this window's tree the floor CSS calls its
|
|
173
|
+
* automatic minimum size, so that `flexShrink`'s default of `1` squeezes a
|
|
174
|
+
* row into the space it has without squeezing its contents out of
|
|
175
|
+
* existence, and lay the tree out with them. See `writeFloors` for what
|
|
176
|
+
* that means and why both halves are needed.
|
|
177
|
+
*
|
|
178
|
+
* Two measurements, in this order because they depend that way round: the
|
|
179
|
+
* widths from a pass with no room on offer at all, then — with those floors
|
|
180
|
+
* already applied — the heights at the width the window is about to be laid
|
|
181
|
+
* out at, since a minimum height is always a height *for a width*.
|
|
182
|
+
*
|
|
183
|
+
* Nothing about this is per frame: the floors are content, so they survive
|
|
184
|
+
* every frame that did not change any (`_floorsDirty`), which is what keeps
|
|
185
|
+
* a wheel notch to the one layout pass it always was. And nothing about
|
|
186
|
+
* it is per node either: every node keeps the extent it was last measured
|
|
187
|
+
* at, and a measurement re-reads only the nodes whose subtree changed
|
|
188
|
+
* (`collectFloorStale`), taking the rest at the number they carry. So a
|
|
189
|
+
* padding change on a container measures the container and nothing
|
|
190
|
+
* below it, a row that mounts measures itself alone, and a colour change
|
|
191
|
+
* measures nothing.
|
|
192
|
+
*
|
|
193
|
+
* The passes are paid for only where a floor is going to be **written**
|
|
194
|
+
* from what they find. The width pass runs when a stale node is one on a
|
|
195
|
+
* row's main axis; and the heights are settled the other way round — the
|
|
196
|
+
* real layout runs first, `probeHeightFloors` walks the nodes it moved
|
|
197
|
+
* and asks each leaf whether its height at its new width is the height
|
|
198
|
+
* it had, and only if one says otherwise (or content changed under a
|
|
199
|
+
* node a floor is written on) do the two height passes run and the
|
|
200
|
+
* layout with them. A relayout of a large tree whose labels all still
|
|
201
|
+
* fit — a panel toggle, a theme switch, a resize that wraps nothing — is
|
|
202
|
+
* one pass over yoga where it was four.
|
|
203
|
+
*/
|
|
204
|
+
_applyContentFloors(width, height) {
|
|
205
|
+
const found = this._collectFloorStale();
|
|
206
|
+
measuringExactly(() => {
|
|
207
|
+
if (found.width) this._measureWidthFloors();
|
|
208
|
+
else this._writeFloors('width');
|
|
209
|
+
// from the extents on hand; a stale one writes the style's minimum,
|
|
210
|
+
// which is the floor coming off ahead of its measurement below
|
|
211
|
+
this._writeFloors('height');
|
|
212
|
+
});
|
|
213
|
+
let heights = found.height;
|
|
214
|
+
let probed = false;
|
|
215
|
+
if (!heights) {
|
|
216
|
+
this._layoutRoot(width, height);
|
|
217
|
+
heights = this._probeHeightFloors().owed;
|
|
218
|
+
probed = true;
|
|
219
|
+
}
|
|
220
|
+
if (heights) {
|
|
221
|
+
measuringExactly(() => this._measureHeightFloors(width, !probed));
|
|
222
|
+
this._layoutRoot(width, height);
|
|
223
|
+
}
|
|
224
|
+
this._floorsDirty = false;
|
|
225
|
+
this._floorsContentDirty = false;
|
|
226
|
+
this._floorsSwept = true;
|
|
227
|
+
this._floorsWidth = width;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Answer a live resize with the floors already in hand, and measure fresh
|
|
232
|
+
* ones once the drag is over.
|
|
233
|
+
*
|
|
234
|
+
* The floors were half of a relayout on a large tree — three extra layout
|
|
235
|
+
* passes and their walks, measured at 21 of a 44ms frame on 3,600 nodes
|
|
236
|
+
* (`npm run bench:presenters -- --scenario=layout`) before they were
|
|
237
|
+
* measured incrementally — and a resize is the one layout change they
|
|
238
|
+
* cannot follow at input rate: AppKit's resize loop calls the frame for
|
|
239
|
+
* every pointer move, from inside the event, and the next move waits for
|
|
240
|
+
* the frame. So a tick of a drag lays the tree out against the floors the
|
|
241
|
+
* last measurement left, which are exact along the main axis (a
|
|
242
|
+
* min-content width is content, and the content did not move) and a
|
|
243
|
+
* frame stale for wrapped text along the other, and the frame after the
|
|
244
|
+
* release measures once and lays out again — "answer the input, then
|
|
245
|
+
* catch up". Only while the window says it is being resized live
|
|
246
|
+
* (`liveResizing`, set between AppKit's begin and end of the drag; an X
|
|
247
|
+
* window has no such thing and takes the measured path every time), only
|
|
248
|
+
* when the floors exist to reuse, and never over a content change the
|
|
249
|
+
* floors have not seen — a row that mounted mid-drag has no floor at all,
|
|
250
|
+
* and no floor is the collapse #249 exists to prevent.
|
|
251
|
+
*/
|
|
252
|
+
_deferContentFloors(width) {
|
|
253
|
+
// nothing to measure: `_applyContentFloors` returns at once, and a
|
|
254
|
+
// catch-up frame would owe nothing
|
|
255
|
+
if (!this._floorsDirty && this._floorsWidth === width) return false;
|
|
256
|
+
return (
|
|
257
|
+
this.window?.liveResizing === true &&
|
|
258
|
+
this._floorsWidth != null &&
|
|
259
|
+
!this._floorsContentDirty
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The frame a deferred measurement owes: a full relayout with fresh
|
|
265
|
+
* floors, run on the first frame tick after the live resize ends. One at
|
|
266
|
+
* a time — a drag is many ticks, and the catch-up waits for the last of
|
|
267
|
+
* them rather than following each.
|
|
268
|
+
*/
|
|
269
|
+
_scheduleFloorsCatchUp() {
|
|
270
|
+
if (this._floorsCatchUp) return;
|
|
271
|
+
this._floorsCatchUp = true;
|
|
272
|
+
const schedule =
|
|
273
|
+
typeof this.window?.requestAnimationFrame === 'function'
|
|
274
|
+
? (cb) => this.window.requestAnimationFrame(cb)
|
|
275
|
+
: (cb) => setImmediate(cb);
|
|
276
|
+
const run = () => {
|
|
277
|
+
if (this.destroyed || !this.window) {
|
|
278
|
+
this._floorsCatchUp = false;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// still dragging — a drag that pauses has not ended: wait on
|
|
282
|
+
if (this.window.liveResizing) {
|
|
283
|
+
schedule(run);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this._floorsCatchUp = false;
|
|
287
|
+
this._floorsDirty = true;
|
|
288
|
+
this.invalidate(true, null, 'resize');
|
|
289
|
+
this.flush();
|
|
290
|
+
};
|
|
291
|
+
schedule(run);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* What the content has to say about this window's size: the size it wants
|
|
296
|
+
* for whichever of `width`/`height` is `'auto'`, and the numbers an
|
|
297
|
+
* `'auto'` bound resolves to.
|
|
298
|
+
*
|
|
299
|
+
* The **natural** size is CSS shrink-to-fit, then height-for-width:
|
|
300
|
+
*
|
|
301
|
+
* 1. Lay the tree out with **no available width**, which is what yoga's
|
|
302
|
+
* `undefined` means: every measure function is asked in
|
|
303
|
+
* `MEASURE_MODE_UNDEFINED`, text does not wrap, and the root reports
|
|
304
|
+
* its max-content width.
|
|
305
|
+
* 2. Clamp that into `[minWidth, min(maxWidth, the screen)]`.
|
|
306
|
+
* 3. **Lay out again at the clamped width.** This is the pass that
|
|
307
|
+
* matters and the one it is tempting to skip: a paragraph that had to
|
|
308
|
+
* wrap at the clamped width is taller than the max-content pass said,
|
|
309
|
+
* and a window sized from that first height would cut its own text off.
|
|
310
|
+
*
|
|
311
|
+
* Where CSS and X part ways: shrink-to-fit is
|
|
312
|
+
* `min(max(min-content, available), max-content)`, and that `max(...)`
|
|
313
|
+
* means a CSS box never goes below its min-content size even when it
|
|
314
|
+
* overflows. A window cannot be wider than the screen, so the clamp wins
|
|
315
|
+
* and the content is cut instead.
|
|
316
|
+
*
|
|
317
|
+
* The two answers are the pair Qt and GTK both hand their toplevels —
|
|
318
|
+
* `sizeHint()`/`minimumSizeHint()`, `gtk_widget_measure`'s
|
|
319
|
+
* `(minimum, natural)` — which is why `'auto'` reads as the natural size
|
|
320
|
+
* on a cap and as the minimum on a floor: it means "ask the content",
|
|
321
|
+
* and the content's answer to *how big* is not its answer to *how small*.
|
|
322
|
+
*
|
|
323
|
+
* Runs before `CreateWindow`, so it must not need one: text measures
|
|
324
|
+
* through `app.fonts`, which is the connection's, and the clamp was
|
|
325
|
+
* resolved during `createRoot`. That is the whole point — the window is
|
|
326
|
+
* *created* at its natural size rather than resized into it after mapping,
|
|
327
|
+
* so nothing is ever on screen at the wrong size.
|
|
328
|
+
*
|
|
329
|
+
* Leaves the tree laid out at a size that is nobody's arrangement, so it
|
|
330
|
+
* may only be called on a frame that goes on to lay out — `realize()`,
|
|
331
|
+
* which invalidates, and `_refit()`, which `flush()` only calls when it
|
|
332
|
+
* owes a layout pass anyway.
|
|
333
|
+
*/
|
|
334
|
+
_measure() {
|
|
335
|
+
// Every number below — yoga's answers, the monitor rects, the window's
|
|
336
|
+
// live size — is device pixels, so the geometry props convert on entry
|
|
337
|
+
// and the rest of the function never thinks about units again.
|
|
338
|
+
const props = scaleWindowGeometry(this.props, this.scale);
|
|
339
|
+
const autoW = isAutoSize(props.width);
|
|
340
|
+
const autoH = isAutoSize(props.height);
|
|
341
|
+
const yoga = this.yoga;
|
|
342
|
+
// Where the window will open, for picking a monitor: next to its owner
|
|
343
|
+
// where it has one, and wherever the WM puts it otherwise.
|
|
344
|
+
const area = availableArea(this.app, screenOriginOf(props.transientFor));
|
|
345
|
+
// An `'auto'` cap never bounds the pass that resolves it: `maxWidth`
|
|
346
|
+
// there *is* the natural width, so letting it in would be the answer
|
|
347
|
+
// bounding the question.
|
|
348
|
+
const limit = (max, screen) =>
|
|
349
|
+
Math.min(
|
|
350
|
+
numericBound(max) ?? Infinity,
|
|
351
|
+
screen ?? Infinity,
|
|
352
|
+
MAX_WINDOW_EXTENT,
|
|
353
|
+
);
|
|
354
|
+
const availW = limit(props.maxWidth, area?.width);
|
|
355
|
+
const availH = limit(props.maxHeight, area?.height);
|
|
356
|
+
const hints = {};
|
|
357
|
+
if (!yoga) {
|
|
358
|
+
// Only reachable on a torn-down window, and a size still has to be a
|
|
359
|
+
// size: fall back to the space on offer rather than handing `'auto'`
|
|
360
|
+
// through to CreateWindow. Nothing left to measure a bound against.
|
|
361
|
+
return {
|
|
362
|
+
width: autoW
|
|
363
|
+
? clampExtent(availW, numericBound(props.minWidth), availW)
|
|
364
|
+
: props.width,
|
|
365
|
+
height: autoH
|
|
366
|
+
? clampExtent(availH, numericBound(props.minHeight), availH)
|
|
367
|
+
: props.height,
|
|
368
|
+
hints,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// A numeric floor applies to the natural size as it always has; an
|
|
373
|
+
// `'auto'` one is measured below. The height's needs a width to be
|
|
374
|
+
// measured for, and it can never exceed the natural height anyway —
|
|
375
|
+
// same width, every node at or below the size it settled at — so
|
|
376
|
+
// nothing is lost by clamping the height without it.
|
|
377
|
+
const minH = numericBound(props.minHeight);
|
|
378
|
+
|
|
379
|
+
// Also run for an axis that is not `'auto'` but whose cap is: a
|
|
380
|
+
// `maxWidth="auto"` on a window with a `width` still has to find out
|
|
381
|
+
// what the content wanted.
|
|
382
|
+
const needW = autoW || isContentBound(props.maxWidth);
|
|
383
|
+
const needH = autoH || isContentBound(props.maxHeight);
|
|
384
|
+
if (!needW && !needH) {
|
|
385
|
+
// Both sizes are the app's, so there is nothing to measure but the
|
|
386
|
+
// bounds — and nothing re-resolves `@width` blocks here: the styles
|
|
387
|
+
// are the ones the window's real size resolved on the last frame,
|
|
388
|
+
// which is the size the floors are wanted for.
|
|
389
|
+
if (isContentBound(props.minWidth)) {
|
|
390
|
+
hints.minWidth = clampBound(this._measureMinimum('width'), availW);
|
|
391
|
+
}
|
|
392
|
+
this._finishHeightFloor(
|
|
393
|
+
hints,
|
|
394
|
+
props,
|
|
395
|
+
this.window?.width ?? props.width,
|
|
396
|
+
availH,
|
|
397
|
+
);
|
|
398
|
+
return { width: props.width, height: props.height, hints };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const dir = this._rootDirection;
|
|
402
|
+
const measure = () => {
|
|
403
|
+
this._sweepLayoutHosts();
|
|
404
|
+
// The root carries whatever size the last flush() pinned on it — and
|
|
405
|
+
// whatever the floor pass below cleared — so this is re-stated per
|
|
406
|
+
// call rather than hoisted: clearing an axis is what makes yoga
|
|
407
|
+
// measure it rather than fill it.
|
|
408
|
+
yoga.setWidth(needW ? undefined : props.width);
|
|
409
|
+
yoga.setHeight(needH ? undefined : props.height);
|
|
410
|
+
let naturalW;
|
|
411
|
+
if (needW) {
|
|
412
|
+
yoga.calculateLayout(undefined, needH ? undefined : props.height, dir);
|
|
413
|
+
naturalW = clampExtent(yoga.getComputedWidth(), undefined, availW);
|
|
414
|
+
}
|
|
415
|
+
const width = autoW ? clampExtent(naturalW, minW, availW) : props.width;
|
|
416
|
+
// The height-for-width pass. Run even when only the width is auto: it
|
|
417
|
+
// is the layout the window is about to be created at, so leaving the
|
|
418
|
+
// tree holding the max-content one would hand `flush()` a stale
|
|
419
|
+
// arrangement.
|
|
420
|
+
yoga.calculateLayout(width, needH ? undefined : props.height, dir);
|
|
421
|
+
const naturalH = needH
|
|
422
|
+
? clampExtent(yoga.getComputedHeight(), undefined, availH)
|
|
423
|
+
: undefined;
|
|
424
|
+
const height = autoH ? clampExtent(naturalH, minH, availH) : props.height;
|
|
425
|
+
return { width, height, naturalW, naturalH };
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// `@width`/`@height` blocks and an auto size are mutually circular: the
|
|
429
|
+
// query wants a size the measurement has not produced yet. Broken the way
|
|
430
|
+
// CSS breaks the same cycle for container queries — measure against the
|
|
431
|
+
// space on offer, then re-resolve against the answer, and measure once
|
|
432
|
+
// more if that moved anything. **Once**: a second look settles the common
|
|
433
|
+
// case (a block that turns on below the width the content would have
|
|
434
|
+
// taken) and a third would only be chasing a layout that oscillates,
|
|
435
|
+
// which no size can satisfy.
|
|
436
|
+
this._resolveSizeQueries(
|
|
437
|
+
autoW ? availW : props.width,
|
|
438
|
+
autoH ? availH : props.height,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// The width floor, measured against the styles the pass below starts
|
|
442
|
+
// from and before it, because it is what the natural width is clamped
|
|
443
|
+
// into. Bounded by the same space the size is: a floor wider than the
|
|
444
|
+
// screen is a window that cannot be put on it, and a floor past
|
|
445
|
+
// `maxWidth` is a `WM_NORMAL_HINTS` that contradicts itself.
|
|
446
|
+
const minW = isContentBound(props.minWidth)
|
|
447
|
+
? (hints.minWidth = clampBound(this._measureMinimum('width'), availW))
|
|
448
|
+
: props.minWidth;
|
|
449
|
+
|
|
450
|
+
let size = measure();
|
|
451
|
+
if (this._resolveSizeQueries(size.width, size.height)) size = measure();
|
|
452
|
+
// …and the container blocks against the arrangement that produced it,
|
|
453
|
+
// so the window is created at the size its content will actually take
|
|
454
|
+
if (this._containerQueryNodes.size !== 0) {
|
|
455
|
+
this._settleContainerQueries(() => {
|
|
456
|
+
size = measure();
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// A cap the content decides is its natural size, never below a floor
|
|
461
|
+
// that was named as a number: `WM_NORMAL_HINTS` with a min above its own
|
|
462
|
+
// max is a struct no window manager can honour.
|
|
463
|
+
if (isContentBound(props.maxWidth)) {
|
|
464
|
+
hints.maxWidth = Math.max(size.naturalW, minW ?? 0);
|
|
465
|
+
}
|
|
466
|
+
if (isContentBound(props.maxHeight)) {
|
|
467
|
+
hints.maxHeight = Math.max(size.naturalH, minH ?? 0);
|
|
468
|
+
}
|
|
469
|
+
// Last, because it is a height *for a width*: the width the window is
|
|
470
|
+
// about to have where the width is still ours to choose, and the one it
|
|
471
|
+
// has where it is not.
|
|
472
|
+
const forWidth =
|
|
473
|
+
autoW && !this._userSized
|
|
474
|
+
? size.width
|
|
475
|
+
: (this.window?.width ?? size.width);
|
|
476
|
+
this._finishHeightFloor(hints, props, forWidth, availH);
|
|
477
|
+
return { width: size.width, height: size.height, hints };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The `minHeight="auto"` floor, measured for the width just settled. */
|
|
481
|
+
_finishHeightFloor(hints, props, forWidth, availH) {
|
|
482
|
+
if (!isContentBound(props.minHeight)) return;
|
|
483
|
+
hints.minHeight = clampBound(
|
|
484
|
+
this._measureMinimum('height', forWidth),
|
|
485
|
+
availH,
|
|
486
|
+
);
|
|
487
|
+
if (isContentBound(props.maxHeight)) {
|
|
488
|
+
hints.maxHeight = Math.max(hints.maxHeight ?? 0, hints.minHeight);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Keep an `'auto'` window the size of its content while it still owns its
|
|
494
|
+
* own size. Called from `flush()` on any frame that lays out, which is
|
|
495
|
+
* every frame where the natural size could have moved.
|
|
496
|
+
*
|
|
497
|
+
* One rule covers both kinds of window, which is why it is a rule and not
|
|
498
|
+
* two behaviours: **auto tracks the content until something else sets the
|
|
499
|
+
* size.** A `<window>` grows as rows are added to it and stops the moment
|
|
500
|
+
* the user drags an edge — GTK's behaviour, and right for the same reason:
|
|
501
|
+
* the size is the app's opinion until it is the user's. Nothing can ever
|
|
502
|
+
* take a `<popup>`'s size over — it is override-redirect and has no
|
|
503
|
+
* resize handles — so a menu tracks its items for good.
|
|
504
|
+
*
|
|
505
|
+
* The result is applied through the window rather than through props: an
|
|
506
|
+
* auto size is not something React said, so nothing about it should read
|
|
507
|
+
* as a prop change or wait for one.
|
|
508
|
+
*
|
|
509
|
+
* A **bound** the content decides is not covered by that rule and outlives
|
|
510
|
+
* it: `minWidth="auto"` still means the same thing after the user has
|
|
511
|
+
* taken the size over — it is what stops them taking it *too far* — and it
|
|
512
|
+
* means it on a window with a `width` of its own, which never tracked
|
|
513
|
+
* anything. So the floor is re-measured on every frame that lays out, and
|
|
514
|
+
* the size only while it is still the window's to choose.
|
|
515
|
+
*/
|
|
516
|
+
_refit() {
|
|
517
|
+
if (this.destroyed) return;
|
|
518
|
+
const wnd = this.window;
|
|
519
|
+
if (!wnd) return;
|
|
520
|
+
const props = this.props;
|
|
521
|
+
const tracking =
|
|
522
|
+
!this._userSized && (isAutoSize(props.width) || isAutoSize(props.height));
|
|
523
|
+
const bounded = CONTENT_BOUND_PROPS.some((key) =>
|
|
524
|
+
isContentBound(props[key]),
|
|
525
|
+
);
|
|
526
|
+
if (!tracking && !bounded) return;
|
|
527
|
+
const asked = this._requestedSize;
|
|
528
|
+
const next = this._measure();
|
|
529
|
+
this._sendSizeHints(props, next.hints);
|
|
530
|
+
if (!tracking) return;
|
|
531
|
+
if (asked && next.width === asked.width && next.height === asked.height) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this._requestedSize = { width: next.width, height: next.height };
|
|
535
|
+
// Asked for, not assumed. `window.width` stays what the server last said
|
|
536
|
+
// until the ConfigureNotify lands, and this frame lays out against that
|
|
537
|
+
// — the echo brings `needsLayout` and an unbounded repaint with it (see
|
|
538
|
+
// the 'resize' listener), which is the same one-frame settle a
|
|
539
|
+
// controlled `width` prop change has always had. Writing the new size
|
|
540
|
+
// onto the window here would be worse than the wait: ntk allocates the
|
|
541
|
+
// backing pixmap from the resize event, so a frame painted at a size the
|
|
542
|
+
// pixmap has not reached yet is a frame clipped to the old one.
|
|
543
|
+
if (typeof wnd.setState === 'function') {
|
|
544
|
+
wnd.setState({ width: next.width, height: next.height });
|
|
545
|
+
} else {
|
|
546
|
+
wnd.resize?.(next.width, next.height);
|
|
547
|
+
}
|
|
548
|
+
// A window that grew is a window whose *placement* moved with it, and
|
|
549
|
+
// the anchored ones have to be told: a completion list that gains a row
|
|
550
|
+
// near the bottom of the screen is one that now flips above the caret.
|
|
551
|
+
// From `next` rather than from the window, which is still the size the
|
|
552
|
+
// server last confirmed.
|
|
553
|
+
this._followAnchor({ width: next.width, height: next.height });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** One layout pass at the window's size, with the content floors it
|
|
557
|
+
* needs: fresh ones when something changed them, the ones in hand during
|
|
558
|
+
* a live resize, none when nothing moved them. */
|
|
559
|
+
_layoutStep(width, height) {
|
|
560
|
+
// whether this step keeps the content floors: a live resize lays out
|
|
561
|
+
// against the ones in hand, and the layout hosts' children follow suit
|
|
562
|
+
let floors = true;
|
|
563
|
+
if (!this._floorsDirty && this._floorsWidth === width) {
|
|
564
|
+
this._layoutRoot(width, height);
|
|
565
|
+
} else if (this._deferContentFloors(width)) {
|
|
566
|
+
this._scheduleFloorsCatchUp();
|
|
567
|
+
this._layoutRoot(width, height);
|
|
568
|
+
floors = false;
|
|
569
|
+
} else {
|
|
570
|
+
this._applyContentFloors(width, height);
|
|
571
|
+
}
|
|
572
|
+
// The layout hosts' final calls, now the pass has given each its box —
|
|
573
|
+
// before anything reads a child's size (the container queries settle on
|
|
574
|
+
// what this leaves) — and, if an algorithm threw, the same step again
|
|
575
|
+
// with it turned off, so the frame it threw in is already the flexbox
|
|
576
|
+
// one.
|
|
577
|
+
if (this._layoutHosts.size !== 0) {
|
|
578
|
+
// A child's height floors are measured after the pass, at the width it
|
|
579
|
+
// was placed at, and off the pixel grid (`measuringExactly`), which
|
|
580
|
+
// leaves yoga holding a tree laid out under a grid it no longer has:
|
|
581
|
+
// the next pass would lay the whole of it out again, a scroll's
|
|
582
|
+
// included. One more pass now, on the grid, and the frame ends where
|
|
583
|
+
// every other frame does.
|
|
584
|
+
if (this._placeLayoutHosts(floors)) {
|
|
585
|
+
this._layoutRoot(width, height);
|
|
586
|
+
this._placeLayoutHosts(false);
|
|
587
|
+
}
|
|
588
|
+
if (this._abandonFailedHosts()) this._layoutStep(width, height);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|