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
package/src/grid.js
ADDED
|
@@ -0,0 +1,1653 @@
|
|
|
1
|
+
// CSS grid, built in (docs/styling.md "Grid"; the design record is
|
|
2
|
+
// docs/architecture/grid-layout.md). A box with `display: 'grid'` — or
|
|
3
|
+
// `layout: 'grid'`, the same request under the name every layout goes by —
|
|
4
|
+
// arranges its children by CSS Grid's placement and track sizing algorithms
|
|
5
|
+
// (css-grid-2 §8.5 and §12), inside the pass that lays the window out. It is
|
|
6
|
+
// a layout on the seam (nodes/layouthost.js), written against the contract a
|
|
7
|
+
// registered one is handed; what makes it CSS's is the style, which is CSS's
|
|
8
|
+
// own and flat — the box says `gridTemplateColumns: 'auto 1fr'`, a child
|
|
9
|
+
// says `gridColumn: '1 / -1'` — so a grid copied from the web lays out here
|
|
10
|
+
// as it was written.
|
|
11
|
+
//
|
|
12
|
+
// Pure: strings and sizes in, rects out. The parsers are memoized on the
|
|
13
|
+
// value written, and styles.js runs them to validate a style, so a track
|
|
14
|
+
// list that would not lay out is an error where it was written rather than a
|
|
15
|
+
// box that fell back to flexbox.
|
|
16
|
+
//
|
|
17
|
+
// Left out, on purpose: subgrid (a child's tree is its own on this seam, so
|
|
18
|
+
// its items cannot join the parent's track sizing), baselines, named lines,
|
|
19
|
+
// and the second column pass of §12.1 step 3 — see the design record.
|
|
20
|
+
|
|
21
|
+
const EPS = 1e-6;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The furthest a line number, a span or a repeat count reaches. CSS lets a
|
|
25
|
+
* UA clamp the grid, and asks that lines within ±10000 be kept, so a list
|
|
26
|
+
* placing its rows by number stays exact while a typo like
|
|
27
|
+
* `gridColumn: 1e9` costs a wide grid rather than the process.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_TRACKS = 10000;
|
|
30
|
+
|
|
31
|
+
// --- values -------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const AUTO = Object.freeze({ kind: 'auto' });
|
|
34
|
+
const MIN_CONTENT = Object.freeze({ kind: 'min-content' });
|
|
35
|
+
const MAX_CONTENT = Object.freeze({ kind: 'max-content' });
|
|
36
|
+
const ZERO = Object.freeze({ kind: 'fixed', px: 0 });
|
|
37
|
+
const track = (min, max, fit = null) => Object.freeze({ min, max, fit });
|
|
38
|
+
|
|
39
|
+
/** `minmax(0, 1fr)`: a share of the room that ignores what is in it — what a
|
|
40
|
+
* bare count of tracks means, as it does in Tailwind's `grid-cols-3`. */
|
|
41
|
+
const EQUAL = track(ZERO, Object.freeze({ kind: 'fr', f: 1 }));
|
|
42
|
+
const AUTO_TRACK = track(AUTO, AUTO);
|
|
43
|
+
|
|
44
|
+
/** An auto-fit track nothing landed in: no size, and no gap either side. */
|
|
45
|
+
const COLLAPSED = Object.freeze({
|
|
46
|
+
min: ZERO,
|
|
47
|
+
max: ZERO,
|
|
48
|
+
fit: null,
|
|
49
|
+
collapsed: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const NO_TRACKS = Object.freeze({ tracks: Object.freeze([]), repeat: null });
|
|
53
|
+
const AUTO_TRACKS = Object.freeze([AUTO_TRACK]);
|
|
54
|
+
|
|
55
|
+
/** A side of a placement that names neither a line nor a span. */
|
|
56
|
+
const AUTO_LINE = Object.freeze({});
|
|
57
|
+
|
|
58
|
+
const FLOWS = Object.freeze({
|
|
59
|
+
row: { column: false, dense: false },
|
|
60
|
+
column: { column: true, dense: false },
|
|
61
|
+
dense: { column: false, dense: true },
|
|
62
|
+
'row dense': { column: false, dense: true },
|
|
63
|
+
'dense row': { column: false, dense: true },
|
|
64
|
+
'column dense': { column: true, dense: true },
|
|
65
|
+
'dense column': { column: true, dense: true },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const ITEM_ALIGNMENTS = ['flex-start', 'center', 'flex-end', 'stretch'];
|
|
69
|
+
const SELF_ALIGNMENTS = ['auto', ...ITEM_ALIGNMENTS];
|
|
70
|
+
|
|
71
|
+
/** The properties a grid reads off the box that yoga never sees — a change
|
|
72
|
+
* to one asks the layout again (nodes/animation.js `_retarget`). */
|
|
73
|
+
export const GRID_CONTAINER_PROPS = Object.freeze([
|
|
74
|
+
'gridTemplateColumns',
|
|
75
|
+
'gridTemplateRows',
|
|
76
|
+
'gridTemplateAreas',
|
|
77
|
+
'gridAutoColumns',
|
|
78
|
+
'gridAutoRows',
|
|
79
|
+
'gridAutoFlow',
|
|
80
|
+
'justifyItems',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** …and off each child. */
|
|
84
|
+
export const GRID_ITEM_PROPS = Object.freeze([
|
|
85
|
+
'gridColumn',
|
|
86
|
+
'gridRow',
|
|
87
|
+
'gridArea',
|
|
88
|
+
'justifySelf',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// --- parsing ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
const parsed = new Map();
|
|
94
|
+
|
|
95
|
+
/** A parse, remembered by the value written. Bounded: a transition over a
|
|
96
|
+
* track list writes a new string every frame. */
|
|
97
|
+
function memo(key, make) {
|
|
98
|
+
let value = parsed.get(key);
|
|
99
|
+
if (value === undefined) {
|
|
100
|
+
if (parsed.size >= 512) parsed.clear();
|
|
101
|
+
value = make();
|
|
102
|
+
parsed.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function invalid(prop, value, why) {
|
|
108
|
+
return new SyntaxError(
|
|
109
|
+
`react-x11: invalid ${prop} ${JSON.stringify(value)} — ${why}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isFixedBreadth = (b) => b.kind === 'fixed' || b.kind === 'percent';
|
|
114
|
+
|
|
115
|
+
/** CSS's `<fixed-size>`: a track whose size can be counted before anything
|
|
116
|
+
* is in it, which is what an auto repeat is made of. */
|
|
117
|
+
const isFixedSize = (t) =>
|
|
118
|
+
t.fit === null && (isFixedBreadth(t.min) || isFixedBreadth(t.max));
|
|
119
|
+
|
|
120
|
+
const LENGTH = /^(\d+(?:\.\d*)?|\.\d+)([a-z%]*)/i;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A CSS track list: sizes, `minmax()`, `fit-content()` and — in a template —
|
|
124
|
+
* `repeat()` with a count, `auto-fill` or `auto-fit`. Lengths are logical
|
|
125
|
+
* pixels and come out in device pixels (`scale`), the way every length in a
|
|
126
|
+
* style arrives at layout.
|
|
127
|
+
*/
|
|
128
|
+
function parseTrackList(src, scale, prop, template) {
|
|
129
|
+
let i = 0;
|
|
130
|
+
const n = src.length;
|
|
131
|
+
const fail = (why) => {
|
|
132
|
+
throw invalid(prop, src, why);
|
|
133
|
+
};
|
|
134
|
+
const space = () => {
|
|
135
|
+
while (i < n && /\s/.test(src[i])) i++;
|
|
136
|
+
};
|
|
137
|
+
const word = () => {
|
|
138
|
+
space();
|
|
139
|
+
const m = /^[a-z][a-z-]*/i.exec(src.slice(i));
|
|
140
|
+
if (m === null) return null;
|
|
141
|
+
i += m[0].length;
|
|
142
|
+
return m[0].toLowerCase();
|
|
143
|
+
};
|
|
144
|
+
const expect = (ch) => {
|
|
145
|
+
space();
|
|
146
|
+
if (src[i] !== ch) {
|
|
147
|
+
fail(
|
|
148
|
+
i < n
|
|
149
|
+
? `expected "${ch}" before "${src.slice(i)}"`
|
|
150
|
+
: `expected "${ch}" at the end`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
i++;
|
|
154
|
+
};
|
|
155
|
+
const breadth = () => {
|
|
156
|
+
space();
|
|
157
|
+
if (src[i] === '[') fail('named lines ("[name]") are not supported');
|
|
158
|
+
if (src[i] === '-') fail('a track size is never negative');
|
|
159
|
+
const m = LENGTH.exec(src.slice(i));
|
|
160
|
+
if (m !== null) {
|
|
161
|
+
i += m[0].length;
|
|
162
|
+
const v = Number(m[1]);
|
|
163
|
+
const unit = m[2].toLowerCase();
|
|
164
|
+
if (unit === 'px') return { kind: 'fixed', px: v * scale };
|
|
165
|
+
if (unit === 'fr') return { kind: 'fr', f: v };
|
|
166
|
+
if (unit === '%') return { kind: 'percent', f: v / 100 };
|
|
167
|
+
if (unit === '' && v === 0) return ZERO;
|
|
168
|
+
if (unit === '') {
|
|
169
|
+
fail(
|
|
170
|
+
`a length in a track list is written with its unit, "${m[1]}px"` +
|
|
171
|
+
(template
|
|
172
|
+
? ` — a number on its own is a count of equal tracks, ${prop}: ${m[1]}`
|
|
173
|
+
: ''),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
fail(`"${unit}" is not a unit a track takes — px, % and fr are`);
|
|
177
|
+
}
|
|
178
|
+
const w = word();
|
|
179
|
+
if (w === 'auto') return AUTO;
|
|
180
|
+
if (w === 'min-content') return MIN_CONTENT;
|
|
181
|
+
if (w === 'max-content') return MAX_CONTENT;
|
|
182
|
+
if (w === 'subgrid') {
|
|
183
|
+
fail('subgrid is not supported: a grid item lays its own children out');
|
|
184
|
+
}
|
|
185
|
+
return fail(
|
|
186
|
+
w === null
|
|
187
|
+
? `expected a track size before "${src.slice(i)}"`
|
|
188
|
+
: `"${w}" is not a track size`,
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
const size = () => {
|
|
192
|
+
const at = i;
|
|
193
|
+
const w = word();
|
|
194
|
+
if (w === 'minmax') {
|
|
195
|
+
expect('(');
|
|
196
|
+
const min = breadth();
|
|
197
|
+
if (min.kind === 'fr') {
|
|
198
|
+
fail(
|
|
199
|
+
'minmax() cannot take a flexible minimum — minmax(0, 1fr) is the usual one',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
expect(',');
|
|
203
|
+
const max = breadth();
|
|
204
|
+
expect(')');
|
|
205
|
+
return track(min, max);
|
|
206
|
+
}
|
|
207
|
+
if (w === 'fit-content') {
|
|
208
|
+
expect('(');
|
|
209
|
+
const limit = breadth();
|
|
210
|
+
if (!isFixedBreadth(limit)) {
|
|
211
|
+
fail('fit-content() takes a length or a percentage');
|
|
212
|
+
}
|
|
213
|
+
expect(')');
|
|
214
|
+
return track(AUTO, MAX_CONTENT, limit);
|
|
215
|
+
}
|
|
216
|
+
i = at;
|
|
217
|
+
const b = breadth();
|
|
218
|
+
// `1fr` is `minmax(auto, 1fr)`: a share of the room, but never narrower
|
|
219
|
+
// than what is in it
|
|
220
|
+
return b.kind === 'fr' ? track(AUTO, b) : track(b, b);
|
|
221
|
+
};
|
|
222
|
+
const tracks = [];
|
|
223
|
+
let repeat = null;
|
|
224
|
+
for (;;) {
|
|
225
|
+
space();
|
|
226
|
+
if (i >= n) break;
|
|
227
|
+
const at = i;
|
|
228
|
+
if (word() !== 'repeat') {
|
|
229
|
+
i = at;
|
|
230
|
+
tracks.push(size());
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!template) {
|
|
234
|
+
fail(
|
|
235
|
+
'repeat() belongs in a template — an implicit track list is its sizes, taken in turn',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
expect('(');
|
|
239
|
+
space();
|
|
240
|
+
let times;
|
|
241
|
+
const m = /^\d+/.exec(src.slice(i));
|
|
242
|
+
if (m !== null) {
|
|
243
|
+
i += m[0].length;
|
|
244
|
+
times = Math.min(Number(m[0]), MAX_TRACKS);
|
|
245
|
+
if (times < 1) fail('repeat() repeats its tracks at least once');
|
|
246
|
+
} else {
|
|
247
|
+
times = word();
|
|
248
|
+
if (times !== 'auto-fill' && times !== 'auto-fit') {
|
|
249
|
+
fail('repeat() takes a count, auto-fill or auto-fit first');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
expect(',');
|
|
253
|
+
const body = [];
|
|
254
|
+
for (;;) {
|
|
255
|
+
space();
|
|
256
|
+
if (i >= n || src[i] === ')') break;
|
|
257
|
+
body.push(size());
|
|
258
|
+
}
|
|
259
|
+
expect(')');
|
|
260
|
+
if (body.length === 0) fail('repeat() has no tracks to repeat');
|
|
261
|
+
if (typeof times === 'number') {
|
|
262
|
+
for (let k = 0; k < times && tracks.length < MAX_TRACKS; k++) {
|
|
263
|
+
for (const t of body) tracks.push(t);
|
|
264
|
+
}
|
|
265
|
+
} else if (repeat !== null) {
|
|
266
|
+
fail('a track list takes one repeat(auto-fill) or repeat(auto-fit)');
|
|
267
|
+
} else if (!body.every(isFixedSize)) {
|
|
268
|
+
fail(
|
|
269
|
+
`repeat(${times}, …) repeats tracks whose size it can count — ` +
|
|
270
|
+
'minmax(200px, 1fr) or 120px, not 1fr or auto',
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
repeat = { at: tracks.length, tracks: body, fit: times === 'auto-fit' };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (repeat !== null && !tracks.every(isFixedSize)) {
|
|
277
|
+
fail(
|
|
278
|
+
`beside repeat(${repeat.fit ? 'auto-fit' : 'auto-fill'}, …) every ` +
|
|
279
|
+
'track has a size it can count, so the room left for the repeats is known',
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (!template && tracks.length === 0) {
|
|
283
|
+
fail('an implicit track list names at least one size');
|
|
284
|
+
}
|
|
285
|
+
return { tracks, repeat };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* `gridTemplateColumns` / `gridTemplateRows`: a CSS track list, or a number
|
|
290
|
+
* — that many equal tracks, `repeat(n, minmax(0, 1fr))`, the way a count
|
|
291
|
+
* reads everywhere grid is written short (Tailwind's `grid-cols-3`). A count
|
|
292
|
+
* rather than a length because a track list is not a length: `maxLines: 3`
|
|
293
|
+
* and `flex: 1` are numbers that are not pixels either.
|
|
294
|
+
*/
|
|
295
|
+
export function templateTracks(value, scale = 1, prop = 'gridTemplateColumns') {
|
|
296
|
+
if (value == null || value === 'none') return NO_TRACKS;
|
|
297
|
+
if (typeof value === 'number') {
|
|
298
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
299
|
+
throw invalid(
|
|
300
|
+
prop,
|
|
301
|
+
value,
|
|
302
|
+
'a number is a count of equal tracks, a whole number from 1',
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return memo(`#${value}`, () => ({
|
|
306
|
+
tracks: Object.freeze(Array(Math.min(value, MAX_TRACKS)).fill(EQUAL)),
|
|
307
|
+
repeat: null,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
if (typeof value !== 'string') {
|
|
311
|
+
throw invalid(
|
|
312
|
+
prop,
|
|
313
|
+
value,
|
|
314
|
+
'expected a track list like "200px 1fr", or a count of equal tracks',
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return memo(`t${scale}|${value}`, () =>
|
|
318
|
+
parseTrackList(value, scale, prop, true),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `gridAutoColumns` / `gridAutoRows`: the sizes an implicit track takes, in
|
|
324
|
+
* turn — a track list without `repeat()`, or a number, which here is a
|
|
325
|
+
* length: an implicit track has a size and never a count.
|
|
326
|
+
*/
|
|
327
|
+
export function autoTracks(value, scale = 1, prop = 'gridAutoRows') {
|
|
328
|
+
if (value == null) return AUTO_TRACKS;
|
|
329
|
+
if (typeof value === 'number') {
|
|
330
|
+
if (!(value >= 0) || !Number.isFinite(value)) {
|
|
331
|
+
throw invalid(prop, value, 'a number is a length, 0 or more');
|
|
332
|
+
}
|
|
333
|
+
return memo(`a${scale}#${value}`, () => {
|
|
334
|
+
const px = { kind: 'fixed', px: value * scale };
|
|
335
|
+
return Object.freeze([track(px, px)]);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (typeof value !== 'string') {
|
|
339
|
+
throw invalid(
|
|
340
|
+
prop,
|
|
341
|
+
value,
|
|
342
|
+
'expected a track size like "120px" or "minmax(40px, auto)"',
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return memo(
|
|
346
|
+
`a${scale}|${value}`,
|
|
347
|
+
() => parseTrackList(value, scale, prop, false).tracks,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const AREA_NAME = /^[\w\--]+$/;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* `gridTemplateAreas`: CSS's one quoted string per row, or — which reads
|
|
355
|
+
* better in a style object — an array of the rows. Every row has as many
|
|
356
|
+
* cells, a run of dots is an empty one, and every name covers a rectangle.
|
|
357
|
+
*/
|
|
358
|
+
export function templateAreas(value, prop = 'gridTemplateAreas') {
|
|
359
|
+
if (value == null || value === 'none') return null;
|
|
360
|
+
if (Array.isArray(value)) {
|
|
361
|
+
if (!value.every((row) => typeof row === 'string')) {
|
|
362
|
+
throw invalid(prop, value, 'an array of areas is one string per row');
|
|
363
|
+
}
|
|
364
|
+
return memo(`r|${value.join('\n')}`, () => parseAreas(value, prop, value));
|
|
365
|
+
}
|
|
366
|
+
if (typeof value !== 'string') {
|
|
367
|
+
throw invalid(
|
|
368
|
+
prop,
|
|
369
|
+
value,
|
|
370
|
+
`expected one quoted string per row — '"head head" "side main"' — or an array of rows`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return memo(`s|${value}`, () =>
|
|
374
|
+
parseAreas(quotedRows(value, prop), prop, value),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function quotedRows(src, prop) {
|
|
379
|
+
const rows = [];
|
|
380
|
+
const quoted = /\s*(?:"([^"]*)"|'([^']*)')\s*/y;
|
|
381
|
+
let i = 0;
|
|
382
|
+
while (i < src.length) {
|
|
383
|
+
quoted.lastIndex = i;
|
|
384
|
+
const m = quoted.exec(src);
|
|
385
|
+
if (m === null) {
|
|
386
|
+
if (/^\s*$/.test(src.slice(i))) break;
|
|
387
|
+
throw invalid(
|
|
388
|
+
prop,
|
|
389
|
+
src,
|
|
390
|
+
`each row is a quoted string — '"head head" "side main"' — or pass an array of rows`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
rows.push(m[1] ?? m[2]);
|
|
394
|
+
i = quoted.lastIndex;
|
|
395
|
+
}
|
|
396
|
+
return rows;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseAreas(rows, prop, written) {
|
|
400
|
+
if (rows.length === 0) throw invalid(prop, written, 'it names no rows');
|
|
401
|
+
const grid = rows.map((row, r) => {
|
|
402
|
+
const cells = row.match(/\.+|[^\s.]+/g) ?? [];
|
|
403
|
+
if (cells.length === 0) {
|
|
404
|
+
throw invalid(prop, written, `row ${r + 1} names no cells`);
|
|
405
|
+
}
|
|
406
|
+
for (const cell of cells) {
|
|
407
|
+
if (cell[0] !== '.' && !AREA_NAME.test(cell)) {
|
|
408
|
+
throw invalid(prop, written, `"${cell}" is not an area name`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return cells;
|
|
412
|
+
});
|
|
413
|
+
const cols = grid[0].length;
|
|
414
|
+
grid.forEach((cells, r) => {
|
|
415
|
+
if (cells.length !== cols) {
|
|
416
|
+
throw invalid(
|
|
417
|
+
prop,
|
|
418
|
+
written,
|
|
419
|
+
`every row has as many cells as the first — row 1 has ${cols}, ` +
|
|
420
|
+
`row ${r + 1} has ${cells.length}`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
const names = new Map();
|
|
425
|
+
grid.forEach((cells, r) =>
|
|
426
|
+
cells.forEach((cell, c) => {
|
|
427
|
+
if (cell[0] === '.') return;
|
|
428
|
+
const a = names.get(cell);
|
|
429
|
+
if (a === undefined) {
|
|
430
|
+
names.set(cell, { r0: r, r1: r + 1, c0: c, c1: c + 1, cells: 1 });
|
|
431
|
+
} else {
|
|
432
|
+
a.r0 = Math.min(a.r0, r);
|
|
433
|
+
a.r1 = Math.max(a.r1, r + 1);
|
|
434
|
+
a.c0 = Math.min(a.c0, c);
|
|
435
|
+
a.c1 = Math.max(a.c1, c + 1);
|
|
436
|
+
a.cells++;
|
|
437
|
+
}
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
for (const [name, a] of names) {
|
|
441
|
+
// every cell in the bounding box is this name's, or there are fewer of
|
|
442
|
+
// them than the box has cells
|
|
443
|
+
if (a.cells !== (a.r1 - a.r0) * (a.c1 - a.c0)) {
|
|
444
|
+
throw invalid(prop, written, `area "${name}" is not a rectangle`);
|
|
445
|
+
}
|
|
446
|
+
// the placement it stands for, made once
|
|
447
|
+
a.rows = Object.freeze({
|
|
448
|
+
start: { line: a.r0 + 1 },
|
|
449
|
+
end: { line: a.r1 + 1 },
|
|
450
|
+
});
|
|
451
|
+
a.columns = Object.freeze({
|
|
452
|
+
start: { line: a.c0 + 1 },
|
|
453
|
+
end: { line: a.c1 + 1 },
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return { names, rows: grid.length, cols };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const clampLine = (line) => Math.max(-MAX_TRACKS, Math.min(MAX_TRACKS, line));
|
|
460
|
+
|
|
461
|
+
/** One side of a placement: a line number, `span <n>`, or `auto`. */
|
|
462
|
+
function gridLine(text, prop, value) {
|
|
463
|
+
const t = text.trim();
|
|
464
|
+
if (t === '') throw invalid(prop, value, 'one side of the "/" is empty');
|
|
465
|
+
if (t === 'auto') return AUTO_LINE;
|
|
466
|
+
const words = t.split(/\s+/);
|
|
467
|
+
if (words.length === 1) {
|
|
468
|
+
if (/^[+-]?\d+$/.test(t)) {
|
|
469
|
+
const line = Number(t);
|
|
470
|
+
if (line === 0) {
|
|
471
|
+
throw invalid(
|
|
472
|
+
prop,
|
|
473
|
+
value,
|
|
474
|
+
'there is no line 0 — lines count from 1 at the start, and from -1 at the end',
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return { line: clampLine(line) };
|
|
478
|
+
}
|
|
479
|
+
if (t === 'span') {
|
|
480
|
+
throw invalid(prop, value, '"span" needs a number of tracks — span 2');
|
|
481
|
+
}
|
|
482
|
+
throw invalid(
|
|
483
|
+
prop,
|
|
484
|
+
value,
|
|
485
|
+
`"${t}" would name a line, and named lines are not supported — name ` +
|
|
486
|
+
"an area in the grid's gridTemplateAreas and place this with gridArea",
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (words.length === 2 && (words[0] === 'span' || words[1] === 'span')) {
|
|
490
|
+
const count = words[0] === 'span' ? words[1] : words[0];
|
|
491
|
+
if (!/^\d+$/.test(count) || Number(count) < 1) {
|
|
492
|
+
throw invalid(
|
|
493
|
+
prop,
|
|
494
|
+
value,
|
|
495
|
+
'span takes a whole number of tracks, 1 or more — span 2',
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return { span: Math.min(Number(count), MAX_TRACKS) };
|
|
499
|
+
}
|
|
500
|
+
throw invalid(
|
|
501
|
+
prop,
|
|
502
|
+
value,
|
|
503
|
+
`"${t}" is not a grid line — a line number, "span 2" or "auto"`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* `gridColumn` / `gridRow`: CSS's shorthand, `start / end`. A number is a
|
|
509
|
+
* line, as it is in CSS (and in React DOM, which never gives it `px`):
|
|
510
|
+
* counting from 1 at the start and from -1 at the end, so `'1 / -1'` is the
|
|
511
|
+
* whole width however many columns there are.
|
|
512
|
+
*/
|
|
513
|
+
export function gridLines(value, prop = 'gridColumn') {
|
|
514
|
+
if (typeof value === 'number') {
|
|
515
|
+
if (!Number.isInteger(value) || value === 0) {
|
|
516
|
+
throw invalid(
|
|
517
|
+
prop,
|
|
518
|
+
value,
|
|
519
|
+
'a number is a line: a whole number, counting from 1 at the start or from -1 at the end',
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return memo(`l#${value}`, () =>
|
|
523
|
+
Object.freeze({ start: { line: clampLine(value) }, end: AUTO_LINE }),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (typeof value !== 'string') {
|
|
527
|
+
throw invalid(prop, value, "expected a line like 2, 'span 2' or '1 / -1'");
|
|
528
|
+
}
|
|
529
|
+
return memo(`l|${value}`, () => {
|
|
530
|
+
const sides = value.split('/');
|
|
531
|
+
if (sides.length > 2) {
|
|
532
|
+
throw invalid(
|
|
533
|
+
prop,
|
|
534
|
+
value,
|
|
535
|
+
'it takes a start and an end — "1 / 3" — with one "/"',
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return Object.freeze({
|
|
539
|
+
start: gridLine(sides[0], prop, value),
|
|
540
|
+
end: sides.length === 2 ? gridLine(sides[1], prop, value) : AUTO_LINE,
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const IDENT = /^-?[A-Za-z_-][\w\--]*$/;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* `gridArea`: the name of an area in the grid's `gridTemplateAreas`, or
|
|
549
|
+
* CSS's four lines, `row-start / column-start / row-end / column-end`.
|
|
550
|
+
* `gridColumn` and `gridRow` written beside it win for their axis, the way a
|
|
551
|
+
* longhand wins over the shorthand it refines.
|
|
552
|
+
*/
|
|
553
|
+
export function gridArea(value, prop = 'gridArea') {
|
|
554
|
+
if (typeof value === 'number') {
|
|
555
|
+
return memo(`g#${value}`, () =>
|
|
556
|
+
Object.freeze({ rows: gridLines(value, prop), columns: null }),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (typeof value !== 'string') {
|
|
560
|
+
throw invalid(
|
|
561
|
+
prop,
|
|
562
|
+
value,
|
|
563
|
+
"expected an area's name, or lines — 'row-start / column-start / row-end / column-end'",
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
return memo(`g|${value}`, () => {
|
|
567
|
+
const t = value.trim();
|
|
568
|
+
if (IDENT.test(t) && t !== 'auto' && t !== 'span') {
|
|
569
|
+
return Object.freeze({ name: t });
|
|
570
|
+
}
|
|
571
|
+
const sides = t.split('/');
|
|
572
|
+
if (sides.length > 4) {
|
|
573
|
+
throw invalid(
|
|
574
|
+
prop,
|
|
575
|
+
value,
|
|
576
|
+
'it takes at most four lines — row-start / column-start / row-end / column-end',
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const side = (k) =>
|
|
580
|
+
k < sides.length ? gridLine(sides[k], prop, value) : AUTO_LINE;
|
|
581
|
+
return Object.freeze({
|
|
582
|
+
rows: { start: side(0), end: side(2) },
|
|
583
|
+
columns: { start: side(1), end: side(3) },
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function oneOf(prop, value, allowed) {
|
|
589
|
+
if (!allowed.includes(value)) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`react-x11: invalid ${prop} ${JSON.stringify(value)} (expected one of ${allowed.join(', ')})`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Throw, naming the property and what is wrong, for a grid value that would
|
|
598
|
+
* not lay out — styles.js's validation, which runs in development wherever a
|
|
599
|
+
* style is written. The same parsers the layout runs, so the two cannot
|
|
600
|
+
* disagree about what is valid.
|
|
601
|
+
*/
|
|
602
|
+
export function validateGridValue(prop, value) {
|
|
603
|
+
if (value == null) return;
|
|
604
|
+
switch (prop) {
|
|
605
|
+
case 'gridTemplateColumns':
|
|
606
|
+
case 'gridTemplateRows':
|
|
607
|
+
templateTracks(value, 1, prop);
|
|
608
|
+
break;
|
|
609
|
+
case 'gridAutoColumns':
|
|
610
|
+
case 'gridAutoRows':
|
|
611
|
+
autoTracks(value, 1, prop);
|
|
612
|
+
break;
|
|
613
|
+
case 'gridTemplateAreas':
|
|
614
|
+
templateAreas(value, prop);
|
|
615
|
+
break;
|
|
616
|
+
case 'gridColumn':
|
|
617
|
+
case 'gridRow':
|
|
618
|
+
gridLines(value, prop);
|
|
619
|
+
break;
|
|
620
|
+
case 'gridArea':
|
|
621
|
+
gridArea(value, prop);
|
|
622
|
+
break;
|
|
623
|
+
case 'gridAutoFlow':
|
|
624
|
+
oneOf(prop, value, Object.keys(FLOWS));
|
|
625
|
+
break;
|
|
626
|
+
case 'justifyItems':
|
|
627
|
+
oneOf(prop, value, ITEM_ALIGNMENTS);
|
|
628
|
+
break;
|
|
629
|
+
case 'justifySelf':
|
|
630
|
+
oneOf(prop, value, SELF_ALIGNMENTS);
|
|
631
|
+
break;
|
|
632
|
+
default:
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// --- placement (§8) -------------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
/** Where an item with no definite line in an axis starts, until placed. */
|
|
640
|
+
const AUTO_POS = -0x40000000;
|
|
641
|
+
|
|
642
|
+
const lineIndex = (line, explicit) =>
|
|
643
|
+
line > 0 ? line - 1 : explicit + 1 + line;
|
|
644
|
+
|
|
645
|
+
/** A placement's two sides, as a start and a span (§8.3.1). Negative lines
|
|
646
|
+
* count back from the end of the explicit grid. */
|
|
647
|
+
function resolve(spec, explicit, starts, spans, i) {
|
|
648
|
+
if (spec === null) {
|
|
649
|
+
starts[i] = AUTO_POS;
|
|
650
|
+
spans[i] = 1;
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const a = spec.start.line;
|
|
654
|
+
const b = spec.end.line;
|
|
655
|
+
if (a !== undefined && b !== undefined) {
|
|
656
|
+
let s = lineIndex(a, explicit);
|
|
657
|
+
let e = lineIndex(b, explicit);
|
|
658
|
+
if (s > e) {
|
|
659
|
+
const t = s;
|
|
660
|
+
s = e;
|
|
661
|
+
e = t;
|
|
662
|
+
}
|
|
663
|
+
starts[i] = s;
|
|
664
|
+
// the same line twice is one track from it
|
|
665
|
+
spans[i] = Math.max(1, e - s);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (a !== undefined) {
|
|
669
|
+
starts[i] = lineIndex(a, explicit);
|
|
670
|
+
spans[i] = spec.end.span ?? 1;
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (b !== undefined) {
|
|
674
|
+
const span = spec.start.span ?? 1;
|
|
675
|
+
starts[i] = lineIndex(b, explicit) - span;
|
|
676
|
+
spans[i] = span;
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// no line in this axis: placed automatically, with the span one side names
|
|
680
|
+
// — the start's, when both do, as CSS drops the end's
|
|
681
|
+
starts[i] = AUTO_POS;
|
|
682
|
+
spans[i] = spec.start.span ?? spec.end.span ?? 1;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* CSS's grid item placement algorithm (§8.5), in the frame of the flow: the
|
|
687
|
+
* _major_ axis is the one the flow runs along and grows (rows, for
|
|
688
|
+
* `gridAutoFlow: 'row'`), the _minor_ one is fixed once the definite items
|
|
689
|
+
* say how many tracks it has. A definite line before the first explicit one
|
|
690
|
+
* adds tracks at the start, and the offset they make is returned.
|
|
691
|
+
*/
|
|
692
|
+
function placeItems(n, cS, cN, rS, rN, explicitCols, explicitRows, flow) {
|
|
693
|
+
let cOff = 0;
|
|
694
|
+
let rOff = 0;
|
|
695
|
+
for (let i = 0; i < n; i++) {
|
|
696
|
+
if (cS[i] !== AUTO_POS && -cS[i] > cOff) cOff = -cS[i];
|
|
697
|
+
if (rS[i] !== AUTO_POS && -rS[i] > rOff) rOff = -rS[i];
|
|
698
|
+
}
|
|
699
|
+
if (cOff !== 0 || rOff !== 0) {
|
|
700
|
+
for (let i = 0; i < n; i++) {
|
|
701
|
+
if (cS[i] !== AUTO_POS) cS[i] += cOff;
|
|
702
|
+
if (rS[i] !== AUTO_POS) rS[i] += rOff;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const byColumn = flow.column;
|
|
706
|
+
const mS = byColumn ? cS : rS;
|
|
707
|
+
const mN = byColumn ? cN : rN;
|
|
708
|
+
const kS = byColumn ? rS : cS;
|
|
709
|
+
const kN = byColumn ? rN : cN;
|
|
710
|
+
const occupied = [];
|
|
711
|
+
const filled = [];
|
|
712
|
+
const free = (m, k, ms, ks) => {
|
|
713
|
+
for (let y = m; y < m + ms; y++) {
|
|
714
|
+
const line = occupied[y];
|
|
715
|
+
if (line === undefined) continue;
|
|
716
|
+
for (let x = k; x < k + ks; x++) if (line[x] === 1) return false;
|
|
717
|
+
}
|
|
718
|
+
return true;
|
|
719
|
+
};
|
|
720
|
+
const take = (m, k, ms, ks) => {
|
|
721
|
+
for (let y = m; y < m + ms; y++) {
|
|
722
|
+
const line = (occupied[y] ??= []);
|
|
723
|
+
for (let x = k; x < k + ks; x++) line[x] = 1;
|
|
724
|
+
filled[y] = (filled[y] ?? 0) + ks;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
// 1. what is definite in both axes
|
|
728
|
+
for (let i = 0; i < n; i++) {
|
|
729
|
+
if (mS[i] !== AUTO_POS && kS[i] !== AUTO_POS) {
|
|
730
|
+
take(mS[i], kS[i], mN[i], kN[i]);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// 2. what is locked to a line of the flow: the first place along it that
|
|
734
|
+
// is free (and, sparse, past what this step put there before)
|
|
735
|
+
const behind = new Map();
|
|
736
|
+
for (let i = 0; i < n; i++) {
|
|
737
|
+
if (mS[i] === AUTO_POS || kS[i] !== AUTO_POS) continue;
|
|
738
|
+
const m = mS[i];
|
|
739
|
+
let k = flow.dense ? 0 : (behind.get(m) ?? 0);
|
|
740
|
+
while (!free(m, k, mN[i], kN[i])) k++;
|
|
741
|
+
kS[i] = k;
|
|
742
|
+
take(m, k, mN[i], kN[i]);
|
|
743
|
+
behind.set(m, k + kN[i]);
|
|
744
|
+
}
|
|
745
|
+
// 3. how many tracks the minor axis has: the explicit ones, every definite
|
|
746
|
+
// item's reach, and the widest span still to place
|
|
747
|
+
let minor = byColumn ? explicitRows + rOff : explicitCols + cOff;
|
|
748
|
+
for (let i = 0; i < n; i++) {
|
|
749
|
+
minor = Math.max(minor, (kS[i] === AUTO_POS ? 0 : kS[i]) + kN[i]);
|
|
750
|
+
}
|
|
751
|
+
// 4. everything else, in order, behind a cursor — reset to the start for
|
|
752
|
+
// every item when dense, from the first line with a free cell in it
|
|
753
|
+
let lowest = 0;
|
|
754
|
+
const firstOpen = () => {
|
|
755
|
+
while ((filled[lowest] ?? 0) >= minor) lowest++;
|
|
756
|
+
return lowest;
|
|
757
|
+
};
|
|
758
|
+
let cm = 0;
|
|
759
|
+
let ck = 0;
|
|
760
|
+
for (let i = 0; i < n; i++) {
|
|
761
|
+
if (mS[i] !== AUTO_POS) continue;
|
|
762
|
+
const ms = mN[i];
|
|
763
|
+
const ks = kN[i];
|
|
764
|
+
if (kS[i] !== AUTO_POS) {
|
|
765
|
+
const k = kS[i];
|
|
766
|
+
if (flow.dense) cm = firstOpen();
|
|
767
|
+
else if (k < ck) cm++;
|
|
768
|
+
ck = k;
|
|
769
|
+
while (!free(cm, k, ms, ks)) cm++;
|
|
770
|
+
mS[i] = cm;
|
|
771
|
+
take(cm, k, ms, ks);
|
|
772
|
+
} else {
|
|
773
|
+
if (flow.dense) {
|
|
774
|
+
cm = firstOpen();
|
|
775
|
+
ck = 0;
|
|
776
|
+
}
|
|
777
|
+
for (;;) {
|
|
778
|
+
if (ck + ks > minor) {
|
|
779
|
+
cm++;
|
|
780
|
+
ck = 0;
|
|
781
|
+
} else if (free(cm, ck, ms, ks)) {
|
|
782
|
+
break;
|
|
783
|
+
} else {
|
|
784
|
+
ck++;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
mS[i] = cm;
|
|
788
|
+
kS[i] = ck;
|
|
789
|
+
take(cm, ck, ms, ks);
|
|
790
|
+
ck += ks;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
let major = byColumn ? explicitCols + cOff : explicitRows + rOff;
|
|
794
|
+
for (let i = 0; i < n; i++) major = Math.max(major, mS[i] + mN[i]);
|
|
795
|
+
return byColumn
|
|
796
|
+
? { cols: major, rows: minor, cOff, rOff }
|
|
797
|
+
: { cols: minor, rows: major, cOff, rOff };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const definite = (b, room) =>
|
|
801
|
+
b.kind === 'fixed'
|
|
802
|
+
? b.px
|
|
803
|
+
: b.kind === 'percent'
|
|
804
|
+
? room === null
|
|
805
|
+
? null
|
|
806
|
+
: b.f * room
|
|
807
|
+
: null;
|
|
808
|
+
|
|
809
|
+
const repeated = new WeakMap();
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* The explicit tracks, with an auto repeat written out: as many repetitions
|
|
813
|
+
* as fit in `room` without overflowing it (§7.2.3.2), each track counted at
|
|
814
|
+
* its maximum if that is definite and its minimum otherwise — and one when
|
|
815
|
+
* there is no room to count against.
|
|
816
|
+
*/
|
|
817
|
+
function repeatOut(list, room, gap) {
|
|
818
|
+
const rep = list.repeat;
|
|
819
|
+
if (rep === null) return { tracks: list.tracks, from: 0, to: 0, fit: false };
|
|
820
|
+
const last = repeated.get(list);
|
|
821
|
+
if (last !== undefined && last.room === room && last.gap === gap) {
|
|
822
|
+
return last.out;
|
|
823
|
+
}
|
|
824
|
+
let times = 1;
|
|
825
|
+
if (room !== null) {
|
|
826
|
+
const size = (t) => definite(t.max, room) ?? definite(t.min, room) ?? 0;
|
|
827
|
+
let others = 0;
|
|
828
|
+
for (const t of list.tracks) others += size(t);
|
|
829
|
+
let body = 0;
|
|
830
|
+
for (const t of rep.tracks) body += size(t);
|
|
831
|
+
const per = body + gap * rep.tracks.length;
|
|
832
|
+
if (per > 0) {
|
|
833
|
+
times = Math.floor(
|
|
834
|
+
(room - others - gap * (list.tracks.length - 1) + EPS) / per,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
times = Math.max(1, Math.min(times, MAX_TRACKS));
|
|
838
|
+
}
|
|
839
|
+
const tracks = list.tracks.slice(0, rep.at);
|
|
840
|
+
for (let k = 0; k < times; k++) for (const t of rep.tracks) tracks.push(t);
|
|
841
|
+
for (let k = rep.at; k < list.tracks.length; k++) tracks.push(list.tracks[k]);
|
|
842
|
+
const out = {
|
|
843
|
+
tracks,
|
|
844
|
+
from: rep.at,
|
|
845
|
+
to: rep.at + times * rep.tracks.length,
|
|
846
|
+
fit: rep.fit,
|
|
847
|
+
};
|
|
848
|
+
repeated.set(list, { room, gap, out });
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* An axis's tracks, explicit and implicit: the template's where it has one,
|
|
854
|
+
* and the implicit sizes in turn around it — forwards after the explicit
|
|
855
|
+
* grid, backwards before it. An auto-fit repeat nothing landed in collapses.
|
|
856
|
+
*/
|
|
857
|
+
function axisTracks(count, offset, explicit, auto, starts, spans, n) {
|
|
858
|
+
const list = explicit.tracks;
|
|
859
|
+
let used = null;
|
|
860
|
+
if (explicit.fit) {
|
|
861
|
+
used = new Uint8Array(count);
|
|
862
|
+
for (let i = 0; i < n; i++) {
|
|
863
|
+
for (let k = starts[i]; k < starts[i] + spans[i]; k++) used[k] = 1;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const a = auto.length;
|
|
867
|
+
const tracks = new Array(count);
|
|
868
|
+
for (let k = 0; k < count; k++) {
|
|
869
|
+
const e = k - offset;
|
|
870
|
+
let t;
|
|
871
|
+
if (e >= 0 && e < list.length) t = list[e];
|
|
872
|
+
else if (e >= 0) t = auto[(e - list.length) % a];
|
|
873
|
+
else t = auto[(a - (-e % a)) % a];
|
|
874
|
+
if (used !== null && e >= explicit.from && e < explicit.to && !used[k]) {
|
|
875
|
+
t = COLLAPSED;
|
|
876
|
+
}
|
|
877
|
+
tracks[k] = t;
|
|
878
|
+
}
|
|
879
|
+
return tracks;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// --- track sizing (§12) -----------------------------------------------------
|
|
883
|
+
|
|
884
|
+
// what a track's minimum and maximum sizing functions are, for the arrays
|
|
885
|
+
const FIXED = 0;
|
|
886
|
+
const AUTO_K = 1;
|
|
887
|
+
const MIN_K = 2;
|
|
888
|
+
const MAX_K = 3;
|
|
889
|
+
const FLEX = 4;
|
|
890
|
+
const FIT = 5;
|
|
891
|
+
|
|
892
|
+
const ALL = () => true;
|
|
893
|
+
|
|
894
|
+
const grow = (limit, v) => (limit === Infinity ? v : Math.max(limit, v));
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* The track sizing algorithm, one axis (§12.3–12.8): base sizes and growth
|
|
898
|
+
* limits from the tracks' own functions, raised by what the items in them
|
|
899
|
+
* contribute — single-track items, then spanning ones in order of span, then
|
|
900
|
+
* the ones that cross a flexible track — then the free space shared out, the
|
|
901
|
+
* `fr` tracks expanded, and the `auto` ones stretched.
|
|
902
|
+
*
|
|
903
|
+
* `ask` is what the items say about this axis — `min(i)` and `max(i)`,
|
|
904
|
+
* their min- and max-content contributions, and `declared(i)`, whether they
|
|
905
|
+
* name a minimum of their own — each asked only if a track needs it, which
|
|
906
|
+
* is what keeps a grid of fixed and `minmax(0, 1fr)` tracks from asking its
|
|
907
|
+
* children anything. `constraint` is `null` for a definite `avail`, or
|
|
908
|
+
* `'min'`/`'max'` for a grid sized to its min- or max-content.
|
|
909
|
+
*/
|
|
910
|
+
function sizeTracks(
|
|
911
|
+
tracks,
|
|
912
|
+
starts,
|
|
913
|
+
spans,
|
|
914
|
+
count,
|
|
915
|
+
ask,
|
|
916
|
+
clips,
|
|
917
|
+
avail,
|
|
918
|
+
gap,
|
|
919
|
+
constraint,
|
|
920
|
+
stretchAuto,
|
|
921
|
+
) {
|
|
922
|
+
const n = tracks.length;
|
|
923
|
+
const base = new Float64Array(n);
|
|
924
|
+
const limit = new Float64Array(n);
|
|
925
|
+
const minK = new Uint8Array(n);
|
|
926
|
+
const maxK = new Uint8Array(n);
|
|
927
|
+
const factor = new Float64Array(n);
|
|
928
|
+
const fixedMax = new Float64Array(n);
|
|
929
|
+
const fitCap = new Float64Array(n).fill(Infinity);
|
|
930
|
+
const dead = new Uint8Array(n);
|
|
931
|
+
let live = 0;
|
|
932
|
+
for (let k = 0; k < n; k++) {
|
|
933
|
+
const t = tracks[k];
|
|
934
|
+
if (t.collapsed) {
|
|
935
|
+
dead[k] = 1;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
live++;
|
|
939
|
+
const lo = definite(t.min, avail);
|
|
940
|
+
if (lo !== null) base[k] = lo;
|
|
941
|
+
// a percentage with nothing to be a percentage of is `auto`
|
|
942
|
+
else if (t.min.kind === 'min-content') minK[k] = MIN_K;
|
|
943
|
+
else if (t.min.kind === 'max-content') minK[k] = MAX_K;
|
|
944
|
+
else minK[k] = AUTO_K;
|
|
945
|
+
if (t.fit !== null) {
|
|
946
|
+
maxK[k] = FIT;
|
|
947
|
+
limit[k] = Infinity;
|
|
948
|
+
const cap = definite(t.fit, avail);
|
|
949
|
+
if (cap !== null) fitCap[k] = cap;
|
|
950
|
+
} else {
|
|
951
|
+
const hi = definite(t.max, avail);
|
|
952
|
+
if (hi !== null) {
|
|
953
|
+
limit[k] = hi;
|
|
954
|
+
fixedMax[k] = hi;
|
|
955
|
+
} else if (t.max.kind === 'fr') {
|
|
956
|
+
maxK[k] = FLEX;
|
|
957
|
+
factor[k] = t.max.f;
|
|
958
|
+
limit[k] = Infinity;
|
|
959
|
+
} else {
|
|
960
|
+
maxK[k] =
|
|
961
|
+
t.max.kind === 'min-content'
|
|
962
|
+
? MIN_K
|
|
963
|
+
: t.max.kind === 'max-content'
|
|
964
|
+
? MAX_K
|
|
965
|
+
: AUTO_K;
|
|
966
|
+
limit[k] = Infinity;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (limit[k] < base[k]) limit[k] = base[k];
|
|
970
|
+
}
|
|
971
|
+
const gaps = gap * Math.max(0, live - 1);
|
|
972
|
+
const intrinsicMax = (k) => maxK[k] !== FIXED && maxK[k] !== FLEX;
|
|
973
|
+
const maxContentMax = (k) =>
|
|
974
|
+
maxK[k] === MAX_K || maxK[k] === AUTO_K || maxK[k] === FIT;
|
|
975
|
+
|
|
976
|
+
// which items go where, and whether CSS's automatic minimum is theirs: a
|
|
977
|
+
// floor of their content where they span an `auto`-minimum track and, if
|
|
978
|
+
// they span more than one, no flexible one — and never for a box that
|
|
979
|
+
// clips what it holds (§6.6)
|
|
980
|
+
const autoMin = new Uint8Array(count);
|
|
981
|
+
const singles = [];
|
|
982
|
+
const spanning = [];
|
|
983
|
+
const flexers = [];
|
|
984
|
+
for (let i = 0; i < count; i++) {
|
|
985
|
+
const s = starts[i];
|
|
986
|
+
const e = s + spans[i];
|
|
987
|
+
let flexed = false;
|
|
988
|
+
let auto = false;
|
|
989
|
+
for (let k = s; k < e; k++) {
|
|
990
|
+
if (maxK[k] === FLEX) flexed = true;
|
|
991
|
+
if (minK[k] === AUTO_K) auto = true;
|
|
992
|
+
}
|
|
993
|
+
if (auto && (spans[i] === 1 || !flexed) && !clips(i)) autoMin[i] = 1;
|
|
994
|
+
if (flexed) flexers.push(i);
|
|
995
|
+
else if (spans[i] === 1) singles.push(i);
|
|
996
|
+
else spanning.push(i);
|
|
997
|
+
}
|
|
998
|
+
// CSS's minimum contribution: the content floor where the automatic
|
|
999
|
+
// minimum applies or a minimum is written down (the floor measured for a
|
|
1000
|
+
// child that says `minWidth` is that minimum), and nothing otherwise
|
|
1001
|
+
const minimum = (i) => (autoMin[i] || ask.declared(i) ? ask.min(i) : 0);
|
|
1002
|
+
// what the spanned tracks' fixed maximums allow, when all of them have one
|
|
1003
|
+
const cap = (i) => {
|
|
1004
|
+
let sum = gap * (spans[i] - 1);
|
|
1005
|
+
for (let k = starts[i], e = k + spans[i]; k < e; k++) {
|
|
1006
|
+
if (maxK[k] === FIXED) sum += fixedMax[k];
|
|
1007
|
+
else if (maxK[k] === FIT && fitCap[k] !== Infinity) sum += fitCap[k];
|
|
1008
|
+
else return Infinity;
|
|
1009
|
+
}
|
|
1010
|
+
return sum;
|
|
1011
|
+
};
|
|
1012
|
+
const limitedMin = (i) => Math.max(Math.min(ask.min(i), cap(i)), minimum(i));
|
|
1013
|
+
const limitedMax = (i) => Math.max(Math.min(ask.max(i), cap(i)), minimum(i));
|
|
1014
|
+
// what an `auto` minimum takes: the min- or max-content contribution when
|
|
1015
|
+
// the grid is being sized to its own, the minimum contribution otherwise
|
|
1016
|
+
const low =
|
|
1017
|
+
constraint === 'min'
|
|
1018
|
+
? limitedMin
|
|
1019
|
+
: constraint === 'max'
|
|
1020
|
+
? limitedMax
|
|
1021
|
+
: minimum;
|
|
1022
|
+
|
|
1023
|
+
// §12.5 step 2: the items in one track, where it is not flexible
|
|
1024
|
+
for (const i of singles) {
|
|
1025
|
+
const k = starts[i];
|
|
1026
|
+
if (minK[k] === MIN_K) base[k] = Math.max(base[k], ask.min(i));
|
|
1027
|
+
else if (minK[k] === MAX_K) base[k] = Math.max(base[k], ask.max(i));
|
|
1028
|
+
else if (minK[k] === AUTO_K) base[k] = Math.max(base[k], low(i));
|
|
1029
|
+
if (maxK[k] === MIN_K) limit[k] = grow(limit[k], ask.min(i));
|
|
1030
|
+
else if (maxK[k] === MAX_K || maxK[k] === AUTO_K) {
|
|
1031
|
+
limit[k] = grow(limit[k], ask.max(i));
|
|
1032
|
+
} else if (maxK[k] === FIT) {
|
|
1033
|
+
limit[k] = grow(limit[k], Math.min(ask.max(i), fitCap[k]));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
for (let k = 0; k < n; k++) if (limit[k] < base[k]) limit[k] = base[k];
|
|
1037
|
+
|
|
1038
|
+
// §12.5.1: share out what a group of items still needs among the tracks
|
|
1039
|
+
// they span — up to each track's limit first, then past it — and take, for
|
|
1040
|
+
// each track, the most any one item asked of it
|
|
1041
|
+
const planned = new Float64Array(n);
|
|
1042
|
+
const incurred = new Float64Array(n);
|
|
1043
|
+
const touched = new Uint8Array(n);
|
|
1044
|
+
const growable = new Uint8Array(n);
|
|
1045
|
+
const fill = (ks, space, weighted, room) => {
|
|
1046
|
+
let open = ks;
|
|
1047
|
+
while (space > EPS && open.length > 0) {
|
|
1048
|
+
let total = 0;
|
|
1049
|
+
for (const k of open) total += weighted ? factor[k] : 1;
|
|
1050
|
+
if (!(total > 0)) {
|
|
1051
|
+
weighted = false;
|
|
1052
|
+
total = open.length;
|
|
1053
|
+
}
|
|
1054
|
+
let given = 0;
|
|
1055
|
+
const next = [];
|
|
1056
|
+
for (const k of open) {
|
|
1057
|
+
const share = (space * (weighted ? factor[k] : 1)) / total;
|
|
1058
|
+
const r = room(k);
|
|
1059
|
+
if (r > share + EPS) {
|
|
1060
|
+
incurred[k] += share;
|
|
1061
|
+
given += share;
|
|
1062
|
+
next.push(k);
|
|
1063
|
+
} else if (r > 0) {
|
|
1064
|
+
incurred[k] += r;
|
|
1065
|
+
given += r;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
space -= given;
|
|
1069
|
+
if (given <= EPS) break;
|
|
1070
|
+
open = next;
|
|
1071
|
+
}
|
|
1072
|
+
return space;
|
|
1073
|
+
};
|
|
1074
|
+
const distribute = (group, toBase, affected, contribution, beyond, flex) => {
|
|
1075
|
+
planned.fill(0);
|
|
1076
|
+
touched.fill(0);
|
|
1077
|
+
const current = (k) =>
|
|
1078
|
+
(toBase || limit[k] === Infinity ? base[k] : limit[k]) + incurred[k];
|
|
1079
|
+
const upTo = toBase
|
|
1080
|
+
? (k) => Math.min(limit[k], fitCap[k]) - current(k)
|
|
1081
|
+
: (k) => (growable[k] || limit[k] === Infinity ? Infinity : 0);
|
|
1082
|
+
// a fit-content() track counts as max-content past its limit only as far
|
|
1083
|
+
// as its argument, and as fixed beyond it
|
|
1084
|
+
const past = (k) => (maxK[k] === FIT ? fitCap[k] - current(k) : Infinity);
|
|
1085
|
+
for (const i of group) {
|
|
1086
|
+
const s = starts[i];
|
|
1087
|
+
const e = s + spans[i];
|
|
1088
|
+
let size = gap * (e - s - 1);
|
|
1089
|
+
const ks = [];
|
|
1090
|
+
for (let k = s; k < e; k++) {
|
|
1091
|
+
size += toBase || limit[k] === Infinity ? base[k] : limit[k];
|
|
1092
|
+
if (!dead[k] && affected(k)) ks.push(k);
|
|
1093
|
+
}
|
|
1094
|
+
if (ks.length === 0) continue;
|
|
1095
|
+
for (const k of ks) {
|
|
1096
|
+
touched[k] = 1;
|
|
1097
|
+
incurred[k] = 0;
|
|
1098
|
+
}
|
|
1099
|
+
let space = contribution(i) - size;
|
|
1100
|
+
if (!(space > EPS)) continue;
|
|
1101
|
+
// flexible tracks share by their factors, when those add up to one
|
|
1102
|
+
let weighted = false;
|
|
1103
|
+
if (flex) {
|
|
1104
|
+
let sum = 0;
|
|
1105
|
+
for (const k of ks) sum += factor[k];
|
|
1106
|
+
weighted = sum >= 1;
|
|
1107
|
+
}
|
|
1108
|
+
space = fill(ks, space, weighted, upTo);
|
|
1109
|
+
if (space > EPS) {
|
|
1110
|
+
let into = ks.filter(beyond);
|
|
1111
|
+
if (into.length === 0) into = ks;
|
|
1112
|
+
space = fill(into, space, weighted, past);
|
|
1113
|
+
if (space > EPS) fill(into, space, weighted, () => Infinity);
|
|
1114
|
+
}
|
|
1115
|
+
for (const k of ks)
|
|
1116
|
+
if (incurred[k] > planned[k]) planned[k] = incurred[k];
|
|
1117
|
+
}
|
|
1118
|
+
for (let k = 0; k < n; k++) {
|
|
1119
|
+
if (touched[k] === 0) continue;
|
|
1120
|
+
if (toBase) base[k] += planned[k];
|
|
1121
|
+
else {
|
|
1122
|
+
const was = limit[k];
|
|
1123
|
+
limit[k] = (was === Infinity ? base[k] : was) + planned[k];
|
|
1124
|
+
if (was === Infinity) growable[k] = 1;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const increase = (group, flex) => {
|
|
1129
|
+
const only = flex
|
|
1130
|
+
? (pred) => (k) => maxK[k] === FLEX && pred(k)
|
|
1131
|
+
: (pred) => pred;
|
|
1132
|
+
distribute(
|
|
1133
|
+
group,
|
|
1134
|
+
true,
|
|
1135
|
+
only((k) => minK[k] !== FIXED),
|
|
1136
|
+
low,
|
|
1137
|
+
intrinsicMax,
|
|
1138
|
+
flex,
|
|
1139
|
+
);
|
|
1140
|
+
distribute(
|
|
1141
|
+
group,
|
|
1142
|
+
true,
|
|
1143
|
+
only((k) => minK[k] === MIN_K || minK[k] === MAX_K),
|
|
1144
|
+
ask.min,
|
|
1145
|
+
intrinsicMax,
|
|
1146
|
+
flex,
|
|
1147
|
+
);
|
|
1148
|
+
distribute(
|
|
1149
|
+
group,
|
|
1150
|
+
true,
|
|
1151
|
+
only(
|
|
1152
|
+
constraint === 'max'
|
|
1153
|
+
? (k) => minK[k] === AUTO_K || minK[k] === MAX_K
|
|
1154
|
+
: (k) => minK[k] === MAX_K,
|
|
1155
|
+
),
|
|
1156
|
+
constraint === 'max' ? limitedMax : ask.max,
|
|
1157
|
+
maxContentMax,
|
|
1158
|
+
flex,
|
|
1159
|
+
);
|
|
1160
|
+
for (let k = 0; k < n; k++) if (limit[k] < base[k]) limit[k] = base[k];
|
|
1161
|
+
// a flexible track's maximum is not intrinsic: nothing below reaches it
|
|
1162
|
+
if (flex) return;
|
|
1163
|
+
// a growth limit going from infinite to finite here is still free to
|
|
1164
|
+
// grow in the next step, and only then (§12.5 step 3)
|
|
1165
|
+
distribute(group, false, intrinsicMax, ask.min, ALL, false);
|
|
1166
|
+
distribute(group, false, maxContentMax, ask.max, ALL, false);
|
|
1167
|
+
growable.fill(0);
|
|
1168
|
+
};
|
|
1169
|
+
// §12.5 step 3: spanning items, the fewest tracks first
|
|
1170
|
+
if (spanning.length > 0) {
|
|
1171
|
+
spanning.sort((a, b) => spans[a] - spans[b]);
|
|
1172
|
+
for (let g = 0; g < spanning.length;) {
|
|
1173
|
+
let e = g + 1;
|
|
1174
|
+
while (e < spanning.length && spans[spanning[e]] === spans[spanning[g]]) {
|
|
1175
|
+
e++;
|
|
1176
|
+
}
|
|
1177
|
+
increase(spanning.slice(g, e), false);
|
|
1178
|
+
g = e;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
// §12.5 step 4: the ones crossing a flexible track, all together, into the
|
|
1182
|
+
// flexible tracks alone
|
|
1183
|
+
if (flexers.length > 0) increase(flexers, true);
|
|
1184
|
+
for (let k = 0; k < n; k++) if (limit[k] === Infinity) limit[k] = base[k];
|
|
1185
|
+
|
|
1186
|
+
const sum = () => {
|
|
1187
|
+
let total = 0;
|
|
1188
|
+
for (let k = 0; k < n; k++) total += base[k];
|
|
1189
|
+
return total;
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// §12.6: the free space, equally, up to each track's growth limit —
|
|
1193
|
+
// all of it, sized to its max-content, and none to its min-content
|
|
1194
|
+
if (constraint === 'max') {
|
|
1195
|
+
for (let k = 0; k < n; k++) base[k] = limit[k];
|
|
1196
|
+
} else if (constraint === null && avail !== null) {
|
|
1197
|
+
let free = avail - sum() - gaps;
|
|
1198
|
+
let open = [];
|
|
1199
|
+
for (let k = 0; k < n; k++) if (limit[k] > base[k] + EPS) open.push(k);
|
|
1200
|
+
while (free > EPS && open.length > 0) {
|
|
1201
|
+
const each = free / open.length;
|
|
1202
|
+
const next = [];
|
|
1203
|
+
for (const k of open) {
|
|
1204
|
+
const room = limit[k] - base[k];
|
|
1205
|
+
if (room > each + EPS) {
|
|
1206
|
+
base[k] += each;
|
|
1207
|
+
free -= each;
|
|
1208
|
+
next.push(k);
|
|
1209
|
+
} else {
|
|
1210
|
+
base[k] = limit[k];
|
|
1211
|
+
free -= room;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
open = next;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// §12.7: the flexible tracks, at the size of an fr
|
|
1219
|
+
const flexed = [];
|
|
1220
|
+
for (let k = 0; k < n; k++) if (maxK[k] === FLEX) flexed.push(k);
|
|
1221
|
+
if (flexed.length > 0 && constraint !== 'min') {
|
|
1222
|
+
const frSize = (from, to, space) => {
|
|
1223
|
+
const inflexible = new Uint8Array(n);
|
|
1224
|
+
for (;;) {
|
|
1225
|
+
let leftover = space;
|
|
1226
|
+
let tracksIn = 0;
|
|
1227
|
+
let factors = 0;
|
|
1228
|
+
for (let k = from; k < to; k++) {
|
|
1229
|
+
if (dead[k]) continue;
|
|
1230
|
+
tracksIn++;
|
|
1231
|
+
if (maxK[k] === FLEX && !inflexible[k]) factors += factor[k];
|
|
1232
|
+
else leftover -= base[k];
|
|
1233
|
+
}
|
|
1234
|
+
leftover -= gap * Math.max(0, tracksIn - 1);
|
|
1235
|
+
const hypothetical = leftover / Math.max(factors, 1);
|
|
1236
|
+
let again = false;
|
|
1237
|
+
for (let k = from; k < to; k++) {
|
|
1238
|
+
if (
|
|
1239
|
+
maxK[k] === FLEX &&
|
|
1240
|
+
!inflexible[k] &&
|
|
1241
|
+
hypothetical * factor[k] < base[k]
|
|
1242
|
+
) {
|
|
1243
|
+
inflexible[k] = 1;
|
|
1244
|
+
again = true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (!again) return Math.max(0, hypothetical);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
let fr = 0;
|
|
1251
|
+
if (constraint === null && avail !== null) {
|
|
1252
|
+
if (avail - sum() - gaps > EPS) fr = frSize(0, n, avail);
|
|
1253
|
+
} else {
|
|
1254
|
+
// no room to share: an fr is as big as it has to be for every flexible
|
|
1255
|
+
// track to keep its base size, and every item crossing one its content
|
|
1256
|
+
for (const k of flexed) {
|
|
1257
|
+
fr = Math.max(fr, factor[k] > 1 ? base[k] / factor[k] : base[k]);
|
|
1258
|
+
}
|
|
1259
|
+
for (const i of flexers) {
|
|
1260
|
+
fr = Math.max(fr, frSize(starts[i], starts[i] + spans[i], ask.max(i)));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
for (const k of flexed) {
|
|
1264
|
+
if (fr * factor[k] > base[k]) base[k] = fr * factor[k];
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// §12.8: what is left goes to the `auto` tracks, unless the box distributes
|
|
1269
|
+
// its content some other way
|
|
1270
|
+
if (stretchAuto && constraint === null && avail !== null) {
|
|
1271
|
+
const free = avail - sum() - gaps;
|
|
1272
|
+
if (free > EPS) {
|
|
1273
|
+
let autos = 0;
|
|
1274
|
+
for (let k = 0; k < n; k++) if (maxK[k] === AUTO_K && !dead[k]) autos++;
|
|
1275
|
+
if (autos > 0) {
|
|
1276
|
+
for (let k = 0; k < n; k++) {
|
|
1277
|
+
if (maxK[k] === AUTO_K && !dead[k]) base[k] += free / autos;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return base;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** The tracks laid end to end, gaps between. */
|
|
1286
|
+
function extent(sizes, tracks, gap) {
|
|
1287
|
+
let total = 0;
|
|
1288
|
+
let live = 0;
|
|
1289
|
+
for (let k = 0; k < sizes.length; k++) {
|
|
1290
|
+
total += sizes[k];
|
|
1291
|
+
if (!tracks[k].collapsed) live++;
|
|
1292
|
+
}
|
|
1293
|
+
return total + gap * Math.max(0, live - 1);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Where each track starts and ends, distributed by `justifyContent` /
|
|
1298
|
+
* `alignContent` when there is room to, and **snapped to whole pixels at the
|
|
1299
|
+
* lines** rather than per item: two items sharing a line share its pixel,
|
|
1300
|
+
* so neighbours in `1fr 1fr 1fr` meet with no seam, and a child's tree is
|
|
1301
|
+
* never laid out at a fraction — the input yoga divides a rounding residue
|
|
1302
|
+
* by (issue #411).
|
|
1303
|
+
*/
|
|
1304
|
+
function trackLines(sizes, tracks, gap, room, distribution) {
|
|
1305
|
+
const n = sizes.length;
|
|
1306
|
+
let live = 0;
|
|
1307
|
+
let total = 0;
|
|
1308
|
+
for (let k = 0; k < n; k++) {
|
|
1309
|
+
total += sizes[k];
|
|
1310
|
+
if (!tracks[k].collapsed) live++;
|
|
1311
|
+
}
|
|
1312
|
+
total += gap * Math.max(0, live - 1);
|
|
1313
|
+
let at = 0;
|
|
1314
|
+
let extra = 0;
|
|
1315
|
+
if (room !== null && distribution != null) {
|
|
1316
|
+
const free = room - total;
|
|
1317
|
+
switch (distribution) {
|
|
1318
|
+
case 'center':
|
|
1319
|
+
at = free / 2;
|
|
1320
|
+
break;
|
|
1321
|
+
case 'flex-end':
|
|
1322
|
+
at = free;
|
|
1323
|
+
break;
|
|
1324
|
+
case 'space-between':
|
|
1325
|
+
if (free > 0 && live > 1) extra = free / (live - 1);
|
|
1326
|
+
break;
|
|
1327
|
+
case 'space-around':
|
|
1328
|
+
if (free > 0 && live > 0) {
|
|
1329
|
+
extra = free / live;
|
|
1330
|
+
at = extra / 2;
|
|
1331
|
+
} else at = free / 2;
|
|
1332
|
+
break;
|
|
1333
|
+
case 'space-evenly':
|
|
1334
|
+
if (free > 0) {
|
|
1335
|
+
extra = free / (live + 1);
|
|
1336
|
+
at = extra;
|
|
1337
|
+
} else at = free / 2;
|
|
1338
|
+
break;
|
|
1339
|
+
default:
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const x0 = new Int32Array(n);
|
|
1344
|
+
const x1 = new Int32Array(n);
|
|
1345
|
+
for (let k = 0; k < n; k++) {
|
|
1346
|
+
x0[k] = Math.round(at);
|
|
1347
|
+
at += sizes[k];
|
|
1348
|
+
x1[k] = Math.round(at);
|
|
1349
|
+
if (!tracks[k].collapsed) at += gap + extra;
|
|
1350
|
+
}
|
|
1351
|
+
return { x0, x1 };
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// --- the layout -----------------------------------------------------------------
|
|
1355
|
+
|
|
1356
|
+
const START = 0;
|
|
1357
|
+
const CENTER = 1;
|
|
1358
|
+
const END = 2;
|
|
1359
|
+
const STRETCH = 3;
|
|
1360
|
+
const MODES = {
|
|
1361
|
+
'flex-start': START,
|
|
1362
|
+
center: CENTER,
|
|
1363
|
+
'flex-end': END,
|
|
1364
|
+
stretch: STRETCH,
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
/** An alignment keyword as a mode: `auto` (or nothing) is the fallback, and a
|
|
1368
|
+
* value grid has no use for — `baseline`, the `space-*` ones — the start. */
|
|
1369
|
+
const modeOf = (value, fallback) =>
|
|
1370
|
+
value == null || value === 'auto' ? fallback : (MODES[value] ?? START);
|
|
1371
|
+
|
|
1372
|
+
const lengthOf = (v) => (typeof v === 'number' && v > 0 ? v : 0);
|
|
1373
|
+
|
|
1374
|
+
/** Up to a whole pixel, with a thousandth of slack for a sum that should have
|
|
1375
|
+
* been whole. */
|
|
1376
|
+
const wholePixels = (v) => Math.ceil(v - 1e-3);
|
|
1377
|
+
|
|
1378
|
+
const offsetBy = (mode, free) =>
|
|
1379
|
+
mode === CENTER ? Math.round(free / 2) : mode === END ? free : 0;
|
|
1380
|
+
|
|
1381
|
+
const noop = () => {};
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* The layout the seam runs (`registerLayout('grid', …)` in layouts.js). One
|
|
1385
|
+
* call answers how big the grid's content is for the room on offer, and —
|
|
1386
|
+
* with both modes `'exactly'` — where each child goes: the rect of its grid
|
|
1387
|
+
* area, or of its own size inside the area where it is not stretched.
|
|
1388
|
+
*/
|
|
1389
|
+
export function gridLayout(children, c, _options, info) {
|
|
1390
|
+
const style = info.style;
|
|
1391
|
+
const scale = info.scale ?? 1;
|
|
1392
|
+
const report = info.report ?? noop;
|
|
1393
|
+
const colGap = lengthOf(style.columnGap ?? style.gap);
|
|
1394
|
+
const rowGap = lengthOf(style.rowGap ?? style.gap);
|
|
1395
|
+
const areas = templateAreas(style.gridTemplateAreas);
|
|
1396
|
+
const flow = FLOWS[style.gridAutoFlow] ?? FLOWS.row;
|
|
1397
|
+
const colRoom = c.widthMode === 'exactly' ? c.width : null;
|
|
1398
|
+
const rowRoom = c.heightMode === 'exactly' ? c.height : null;
|
|
1399
|
+
const colList = repeatOut(
|
|
1400
|
+
templateTracks(style.gridTemplateColumns, scale, 'gridTemplateColumns'),
|
|
1401
|
+
colRoom,
|
|
1402
|
+
colGap,
|
|
1403
|
+
);
|
|
1404
|
+
const rowList = repeatOut(
|
|
1405
|
+
templateTracks(style.gridTemplateRows, scale, 'gridTemplateRows'),
|
|
1406
|
+
rowRoom,
|
|
1407
|
+
rowGap,
|
|
1408
|
+
);
|
|
1409
|
+
const explicitCols = Math.max(
|
|
1410
|
+
colList.tracks.length,
|
|
1411
|
+
areas === null ? 0 : areas.cols,
|
|
1412
|
+
);
|
|
1413
|
+
const explicitRows = Math.max(
|
|
1414
|
+
rowList.tracks.length,
|
|
1415
|
+
areas === null ? 0 : areas.rows,
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
// where each child asked to go
|
|
1419
|
+
const n = children.length;
|
|
1420
|
+
const cS = new Int32Array(n);
|
|
1421
|
+
const cN = new Int32Array(n);
|
|
1422
|
+
const rS = new Int32Array(n);
|
|
1423
|
+
const rN = new Int32Array(n);
|
|
1424
|
+
const justify = new Uint8Array(n);
|
|
1425
|
+
const align = new Uint8Array(n);
|
|
1426
|
+
const itemsJustify = modeOf(style.justifyItems, STRETCH);
|
|
1427
|
+
const itemsAlign = modeOf(style.alignItems, STRETCH);
|
|
1428
|
+
for (let i = 0; i < n; i++) {
|
|
1429
|
+
const s = children[i].style;
|
|
1430
|
+
let columns = null;
|
|
1431
|
+
let rows = null;
|
|
1432
|
+
if (s.gridArea != null) {
|
|
1433
|
+
const a = gridArea(s.gridArea);
|
|
1434
|
+
if (a.name === undefined) {
|
|
1435
|
+
rows = a.rows;
|
|
1436
|
+
columns = a.columns;
|
|
1437
|
+
} else {
|
|
1438
|
+
const box = areas === null ? undefined : areas.names.get(a.name);
|
|
1439
|
+
if (box !== undefined) {
|
|
1440
|
+
rows = box.rows;
|
|
1441
|
+
columns = box.columns;
|
|
1442
|
+
} else {
|
|
1443
|
+
report(
|
|
1444
|
+
`react-x11: gridArea ${JSON.stringify(a.name)} names no area of ` +
|
|
1445
|
+
(areas === null
|
|
1446
|
+
? 'this grid, which has no gridTemplateAreas'
|
|
1447
|
+
: `this grid's gridTemplateAreas (${[...areas.names.keys()]
|
|
1448
|
+
.map((name) => JSON.stringify(name))
|
|
1449
|
+
.join(', ')})`),
|
|
1450
|
+
'The child is placed automatically',
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (s.gridColumn != null) columns = gridLines(s.gridColumn, 'gridColumn');
|
|
1456
|
+
if (s.gridRow != null) rows = gridLines(s.gridRow, 'gridRow');
|
|
1457
|
+
resolve(columns, explicitCols, cS, cN, i);
|
|
1458
|
+
resolve(rows, explicitRows, rS, rN, i);
|
|
1459
|
+
justify[i] = modeOf(s.justifySelf, itemsJustify);
|
|
1460
|
+
align[i] = modeOf(s.alignSelf, itemsAlign);
|
|
1461
|
+
if (s.layoutItem != null) {
|
|
1462
|
+
report(
|
|
1463
|
+
'react-x11: a grid does not read layoutItem — its children are ' +
|
|
1464
|
+
'placed by gridColumn, gridRow and gridArea in their own style',
|
|
1465
|
+
'It is ignored',
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const placed = placeItems(
|
|
1470
|
+
n,
|
|
1471
|
+
cS,
|
|
1472
|
+
cN,
|
|
1473
|
+
rS,
|
|
1474
|
+
rN,
|
|
1475
|
+
explicitCols,
|
|
1476
|
+
explicitRows,
|
|
1477
|
+
flow,
|
|
1478
|
+
);
|
|
1479
|
+
const colTracks = axisTracks(
|
|
1480
|
+
placed.cols,
|
|
1481
|
+
placed.cOff,
|
|
1482
|
+
colList,
|
|
1483
|
+
autoTracks(style.gridAutoColumns, scale, 'gridAutoColumns'),
|
|
1484
|
+
cS,
|
|
1485
|
+
cN,
|
|
1486
|
+
n,
|
|
1487
|
+
);
|
|
1488
|
+
const rowTracks = axisTracks(
|
|
1489
|
+
placed.rows,
|
|
1490
|
+
placed.rOff,
|
|
1491
|
+
rowList,
|
|
1492
|
+
autoTracks(style.gridAutoRows, scale, 'gridAutoRows'),
|
|
1493
|
+
rS,
|
|
1494
|
+
rN,
|
|
1495
|
+
n,
|
|
1496
|
+
);
|
|
1497
|
+
const clips = (i) => {
|
|
1498
|
+
const o = children[i].style.overflow;
|
|
1499
|
+
return o === 'hidden' || o === 'scroll';
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
// the columns, from what each child says about its width
|
|
1503
|
+
const minW = new Float64Array(n).fill(Number.NaN);
|
|
1504
|
+
const maxW = new Float64Array(n).fill(Number.NaN);
|
|
1505
|
+
const widths = {
|
|
1506
|
+
min: (i) => {
|
|
1507
|
+
if (Number.isNaN(minW[i])) {
|
|
1508
|
+
minW[i] = children[i].intrinsicSizes().minContentWidth;
|
|
1509
|
+
}
|
|
1510
|
+
return minW[i];
|
|
1511
|
+
},
|
|
1512
|
+
max: (i) => {
|
|
1513
|
+
if (Number.isNaN(maxW[i])) {
|
|
1514
|
+
maxW[i] = children[i].intrinsicSizes().maxContentWidth;
|
|
1515
|
+
}
|
|
1516
|
+
return maxW[i];
|
|
1517
|
+
},
|
|
1518
|
+
declared: (i) => {
|
|
1519
|
+
const s = children[i].style;
|
|
1520
|
+
return s.minWidth != null || typeof s.width === 'number';
|
|
1521
|
+
},
|
|
1522
|
+
};
|
|
1523
|
+
const columnSizes = (avail, constraint) =>
|
|
1524
|
+
sizeTracks(
|
|
1525
|
+
colTracks,
|
|
1526
|
+
cS,
|
|
1527
|
+
cN,
|
|
1528
|
+
n,
|
|
1529
|
+
widths,
|
|
1530
|
+
clips,
|
|
1531
|
+
avail,
|
|
1532
|
+
colGap,
|
|
1533
|
+
constraint,
|
|
1534
|
+
constraint === null && style.justifyContent == null,
|
|
1535
|
+
);
|
|
1536
|
+
let colSizes;
|
|
1537
|
+
let width;
|
|
1538
|
+
if (c.widthMode === 'exactly') {
|
|
1539
|
+
colSizes = columnSizes(c.width, null);
|
|
1540
|
+
width = c.width;
|
|
1541
|
+
} else if (c.widthMode === 'at-most' && c.width > 0) {
|
|
1542
|
+
// shrink to fit: the width its columns would like, if there is room for
|
|
1543
|
+
// it, the narrowest they can be if there is not room even for that, and
|
|
1544
|
+
// otherwise the room
|
|
1545
|
+
colSizes = columnSizes(null, 'max');
|
|
1546
|
+
width = extent(colSizes, colTracks, colGap);
|
|
1547
|
+
if (width > c.width + EPS) {
|
|
1548
|
+
const narrowest = columnSizes(null, 'min');
|
|
1549
|
+
const least = extent(narrowest, colTracks, colGap);
|
|
1550
|
+
if (least >= c.width - EPS) {
|
|
1551
|
+
colSizes = narrowest;
|
|
1552
|
+
width = least;
|
|
1553
|
+
} else {
|
|
1554
|
+
colSizes = columnSizes(c.width, null);
|
|
1555
|
+
width = c.width;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
} else {
|
|
1559
|
+
colSizes = columnSizes(
|
|
1560
|
+
null,
|
|
1561
|
+
c.widthMode === 'unconstrained' ? 'max' : 'min',
|
|
1562
|
+
);
|
|
1563
|
+
width = extent(colSizes, colTracks, colGap);
|
|
1564
|
+
}
|
|
1565
|
+
const cols = trackLines(
|
|
1566
|
+
colSizes,
|
|
1567
|
+
colTracks,
|
|
1568
|
+
colGap,
|
|
1569
|
+
colRoom,
|
|
1570
|
+
style.justifyContent,
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
// the rows, from each child's height at the width its columns came to —
|
|
1574
|
+
// asked at the snapped width the child is then laid out at, so the answer
|
|
1575
|
+
// the seam remembers is the one the placement reads
|
|
1576
|
+
const measured = new Array(n);
|
|
1577
|
+
const measureItem = (i) => {
|
|
1578
|
+
let m = measured[i];
|
|
1579
|
+
if (m === undefined) {
|
|
1580
|
+
const w = cols.x1[cS[i] + cN[i] - 1] - cols.x0[cS[i]];
|
|
1581
|
+
m = measured[i] =
|
|
1582
|
+
justify[i] === STRETCH
|
|
1583
|
+
? children[i].measure({ width: w })
|
|
1584
|
+
: children[i].measure({ width: w, widthMode: 'at-most' });
|
|
1585
|
+
}
|
|
1586
|
+
return m;
|
|
1587
|
+
};
|
|
1588
|
+
const heights = {
|
|
1589
|
+
min: (i) => measureItem(i).height,
|
|
1590
|
+
max: (i) => measureItem(i).height,
|
|
1591
|
+
declared: (i) => {
|
|
1592
|
+
const s = children[i].style;
|
|
1593
|
+
return s.minHeight != null || typeof s.height === 'number';
|
|
1594
|
+
},
|
|
1595
|
+
};
|
|
1596
|
+
let rowSizes = null;
|
|
1597
|
+
let height;
|
|
1598
|
+
if (c.heightMode === 'exactly' && c.widthMode !== 'exactly') {
|
|
1599
|
+
// nothing asks where the rows are, and the height is given
|
|
1600
|
+
height = c.height;
|
|
1601
|
+
} else {
|
|
1602
|
+
const content = style.alignContent;
|
|
1603
|
+
rowSizes = sizeTracks(
|
|
1604
|
+
rowTracks,
|
|
1605
|
+
rS,
|
|
1606
|
+
rN,
|
|
1607
|
+
n,
|
|
1608
|
+
heights,
|
|
1609
|
+
clips,
|
|
1610
|
+
rowRoom,
|
|
1611
|
+
rowGap,
|
|
1612
|
+
rowRoom === null ? 'max' : null,
|
|
1613
|
+
content == null ||
|
|
1614
|
+
content === 'stretch' ||
|
|
1615
|
+
content === 'auto' ||
|
|
1616
|
+
content === 'baseline',
|
|
1617
|
+
);
|
|
1618
|
+
height = rowRoom ?? extent(rowSizes, rowTracks, rowGap);
|
|
1619
|
+
}
|
|
1620
|
+
if (c.widthMode !== 'exactly' || c.heightMode !== 'exactly') {
|
|
1621
|
+
return { width, height };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const rows = trackLines(
|
|
1625
|
+
rowSizes,
|
|
1626
|
+
rowTracks,
|
|
1627
|
+
rowGap,
|
|
1628
|
+
rowRoom,
|
|
1629
|
+
style.alignContent,
|
|
1630
|
+
);
|
|
1631
|
+
const rects = new Array(n);
|
|
1632
|
+
for (let i = 0; i < n; i++) {
|
|
1633
|
+
const x0 = cols.x0[cS[i]];
|
|
1634
|
+
const areaW = cols.x1[cS[i] + cN[i] - 1] - x0;
|
|
1635
|
+
const y0 = rows.x0[rS[i]];
|
|
1636
|
+
const areaH = rows.x1[rS[i] + rN[i] - 1] - y0;
|
|
1637
|
+
const rect = { x: x0, y: y0, width: areaW };
|
|
1638
|
+
if (justify[i] !== STRETCH) {
|
|
1639
|
+
// its own width — CSS's fit-content in the area — placed in it
|
|
1640
|
+
const w = wholePixels(measureItem(i).width);
|
|
1641
|
+
rect.width = w;
|
|
1642
|
+
rect.x += offsetBy(justify[i], areaW - w);
|
|
1643
|
+
}
|
|
1644
|
+
if (align[i] === STRETCH) {
|
|
1645
|
+
rect.height = areaH;
|
|
1646
|
+
} else {
|
|
1647
|
+
// its own height, at that width; the rect leaves it to the child
|
|
1648
|
+
rect.y += offsetBy(align[i], areaH - wholePixels(measureItem(i).height));
|
|
1649
|
+
}
|
|
1650
|
+
rects[i] = rect;
|
|
1651
|
+
}
|
|
1652
|
+
return { width, height, children: rects };
|
|
1653
|
+
}
|