import { createImage, createEffect, Loading, Errored, pct } from "@solidrt/core" import type { ImageSource, LayoutProps, Pct, PointerProps, TextureProps } from "@solidrt/core" import type { StyleProps } from "./types" export interface ImageProps extends PointerProps { src: string | Uint8Array /** * How the image maps into the Image's box (CSS object-fit): "fill" * stretches, "cover"/"none" crop, "contain"/"scale-down" letterbox, * everything centered. Requires the Image to have a box: give `layout` a * size in any form (numbers, `pct()`, flex). Without `fit` the image keeps * its legacy sizing: numeric layout sizes are honored, anything else draws * at intrinsic size. */ fit?: TextureProps["fit"] /** Image source to show when `src` fails to load. If this also fails, only * the `backgroundColor` placeholder remains. */ fallback?: ImageSource /** Called each time a source finishes loading (including reloads on a * reactive `src`). */ onLoad?: () => void /** Called when `src` fails to load or decode. The fallback (if any) is shown * regardless; without one the error stays contained here instead of * propagating to an outer boundary. */ onError?: (err: unknown) => void layout?: LayoutProps style?: StyleProps } // Renders the fallback source when the main one failed. Its own // keeps a broken fallback from escaping: the placeholder stays instead. // Both fallbacks here take the error argument: an arity >= 1 fallback tells // the error is handled, so dev builds do not console.error it. function FallbackTexture(props: { src: ImageSource fit?: TextureProps["fit"] width?: number | Pct height?: number | Pct }) { let tex = createImage(() => props.src) return ( null}> ) } export function Image(props: ImageProps) { // createImage fetches/decodes/uploads, swaps the texture when src changes, and // frees it on cleanup, returning the texture id. Pass an accessor so a reactive // src reloads. Reading src() suspends until ready (a SolidJS 2.0 async value), // so the below sits in a boundary. let src = createImage(() => props.src) let hasBorder = () => (props.style?.borderWidth ?? 0) > 0 // Load/error callbacks ride a separate effect read of the same async value; // the error handler also keeps the failure out of the console when the // render side already contains it with a fallback. createEffect(() => src(), { effect: () => props.onLoad?.(), error: (err: unknown) => props.onError?.(err), }) // A texture sizes from its own width/height (not the box around it). With // `fit` the texture simply fills the Image's box and the fit maps the pixels // into it. Without it, legacy sizing: numeric layout dimensions are // forwarded, and omitting height lets the texture follow the image's // intrinsic aspect ratio. let texW = () => (props.fit != null ? pct(100) : typeof props.layout?.width === "number" ? props.layout.width : undefined) let texH = () => props.fit != null ? pct(100) : typeof props.layout?.height === "number" ? props.layout.height : undefined return ( {props.style?.backgroundColor != null ? ( ) : null} props.fallback != null ? ( ) : null } > {hasBorder() ? ( ) : null} ) }