packages/core/src/lib/layout/measure.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import { getStyleMeasurementMode, withStyleMeasurement, rejectResponsiveMeasurement, UnsupportedResponsiveMeasurement } from '../style/responsive.js';
2 import { layoutDiagnostic } from '../style/diagnostics.js';
3 import { GLOBAL_STATE } from '../global-state.js';
4 import { EMPTY_STYLE } from '../style/empty-style.js';
5 import {
6 takePreNormalizedStyle,
7 withoutSystemFilter,
8 } from '../style/resolve-dispatch.js';
9 import {
10 createPrimitiveReceiverToken,
11 getPrimitiveDefinition,
12 getPrimitiveReceiverToken,
13 getPolicyPreparationSource,
14 } from '../element/metadata.js';
15 import { resolvePrimitiveInputs } from '../element/resolve-primitive-inputs.js';
16 import { recordLayoutMeasurement } from './diagnostics.js';
17 import type {
18 CanvasMeasurementService,
19 MeasureResult,
20 } from '../types.js';
21 import type { PibblElement } from '../element/types.js';
22 import { prepareElementPolicies } from '../element/children.js';
23 import type { Constraints } from './types.js';
24 import { untracked, withSignalWriteForbidden } from '../signals/graph.js';
25
26 export type {
27 CanvasMeasurementService,
28 MeasureInput,
29 MeasureResult,
30 } from '../types.js';
31
32 /**
33 * Measures an element against finite constraints using the supplied Canvas measurement service.
34 *
35 * @param element - Element whose declared measurement capability is used. See {@link PibblElement}.
36 * @param constraints - Available size constraints in logical units. See {@link Constraints}.
37 * @param service - Canvas text measurement service. See {@link CanvasMeasurementService}.
38 * @returns Measured dimensions, or an unsupported result with its reason. See
39 * {@link MeasureResult} .
40 *
41 * @see {@link PibblElement}
42 * @see {@link Constraints}
43 * @see {@link CanvasMeasurementService}
44 * @see {@link MeasureResult}
45 */
46 export function measureElement(
47 element: PibblElement<any>,
48 constraints: Readonly<Constraints>,
49 service: CanvasMeasurementService,
50 ): MeasureResult {
51 const context: MeasurementContext = {
52 active: new Set(),
53 stack: [],
54 };
55 const mode = getStyleMeasurementMode() ? 'recursive' :
56 GLOBAL_STATE.componentRefs ? 'mounted-intrinsic' : 'standalone-root';
57 return withSignalWriteForbidden('Signal writes are forbidden during Pibbl measurement', () =>
58 withStyleMeasurement(mode, () => withIsolatedRenderState(() =>
59 measureElementInternal(element, constraints, service, context),
60 )),
61 );
62 }
63
64 interface MeasurementContext {
65 active: Set<PibblElement<any>>;
66 stack: PibblElement<any>[];
67 }
68
69 function measureElementInternal(
70 element: PibblElement<any>,
71 constraints: Readonly<Constraints>,
72 service: CanvasMeasurementService,
73 context: MeasurementContext,
74 ): MeasureResult {
75 element = prepareElementPolicies(element, 'measureElement');
76 recordLayoutMeasurement();
77 const type = element.type;
78 const definition = getPrimitiveDefinition(type);
79 const component = type.name || 'Anonymous';
80 const preNormalized = takePreNormalizedStyle(element);
81 const source = getPolicyPreparationSource(element) ??
82 preNormalized?.source ??
83 element;
84 if (context.active.has(source)) {
85 throw layoutDiagnostic({
86 component,
87 property: 'measurement',
88 value: context.stack.map(item => item.type.name || 'Anonymous'),
89 algorithm: 'measurement',
90 constraints,
91 reason: 'cyclic measurement dependency is not supported',
92 });
93 }
94 if (!definition?.measure) {
95 return {
96 status: 'unsupported',
97 reason: `${component} does not provide measurement`,
98 };
99 }
100
101 context.active.add(source);
102 context.stack.push(element);
103 try {
104 if (preNormalized?.query && getStyleMeasurementMode() !== 'standalone-root') {
105 rejectResponsiveMeasurement(element);
106 }
107 const inputs = untracked(() => resolvePrimitiveInputs(element, definition, preNormalized, {
108 width: constraints.maxWidth, height: constraints.maxHeight,
109 }));
110 const specifiedStyle = withoutSystemFilter(
111 preNormalized?.style ?? inputs.style ?? EMPTY_STYLE,
112 );
113 const normalized = preNormalized ?
114 specifiedStyle :
115 definition.normalizeStyle ?
116 definition.normalizeStyle(specifiedStyle) :
117 specifiedStyle;
118 const previousReceiverToken = GLOBAL_STATE.primitiveReceiverToken;
119 GLOBAL_STATE.primitiveReceiverToken =
120 getPrimitiveReceiverToken(element) ?? createPrimitiveReceiverToken();
121 let result: MeasureResult;
122 try {
123 result = withStyleMeasurement('recursive', () => definition.measure!({
124 props: inputs.props,
125 style: normalized,
126 constraints,
127 service,
128 measureElement: (child, childConstraints) =>
129 measureElementInternal(child, childConstraints, service, context),
130 }));
131 } finally {
132 GLOBAL_STATE.primitiveReceiverToken = previousReceiverToken;
133 }
134 if (result.status === 'unsupported') return result;
135 validateMeasuredSize(result.size.width, 'width', component, constraints);
136 validateMeasuredSize(result.size.height, 'height', component, constraints);
137 return result;
138 } catch (error) {
139 if (error instanceof UnsupportedResponsiveMeasurement) {
140 return { status: 'unsupported', reason: error.message };
141 }
142 throw error;
143 } finally {
144 context.stack.pop();
145 context.active.delete(source);
146 }
147 }
148
149 function withIsolatedRenderState<T>(work: () => T): T {
150 const snapshot = {
151 currentPibblInstance: GLOBAL_STATE.currentPibblInstance,
152 componentRefs: GLOBAL_STATE.componentRefs,
153 canvas: GLOBAL_STATE.canvas,
154 componentIsMounting: GLOBAL_STATE.componentIsMounting,
155 componentHookIndex: GLOBAL_STATE.componentHookIndex,
156 renderContext: GLOBAL_STATE.renderContext,
157 primitiveReceiverToken: GLOBAL_STATE.primitiveReceiverToken,
158 seenIds: GLOBAL_STATE.seenIds,
159 seenKeys: GLOBAL_STATE.seenKeys,
160 clippingStack: GLOBAL_STATE.clippingStack,
161 layoutStack: GLOBAL_STATE.layoutStack,
162 };
163 GLOBAL_STATE.currentPibblInstance = undefined;
164 GLOBAL_STATE.componentRefs = undefined;
165 GLOBAL_STATE.canvas = undefined;
166 GLOBAL_STATE.componentIsMounting = false;
167 GLOBAL_STATE.componentHookIndex = 0;
168 GLOBAL_STATE.renderContext = undefined;
169 GLOBAL_STATE.primitiveReceiverToken = undefined;
170 GLOBAL_STATE.seenIds = undefined;
171 GLOBAL_STATE.seenKeys = undefined;
172 GLOBAL_STATE.clippingStack = [];
173 GLOBAL_STATE.layoutStack = undefined;
174 try {
175 return work();
176 } finally {
177 Object.assign(GLOBAL_STATE, snapshot);
178 }
179 }
180
181 function validateMeasuredSize(
182 value: number,
183 property: 'width' | 'height',
184 component: string,
185 constraints: Readonly<Constraints>,
186 ): void {
187 if (!Number.isFinite(value) || value < 0) {
188 throw layoutDiagnostic({
189 component,
190 property,
191 value,
192 algorithm: 'measurement',
193 constraints,
194 reason: 'measured dimensions must be finite and nonnegative',
195 });
196 }
197 }
198
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.