// The region components: (createRegion) and // (createRegionGrid) with the content as a RENDER PROP - `build` returns // the JSX of a region's content (, , ), mounted // under the region's content group once its data is loaded, and `proxy` // the JSX drawn while the content is not shown. A JSX build is one step // of the slice budget (the mount runs in the frame's flush), so the // imperative generator form is the one for a region whose creates must // spread over several frames. The same shape as @solidrt/3d's // components/region.tsx. import { callRef, createEffect, createSignal, For, onCleanup, Show, untrack, useContext } from "@solidrt/core" import type { Element } from "@solidrt/core" import { GroupContext, LayerContext } from "./context.ts" import { createRegion, createRegionGrid } from "../region.ts" import type { Region as RegionHandle, RegionGrid as RegionGridHandle, RegionGridOptions, RegionOptions, RegionState } from "../region.ts" import type { SpriteGroup } from "../layer.ts" type ContentProps = { /** The region's content as JSX, mounted under the region's content * group with the loaded data (positions cell-local); unmounted at * leave. */ build: (region: RegionHandle, data: T) => Element /** The JSX drawn under the region's group while the content is not * shown: a single stand-in sprite, a silhouette. */ proxy?: (region: RegionHandle) => Element load?: RegionOptions["load"] unload?: RegionOptions["unload"] onStateChange?: (region: RegionHandle, state: RegionState) => void onError?: (region: RegionHandle, error: unknown) => void } export type RegionProps = ContentProps & Omit, "build" | "proxy" | "load" | "unload" | "parent"> & { ref?: (region: RegionHandle & { dispose(): void }) => void } export type RegionGridProps = ContentProps & Omit, "build" | "proxy" | "load" | "unload"> & { ref?: (grid: RegionGridHandle) => void } // One held region's reactive half: its data once built (the content // mounts on it) and whether it is shown (the proxy unmounts on it). type Held = { region: RegionHandle data: () => { value: T } | null setData: (d: { value: T } | null) => void shown: () => boolean setShown: (s: boolean) => void } function held(region: RegionHandle): Held { let [data, setData] = createSignal<{ value: T } | null>(null) let [shown, setShown] = createSignal(false) return { region, data, setData, shown, setShown } } // The JSX of one held region: the content under its content group once // the data is there, measured again once mounted (the build step ran // before the mount); the proxy under the region's group while not shown. function Content(props: { held: Held; build: ContentProps["build"]; proxy: ContentProps["proxy"] }): Element { let region = props.held.region let content = (): Element | undefined => { let d = props.held.data() return d === null ? undefined : props.build(region, d.value) } createEffect( () => props.held.data(), d => { if (d !== null) region.measure() }, ) let scope = (parent: SpriteGroup, children: () => Element | undefined): Element => {children()} return ( <> {scope(region.content, content)} {scope(region.group, () => props.proxy!(region))} ) } /** * An explicit region (createRegion as a component): a bounded group at * (`x`, `y`) under the enclosing `` (or the layer root), whose * `build` JSX exists while a viewer is within `enter` of its box. `load` * prepares the data (async), `unload` runs before the content goes, * `proxy` is drawn while it is not shown. The creation props are fixed * at mount; move the region through `ref` with setGroup on its group. */ export function Region(props: RegionProps): Element { let layer = useContext(LayerContext) let parent = useContext(GroupContext) let h: Held = null! let region = untrack(() => createRegion(layer, { x: props.x, y: props.y, width: props.width, height: props.height, enter: props.enter, leave: props.leave, weight: props.weight, priority: props.priority, retain: props.retain, label: props.label, parent, load: props.load === undefined ? undefined : (r, signal) => props.load!(r, signal), build: (_, data) => { h.setData({ value: data }) }, unload: props.unload === undefined ? undefined : (r, data) => props.unload!(r, data), }), ) h = held(region) region.onStateChange = (r, s) => { h.setShown(s === "shown") if (s === "unwanted" || s === "pending" || s === "stalled" || s === "loading") h.setData(null) props.onStateChange?.(r, s) } region.onError = (r, e) => props.onError?.(r, e) callRef(() => props.ref, region) onCleanup(() => region.dispose()) return } /** * A region grid (createRegionGrid as a component): every cell a viewer * wants mounts its `build` JSX under a child region at the cell's * corner, and unmounts it at leave. The gate's grid is layer space, so * the component mounts at the layer root: inside a `` it throws. */ export function RegionGrid(props: RegionGridProps): Element { let layer = useContext(LayerContext) if (useContext(GroupContext) !== null) throw new Error("RegionGrid: a region grid is layer space; mount it at the layer root, not inside a ") let [cells, setCells] = createSignal[]>([]) let byRegion = new Map, Held>() // A cell's reactive half exists from its first state on (loading, or // building when there is no load), so its proxy mounts while it loads. let ensure = (r: RegionHandle): Held => { let h = byRegion.get(r) if (h === undefined) { h = held(r) byRegion.set(r, h) setCells(list => [...list, h!]) } return h } let grid = untrack(() => createRegionGrid(layer, { cell: props.cell, origin: props.origin, enter: props.enter, leave: props.leave, bounds: props.bounds, weight: props.weight, limit: props.limit, priority: props.priority, retain: props.retain, label: props.label, load: props.load === undefined ? undefined : (r, signal) => props.load!(r, signal), build: (r, data) => ensure(r).setData({ value: data }), unload: props.unload === undefined ? undefined : (r, data) => props.unload!(r, data), }), ) grid.onStateChange = (r, s) => { if (s === "unwanted") { let h = byRegion.get(r) if (h !== undefined) { byRegion.delete(r) setCells(list => list.filter(x => x !== h)) } } else ensure(r).setShown(s === "shown") props.onStateChange?.(r, s) } grid.onError = (r, e) => props.onError?.(r, e) callRef(() => props.ref, grid) onCleanup(() => grid.dispose()) return {h => } }