packages/core/src/lib/components/layer.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import { definePrimitive } from '../define-primitive.js';
2 import { materializeContainerChildren } from '../element/children.js';
3 import { createElement } from '../element/create-element.js';
4 import { Fragment, type FragmentProps } from '../element/fragment.js';
5 import { executeRender } from '../execute-render.js';
6 import {
7 getPibblInstance,
8 getCurrentPibblInstance,
9 GLOBAL_STATE,
10 } from '../global-state.js';
11 import { mountPibbl } from '../mount-pibbl.js';
12 import {
13 PibblNodeInput,
14 PibblInteractionLayer,
15 RenderingContext2D,
16 SystemStyle,
17 } from '../types.js';
18 import { PibblPaintEntryKind } from '../events/paint-entry.js';
19 import {
20 resolveRequiredLength,
21 stripLayoutParticipation,
22 } from '../style/resolve-built-in.js';
23 import type {
24 BoxStyle,
25 LayoutItemStyle,
26 ResolvedDrawingStyle,
27 } from '../style/types.js';
28 import {
29 getSurfaceSnapshot,
30 } from '../surface/transform.js';
31 import { getCurrentWorldTransform } from '../render-target.js';
32 import { createLayerSurface } from '../surface/resolve-surface.js';
33 import {
34 completeInstanceRenderRevision,
35 hasPendingInstanceRender,
36 } from '../scheduler/realm-scheduler.js';
37 import type { PibblElement } from '../element/types.js';
38 import {
39 haveSameLayerNodeTree,
40 snapshotLayerNodeTree,
41 type LayerNodeSnapshot,
42 } from './layer-node-equality.js';
43 import {
44 type PibblStructuralEventProps,
45 wireStructuralEvents,
46 } from './structural-events.js';
47
48 /**
49 * Authored inputs for Layer, including the declared data and presentation options.
50 *
51 * @see {@link PibblNodeInput}
52 * @see {@link Layer}
53 */
54 export interface LayerProps extends PibblStructuralEventProps {
55 /** Descendant content or the callback that supplies it. See {@link PibblNodeInput}. */
56 children: PibblNodeInput;
57 }
58
59 /**
60 * Supported geometry and presentation properties for Layer.
61 *
62 * @see {@link SystemStyle}
63 * @see {@link BoxStyle}
64 * @see {@link LayoutItemStyle}
65 * @see {@link Layer}
66 */
67 export interface LayerStyle extends SystemStyle, BoxStyle, LayoutItemStyle {}
68
69 type ResolvedLayerStyle = ResolvedDrawingStyle<LayerStyle> & {
70 width: number;
71 height: number;
72 };
73
74 interface LayerSlot {
75 canvas: OffscreenCanvas;
76 interactionLayer: PibblInteractionLayer;
77 logicalWidth: number;
78 logicalHeight: number;
79 focusRenderRevision: number | undefined;
80 retainedTree: LayerNodeSnapshot | undefined;
81 }
82
83 function renderLayer(
84 props: LayerProps,
85 style: Readonly<ResolvedLayerStyle>,
86 ctx: RenderingContext2D,
87 ) {
88 wireStructuralEvents(props);
89 const materializedChildren = materializeContainerChildren(
90 props.children,
91 'Layer',
92 );
93 const nextRoot = createPrivateLayerRoot(materializedChildren);
94 const canvas = GLOBAL_STATE.canvas!;
95 const { width = canvas.width, height = canvas.height } = style;
96 const componentRefs = GLOBAL_STATE.componentRefs!;
97 const parentInstance = getCurrentPibblInstance();
98 const parentSurface = getSurfaceSnapshot(parentInstance);
99 const layerSurface = createLayerSurface(
100 width,
101 height,
102 parentSurface.backingPixelsPerLogicalUnitX,
103 parentSurface.backingPixelsPerLogicalUnitY,
104 );
105 if (GLOBAL_STATE.componentIsMounting) {
106 const offscreenCanvas = new OffscreenCanvas(
107 layerSurface.backingWidth,
108 layerSurface.backingHeight,
109 );
110 const controller = mountPibbl(offscreenCanvas, nextRoot, {
111 parent: canvas,
112 surface: layerSurface,
113 });
114 const childInstance = getPibblInstance(offscreenCanvas);
115 childInstance.eventParent = componentRefs;
116 const interactionLayer: PibblInteractionLayer = {
117 paintKind: PibblPaintEntryKind.NestedInstance,
118 instance: childInstance,
119 transform: new DOMMatrix(),
120 clippingStack: [],
121 bounds: createBounds(width, height),
122 resolveLocal: undefined,
123 };
124 componentRefs.refs[0] = {
125 canvas: offscreenCanvas,
126 interactionLayer,
127 logicalWidth: width,
128 logicalHeight: height,
129 focusRenderRevision: undefined,
130 retainedTree: undefined,
131 } satisfies LayerSlot;
132 componentRefs.teardowns.add(() => {
133 childInstance.eventParent = undefined;
134 parentInstance.paintEntries.delete(interactionLayer);
135 controller.dispose();
136 });
137 }
138
139 const slot = componentRefs.refs[0] as LayerSlot;
140 const offscreenCanvas = slot.canvas;
141 const layerPibbl = getPibblInstance(offscreenCanvas);
142 const dimensionsChanged = slot.logicalWidth !== width ||
143 slot.logicalHeight !== height;
144 const surfaceChanged =
145 offscreenCanvas.width !== layerSurface.backingWidth ||
146 offscreenCanvas.height !== layerSurface.backingHeight;
147
148 const { interactionLayer } = slot;
149 parentInstance.paintEntries.delete(interactionLayer);
150 interactionLayer.transform = getCurrentWorldTransform();
151 interactionLayer.clippingStack = Array.from(GLOBAL_STATE.clippingStack);
152 if (dimensionsChanged) {
153 interactionLayer.bounds = createBounds(width, height);
154 }
155 // Install the current bridge before entering the child render. Focus bounds
156 // collected by the child must never see the previous frame's transform.
157 parentInstance.paintEntries.add(interactionLayer);
158
159 const focusPolicy = parentInstance.eventRoot.focusPolicy;
160 const focusRenderRevision = focusPolicy?.layerFocusRenderRevision?.(layerPibbl);
161
162 const shouldRender =
163 GLOBAL_STATE.componentIsMounting ||
164 dimensionsChanged ||
165 surfaceChanged ||
166 !haveSameLayerNodeTree(slot.retainedTree, nextRoot) ||
167 slot.focusRenderRevision !== focusRenderRevision ||
168 hasPendingInstanceRender(layerPibbl);
169 if (shouldRender) {
170 if (layerPibbl.surface.kind !== 'layer') {
171 throw new Error('Pibbl layer mounted without a derived surface.');
172 }
173 layerPibbl.surface.snapshot = layerSurface;
174 layerPibbl.surface.managed = true;
175 offscreenCanvas.width = layerSurface.backingWidth;
176 offscreenCanvas.height = layerSurface.backingHeight;
177 slot.logicalWidth = width;
178 slot.logicalHeight = height;
179
180 const renderedRevision = layerPibbl.requestedRenderRevision;
181 executeRender(
182 offscreenCanvas,
183 getCurrentPibblInstance().renderTimestamp,
184 nextRoot,
185 false,
186 );
187 completeInstanceRenderRevision(layerPibbl, renderedRevision);
188 slot.retainedTree = snapshotLayerNodeTree(nextRoot);
189 } else {
190 parentInstance.eventRoot.focusPolicy?.replayRetainedSubtree(layerPibbl);
191 }
192 const renderedFocusPolicy = parentInstance.eventRoot.focusPolicy;
193 slot.focusRenderRevision =
194 renderedFocusPolicy?.layerFocusRenderRevision?.(layerPibbl);
195
196 parentInstance.eventRoot.html?.recordLayer(parentInstance, layerPibbl, interactionLayer, ctx.globalAlpha, ctx.globalCompositeOperation);
197
198 ctx.drawImage(
199 offscreenCanvas,
200 0,
201 0,
202 offscreenCanvas.width,
203 offscreenCanvas.height,
204 0,
205 0,
206 width,
207 height,
208 );
209
210 }
211
212 /**
213 * Describes a retained offscreen composition layer.
214 *
215 * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
216 * props and style types.
217 * @throws When called directly; Pibbl mounts this component through JSX or createElement.
218 *
219 * @see {@link LayerProps}
220 * @see {@link LayerStyle}
221 */
222 export const Layer = definePrimitive<
223 LayerProps,
224 LayerStyle,
225 LayerStyle,
226 ResolvedLayerStyle
227 >(renderLayer, {
228 childInput: 'structural',
229 resolveStyle: resolveLayerStyle,
230 });
231
232 function resolveLayerStyle(
233 style: Readonly<LayerStyle>,
234 context: Parameters<NonNullable<
235 import('../types.js').PrimitiveCapabilities<
236 LayerProps,
237 LayerStyle,
238 LayerStyle,
239 ResolvedLayerStyle
240 >['resolveStyle']
241 >>[1],
242 ): ResolvedLayerStyle {
243 return {
244 ...stripLayoutParticipation(style),
245 width: resolveRequiredLength(
246 style.width ?? context.allocation.width,
247 context.percentageBasis.width,
248 'width',
249 context,
250 ),
251 height: resolveRequiredLength(
252 style.height ?? context.allocation.height,
253 context.percentageBasis.height,
254 'height',
255 context,
256 ),
257 } as ResolvedLayerStyle;
258 }
259
260 function createPrivateLayerRoot(
261 children: readonly PibblElement<any>[],
262 ): PibblElement<FragmentProps> {
263 return createElement(Fragment, {
264 key: 'pibbl-layer-root',
265 children,
266 });
267 }
268
269 function createBounds(width: number, height: number): Path2D {
270 const bounds = new Path2D();
271 bounds.rect(0, 0, width, height);
272 return bounds;
273 }
274
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.