Skip to content

packages/core/src/lib/components/layer.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 { 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 built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.