packages/core/src/lib/layout/box.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import {
2 layoutDiagnostic,
3 type LayoutDiagnosticContext,
4 } from '../style/diagnostics.js';
5 import { normalizeEdges } from '../style/normalize.js';
6 import { resolveLength } from '../style/resolve-length.js';
7 import type { BoxStyle, Length, ResolvedBoxStyle } from '../style/types.js';
8 import type { Constraints } from './types.js';
9
10 /**
11 * Resolves declared box sizing and padding into border and content boxes under parent constraints.
12 *
13 * @param style - Declared box sizing and edge properties. See {@link BoxStyle}.
14 * @param constraints - Available dimensions used to resolve percentages and constraints. See
15 * {@link Constraints} .
16 * @param context - Component and property context for diagnostic messages. See
17 * {@link LayoutDiagnosticContext} .
18 * @returns Resolved box dimensions and edge values. See {@link ResolvedBoxStyle}.
19 *
20 * @see {@link BoxStyle}
21 * @see {@link Constraints}
22 * @see {@link LayoutDiagnosticContext}
23 * @see {@link ResolvedBoxStyle}
24 */
25 export function resolveBox(
26 style: Readonly<BoxStyle>,
27 constraints: Readonly<Constraints>,
28 context: Readonly<LayoutDiagnosticContext> = {},
29 ): ResolvedBoxStyle {
30 const diagnosticContext = {
31 component: context.component ?? 'layout',
32 algorithm: context.algorithm ?? 'box-resolution',
33 constraints,
34 };
35 validateConstraints(constraints, diagnosticContext);
36 const padding = normalizeEdges(style.padding, 'padding', diagnosticContext);
37
38 const horizontal = resolveAxis(
39 'width',
40 style.width,
41 style.minWidth,
42 style.maxWidth,
43 constraints.minWidth,
44 constraints.maxWidth,
45 padding.left,
46 padding.right,
47 diagnosticContext,
48 );
49 const vertical = resolveAxis(
50 'height',
51 style.height,
52 style.minHeight,
53 style.maxHeight,
54 constraints.minHeight,
55 constraints.maxHeight,
56 padding.top,
57 padding.bottom,
58 diagnosticContext,
59 );
60
61 return {
62 borderBox: {
63 x: 0,
64 y: 0,
65 width: horizontal.border,
66 height: vertical.border,
67 },
68 contentBox: {
69 x: padding.left,
70 y: padding.top,
71 width: horizontal.content,
72 height: vertical.content,
73 },
74 };
75 }
76
77 function resolveAxis(
78 axis: 'width' | 'height',
79 specified: Length | undefined,
80 specifiedMinimum: Exclude<Length, 'auto'> | undefined,
81 specifiedMaximum: Exclude<Length, 'auto'> | undefined,
82 constraintMinimum: number,
83 constraintMaximum: number,
84 paddingStart: number,
85 paddingEnd: number,
86 context: Readonly<LayoutDiagnosticContext>,
87 ): { border: number; content: number } {
88 const minimumProperty = axis === 'width' ? 'minWidth' : 'minHeight';
89 const maximumProperty = axis === 'width' ? 'maxWidth' : 'maxHeight';
90 const minimum =
91 specifiedMinimum === undefined
92 ? 0
93 : resolveAllocationLength(
94 specifiedMinimum,
95 constraintMaximum,
96 minimumProperty,
97 context,
98 );
99 const maximum =
100 specifiedMaximum === undefined
101 ? Number.POSITIVE_INFINITY
102 : resolveAllocationLength(
103 specifiedMaximum,
104 constraintMaximum,
105 maximumProperty,
106 context,
107 );
108
109 if (minimum > maximum) {
110 throw layoutDiagnostic({
111 ...context,
112 property: minimumProperty,
113 value: minimum,
114 reason: `${minimumProperty} ${minimum} must not exceed ${maximumProperty} ${maximum}`,
115 });
116 }
117
118 const padding = requireFiniteArithmetic(
119 paddingStart + paddingEnd,
120 axis,
121 context,
122 );
123 const resolved =
124 specified === undefined
125 ? undefined
126 : resolveLength(specified, constraintMaximum, axis, context);
127 let content: number;
128 if (resolved === undefined) {
129 if (!Number.isFinite(constraintMaximum)) {
130 throw layoutDiagnostic({
131 ...context,
132 property: axis,
133 value: specified ?? 'auto',
134 reason: 'requires a definite finite allocation for auto sizing',
135 });
136 }
137 content = Math.max(
138 0,
139 requireFiniteArithmetic(constraintMaximum - padding, axis, context),
140 );
141 } else {
142 if (resolved < 0) {
143 throw layoutDiagnostic({
144 ...context,
145 property: axis,
146 value: specified,
147 reason: 'must be nonnegative when used as a layout allocation',
148 });
149 }
150 content = resolved;
151 }
152
153 content = Math.min(maximum, Math.max(minimum, content));
154 const paddedContent = requireFiniteArithmetic(
155 content + padding,
156 axis,
157 context,
158 );
159 const border = Math.min(
160 constraintMaximum,
161 Math.max(constraintMinimum, paddedContent),
162 );
163 return {
164 border: requireFiniteArithmetic(border, axis, context),
165 content: Math.max(
166 0,
167 requireFiniteArithmetic(border - padding, axis, context),
168 ),
169 };
170 }
171
172 function requireFiniteArithmetic(
173 value: number,
174 property: string,
175 context: Readonly<LayoutDiagnosticContext>,
176 ): number {
177 if (!Number.isFinite(value)) {
178 throw layoutDiagnostic({
179 ...context,
180 property,
181 value,
182 reason: 'resolved box arithmetic must remain finite',
183 });
184 }
185 return value;
186 }
187
188 function resolveAllocationLength(
189 value: Exclude<Length, 'auto'>,
190 reference: number,
191 property: string,
192 context: Readonly<LayoutDiagnosticContext>,
193 ): number {
194 const resolved = resolveLength(value, reference, property, context)!;
195 if (resolved < 0) {
196 throw layoutDiagnostic({
197 ...context,
198 property,
199 value,
200 reason: 'must be nonnegative when used as a layout allocation',
201 });
202 }
203 return resolved;
204 }
205
206 function validateConstraints(
207 constraints: Readonly<Constraints>,
208 context: Readonly<LayoutDiagnosticContext>,
209 ): void {
210 validateConstraint(
211 constraints.minWidth,
212 'constraints.minWidth',
213 false,
214 context,
215 );
216 validateConstraint(
217 constraints.maxWidth,
218 'constraints.maxWidth',
219 true,
220 context,
221 );
222 validateConstraint(
223 constraints.minHeight,
224 'constraints.minHeight',
225 false,
226 context,
227 );
228 validateConstraint(
229 constraints.maxHeight,
230 'constraints.maxHeight',
231 true,
232 context,
233 );
234
235 if (constraints.minWidth > constraints.maxWidth) {
236 throw layoutDiagnostic({
237 ...context,
238 property: 'constraints.minWidth',
239 value: constraints.minWidth,
240 reason: `constraints.minWidth ${constraints.minWidth} must not exceed constraints.maxWidth ${constraints.maxWidth}`,
241 });
242 }
243 if (constraints.minHeight > constraints.maxHeight) {
244 throw layoutDiagnostic({
245 ...context,
246 property: 'constraints.minHeight',
247 value: constraints.minHeight,
248 reason: `constraints.minHeight ${constraints.minHeight} must not exceed constraints.maxHeight ${constraints.maxHeight}`,
249 });
250 }
251 }
252
253 function validateConstraint(
254 value: number,
255 property: string,
256 allowPositiveInfinity: boolean,
257 context: Readonly<LayoutDiagnosticContext>,
258 ): void {
259 if (
260 !Number.isFinite(value) &&
261 !(allowPositiveInfinity && value === Number.POSITIVE_INFINITY)
262 ) {
263 throw layoutDiagnostic({
264 ...context,
265 property,
266 value,
267 reason: 'must be finite',
268 });
269 }
270 if (value < 0) {
271 throw layoutDiagnostic({
272 ...context,
273 property,
274 value,
275 reason: 'must be nonnegative',
276 });
277 }
278 }
279
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.