@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.
Files changed (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/index.d.ts +12 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +11 -0
  6. package/dist/app/app.d.ts +200 -0
  7. package/dist/app/app.d.ts.map +1 -0
  8. package/dist/app/app.js +980 -0
  9. package/dist/core/animation.d.ts +34 -0
  10. package/dist/core/animation.d.ts.map +1 -0
  11. package/dist/core/animation.js +125 -0
  12. package/dist/core/clipboard.d.ts +29 -0
  13. package/dist/core/clipboard.d.ts.map +1 -0
  14. package/dist/core/clipboard.js +30 -0
  15. package/dist/core/commands.d.ts +42 -0
  16. package/dist/core/commands.d.ts.map +1 -0
  17. package/dist/core/commands.js +146 -0
  18. package/dist/core/components.d.ts +27 -0
  19. package/dist/core/components.d.ts.map +1 -0
  20. package/dist/core/components.js +78 -0
  21. package/dist/core/events.d.ts +23 -0
  22. package/dist/core/events.d.ts.map +1 -0
  23. package/dist/core/events.js +77 -0
  24. package/dist/core/focus.d.ts +69 -0
  25. package/dist/core/focus.d.ts.map +1 -0
  26. package/dist/core/focus.js +336 -0
  27. package/dist/core/i18n.d.ts +27 -0
  28. package/dist/core/i18n.d.ts.map +1 -0
  29. package/dist/core/i18n.js +88 -0
  30. package/dist/core/keybindings.d.ts +53 -0
  31. package/dist/core/keybindings.d.ts.map +1 -0
  32. package/dist/core/keybindings.js +163 -0
  33. package/dist/core/layers.d.ts +28 -0
  34. package/dist/core/layers.d.ts.map +1 -0
  35. package/dist/core/layers.js +84 -0
  36. package/dist/core/manifest.d.ts +22 -0
  37. package/dist/core/manifest.d.ts.map +1 -0
  38. package/dist/core/manifest.js +85 -0
  39. package/dist/core/navigation.d.ts +47 -0
  40. package/dist/core/navigation.d.ts.map +1 -0
  41. package/dist/core/navigation.js +110 -0
  42. package/dist/core/resources.d.ts +115 -0
  43. package/dist/core/resources.d.ts.map +1 -0
  44. package/dist/core/resources.js +321 -0
  45. package/dist/core/services.d.ts +21 -0
  46. package/dist/core/services.d.ts.map +1 -0
  47. package/dist/core/services.js +63 -0
  48. package/dist/core/store.d.ts +60 -0
  49. package/dist/core/store.d.ts.map +1 -0
  50. package/dist/core/store.js +593 -0
  51. package/dist/core/surfaces.d.ts +81 -0
  52. package/dist/core/surfaces.d.ts.map +1 -0
  53. package/dist/core/surfaces.js +237 -0
  54. package/dist/core/syntax.d.ts +49 -0
  55. package/dist/core/syntax.d.ts.map +1 -0
  56. package/dist/core/syntax.js +172 -0
  57. package/dist/core/when.d.ts +15 -0
  58. package/dist/core/when.d.ts.map +1 -0
  59. package/dist/core/when.js +224 -0
  60. package/dist/index.d.ts +49 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +54 -0
  63. package/dist/jsx/factory.d.ts +27 -0
  64. package/dist/jsx/factory.d.ts.map +1 -0
  65. package/dist/jsx/factory.js +102 -0
  66. package/dist/jsx/intrinsics.d.ts +96 -0
  67. package/dist/jsx/intrinsics.d.ts.map +1 -0
  68. package/dist/jsx/intrinsics.js +1 -0
  69. package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
  70. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
  71. package/dist/jsx/jsx-dev-runtime.js +12 -0
  72. package/dist/jsx/jsx-runtime.d.ts +42 -0
  73. package/dist/jsx/jsx-runtime.d.ts.map +1 -0
  74. package/dist/jsx/jsx-runtime.js +16 -0
  75. package/dist/render/buffer.d.ts +77 -0
  76. package/dist/render/buffer.d.ts.map +1 -0
  77. package/dist/render/buffer.js +275 -0
  78. package/dist/render/color.d.ts +35 -0
  79. package/dist/render/color.d.ts.map +1 -0
  80. package/dist/render/color.js +160 -0
  81. package/dist/render/diff.d.ts +37 -0
  82. package/dist/render/diff.d.ts.map +1 -0
  83. package/dist/render/diff.js +66 -0
  84. package/dist/render/layout.d.ts +72 -0
  85. package/dist/render/layout.d.ts.map +1 -0
  86. package/dist/render/layout.js +633 -0
  87. package/dist/render/static.d.ts +63 -0
  88. package/dist/render/static.d.ts.map +1 -0
  89. package/dist/render/static.js +209 -0
  90. package/dist/runtime/bindings.d.ts +46 -0
  91. package/dist/runtime/bindings.d.ts.map +1 -0
  92. package/dist/runtime/bindings.js +116 -0
  93. package/dist/runtime/hooks.d.ts +284 -0
  94. package/dist/runtime/hooks.d.ts.map +1 -0
  95. package/dist/runtime/hooks.js +846 -0
  96. package/dist/runtime/instance.d.ts +106 -0
  97. package/dist/runtime/instance.d.ts.map +1 -0
  98. package/dist/runtime/instance.js +181 -0
  99. package/dist/runtime/paint.d.ts +27 -0
  100. package/dist/runtime/paint.d.ts.map +1 -0
  101. package/dist/runtime/paint.js +567 -0
  102. package/dist/runtime/reconcile.d.ts +23 -0
  103. package/dist/runtime/reconcile.d.ts.map +1 -0
  104. package/dist/runtime/reconcile.js +260 -0
  105. package/dist/runtime/runtime.d.ts +42 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -0
  107. package/dist/runtime/runtime.js +1 -0
  108. package/dist/runtime/style.d.ts +55 -0
  109. package/dist/runtime/style.d.ts.map +1 -0
  110. package/dist/runtime/style.js +143 -0
  111. package/dist/themes/borders.d.ts +10 -0
  112. package/dist/themes/borders.d.ts.map +1 -0
  113. package/dist/themes/borders.js +88 -0
  114. package/dist/themes/builtin.d.ts +31 -0
  115. package/dist/themes/builtin.d.ts.map +1 -0
  116. package/dist/themes/builtin.js +291 -0
  117. package/dist/themes/glyphs.d.ts +15 -0
  118. package/dist/themes/glyphs.d.ts.map +1 -0
  119. package/dist/themes/glyphs.js +97 -0
  120. package/dist/themes/index.d.ts +5 -0
  121. package/dist/themes/index.d.ts.map +1 -0
  122. package/dist/themes/index.js +4 -0
  123. package/dist/themes/registry.d.ts +20 -0
  124. package/dist/themes/registry.d.ts.map +1 -0
  125. package/dist/themes/registry.js +204 -0
  126. package/dist/types/adapter.d.ts +38 -0
  127. package/dist/types/adapter.d.ts.map +1 -0
  128. package/dist/types/adapter.js +1 -0
  129. package/dist/types/animation.d.ts +31 -0
  130. package/dist/types/animation.d.ts.map +1 -0
  131. package/dist/types/animation.js +1 -0
  132. package/dist/types/app.d.ts +148 -0
  133. package/dist/types/app.d.ts.map +1 -0
  134. package/dist/types/app.js +1 -0
  135. package/dist/types/async.d.ts +30 -0
  136. package/dist/types/async.d.ts.map +1 -0
  137. package/dist/types/async.js +1 -0
  138. package/dist/types/capabilities.d.ts +44 -0
  139. package/dist/types/capabilities.d.ts.map +1 -0
  140. package/dist/types/capabilities.js +33 -0
  141. package/dist/types/cells.d.ts +72 -0
  142. package/dist/types/cells.d.ts.map +1 -0
  143. package/dist/types/cells.js +10 -0
  144. package/dist/types/command.d.ts +127 -0
  145. package/dist/types/command.d.ts.map +1 -0
  146. package/dist/types/command.js +1 -0
  147. package/dist/types/component-registry.d.ts +77 -0
  148. package/dist/types/component-registry.d.ts.map +1 -0
  149. package/dist/types/component-registry.js +1 -0
  150. package/dist/types/disposable.d.ts +8 -0
  151. package/dist/types/disposable.d.ts.map +1 -0
  152. package/dist/types/disposable.js +1 -0
  153. package/dist/types/focus.d.ts +72 -0
  154. package/dist/types/focus.d.ts.map +1 -0
  155. package/dist/types/focus.js +1 -0
  156. package/dist/types/geometry.d.ts +28 -0
  157. package/dist/types/geometry.d.ts.map +1 -0
  158. package/dist/types/geometry.js +14 -0
  159. package/dist/types/graph.d.ts +129 -0
  160. package/dist/types/graph.d.ts.map +1 -0
  161. package/dist/types/graph.js +31 -0
  162. package/dist/types/i18n.d.ts +23 -0
  163. package/dist/types/i18n.d.ts.map +1 -0
  164. package/dist/types/i18n.js +1 -0
  165. package/dist/types/index.d.ts +32 -0
  166. package/dist/types/index.d.ts.map +1 -0
  167. package/dist/types/index.js +31 -0
  168. package/dist/types/input.d.ts +65 -0
  169. package/dist/types/input.d.ts.map +1 -0
  170. package/dist/types/input.js +1 -0
  171. package/dist/types/keybinding.d.ts +49 -0
  172. package/dist/types/keybinding.d.ts.map +1 -0
  173. package/dist/types/keybinding.js +1 -0
  174. package/dist/types/layer.d.ts +54 -0
  175. package/dist/types/layer.d.ts.map +1 -0
  176. package/dist/types/layer.js +1 -0
  177. package/dist/types/manifest.d.ts +114 -0
  178. package/dist/types/manifest.d.ts.map +1 -0
  179. package/dist/types/manifest.js +1 -0
  180. package/dist/types/markdown.d.ts +58 -0
  181. package/dist/types/markdown.d.ts.map +1 -0
  182. package/dist/types/markdown.js +1 -0
  183. package/dist/types/navigation.d.ts +40 -0
  184. package/dist/types/navigation.d.ts.map +1 -0
  185. package/dist/types/navigation.js +1 -0
  186. package/dist/types/render.d.ts +72 -0
  187. package/dist/types/render.d.ts.map +1 -0
  188. package/dist/types/render.js +1 -0
  189. package/dist/types/resource.d.ts +170 -0
  190. package/dist/types/resource.d.ts.map +1 -0
  191. package/dist/types/resource.js +1 -0
  192. package/dist/types/services.d.ts +24 -0
  193. package/dist/types/services.d.ts.map +1 -0
  194. package/dist/types/services.js +3 -0
  195. package/dist/types/shell.d.ts +32 -0
  196. package/dist/types/shell.d.ts.map +1 -0
  197. package/dist/types/shell.js +1 -0
  198. package/dist/types/store.d.ts +124 -0
  199. package/dist/types/store.d.ts.map +1 -0
  200. package/dist/types/store.js +1 -0
  201. package/dist/types/stream.d.ts +18 -0
  202. package/dist/types/stream.d.ts.map +1 -0
  203. package/dist/types/stream.js +1 -0
  204. package/dist/types/style.d.ts +166 -0
  205. package/dist/types/style.d.ts.map +1 -0
  206. package/dist/types/style.js +1 -0
  207. package/dist/types/surface.d.ts +97 -0
  208. package/dist/types/surface.d.ts.map +1 -0
  209. package/dist/types/surface.js +1 -0
  210. package/dist/types/syntax.d.ts +68 -0
  211. package/dist/types/syntax.d.ts.map +1 -0
  212. package/dist/types/syntax.js +5 -0
  213. package/dist/types/terminal.d.ts +52 -0
  214. package/dist/types/terminal.d.ts.map +1 -0
  215. package/dist/types/terminal.js +1 -0
  216. package/dist/types/theme.d.ts +117 -0
  217. package/dist/types/theme.d.ts.map +1 -0
  218. package/dist/types/theme.js +1 -0
  219. package/dist/types/when.d.ts +19 -0
  220. package/dist/types/when.d.ts.map +1 -0
  221. package/dist/types/when.js +1 -0
  222. package/dist/ui/primitives.d.ts +56 -0
  223. package/dist/ui/primitives.d.ts.map +1 -0
  224. package/dist/ui/primitives.js +108 -0
  225. package/dist/ui/screen.d.ts +23 -0
  226. package/dist/ui/screen.d.ts.map +1 -0
  227. package/dist/ui/screen.js +18 -0
  228. package/dist/util/disposable.d.ts +6 -0
  229. package/dist/util/disposable.d.ts.map +1 -0
  230. package/dist/util/disposable.js +49 -0
  231. package/dist/util/markdown.d.ts +24 -0
  232. package/dist/util/markdown.d.ts.map +1 -0
  233. package/dist/util/markdown.js +220 -0
  234. package/dist/util/paths.d.ts +47 -0
  235. package/dist/util/paths.d.ts.map +1 -0
  236. package/dist/util/paths.js +134 -0
  237. package/dist/util/stream.d.ts +32 -0
  238. package/dist/util/stream.d.ts.map +1 -0
  239. package/dist/util/stream.js +206 -0
  240. package/dist/util/text.d.ts +65 -0
  241. package/dist/util/text.d.ts.map +1 -0
  242. package/dist/util/text.js +419 -0
  243. package/package.json +70 -0
  244. package/src/adapters/index.ts +11 -0
  245. package/src/app/app.ts +1096 -0
  246. package/src/core/animation.ts +144 -0
  247. package/src/core/clipboard.ts +40 -0
  248. package/src/core/commands.ts +169 -0
  249. package/src/core/components.ts +91 -0
  250. package/src/core/events.ts +95 -0
  251. package/src/core/focus.ts +358 -0
  252. package/src/core/i18n.ts +107 -0
  253. package/src/core/keybindings.ts +184 -0
  254. package/src/core/layers.ts +94 -0
  255. package/src/core/manifest.ts +84 -0
  256. package/src/core/navigation.ts +135 -0
  257. package/src/core/resources.ts +362 -0
  258. package/src/core/services.ts +69 -0
  259. package/src/core/store.ts +640 -0
  260. package/src/core/surfaces.ts +292 -0
  261. package/src/core/syntax.ts +200 -0
  262. package/src/core/when.ts +238 -0
  263. package/src/index.ts +76 -0
  264. package/src/jsx/factory.ts +124 -0
  265. package/src/jsx/intrinsics.ts +99 -0
  266. package/src/jsx/jsx-dev-runtime.ts +23 -0
  267. package/src/jsx/jsx-runtime.ts +76 -0
  268. package/src/render/buffer.ts +318 -0
  269. package/src/render/color.ts +180 -0
  270. package/src/render/diff.ts +99 -0
  271. package/src/render/layout.ts +764 -0
  272. package/src/render/static.ts +290 -0
  273. package/src/runtime/bindings.ts +150 -0
  274. package/src/runtime/hooks.ts +1057 -0
  275. package/src/runtime/instance.ts +284 -0
  276. package/src/runtime/paint.ts +737 -0
  277. package/src/runtime/reconcile.ts +335 -0
  278. package/src/runtime/runtime.ts +47 -0
  279. package/src/runtime/style.ts +205 -0
  280. package/src/themes/borders.ts +95 -0
  281. package/src/themes/builtin.ts +301 -0
  282. package/src/themes/glyphs.ts +101 -0
  283. package/src/themes/index.ts +4 -0
  284. package/src/themes/registry.ts +224 -0
  285. package/src/types/adapter.ts +41 -0
  286. package/src/types/animation.ts +34 -0
  287. package/src/types/app.ts +149 -0
  288. package/src/types/async.ts +35 -0
  289. package/src/types/capabilities.ts +79 -0
  290. package/src/types/cells.ts +79 -0
  291. package/src/types/command.ts +139 -0
  292. package/src/types/component-registry.ts +82 -0
  293. package/src/types/disposable.ts +8 -0
  294. package/src/types/focus.ts +77 -0
  295. package/src/types/geometry.ts +50 -0
  296. package/src/types/graph.ts +174 -0
  297. package/src/types/i18n.ts +25 -0
  298. package/src/types/index.ts +31 -0
  299. package/src/types/input.ts +81 -0
  300. package/src/types/keybinding.ts +51 -0
  301. package/src/types/layer.ts +46 -0
  302. package/src/types/manifest.ts +101 -0
  303. package/src/types/markdown.ts +47 -0
  304. package/src/types/navigation.ts +42 -0
  305. package/src/types/render.ts +86 -0
  306. package/src/types/resource.ts +185 -0
  307. package/src/types/services.ts +28 -0
  308. package/src/types/shell.ts +30 -0
  309. package/src/types/store.ts +148 -0
  310. package/src/types/stream.ts +24 -0
  311. package/src/types/style.ts +205 -0
  312. package/src/types/surface.ts +119 -0
  313. package/src/types/syntax.ts +93 -0
  314. package/src/types/terminal.ts +58 -0
  315. package/src/types/theme.ts +121 -0
  316. package/src/types/when.ts +21 -0
  317. package/src/ui/primitives.ts +118 -0
  318. package/src/ui/screen.ts +41 -0
  319. package/src/util/disposable.ts +49 -0
  320. package/src/util/markdown.ts +225 -0
  321. package/src/util/paths.ts +138 -0
  322. package/src/util/stream.ts +213 -0
  323. 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
+ }