Skip to content

packages/core/src/lib/layout/box.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

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 built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.