Skip to content

packages/core/src/lib/layout/absolute.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 { ResponsiveStylePreparation, preparedStyleValue, preparedStyleQuery, type PreparedQuery } from '../style/responsive.js';
2 import { definePrimitive } from '../define-primitive.js';
3 import { EMPTY_STYLE } from '../style/empty-style.js';
4 import { materializeContainerChildren } from '../element/children.js';
5 import { clonePibblElement } from '../element/create-element.js';
6 import {
7   getPrimitiveDefinition,
8   getChildPrimitiveReceiverToken,
9   setPlacementConsumed,
10 } from '../element/metadata.js';
11 import { resolvePrimitiveStyleInput } from '../element/resolve-primitive-inputs.js';
12 import type { PibblElement } from '../element/types.js';
13 import { GLOBAL_STATE } from '../global-state.js';
14 import { normalizeEdges } from '../style/normalize.js';
15 import { resolveLength } from '../style/resolve-length.js';
16 import {
17   extractSystemFilter,
18   setPreNormalizedStyle,
19   withoutSystemFilter,
20 } from '../style/resolve-dispatch.js';
21 import type {
22   BoxStyle,
23   LayoutItemStyle,
24   ResolvedBoxStyle,
25 } from '../style/types.js';
26 import type {
27   PibblNodeInput,
28   PibblRenderTransaction,
29   RenderingContext2D,
30   StyleResolutionContext,
31   SystemStyle,
32 } from '../types.js';
33 import { layoutDiagnostic } from '../style/diagnostics.js';
34 import {
35   type PibblStructuralEventProps,
36   wireStructuralEvents,
37 } from '../components/structural-events.js';
38 import { pushLayoutBox } from './context.js';
39 import { recordLayoutEvaluation } from './diagnostics.js';
40 import { resolveBox } from './box.js';
41 import {
42   createPlacementIdentityHint,
43   placeChild,
44 } from './place-child.js';
45 import type { LayoutBox } from './types.js';
46 import { withReceiverInputProvision } from '../signals/input-provision.js';
47 
48 /**
49  * Supported geometry and presentation properties for Absolute.
50  *
51  * @see {@link BoxStyle}
52  * @see {@link LayoutItemStyle}
53  * @see {@link Absolute}
54  */
55 export type AbsoluteStyle = BoxStyle & LayoutItemStyle;
56 
57 /**
58  * Authored inputs for Absolute, including the declared data and presentation options.
59  *
60  * @see {@link PibblNodeInput}
61  * @see {@link Absolute}
62  */
63 export interface AbsoluteProps extends PibblStructuralEventProps {
64   /** Descendant content or the callback that supplies it. See {@link PibblNodeInput}. */
65   children: PibblNodeInput;
66 }
67 
68 interface ResolvedAbsoluteStyle extends SystemStyle {
69   box: ResolvedBoxStyle;
70   overflow: 'visible' | 'clip';
71 }
72 
73 type LayoutReceiver = 'Absolute' | 'Overlay' | 'Flow' | 'Flex' | 'Grid';
74 
75 const preparedLayoutTransactions = new WeakMap<
76   PibblRenderTransaction,
77   WeakMap<object, readonly PibblElement<any>[]>
78 >();
79 
80 function renderAbsolute(
81   props: AbsoluteProps,
82   style: Readonly<ResolvedAbsoluteStyle>,
83   ctx: RenderingContext2D,
84 ) {
85   wireStructuralEvents(props);
86   const children = prepareLayoutChildren(props.children, 'Absolute');
87   recordLayoutEvaluation();
88   const content = style.box.contentBox;
89   ctx.translate(content.x, content.y);
90   const localContent = Object.freeze({
91     x: 0,
92     y: 0,
93     width: content.width,
94     height: content.height,
95   });
96   const releaseLayout = pushLayoutBox(localContent);
97   GLOBAL_STATE.componentRefs!.onAfterRender = releaseLayout;
98 
99   let occurrence = 0;
100   return placeDirectChildren(
101     children,
102     localContent,
103     child => resolveAbsoluteChildPlacement(
104       localContent,
105       child,
106       getChildPrimitiveReceiverToken(
107         GLOBAL_STATE.renderTransaction,
108         GLOBAL_STATE.primitiveReceiverToken,
109         occurrence++,
110       ),
111     ),
112     style.overflow,
113   );
114 }
115 
116 /**
117  * Describes absolute layout for JSX or createElement authoring.
118  *
119  * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
120  * props and style types.
121  * @throws When called directly; Pibbl mounts this component through JSX or createElement.
122  *
123  * @see {@link AbsoluteProps}
124  * @see {@link AbsoluteStyle}
125  */
126 export const Absolute = definePrimitive<
127   AbsoluteProps,
128   AbsoluteStyle,
129   AbsoluteStyle,
130   ResolvedAbsoluteStyle
131 >(renderAbsolute, {
132   childInput: 'structural',
133   resolveStyle: (style, context) => ({
134     box: resolveContainerBox(style, context, 'Absolute', 'absolute'),
135     overflow: style.overflow ?? 'visible',
136   }),
137 });
138 
139 /** @internal Resolves a layout container without inspecting its children. */
140 export function resolveContainerBox(
141   style: Readonly<BoxStyle>,
142   context: Readonly<StyleResolutionContext>,
143   component: string,
144   algorithm: string,
145 ): ResolvedBoxStyle {
146   return resolveLayoutBox(style, context, component, algorithm, false);
147 }
148 
149 export function placeAbsoluteChild(
150   container: Readonly<LayoutBox>,
151   child: PibblElement<any>,
152 ): LayoutBox {
153   return resolveAbsoluteChildPlacement(container, child).box;
154 }
155 
156 function resolveAbsoluteChildPlacement(
157   container: Readonly<LayoutBox>,
158   child: PibblElement<any>,
159   receiverToken?: object,
160 ): { box: LayoutBox; element: PibblElement<any> } {
161   const prepared = prepareDirectChildStyle(child, receiverToken, container);
162   const style = preparedStyleValue(prepared) as Readonly<
163     BoxStyle & LayoutItemStyle
164   >;
165   const resolved = resolveDirectChildBox(container, child, 'absolute', style);
166   const context = childDiagnosticContext(child, container, 'absolute');
167   const left = resolveLength(style.left ?? 0, container.width, 'left', context)!;
168   const top = resolveLength(style.top ?? 0, container.height, 'top', context)!;
169   const box = {
170     x: finitePlacement(container.x + left, 'left', left, context),
171     y: finitePlacement(container.y + top, 'top', top, context),
172     width: resolved.borderBox.width,
173     height: resolved.borderBox.height,
174   };
175   return {
176     box,
177     element: childWithUsedSize(
178       child,
179       style,
180       resolved.contentBox.width,
181       resolved.contentBox.height,
182       receiverToken,
183       preparedStyleQuery(prepared),
184     ),
185   };
186 }
187 
188 /** @internal Carries normalized finite content size into the eventual render. */
189 export function childWithUsedSize(
190   child: PibblElement<any>,
191   style: Readonly<SystemStyle>,
192   width: number,
193   height: number,
194   receiverToken?: object,
195   query?: PreparedQuery,
196 ): PibblElement<any> {
197   const renderedChild = clonePibblElement(child);
198   setPlacementConsumed(renderedChild);
199   setPreNormalizedStyle(renderedChild, Object.freeze({
200     ...style,
201     width,
202     height,
203   }), child, receiverToken, query);
204   return renderedChild;
205 }
206 
207 /** @internal Shared by the initial direct-child layout algorithms. */
208 export function resolveDirectChildBox(
209   container: Readonly<LayoutBox>,
210   child: PibblElement<any>,
211   algorithm: string,
212   style: Readonly<SystemStyle> = normalizeDirectChildStyle(child, undefined, container),
213 ): ResolvedBoxStyle {
214   return resolveLayoutBox(
215     style as Readonly<BoxStyle>,
216     {
217       allocation: container,
218       percentageBasis: container,
219       constraints: {
220         minWidth: 0,
221         maxWidth: container.width,
222         minHeight: 0,
223         maxHeight: container.height,
224       },
225       component: child.type.name || 'Anonymous',
226       algorithm: 'render-dispatch',
227     },
228     child.type.name || 'Anonymous',
229     algorithm,
230     true,
231   );
232 }
233 
234 /** @internal Normalizes a descriptor without resolving or invoking its callback. */
235 export function normalizeDirectChildStyle(
236   child: PibblElement<any>,
237   receiverToken?: object,
238   queryBasis?: Readonly<LayoutBox>,
239 ): Readonly<SystemStyle> {
240   return preparedStyleValue(prepareDirectChildStyle(child, receiverToken, queryBasis));
241 }
242 
243 /** Carries responsive eligibility with this normalization, never with a token. */
244 export function prepareDirectChildStyle(
245   child: PibblElement<any>,
246   receiverToken?: object,
247   queryBasis?: Readonly<LayoutBox>,
248 ): Readonly<SystemStyle> | ResponsiveStylePreparation<Readonly<SystemStyle>> {
249   const definition = getPrimitiveDefinition(child.type);
250   const prepared = withReceiverInputProvision(
251     GLOBAL_STATE.renderTransaction,
252     receiverToken,
253     () => resolvePrimitiveStyleInput(
254       (child.props as { readonly style?: unknown }).style,
255       child, queryBasis,
256     ) ?? EMPTY_STYLE,
257   );
258   const style = preparedStyleValue(prepared);
259   const filter = extractSystemFilter(style);
260   const normalized = definition?.normalizeStyle ?
261       definition.normalizeStyle(withoutSystemFilter(style)) :
262       withoutSystemFilter(style);
263   const final = { ...normalized, ...(filter === undefined ? {} : { filter }), ...(style.transition === undefined ? {} : { transition: (style as Readonly<SystemStyle>).transition }) };
264   if (prepared instanceof ResponsiveStylePreparation) {
265     prepared.style = final;
266     return prepared;
267   }
268   return final;
269 }
270 
271 /** @internal Converts source-order descriptors to stable placed children. */
272 export function placeDirectChildren(
273   children: readonly PibblElement<any>[],
274   contentBox: Readonly<LayoutBox>,
275   place: (child: PibblElement<any>) => LayoutBox | {
276     box: LayoutBox;
277     element: PibblElement<any>;
278   },
279   overflow: 'visible' | 'clip',
280 ): PibblElement<any>[] {
281   const occurrences = new Map<Function, number>();
282   const placed: PibblElement<any>[] = [];
283   for (const child of children) {
284     const occurrence = child.key === undefined ?
285       occurrences.get(child.type) ?? 0 :
286       0;
287     if (child.key === undefined) {
288       occurrences.set(child.type, occurrence + 1);
289     }
290     const placement = place(child);
291     const box = 'box' in placement ? placement.box : placement;
292     const element = 'box' in placement ? placement.element : child;
293     placed.push(placeChild({
294       element,
295       box,
296       clipInChildSpace: overflow === 'clip' ? {
297         x: contentBox.x - box.x,
298         y: contentBox.y - box.y,
299         width: contentBox.width,
300         height: contentBox.height,
301       } : undefined,
302       identityHint: createPlacementIdentityHint(child, occurrence),
303     }));
304   }
305   return placed;
306 }
307 
308 /** @internal Prepares one finite direct-child sequence for a layout boundary. */
309 export function materializeLayoutChildren(
310   children: PibblNodeInput,
311   receiver: LayoutReceiver,
312 ): readonly PibblElement<any>[] {
313   // Placement identity owns layout sibling keys. Deferring its check preserves
314   // fail-closed teardown when a later duplicate follows an acquired child.
315   return materializeContainerChildren(children, receiver, {
316     validateKeys: false,
317   });
318 }
319 
320 /** @internal Shares one direct sequence between an occurrence's measurement and paint. */
321 export function prepareLayoutChildren(
322   children: PibblNodeInput,
323   receiver: LayoutReceiver,
324 ): readonly PibblElement<any>[] {
325   const transaction = GLOBAL_STATE.renderTransaction;
326   const receiverToken = GLOBAL_STATE.primitiveReceiverToken;
327   if (!transaction || !receiverToken) {
328     return materializeLayoutChildren(children, receiver);
329   }
330 
331   let receivers = preparedLayoutTransactions.get(transaction);
332   if (!receivers) {
333     receivers = new WeakMap();
334     preparedLayoutTransactions.set(transaction, receivers);
335   }
336   const prepared = receivers.get(receiverToken);
337   if (prepared) return prepared;
338   const materialized = withReceiverInputProvision(
339     transaction,
340     receiverToken,
341     () => materializeLayoutChildren(children, receiver),
342   );
343   receivers.set(receiverToken, materialized);
344   return materialized;
345 }
346 
347 function resolveLayoutBox(
348   style: Readonly<BoxStyle>,
349   context: Readonly<StyleResolutionContext>,
350   component: string,
351   algorithm: string,
352   requireDefinite: boolean,
353 ): ResolvedBoxStyle {
354   if (requireDefinite && (style.width === undefined || style.width === 'auto')) {
355     throw requiredDimension(component, 'width', style.width, context, algorithm);
356   }
357   if (requireDefinite && (style.height === undefined || style.height === 'auto')) {
358     throw requiredDimension(component, 'height', style.height, context, algorithm);
359   }
360 
361   const diagnosticContext = {
362     component,
363     algorithm,
364     constraints: context.constraints,
365   };
366   const width = resolveOptionalAllocationLength(
367     style.width,
368     context.percentageBasis.width,
369     'width',
370     diagnosticContext,
371   );
372   const height = resolveOptionalAllocationLength(
373     style.height,
374     context.percentageBasis.height,
375     'height',
376     diagnosticContext,
377   );
378   const minWidth = resolveOptionalAllocationLength(
379     style.minWidth,
380     context.percentageBasis.width,
381     'minWidth',
382     diagnosticContext,
383   );
384   const maxWidth = resolveOptionalAllocationLength(
385     style.maxWidth,
386     context.percentageBasis.width,
387     'maxWidth',
388     diagnosticContext,
389   );
390   const minHeight = resolveOptionalAllocationLength(
391     style.minHeight,
392     context.percentageBasis.height,
393     'minHeight',
394     diagnosticContext,
395   );
396   const maxHeight = resolveOptionalAllocationLength(
397     style.maxHeight,
398     context.percentageBasis.height,
399     'maxHeight',
400     diagnosticContext,
401   );
402   const padding = normalizeEdges(style.padding, 'padding', diagnosticContext);
403   const allowOverflow = requireDefinite;
404   const maxConstraintWidth = allowOverflow ? allocationCeiling(
405     context.allocation.width,
406     [width, minWidth, maxWidth],
407     padding.left + padding.right,
408     'width',
409     diagnosticContext,
410   ) : context.allocation.width;
411   const maxConstraintHeight = allowOverflow ? allocationCeiling(
412     context.allocation.height,
413     [height, minHeight, maxHeight],
414     padding.top + padding.bottom,
415     'height',
416     diagnosticContext,
417   ) : context.allocation.height;
418 
419   return resolveBox({
420     ...style,
421     width,
422     height,
423     minWidth,
424     maxWidth,
425     minHeight,
426     maxHeight,
427   }, {
428     minWidth: 0,
429     maxWidth: maxConstraintWidth,
430     minHeight: 0,
431     maxHeight: maxConstraintHeight,
432   }, diagnosticContext);
433 }
434 
435 function resolveOptionalAllocationLength(
436   value: BoxStyle['width'],
437   reference: number,
438   property: string,
439   context: Parameters<typeof resolveLength>[3],
440 ): number | undefined {
441   if (value === undefined) return undefined;
442   return resolveLength(value, reference, property, context);
443 }
444 
445 function allocationCeiling(
446   allocation: number,
447   values: readonly (number | undefined)[],
448   padding: number,
449   property: string,
450   context: Parameters<typeof resolveLength>[3],
451 ): number {
452   const ceiling = Math.max(allocation, ...values.filter(
453     (value): value is number => value !== undefined,
454   )) + padding;
455   if (!Number.isFinite(ceiling)) {
456     throw layoutDiagnostic({
457       ...context,
458       property,
459       value: ceiling,
460       reason: 'resolved allocation arithmetic must remain finite',
461     });
462   }
463   return ceiling;
464 }
465 
466 function requiredDimension(
467   component: string,
468   property: 'width' | 'height',
469   value: unknown,
470   context: Readonly<StyleResolutionContext>,
471   algorithm: string,
472 ) {
473   return layoutDiagnostic({
474     component,
475     property,
476     value: value ?? 'auto',
477     algorithm,
478     constraints: context.constraints,
479     reason: 'requires a definite dimension until measurement is available',
480   });
481 }
482 
483 function childDiagnosticContext(
484   child: PibblElement<any>,
485   container: Readonly<LayoutBox>,
486   algorithm: string,
487 ) {
488   return {
489     component: child.type.name || 'Anonymous',
490     algorithm,
491     constraints: {
492       minWidth: 0,
493       maxWidth: container.width,
494       minHeight: 0,
495       maxHeight: container.height,
496     },
497   };
498 }
499 
500 function finitePlacement(
501   resolved: number,
502   property: string,
503   value: unknown,
504   context: ReturnType<typeof childDiagnosticContext>,
505 ): number {
506   if (!Number.isFinite(resolved)) {
507     throw layoutDiagnostic({
508       ...context,
509       property,
510       value,
511       reason: 'resolved placement arithmetic must remain finite',
512     });
513   }
514   return resolved;
515 }
516 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.