@textui/core 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/app/app.d.ts +200 -0
- package/dist/app/app.d.ts.map +1 -0
- package/dist/app/app.js +980 -0
- package/dist/core/animation.d.ts +34 -0
- package/dist/core/animation.d.ts.map +1 -0
- package/dist/core/animation.js +125 -0
- package/dist/core/clipboard.d.ts +29 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/clipboard.js +30 -0
- package/dist/core/commands.d.ts +42 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +146 -0
- package/dist/core/components.d.ts +27 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +78 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +77 -0
- package/dist/core/focus.d.ts +69 -0
- package/dist/core/focus.d.ts.map +1 -0
- package/dist/core/focus.js +336 -0
- package/dist/core/i18n.d.ts +27 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +88 -0
- package/dist/core/keybindings.d.ts +53 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +163 -0
- package/dist/core/layers.d.ts +28 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +84 -0
- package/dist/core/manifest.d.ts +22 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +85 -0
- package/dist/core/navigation.d.ts +47 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +110 -0
- package/dist/core/resources.d.ts +115 -0
- package/dist/core/resources.d.ts.map +1 -0
- package/dist/core/resources.js +321 -0
- package/dist/core/services.d.ts +21 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +63 -0
- package/dist/core/store.d.ts +60 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +593 -0
- package/dist/core/surfaces.d.ts +81 -0
- package/dist/core/surfaces.d.ts.map +1 -0
- package/dist/core/surfaces.js +237 -0
- package/dist/core/syntax.d.ts +49 -0
- package/dist/core/syntax.d.ts.map +1 -0
- package/dist/core/syntax.js +172 -0
- package/dist/core/when.d.ts +15 -0
- package/dist/core/when.d.ts.map +1 -0
- package/dist/core/when.js +224 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/jsx/factory.d.ts +27 -0
- package/dist/jsx/factory.d.ts.map +1 -0
- package/dist/jsx/factory.js +102 -0
- package/dist/jsx/intrinsics.d.ts +96 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -0
- package/dist/jsx/intrinsics.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime.js +12 -0
- package/dist/jsx/jsx-runtime.d.ts +42 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +16 -0
- package/dist/render/buffer.d.ts +77 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +275 -0
- package/dist/render/color.d.ts +35 -0
- package/dist/render/color.d.ts.map +1 -0
- package/dist/render/color.js +160 -0
- package/dist/render/diff.d.ts +37 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +66 -0
- package/dist/render/layout.d.ts +72 -0
- package/dist/render/layout.d.ts.map +1 -0
- package/dist/render/layout.js +633 -0
- package/dist/render/static.d.ts +63 -0
- package/dist/render/static.d.ts.map +1 -0
- package/dist/render/static.js +209 -0
- package/dist/runtime/bindings.d.ts +46 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +116 -0
- package/dist/runtime/hooks.d.ts +284 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +846 -0
- package/dist/runtime/instance.d.ts +106 -0
- package/dist/runtime/instance.d.ts.map +1 -0
- package/dist/runtime/instance.js +181 -0
- package/dist/runtime/paint.d.ts +27 -0
- package/dist/runtime/paint.d.ts.map +1 -0
- package/dist/runtime/paint.js +567 -0
- package/dist/runtime/reconcile.d.ts +23 -0
- package/dist/runtime/reconcile.d.ts.map +1 -0
- package/dist/runtime/reconcile.js +260 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/style.d.ts +55 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/style.js +143 -0
- package/dist/themes/borders.d.ts +10 -0
- package/dist/themes/borders.d.ts.map +1 -0
- package/dist/themes/borders.js +88 -0
- package/dist/themes/builtin.d.ts +31 -0
- package/dist/themes/builtin.d.ts.map +1 -0
- package/dist/themes/builtin.js +291 -0
- package/dist/themes/glyphs.d.ts +15 -0
- package/dist/themes/glyphs.d.ts.map +1 -0
- package/dist/themes/glyphs.js +97 -0
- package/dist/themes/index.d.ts +5 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/registry.d.ts +20 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +204 -0
- package/dist/types/adapter.d.ts +38 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/animation.d.ts +31 -0
- package/dist/types/animation.d.ts.map +1 -0
- package/dist/types/animation.js +1 -0
- package/dist/types/app.d.ts +148 -0
- package/dist/types/app.d.ts.map +1 -0
- package/dist/types/app.js +1 -0
- package/dist/types/async.d.ts +30 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/async.js +1 -0
- package/dist/types/capabilities.d.ts +44 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +33 -0
- package/dist/types/cells.d.ts +72 -0
- package/dist/types/cells.d.ts.map +1 -0
- package/dist/types/cells.js +10 -0
- package/dist/types/command.d.ts +127 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +1 -0
- package/dist/types/component-registry.d.ts +77 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/component-registry.js +1 -0
- package/dist/types/disposable.d.ts +8 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/focus.d.ts +72 -0
- package/dist/types/focus.d.ts.map +1 -0
- package/dist/types/focus.js +1 -0
- package/dist/types/geometry.d.ts +28 -0
- package/dist/types/geometry.d.ts.map +1 -0
- package/dist/types/geometry.js +14 -0
- package/dist/types/graph.d.ts +129 -0
- package/dist/types/graph.d.ts.map +1 -0
- package/dist/types/graph.js +31 -0
- package/dist/types/i18n.d.ts +23 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +31 -0
- package/dist/types/input.d.ts +65 -0
- package/dist/types/input.d.ts.map +1 -0
- package/dist/types/input.js +1 -0
- package/dist/types/keybinding.d.ts +49 -0
- package/dist/types/keybinding.d.ts.map +1 -0
- package/dist/types/keybinding.js +1 -0
- package/dist/types/layer.d.ts +54 -0
- package/dist/types/layer.d.ts.map +1 -0
- package/dist/types/layer.js +1 -0
- package/dist/types/manifest.d.ts +114 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/markdown.d.ts +58 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/markdown.js +1 -0
- package/dist/types/navigation.d.ts +40 -0
- package/dist/types/navigation.d.ts.map +1 -0
- package/dist/types/navigation.js +1 -0
- package/dist/types/render.d.ts +72 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +1 -0
- package/dist/types/resource.d.ts +170 -0
- package/dist/types/resource.d.ts.map +1 -0
- package/dist/types/resource.js +1 -0
- package/dist/types/services.d.ts +24 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +3 -0
- package/dist/types/shell.d.ts +32 -0
- package/dist/types/shell.d.ts.map +1 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/store.d.ts +124 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/store.js +1 -0
- package/dist/types/stream.d.ts +18 -0
- package/dist/types/stream.d.ts.map +1 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/style.d.ts +166 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/style.js +1 -0
- package/dist/types/surface.d.ts +97 -0
- package/dist/types/surface.d.ts.map +1 -0
- package/dist/types/surface.js +1 -0
- package/dist/types/syntax.d.ts +68 -0
- package/dist/types/syntax.d.ts.map +1 -0
- package/dist/types/syntax.js +5 -0
- package/dist/types/terminal.d.ts +52 -0
- package/dist/types/terminal.d.ts.map +1 -0
- package/dist/types/terminal.js +1 -0
- package/dist/types/theme.d.ts +117 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/when.d.ts +19 -0
- package/dist/types/when.d.ts.map +1 -0
- package/dist/types/when.js +1 -0
- package/dist/ui/primitives.d.ts +56 -0
- package/dist/ui/primitives.d.ts.map +1 -0
- package/dist/ui/primitives.js +108 -0
- package/dist/ui/screen.d.ts +23 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +18 -0
- package/dist/util/disposable.d.ts +6 -0
- package/dist/util/disposable.d.ts.map +1 -0
- package/dist/util/disposable.js +49 -0
- package/dist/util/markdown.d.ts +24 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +220 -0
- package/dist/util/paths.d.ts +47 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +134 -0
- package/dist/util/stream.d.ts +32 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +206 -0
- package/dist/util/text.d.ts +65 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +419 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +11 -0
- package/src/app/app.ts +1096 -0
- package/src/core/animation.ts +144 -0
- package/src/core/clipboard.ts +40 -0
- package/src/core/commands.ts +169 -0
- package/src/core/components.ts +91 -0
- package/src/core/events.ts +95 -0
- package/src/core/focus.ts +358 -0
- package/src/core/i18n.ts +107 -0
- package/src/core/keybindings.ts +184 -0
- package/src/core/layers.ts +94 -0
- package/src/core/manifest.ts +84 -0
- package/src/core/navigation.ts +135 -0
- package/src/core/resources.ts +362 -0
- package/src/core/services.ts +69 -0
- package/src/core/store.ts +640 -0
- package/src/core/surfaces.ts +292 -0
- package/src/core/syntax.ts +200 -0
- package/src/core/when.ts +238 -0
- package/src/index.ts +76 -0
- package/src/jsx/factory.ts +124 -0
- package/src/jsx/intrinsics.ts +99 -0
- package/src/jsx/jsx-dev-runtime.ts +23 -0
- package/src/jsx/jsx-runtime.ts +76 -0
- package/src/render/buffer.ts +318 -0
- package/src/render/color.ts +180 -0
- package/src/render/diff.ts +99 -0
- package/src/render/layout.ts +764 -0
- package/src/render/static.ts +290 -0
- package/src/runtime/bindings.ts +150 -0
- package/src/runtime/hooks.ts +1057 -0
- package/src/runtime/instance.ts +284 -0
- package/src/runtime/paint.ts +737 -0
- package/src/runtime/reconcile.ts +335 -0
- package/src/runtime/runtime.ts +47 -0
- package/src/runtime/style.ts +205 -0
- package/src/themes/borders.ts +95 -0
- package/src/themes/builtin.ts +301 -0
- package/src/themes/glyphs.ts +101 -0
- package/src/themes/index.ts +4 -0
- package/src/themes/registry.ts +224 -0
- package/src/types/adapter.ts +41 -0
- package/src/types/animation.ts +34 -0
- package/src/types/app.ts +149 -0
- package/src/types/async.ts +35 -0
- package/src/types/capabilities.ts +79 -0
- package/src/types/cells.ts +79 -0
- package/src/types/command.ts +139 -0
- package/src/types/component-registry.ts +82 -0
- package/src/types/disposable.ts +8 -0
- package/src/types/focus.ts +77 -0
- package/src/types/geometry.ts +50 -0
- package/src/types/graph.ts +174 -0
- package/src/types/i18n.ts +25 -0
- package/src/types/index.ts +31 -0
- package/src/types/input.ts +81 -0
- package/src/types/keybinding.ts +51 -0
- package/src/types/layer.ts +46 -0
- package/src/types/manifest.ts +101 -0
- package/src/types/markdown.ts +47 -0
- package/src/types/navigation.ts +42 -0
- package/src/types/render.ts +86 -0
- package/src/types/resource.ts +185 -0
- package/src/types/services.ts +28 -0
- package/src/types/shell.ts +30 -0
- package/src/types/store.ts +148 -0
- package/src/types/stream.ts +24 -0
- package/src/types/style.ts +205 -0
- package/src/types/surface.ts +119 -0
- package/src/types/syntax.ts +93 -0
- package/src/types/terminal.ts +58 -0
- package/src/types/theme.ts +121 -0
- package/src/types/when.ts +21 -0
- package/src/ui/primitives.ts +118 -0
- package/src/ui/screen.ts +41 -0
- package/src/util/disposable.ts +49 -0
- package/src/util/markdown.ts +225 -0
- package/src/util/paths.ts +138 -0
- package/src/util/stream.ts +213 -0
- package/src/util/text.ts +428 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import type { Edges, EdgeSpec, Rect, Size } from '../types/geometry.js';
|
|
2
|
+
import type { Align, Dimension, Justify, Overflow, Style } from '../types/style.js';
|
|
3
|
+
import { ZERO_EDGES } from '../types/geometry.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Layout is a flexbox subset sized in whole cells.
|
|
7
|
+
*
|
|
8
|
+
* The subset is deliberate: grow and shrink along the main axis, stretch or
|
|
9
|
+
* align on the cross axis, optional wrapping into further lines, and absolute
|
|
10
|
+
* positioning for layers. Everything a terminal UI actually needs fits in
|
|
11
|
+
* that, and leaving out the rest keeps a pass over a full screen cheap enough
|
|
12
|
+
* to run on every frame.
|
|
13
|
+
*
|
|
14
|
+
* Wrapping is off by default, and that is a performance decision as much as a
|
|
15
|
+
* design one: a non-wrapping container measures each child once, and a
|
|
16
|
+
* wrapping one has to measure them all before it knows where the first line
|
|
17
|
+
* ends.
|
|
18
|
+
*
|
|
19
|
+
* Sizes are integers throughout. A "half cell" does not exist, so fractions
|
|
20
|
+
* are distributed by largest-remainder rather than rounded independently -
|
|
21
|
+
* three flex-1 children of a 10-cell row get 4, 3, 3, never 3, 3, 3 with a
|
|
22
|
+
* gap left over.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface LayoutBox {
|
|
26
|
+
style: Style;
|
|
27
|
+
/** Border thickness actually drawn, per side. Consumes content space. */
|
|
28
|
+
borderEdges: Edges;
|
|
29
|
+
children: LayoutBox[];
|
|
30
|
+
/** Intrinsic size for leaves. Containers derive theirs from children. */
|
|
31
|
+
measure?(maxWidth: number, maxHeight: number): Size;
|
|
32
|
+
/** Scroll offset, in cells. Only meaningful with `overflow: 'scroll'`. */
|
|
33
|
+
scrollTop?: number;
|
|
34
|
+
scrollLeft?: number;
|
|
35
|
+
|
|
36
|
+
// --- computed by `layout` ---
|
|
37
|
+
rect: Rect;
|
|
38
|
+
/** Inside border and padding. Where children and paint go. */
|
|
39
|
+
content: Rect;
|
|
40
|
+
/** Content larger than `content`, when overflow is 'scroll'. */
|
|
41
|
+
scrollSize?: Size;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The last answer `measureBox` gave for this box, and what it was asked.
|
|
45
|
+
*
|
|
46
|
+
* Sizing a flex container measures its children to find their intrinsic
|
|
47
|
+
* size, then measures them again against the space they were given - so a
|
|
48
|
+
* frame asks the same subtree the same question about twice, and the deeper
|
|
49
|
+
* the tree the more of it is repeated work. Counted on a 400-row list: 866
|
|
50
|
+
* calls, 407 of them distinct.
|
|
51
|
+
*
|
|
52
|
+
* One slot, not a map: the repeats are adjacent, and a map per box is an
|
|
53
|
+
* allocation per box to avoid an allocation per box. No invalidation either,
|
|
54
|
+
* because layout boxes are rebuilt from the instance tree every frame - the
|
|
55
|
+
* cache cannot outlive the thing it describes.
|
|
56
|
+
*/
|
|
57
|
+
measured?: { w: number; h: number; size: Size };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveEdges(spec: EdgeSpec | undefined): Edges {
|
|
61
|
+
if (spec === undefined) return ZERO_EDGES;
|
|
62
|
+
if (typeof spec === 'number') {
|
|
63
|
+
return { top: spec, right: spec, bottom: spec, left: spec };
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(spec)) {
|
|
66
|
+
if (spec.length === 2) {
|
|
67
|
+
const [v, h] = spec;
|
|
68
|
+
return { top: v, right: h, bottom: v, left: h };
|
|
69
|
+
}
|
|
70
|
+
if (spec.length === 3) {
|
|
71
|
+
const [t, h, b] = spec;
|
|
72
|
+
return { top: t, right: h, bottom: b, left: h };
|
|
73
|
+
}
|
|
74
|
+
const [t, r, b, l] = spec;
|
|
75
|
+
return { top: t, right: r, bottom: b, left: l };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
top: spec.top ?? 0,
|
|
79
|
+
right: spec.right ?? 0,
|
|
80
|
+
bottom: spec.bottom ?? 0,
|
|
81
|
+
left: spec.left ?? 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function edgeH(e: Edges): number {
|
|
86
|
+
return e.left + e.right;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function edgeV(e: Edges): number {
|
|
90
|
+
return e.top + e.bottom;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve a dimension against an available extent. `auto` returns null. */
|
|
94
|
+
function resolveDimension(dim: Dimension | undefined, available: number): number | null {
|
|
95
|
+
if (dim === undefined || dim === 'auto') return null;
|
|
96
|
+
if (typeof dim === 'number') return Math.max(0, Math.round(dim));
|
|
97
|
+
const pct = Number.parseFloat(dim);
|
|
98
|
+
if (Number.isNaN(pct)) return null;
|
|
99
|
+
return Math.max(0, Math.round((available * pct) / 100));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clamp(v: number, min: number | undefined, max: number | undefined): number {
|
|
103
|
+
let out = v;
|
|
104
|
+
if (min !== undefined) out = Math.max(out, min);
|
|
105
|
+
if (max !== undefined) out = Math.min(out, max);
|
|
106
|
+
return Math.max(0, out);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isColumn(box: LayoutBox): boolean {
|
|
110
|
+
return (box.style.direction ?? 'column') === 'column';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isAbsolute(box: LayoutBox): boolean {
|
|
114
|
+
return box.style.position === 'absolute';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isHidden(box: LayoutBox): boolean {
|
|
118
|
+
return box.style.display === 'none';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isWrapping(box: LayoutBox): boolean {
|
|
122
|
+
return box.style.flexWrap === 'wrap';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `overflow` for one axis. The specific prop wins over the shorthand, so
|
|
127
|
+
* `overflow: 'hidden', overflowY: 'scroll'` is a pane that scrolls down and
|
|
128
|
+
* clips sideways - which is what a log viewer is.
|
|
129
|
+
*/
|
|
130
|
+
export function overflowOn(style: Style, axis: 'x' | 'y'): Overflow {
|
|
131
|
+
return (axis === 'x' ? style.overflowX : style.overflowY) ?? style.overflow ?? 'visible';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The overflow governing this container's own main axis. */
|
|
135
|
+
function mainOverflow(box: LayoutBox): Overflow {
|
|
136
|
+
return overflowOn(box.style, isColumn(box) ? 'y' : 'x');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gaps, resolved to this container's axes.
|
|
141
|
+
*
|
|
142
|
+
* `columnGap` and `rowGap` are named for the screen, not for the container:
|
|
143
|
+
* `columnGap` is always the horizontal one. So on a row it is the gap between
|
|
144
|
+
* children and on a column it is the gap between wrapped lines, and a style
|
|
145
|
+
* that sets both reads the same either way round.
|
|
146
|
+
*/
|
|
147
|
+
function gapsOf(box: LayoutBox): { main: number; cross: number } {
|
|
148
|
+
const vertical = box.style.rowGap ?? box.style.gap ?? 0;
|
|
149
|
+
const horizontal = box.style.columnGap ?? box.style.gap ?? 0;
|
|
150
|
+
return isColumn(box)
|
|
151
|
+
? { main: vertical, cross: horizontal }
|
|
152
|
+
: { main: horizontal, cross: vertical };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Cut a run of main-axis sizes into lines that each fit `limit`.
|
|
157
|
+
*
|
|
158
|
+
* A child bigger than the whole line still gets a line to itself rather than
|
|
159
|
+
* an empty one pushed in front of it: it is going to overflow either way, and
|
|
160
|
+
* a blank row above it helps nobody.
|
|
161
|
+
*/
|
|
162
|
+
function splitLines(mains: number[], gap: number, limit: number): [number, number][] {
|
|
163
|
+
const lines: [number, number][] = [];
|
|
164
|
+
let start = 0;
|
|
165
|
+
let used = 0;
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < mains.length; i++) {
|
|
168
|
+
const size = mains[i] as number;
|
|
169
|
+
const next = used === 0 ? size : used + gap + size;
|
|
170
|
+
if (used > 0 && next > limit) {
|
|
171
|
+
lines.push([start, i]);
|
|
172
|
+
start = i;
|
|
173
|
+
used = size;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
used = next;
|
|
177
|
+
}
|
|
178
|
+
lines.push([start, mains.length]);
|
|
179
|
+
return lines;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Space this box consumes outside its content: margin, border, padding. */
|
|
183
|
+
function frameOf(box: LayoutBox): { margin: Edges; inset: Edges } {
|
|
184
|
+
const margin = resolveEdges(box.style.margin);
|
|
185
|
+
const padding = resolveEdges(box.style.padding);
|
|
186
|
+
const b = box.borderEdges;
|
|
187
|
+
return {
|
|
188
|
+
margin,
|
|
189
|
+
inset: {
|
|
190
|
+
top: b.top + padding.top,
|
|
191
|
+
right: b.right + padding.right,
|
|
192
|
+
bottom: b.bottom + padding.bottom,
|
|
193
|
+
left: b.left + padding.left,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Intrinsic size, ignoring grow. Containers ask their children; leaves are
|
|
200
|
+
* asked directly. `available` bounds text wrapping, so a paragraph measures
|
|
201
|
+
* against the width it will actually get.
|
|
202
|
+
*/
|
|
203
|
+
export function measureBox(box: LayoutBox, availW: number, availH: number): Size {
|
|
204
|
+
if (isHidden(box)) return { width: 0, height: 0 };
|
|
205
|
+
|
|
206
|
+
const seen = box.measured;
|
|
207
|
+
if (seen !== undefined && seen.w === availW && seen.h === availH) return seen.size;
|
|
208
|
+
|
|
209
|
+
const { margin, inset } = frameOf(box);
|
|
210
|
+
|
|
211
|
+
const fixedW = resolveDimension(box.style.width, availW);
|
|
212
|
+
const fixedH = resolveDimension(box.style.height, availH);
|
|
213
|
+
|
|
214
|
+
// What the children get to measure against.
|
|
215
|
+
//
|
|
216
|
+
// A stated width is a promise to the children, not just to the parent: a
|
|
217
|
+
// paragraph inside `width: 7` wraps at seven cells, and measuring it against
|
|
218
|
+
// whatever room the *parent* had leaves it one row tall with half of it
|
|
219
|
+
// unpainted. `maxWidth` binds the same way - it is the width the content
|
|
220
|
+
// will end up with, so it is the width the content should be measured at.
|
|
221
|
+
const boundW = Math.min(fixedW ?? availW - edgeH(margin), box.style.maxWidth ?? Infinity);
|
|
222
|
+
const boundH = Math.min(fixedH ?? availH - edgeV(margin), box.style.maxHeight ?? Infinity);
|
|
223
|
+
const innerAvailW = Math.max(0, boundW - edgeH(inset));
|
|
224
|
+
const innerAvailH = Math.max(0, boundH - edgeV(inset));
|
|
225
|
+
|
|
226
|
+
let contentW = 0;
|
|
227
|
+
let contentH = 0;
|
|
228
|
+
|
|
229
|
+
if (fixedW !== null && fixedH !== null) {
|
|
230
|
+
contentW = Math.max(0, fixedW - edgeH(inset));
|
|
231
|
+
contentH = Math.max(0, fixedH - edgeV(inset));
|
|
232
|
+
} else if (box.measure) {
|
|
233
|
+
const m = box.measure(innerAvailW, innerAvailH);
|
|
234
|
+
contentW = fixedW !== null ? Math.max(0, fixedW - edgeH(inset)) : m.width;
|
|
235
|
+
contentH = fixedH !== null ? Math.max(0, fixedH - edgeV(inset)) : m.height;
|
|
236
|
+
} else {
|
|
237
|
+
const gaps = gapsOf(box);
|
|
238
|
+
const column = isColumn(box);
|
|
239
|
+
|
|
240
|
+
// Skipped in the loop rather than filtered into an array first: measuring
|
|
241
|
+
// is the hot recursive walk of the whole tree, and this is one throwaway
|
|
242
|
+
// array per box per measure. `count` is what `flow.length` used to say.
|
|
243
|
+
let count = 0;
|
|
244
|
+
const mains: number[] = [];
|
|
245
|
+
const crosses: number[] = [];
|
|
246
|
+
for (const child of box.children) {
|
|
247
|
+
if (isAbsolute(child) || isHidden(child)) continue;
|
|
248
|
+
count++;
|
|
249
|
+
const m = measureBox(child, innerAvailW, innerAvailH);
|
|
250
|
+
const cm = resolveEdges(child.style.margin);
|
|
251
|
+
mains.push(column ? m.height + edgeV(cm) : m.width + edgeH(cm));
|
|
252
|
+
crosses.push(column ? m.width + edgeH(cm) : m.height + edgeV(cm));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// What a flexible child in a row will *actually* be given.
|
|
256
|
+
//
|
|
257
|
+
// Every child above was measured against the full inner width, which is
|
|
258
|
+
// only the width it gets when it is the only one there. In a row beside a
|
|
259
|
+
// rigid sibling it will be narrower - and a child whose height depends on
|
|
260
|
+
// its width, which is any wrapping text, then reports a height for a width
|
|
261
|
+
// it never has. `Alert` is `row(icon, column(flex: 1)(text wrap: 'word'))`,
|
|
262
|
+
// so a message one cell too long to fit measured as one row, was laid out
|
|
263
|
+
// two rows tall, and lost its last word off the bottom.
|
|
264
|
+
//
|
|
265
|
+
// `room` is what the flexible children share: the width left after the
|
|
266
|
+
// rigid ones and the gaps. That is their final size whichever way the
|
|
267
|
+
// distribution went - if there was space over they grow into all of it, and
|
|
268
|
+
// if there was not they are the ones that give way first (see `layoutLine`)
|
|
269
|
+
// - so it is the width to measure their height at.
|
|
270
|
+
//
|
|
271
|
+
// Rows only. The mirror case in a column would be a child whose *width*
|
|
272
|
+
// depends on its height, and nothing measures that way round.
|
|
273
|
+
if (!column && count > 1) {
|
|
274
|
+
let totalFlex = 0;
|
|
275
|
+
let rigidMain = 0;
|
|
276
|
+
let index = 0;
|
|
277
|
+
for (const child of box.children) {
|
|
278
|
+
if (isAbsolute(child) || isHidden(child)) continue;
|
|
279
|
+
const flex = Math.max(0, child.style.flex ?? 0);
|
|
280
|
+
if (flex > 0) totalFlex += flex;
|
|
281
|
+
else rigidMain += mains[index] as number;
|
|
282
|
+
index++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (totalFlex > 0) {
|
|
286
|
+
const room = Math.max(0, innerAvailW - rigidMain - gaps.main * (count - 1));
|
|
287
|
+
index = 0;
|
|
288
|
+
for (const child of box.children) {
|
|
289
|
+
if (isAbsolute(child) || isHidden(child)) continue;
|
|
290
|
+
const flex = Math.max(0, child.style.flex ?? 0);
|
|
291
|
+
if (flex > 0) {
|
|
292
|
+
const cm = resolveEdges(child.style.margin);
|
|
293
|
+
const share = Math.max(0, Math.floor((room * flex) / totalFlex) - edgeH(cm));
|
|
294
|
+
const m = measureBox(child, share, innerAvailH);
|
|
295
|
+
crosses[index] = m.height + edgeV(cm);
|
|
296
|
+
}
|
|
297
|
+
index++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let main = 0;
|
|
303
|
+
let cross = 0;
|
|
304
|
+
if (isWrapping(box) && count > 1) {
|
|
305
|
+
// Wrapping trades one axis for the other: the main extent is whatever
|
|
306
|
+
// the widest line came to - never more than the room it was given - and
|
|
307
|
+
// the cross extent grows by a line at a time. Measuring it any other way
|
|
308
|
+
// makes the parent size this box as though it were still one long line,
|
|
309
|
+
// and then it wraps inside a box too short to hold what it wrapped into.
|
|
310
|
+
const limit = column ? innerAvailH : innerAvailW;
|
|
311
|
+
const lines = splitLines(mains, gaps.main, limit);
|
|
312
|
+
for (const [from, to] of lines) {
|
|
313
|
+
let lineMain = 0;
|
|
314
|
+
let lineCross = 0;
|
|
315
|
+
for (let i = from; i < to; i++) {
|
|
316
|
+
lineMain += (mains[i] as number) + (i > from ? gaps.main : 0);
|
|
317
|
+
lineCross = Math.max(lineCross, crosses[i] as number);
|
|
318
|
+
}
|
|
319
|
+
main = Math.max(main, lineMain);
|
|
320
|
+
cross += lineCross;
|
|
321
|
+
}
|
|
322
|
+
if (lines.length > 1) cross += gaps.cross * (lines.length - 1);
|
|
323
|
+
} else {
|
|
324
|
+
for (let i = 0; i < mains.length; i++) {
|
|
325
|
+
main += mains[i] as number;
|
|
326
|
+
cross = Math.max(cross, crosses[i] as number);
|
|
327
|
+
}
|
|
328
|
+
if (mains.length > 1) main += gaps.main * (mains.length - 1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
contentW = column ? cross : main;
|
|
332
|
+
contentH = column ? main : cross;
|
|
333
|
+
|
|
334
|
+
// A scroll container that will be grown into place is a viewport, not a
|
|
335
|
+
// bag: what is inside it scrolls, so letting that content set its
|
|
336
|
+
// intrinsic size makes every sibling move when the document changes -
|
|
337
|
+
// which is what a file viewer does to a pane it shares. Grow decides its
|
|
338
|
+
// size; `minHeight`/`minWidth` still set the floor, via `clamp` below.
|
|
339
|
+
if (mainOverflow(box) === 'scroll' && (box.style.flex ?? 0) > 0) {
|
|
340
|
+
if (column) contentH = 0;
|
|
341
|
+
else contentW = 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (fixedW !== null) contentW = Math.max(0, fixedW - edgeH(inset));
|
|
345
|
+
if (fixedH !== null) contentH = Math.max(0, fixedH - edgeV(inset));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const width = clamp(contentW + edgeH(inset), box.style.minWidth, box.style.maxWidth);
|
|
349
|
+
const height = clamp(contentH + edgeV(inset), box.style.minHeight, box.style.maxHeight);
|
|
350
|
+
const size = { width, height };
|
|
351
|
+
box.measured = { w: availW, h: availH, size };
|
|
352
|
+
return size;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Distribute `total` cells across weights, largest remainder first, so the
|
|
357
|
+
* parts sum to exactly `total` with no cell lost to rounding.
|
|
358
|
+
*/
|
|
359
|
+
function distribute(total: number, weights: number[]): number[] {
|
|
360
|
+
const sum = weights.reduce((a, b) => a + b, 0);
|
|
361
|
+
if (sum <= 0 || total <= 0) return weights.map(() => 0);
|
|
362
|
+
|
|
363
|
+
const exact = weights.map((w) => (total * w) / sum);
|
|
364
|
+
const floors = exact.map(Math.floor);
|
|
365
|
+
let used = floors.reduce((a, b) => a + b, 0);
|
|
366
|
+
|
|
367
|
+
const order = exact
|
|
368
|
+
.map((v, i) => ({ i, frac: v - Math.floor(v) }))
|
|
369
|
+
.sort((a, b) => b.frac - a.frac);
|
|
370
|
+
|
|
371
|
+
let k = 0;
|
|
372
|
+
while (used < total && order.length > 0) {
|
|
373
|
+
const entry = order[k % order.length] as { i: number; frac: number };
|
|
374
|
+
floors[entry.i] = (floors[entry.i] as number) + 1;
|
|
375
|
+
used++;
|
|
376
|
+
k++;
|
|
377
|
+
}
|
|
378
|
+
return floors;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Distribute a shrink deficit across weights.
|
|
383
|
+
*
|
|
384
|
+
* Deliberately not `distribute`: largest-remainder gives the leftover cells to
|
|
385
|
+
* the largest *fractions*, which are the small children, so a 200-row pane
|
|
386
|
+
* next to a one-row status bar rounds the status bar out of existence. Here
|
|
387
|
+
* the leftovers go to the largest weights instead - the pane that is too big
|
|
388
|
+
* pays for the rounding, which is the only answer that keeps chrome on screen.
|
|
389
|
+
*/
|
|
390
|
+
function distributeCuts(total: number, weights: number[]): number[] {
|
|
391
|
+
const sum = weights.reduce((a, b) => a + b, 0);
|
|
392
|
+
if (sum <= 0 || total <= 0) return weights.map(() => 0);
|
|
393
|
+
|
|
394
|
+
const cuts = weights.map((w) => Math.floor((total * w) / sum));
|
|
395
|
+
const used = cuts.reduce((a, b) => a + b, 0);
|
|
396
|
+
|
|
397
|
+
// Every leftover cell goes to the heaviest child, not one each: spreading
|
|
398
|
+
// them is what costs a one-cell row its only cell.
|
|
399
|
+
let heaviest = 0;
|
|
400
|
+
for (let i = 1; i < weights.length; i++) {
|
|
401
|
+
if ((weights[i] as number) > (weights[heaviest] as number)) heaviest = i;
|
|
402
|
+
}
|
|
403
|
+
cuts[heaviest] = (cuts[heaviest] as number) + (total - used);
|
|
404
|
+
return cuts;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function alignOffset(align: Align, free: number): number {
|
|
408
|
+
if (free <= 0) return 0;
|
|
409
|
+
switch (align) {
|
|
410
|
+
case 'center': return Math.floor(free / 2);
|
|
411
|
+
case 'end': return free;
|
|
412
|
+
default: return 0;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
interface JustifySpacing {
|
|
417
|
+
leading: number;
|
|
418
|
+
between: number;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function justifySpacing(justify: Justify, free: number, count: number): JustifySpacing {
|
|
422
|
+
if (free <= 0 || count === 0) return { leading: 0, between: 0 };
|
|
423
|
+
switch (justify) {
|
|
424
|
+
case 'center': return { leading: Math.floor(free / 2), between: 0 };
|
|
425
|
+
case 'end': return { leading: free, between: 0 };
|
|
426
|
+
case 'between':
|
|
427
|
+
return count > 1
|
|
428
|
+
? { leading: 0, between: Math.floor(free / (count - 1)) }
|
|
429
|
+
: { leading: 0, between: 0 };
|
|
430
|
+
case 'around': {
|
|
431
|
+
const each = Math.floor(free / count);
|
|
432
|
+
return { leading: Math.floor(each / 2), between: each };
|
|
433
|
+
}
|
|
434
|
+
case 'evenly': {
|
|
435
|
+
const each = Math.floor(free / (count + 1));
|
|
436
|
+
return { leading: each, between: each };
|
|
437
|
+
}
|
|
438
|
+
default:
|
|
439
|
+
return { leading: 0, between: 0 };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Lay a tree out into `viewport`. Mutates `rect` and `content` in place. */
|
|
444
|
+
export function layout(root: LayoutBox, viewport: Rect): void {
|
|
445
|
+
layoutBox(root, viewport);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function layoutBox(box: LayoutBox, rect: Rect): void {
|
|
449
|
+
const { inset } = frameOf(box);
|
|
450
|
+
box.rect = {
|
|
451
|
+
x: rect.x,
|
|
452
|
+
y: rect.y,
|
|
453
|
+
width: Math.max(0, rect.width),
|
|
454
|
+
height: Math.max(0, rect.height),
|
|
455
|
+
};
|
|
456
|
+
box.content = {
|
|
457
|
+
x: box.rect.x + inset.left,
|
|
458
|
+
y: box.rect.y + inset.top,
|
|
459
|
+
width: Math.max(0, box.rect.width - edgeH(inset)),
|
|
460
|
+
height: Math.max(0, box.rect.height - edgeV(inset)),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
if (isHidden(box)) {
|
|
464
|
+
box.rect = { ...box.rect, width: 0, height: 0 };
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const flow = box.children.filter((c) => !isAbsolute(c) && !isHidden(c));
|
|
469
|
+
const absolute = box.children.filter((c) => isAbsolute(c) && !isHidden(c));
|
|
470
|
+
|
|
471
|
+
if (flow.length > 0) layoutFlow(box, flow);
|
|
472
|
+
for (const child of absolute) layoutAbsolute(box, child);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function layoutFlow(box: LayoutBox, flow: LayoutBox[]): void {
|
|
476
|
+
if (!isWrapping(box) || flow.length < 2) {
|
|
477
|
+
const column = isColumn(box);
|
|
478
|
+
const mainAvail = column ? box.content.height : box.content.width;
|
|
479
|
+
const scrolling = mainOverflow(box) === 'scroll';
|
|
480
|
+
const extent = layoutLine(
|
|
481
|
+
box, flow, box.content,
|
|
482
|
+
scrolling ? (column ? box.scrollTop ?? 0 : box.scrollLeft ?? 0) : 0,
|
|
483
|
+
);
|
|
484
|
+
box.scrollSize = extent > mainAvail
|
|
485
|
+
? (column
|
|
486
|
+
? { width: box.content.width, height: extent }
|
|
487
|
+
: { width: extent, height: box.content.height })
|
|
488
|
+
: undefined;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
layoutWrapped(box, flow);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Wrapping: cut the children into lines, then lay each line out on its own.
|
|
496
|
+
*
|
|
497
|
+
* Each line is a flow in its own right - it grows, shrinks, justifies and
|
|
498
|
+
* aligns exactly as an unwrapped container does - which is why this is a loop
|
|
499
|
+
* around `layoutLine` rather than a second layout algorithm. The lines
|
|
500
|
+
* themselves stack along the cross axis at their natural heights.
|
|
501
|
+
*
|
|
502
|
+
* A wrapping container overflows across, not along: the main axis is the one
|
|
503
|
+
* it just fitted everything into. So its scroll offset and its recorded
|
|
504
|
+
* `scrollSize` are both the cross axis here, which is what a wrapping row
|
|
505
|
+
* inside a ScrollView needs to scroll down through its lines.
|
|
506
|
+
*/
|
|
507
|
+
function layoutWrapped(box: LayoutBox, flow: LayoutBox[]): void {
|
|
508
|
+
const column = isColumn(box);
|
|
509
|
+
const content = box.content;
|
|
510
|
+
const gaps = gapsOf(box);
|
|
511
|
+
const mainAvail = column ? content.height : content.width;
|
|
512
|
+
const crossAvail = column ? content.width : content.height;
|
|
513
|
+
|
|
514
|
+
const mains: number[] = [];
|
|
515
|
+
const crosses: number[] = [];
|
|
516
|
+
for (const child of flow) {
|
|
517
|
+
const margin = resolveEdges(child.style.margin);
|
|
518
|
+
const basis = child.style.basis ?? (column ? child.style.height : child.style.width);
|
|
519
|
+
const fixed = resolveDimension(basis, mainAvail);
|
|
520
|
+
const m = measureBox(
|
|
521
|
+
child,
|
|
522
|
+
column ? crossAvail : mainAvail,
|
|
523
|
+
column ? mainAvail : crossAvail,
|
|
524
|
+
);
|
|
525
|
+
mains.push((fixed ?? (column ? m.height : m.width)) + (column ? edgeV(margin) : edgeH(margin)));
|
|
526
|
+
crosses.push((column ? m.width + edgeH(margin) : m.height + edgeV(margin)));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const lines = splitLines(mains, gaps.main, mainAvail);
|
|
530
|
+
const scrolling = overflowOn(box.style, column ? 'x' : 'y') === 'scroll';
|
|
531
|
+
const offset = scrolling ? (column ? box.scrollLeft ?? 0 : box.scrollTop ?? 0) : 0;
|
|
532
|
+
const origin = column ? content.x : content.y;
|
|
533
|
+
let cursor = origin - offset;
|
|
534
|
+
|
|
535
|
+
for (const [from, to] of lines) {
|
|
536
|
+
let lineCross = 0;
|
|
537
|
+
for (let i = from; i < to; i++) lineCross = Math.max(lineCross, crosses[i] as number);
|
|
538
|
+
|
|
539
|
+
layoutLine(
|
|
540
|
+
box,
|
|
541
|
+
flow.slice(from, to),
|
|
542
|
+
column
|
|
543
|
+
? { x: cursor, y: content.y, width: lineCross, height: content.height }
|
|
544
|
+
: { x: content.x, y: cursor, width: content.width, height: lineCross },
|
|
545
|
+
0,
|
|
546
|
+
);
|
|
547
|
+
cursor += lineCross + gaps.cross;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const extent = cursor + offset - origin - gaps.cross;
|
|
551
|
+
box.scrollSize = extent > crossAvail
|
|
552
|
+
? (column
|
|
553
|
+
? { width: extent, height: content.height }
|
|
554
|
+
: { width: content.width, height: extent })
|
|
555
|
+
: undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* One line of a flow, laid out into `region`.
|
|
560
|
+
*
|
|
561
|
+
* Returns the main-axis extent the children actually came to, which is what
|
|
562
|
+
* tells the caller whether there is anything to scroll.
|
|
563
|
+
*/
|
|
564
|
+
function layoutLine(
|
|
565
|
+
box: LayoutBox,
|
|
566
|
+
flow: LayoutBox[],
|
|
567
|
+
content: Rect,
|
|
568
|
+
scrollOffset: number,
|
|
569
|
+
): number {
|
|
570
|
+
const column = isColumn(box);
|
|
571
|
+
const gap = gapsOf(box).main;
|
|
572
|
+
|
|
573
|
+
const mainAvail = column ? content.height : content.width;
|
|
574
|
+
const crossAvail = column ? content.width : content.height;
|
|
575
|
+
const gapTotal = flow.length > 1 ? gap * (flow.length - 1) : 0;
|
|
576
|
+
|
|
577
|
+
// 1. base sizes
|
|
578
|
+
const margins = flow.map((c) => resolveEdges(c.style.margin));
|
|
579
|
+
const bases: number[] = [];
|
|
580
|
+
for (let i = 0; i < flow.length; i++) {
|
|
581
|
+
const child = flow[i] as LayoutBox;
|
|
582
|
+
const m = margins[i] as Edges;
|
|
583
|
+
const marginMain = column ? edgeV(m) : edgeH(m);
|
|
584
|
+
|
|
585
|
+
const basis = child.style.basis ?? (column ? child.style.height : child.style.width);
|
|
586
|
+
const fixed = resolveDimension(basis, mainAvail);
|
|
587
|
+
if (fixed !== null) {
|
|
588
|
+
bases.push(fixed + marginMain);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const measured = measureBox(
|
|
593
|
+
child,
|
|
594
|
+
column ? crossAvail : Math.max(0, mainAvail - gapTotal),
|
|
595
|
+
column ? Math.max(0, mainAvail - gapTotal) : crossAvail,
|
|
596
|
+
);
|
|
597
|
+
bases.push((column ? measured.height : measured.width) + marginMain);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 2. grow or shrink into the space that is actually there
|
|
601
|
+
//
|
|
602
|
+
// A scroll container is the exception: shrinking its children to fit is
|
|
603
|
+
// exactly what it must not do, because then there is nothing to scroll.
|
|
604
|
+
const scrolling = mainOverflow(box) === 'scroll';
|
|
605
|
+
const used = bases.reduce((a, b) => a + b, 0) + gapTotal;
|
|
606
|
+
let free = scrolling ? Math.max(0, mainAvail - used) : mainAvail - used;
|
|
607
|
+
const sizes = bases.slice();
|
|
608
|
+
|
|
609
|
+
if (free > 0) {
|
|
610
|
+
const grows = flow.map((c) => Math.max(0, c.style.flex ?? 0));
|
|
611
|
+
if (grows.some((g) => g > 0)) {
|
|
612
|
+
const extra = distribute(free, grows);
|
|
613
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
614
|
+
sizes[i] = (sizes[i] as number) + (extra[i] as number);
|
|
615
|
+
}
|
|
616
|
+
free = 0;
|
|
617
|
+
}
|
|
618
|
+
} else if (free < 0) {
|
|
619
|
+
// Shrink proportionally to size * shrink factor, never below minWidth.
|
|
620
|
+
const deficit = -free;
|
|
621
|
+
//
|
|
622
|
+
// Who gives way, and how a terminal should degrade when it cannot fit
|
|
623
|
+
// everything:
|
|
624
|
+
//
|
|
625
|
+
// A child that asked to grow gives way first. `flex` means elastic, and
|
|
626
|
+
// treating it as rigid is what makes a long document push a header and a
|
|
627
|
+
// status bar out of the frame - they are the fixed-size rows, so they are
|
|
628
|
+
// the ones the deficit would land on.
|
|
629
|
+
//
|
|
630
|
+
// After that the axis decides. Sideways, a rigid child shrinks and its
|
|
631
|
+
// text truncates, which is how terminals have always narrowed. Downwards,
|
|
632
|
+
// it does not: a panel with half a border and no bottom edge is unreadable
|
|
633
|
+
// in a way that a panel below the fold is not, so the overflow is clipped
|
|
634
|
+
// instead (see the clamp below). Either way an explicit `shrink` wins.
|
|
635
|
+
const rigidShrink = column ? 0 : 1;
|
|
636
|
+
const weights = flow.map((c, i) => {
|
|
637
|
+
const shrink = c.style.shrink
|
|
638
|
+
?? (c.style.flex !== undefined ? Math.max(1, c.style.flex) : rigidShrink);
|
|
639
|
+
return Math.max(0, shrink) * (bases[i] as number);
|
|
640
|
+
});
|
|
641
|
+
if (weights.some((w) => w > 0)) {
|
|
642
|
+
const cuts = distributeCuts(deficit, weights);
|
|
643
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
644
|
+
const child = flow[i] as LayoutBox;
|
|
645
|
+
const min = column ? child.style.minHeight : child.style.minWidth;
|
|
646
|
+
const next = (sizes[i] as number) - (cuts[i] as number);
|
|
647
|
+
sizes[i] = Math.max(min ?? 0, next);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
free = mainAvail - (sizes.reduce((a, b) => a + b, 0) + gapTotal);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Whatever is left over after growing and shrinking, nothing may be placed
|
|
654
|
+
// outside the container. A terminal clips at the edge, so a child that keeps
|
|
655
|
+
// its oversized rect is not "overflowing" in any useful sense - it is a
|
|
656
|
+
// child that measures itself wrong, and a component that sizes itself from
|
|
657
|
+
// that measurement never converges. Cells run out in painting order: earlier
|
|
658
|
+
// children keep their size, later ones get what remains.
|
|
659
|
+
if (!scrolling) {
|
|
660
|
+
let room = mainAvail;
|
|
661
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
662
|
+
if (i > 0) room -= gap;
|
|
663
|
+
const size = sizes[i] as number;
|
|
664
|
+
if (size > room) sizes[i] = Math.max(0, room);
|
|
665
|
+
room -= sizes[i] as number;
|
|
666
|
+
}
|
|
667
|
+
free = Math.max(0, room);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 3. place
|
|
671
|
+
const spacing = justifySpacing(box.style.justify ?? 'start', Math.max(0, free), flow.length);
|
|
672
|
+
const origin = (column ? content.y : content.x) + spacing.leading;
|
|
673
|
+
let cursor = origin - scrollOffset;
|
|
674
|
+
|
|
675
|
+
for (let i = 0; i < flow.length; i++) {
|
|
676
|
+
const child = flow[i] as LayoutBox;
|
|
677
|
+
const m = margins[i] as Edges;
|
|
678
|
+
const mainSize = Math.max(0, (sizes[i] as number) - (column ? edgeV(m) : edgeH(m)));
|
|
679
|
+
|
|
680
|
+
const align = child.style.alignSelf ?? box.style.align ?? 'stretch';
|
|
681
|
+
const crossMargin = column ? edgeH(m) : edgeV(m);
|
|
682
|
+
const crossRoom = Math.max(0, crossAvail - crossMargin);
|
|
683
|
+
|
|
684
|
+
let crossSize: number;
|
|
685
|
+
const crossDim = column ? child.style.width : child.style.height;
|
|
686
|
+
const fixedCross = resolveDimension(crossDim, crossAvail);
|
|
687
|
+
if (fixedCross !== null) {
|
|
688
|
+
crossSize = fixedCross;
|
|
689
|
+
} else if (align === 'stretch') {
|
|
690
|
+
crossSize = crossRoom;
|
|
691
|
+
} else {
|
|
692
|
+
const measured = measureBox(
|
|
693
|
+
child,
|
|
694
|
+
column ? crossRoom : mainSize,
|
|
695
|
+
column ? mainSize : crossRoom,
|
|
696
|
+
);
|
|
697
|
+
crossSize = column ? measured.width : measured.height;
|
|
698
|
+
}
|
|
699
|
+
crossSize = clamp(
|
|
700
|
+
crossSize,
|
|
701
|
+
column ? child.style.minWidth : child.style.minHeight,
|
|
702
|
+
column ? child.style.maxWidth : child.style.maxHeight,
|
|
703
|
+
);
|
|
704
|
+
crossSize = Math.min(crossSize, crossRoom);
|
|
705
|
+
|
|
706
|
+
const crossStart =
|
|
707
|
+
(column ? content.x + m.left : content.y + m.top) +
|
|
708
|
+
alignOffset(align === 'stretch' ? 'start' : align, crossRoom - crossSize);
|
|
709
|
+
|
|
710
|
+
const mainStart = cursor + (column ? m.top : m.left);
|
|
711
|
+
|
|
712
|
+
layoutBox(
|
|
713
|
+
child,
|
|
714
|
+
column
|
|
715
|
+
? { x: crossStart, y: mainStart, width: crossSize, height: mainSize }
|
|
716
|
+
: { x: mainStart, y: crossStart, width: mainSize, height: crossSize },
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
cursor += (sizes[i] as number) + gap + (i < flow.length - 1 ? spacing.between : 0);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// 4. hand back the extent, so the caller knows how far it can scroll
|
|
723
|
+
return cursor + scrollOffset - origin - gap;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Absolute children position against the parent's content box. Opposite
|
|
728
|
+
* offsets both set means "stretch between them", which is how a full-bleed
|
|
729
|
+
* scrim or a bottom-anchored bar is written.
|
|
730
|
+
*/
|
|
731
|
+
function layoutAbsolute(parent: LayoutBox, child: LayoutBox): void {
|
|
732
|
+
const box = parent.content;
|
|
733
|
+
const s = child.style;
|
|
734
|
+
|
|
735
|
+
const measured = measureBox(child, box.width, box.height);
|
|
736
|
+
|
|
737
|
+
let width = resolveDimension(s.width, box.width) ?? measured.width;
|
|
738
|
+
let height = resolveDimension(s.height, box.height) ?? measured.height;
|
|
739
|
+
|
|
740
|
+
let x: number;
|
|
741
|
+
if (s.left !== undefined && s.right !== undefined) {
|
|
742
|
+
x = box.x + s.left;
|
|
743
|
+
width = Math.max(0, box.width - s.left - s.right);
|
|
744
|
+
} else if (s.right !== undefined) {
|
|
745
|
+
x = box.x + box.width - s.right - width;
|
|
746
|
+
} else {
|
|
747
|
+
x = box.x + (s.left ?? 0);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let y: number;
|
|
751
|
+
if (s.top !== undefined && s.bottom !== undefined) {
|
|
752
|
+
y = box.y + s.top;
|
|
753
|
+
height = Math.max(0, box.height - s.top - s.bottom);
|
|
754
|
+
} else if (s.bottom !== undefined) {
|
|
755
|
+
y = box.y + box.height - s.bottom - height;
|
|
756
|
+
} else {
|
|
757
|
+
y = box.y + (s.top ?? 0);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
width = clamp(width, s.minWidth, s.maxWidth);
|
|
761
|
+
height = clamp(height, s.minHeight, s.maxHeight);
|
|
762
|
+
|
|
763
|
+
layoutBox(child, { x, y, width, height });
|
|
764
|
+
}
|