Skip to content

packages/core/src/lib/layout/overlay.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 { preparedStyleValue, preparedStyleQuery } from '../style/responsive.js';
2 import { definePrimitive } from '../define-primitive.js';
3 import { GLOBAL_STATE } from '../global-state.js';
4 import { layoutDiagnostic } from '../style/diagnostics.js';
5 import {
6   type PibblStructuralEventProps,
7   wireStructuralEvents,
8 } from '../components/structural-events.js';
9 import type { BoxStyle, LayoutItemStyle, ResolvedBoxStyle } from '../style/types.js';
10 import type {
11   PibblNodeInput,
12   RenderingContext2D,
13   SystemStyle,
14 } from '../types.js';
15 import type { PibblElement } from '../element/types.js';
16 import { getChildPrimitiveReceiverToken } from '../element/metadata.js';
17 import {
18   childWithUsedSize,
19   prepareLayoutChildren,
20   prepareDirectChildStyle,
21   placeDirectChildren,
22   resolveContainerBox,
23   resolveDirectChildBox,
24 } from './absolute.js';
25 import { pushLayoutBox } from './context.js';
26 import { recordLayoutEvaluation } from './diagnostics.js';
27 import type { LayoutBox } from './types.js';
28 
29 type OverlayAlignment = 'start' | 'center' | 'end' | 'stretch';
30 
31 /**
32  * Supported geometry and presentation properties for Overlay.
33  *
34  * @see {@link BoxStyle}
35  * @see {@link LayoutItemStyle}
36  * @see {@link Overlay}
37  */
38 export interface OverlayStyle extends BoxStyle, LayoutItemStyle {
39   /** Default alignment of children on the cross or block axis. See {@link OverlayAlignment}. */
40   alignItems?: OverlayAlignment;
41   /** Default inline-axis alignment of grid or overlay children. See {@link OverlayAlignment}. */
42   justifyItems?: OverlayAlignment;
43 }
44 
45 /**
46  * Authored inputs for Overlay, including the declared data and presentation options.
47  *
48  * @see {@link PibblNodeInput}
49  * @see {@link Overlay}
50  */
51 export interface OverlayProps extends PibblStructuralEventProps {
52   /** Descendant content or the callback that supplies it. See {@link PibblNodeInput}. */
53   children: PibblNodeInput;
54 }
55 
56 interface ResolvedOverlayStyle extends SystemStyle {
57   box: ResolvedBoxStyle;
58   overflow: 'visible' | 'clip';
59   alignItems: OverlayAlignment;
60   justifyItems: OverlayAlignment;
61 }
62 
63 function renderOverlay(
64   props: OverlayProps,
65   style: Readonly<ResolvedOverlayStyle>,
66   ctx: RenderingContext2D,
67 ) {
68   wireStructuralEvents(props);
69   const children = prepareLayoutChildren(props.children, 'Overlay');
70   recordLayoutEvaluation();
71   const content = style.box.contentBox;
72   ctx.translate(content.x, content.y);
73   const localContent = Object.freeze({
74     x: 0,
75     y: 0,
76     width: content.width,
77     height: content.height,
78   });
79   const releaseLayout = pushLayoutBox(localContent);
80   GLOBAL_STATE.componentRefs!.onAfterRender = releaseLayout;
81 
82   let occurrence = 0;
83   return placeDirectChildren(
84     children,
85     localContent,
86     child => resolveOverlayChildPlacement(
87       localContent,
88       child,
89       style,
90       getChildPrimitiveReceiverToken(
91         GLOBAL_STATE.renderTransaction,
92         GLOBAL_STATE.primitiveReceiverToken,
93         occurrence++,
94       ),
95     ),
96     style.overflow,
97   );
98 }
99 
100 /**
101  * Describes overlay layout for JSX or createElement authoring.
102  *
103  * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
104  * props and style types.
105  * @throws When called directly; Pibbl mounts this component through JSX or createElement.
106  *
107  * @see {@link OverlayProps}
108  * @see {@link OverlayStyle}
109  */
110 export const Overlay = definePrimitive<
111   OverlayProps,
112   OverlayStyle,
113   OverlayStyle,
114   ResolvedOverlayStyle
115 >(renderOverlay, {
116   childInput: 'structural',
117   resolveStyle: (style, context) => ({
118     box: resolveContainerBox(style, context, 'Overlay', 'overlay'),
119     overflow: style.overflow ?? 'visible',
120     alignItems: style.alignItems ?? 'stretch',
121     justifyItems: style.justifyItems ?? 'stretch',
122   }),
123 });
124 
125 export function placeOverlayChild(
126   container: Readonly<LayoutBox>,
127   child: PibblElement<any>,
128   alignment: Readonly<Pick<OverlayStyle, 'alignItems' | 'justifyItems'>> = {},
129 ): LayoutBox {
130   return resolveOverlayChildPlacement(container, child, alignment).box;
131 }
132 
133 function resolveOverlayChildPlacement(
134   container: Readonly<LayoutBox>,
135   child: PibblElement<any>,
136   alignment: Readonly<Pick<OverlayStyle, 'alignItems' | 'justifyItems'>> = {},
137   receiverToken?: object,
138 ): { box: LayoutBox; element: PibblElement<any> } {
139   const prepared = prepareDirectChildStyle(child, receiverToken, container);
140   const item = preparedStyleValue(prepared) as Readonly<
141     BoxStyle & LayoutItemStyle
142   >;
143   const resolved = resolveDirectChildBox(
144     container,
145     child,
146     'overlay',
147     item,
148   );
149   const declaredBox = resolved.borderBox;
150   const horizontal = resolveItemAlignment(
151     item.justifySelf,
152     alignment.justifyItems ?? 'stretch',
153     child,
154     container,
155     'justifySelf',
156   );
157   const vertical = resolveItemAlignment(
158     item.alignSelf,
159     alignment.alignItems ?? 'stretch',
160     child,
161     container,
162     'alignSelf',
163   );
164   const x = alignAxis(
165     container.x,
166     container.width,
167     declaredBox.width,
168     horizontal,
169   );
170   const y = alignAxis(
171     container.y,
172     container.height,
173     declaredBox.height,
174     vertical,
175   );
176   const box = {
177     x,
178     y,
179     width: horizontal === 'stretch' ?
180         Math.max(0, container.width) :
181         declaredBox.width,
182     height: vertical === 'stretch' ?
183         Math.max(0, container.height) :
184         declaredBox.height,
185   };
186   const horizontalPadding = declaredBox.width - resolved.contentBox.width;
187   const verticalPadding = declaredBox.height - resolved.contentBox.height;
188   return {
189     box,
190     element: childWithUsedSize(
191       child,
192       item,
193       Math.max(0, box.width - horizontalPadding),
194       Math.max(0, box.height - verticalPadding),
195       receiverToken,
196       preparedStyleQuery(prepared),
197     ),
198   };
199 }
200 
201 function resolveItemAlignment(
202   item: LayoutItemStyle['alignSelf'] | LayoutItemStyle['justifySelf'],
203   fallback: OverlayAlignment,
204   child: PibblElement<any>,
205   container: Readonly<LayoutBox>,
206   property: 'alignSelf' | 'justifySelf',
207 ): OverlayAlignment {
208   if (item === undefined || item === 'auto') return fallback;
209   if (item === 'start' || item === 'center' || item === 'end' || item === 'stretch') {
210     return item;
211   }
212   throw layoutDiagnostic({
213     component: child.type.name || 'Anonymous',
214     property,
215     value: item,
216     algorithm: 'overlay',
217     constraints: {
218       minWidth: 0,
219       maxWidth: container.width,
220       minHeight: 0,
221       maxHeight: container.height,
222     },
223     reason: 'uses an unsupported overlay alignment value',
224   });
225 }
226 
227 function alignAxis(
228   start: number,
229   available: number,
230   size: number,
231   alignment: OverlayAlignment,
232 ): number {
233   switch (alignment) {
234     case 'center': return start + (available - size) / 2;
235     case 'end': return start + available - size;
236     default: return start;
237   }
238 }
239 

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