Skip to content

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