packages/core/src/lib/layout/absolute.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.