@solidrt/core 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +55 -0
- package/examples/README.md +10 -2
- package/examples/binary-import.tsx +4 -2
- package/examples/gpu-shader.tsx +8 -6
- package/examples/image.tsx +13 -2
- package/examples/inline-image.tsx +27 -0
- package/examples/repaint-boundary.tsx +56 -0
- package/examples/responsive-grid.tsx +42 -0
- package/examples/scroll.tsx +56 -0
- package/examples/svg.tsx +4 -0
- package/jsx-runtime.d.ts +21 -17
- package/package.json +2 -3
- package/src/gpu.ts +22 -15
- package/src/image.ts +6 -0
- package/src/index.ts +2 -0
- package/src/types.d.ts +29 -4
package/AGENTS.md
CHANGED
|
@@ -7,6 +7,61 @@ trust this file and the types in src/types.d.ts and jsx-runtime.d.ts.
|
|
|
7
7
|
SolidRT is a custom SolidJS renderer: it paints through a Rust runtime, not the
|
|
8
8
|
DOM. There is no HTML, no CSS cascade, no `className`.
|
|
9
9
|
|
|
10
|
+
## The window is device-sized - design fluid
|
|
11
|
+
|
|
12
|
+
A SolidRT window is host-sized and resizable, and the SAME app runs on phones,
|
|
13
|
+
tablets, and desktops. There is no default "desktop" size to design against.
|
|
14
|
+
Design fluid by default: derive sizing and layout from the live window, do not
|
|
15
|
+
hardcode desktop pixels.
|
|
16
|
+
|
|
17
|
+
- Size the layout from `windowSize()` / `capabilities.windowSizeClass`, not from
|
|
18
|
+
fixed pixel widths. Let flex (`flex`, `flexWrap`, `gap`) and percentages carry
|
|
19
|
+
the layout so it reflows on resize instead of clipping or leaving dead space.
|
|
20
|
+
- Gate hover-only affordances on `capabilities.hover`. Hover-zoom, hover-reveal,
|
|
21
|
+
and tooltips are dead on a touch device with no pointer that can rest. When
|
|
22
|
+
`capabilities.touch`, provide a tap/press path to the same action.
|
|
23
|
+
- `windowSizeClass` uses Material 3 width breakpoints (logical px): `compact`
|
|
24
|
+
(<600), `medium` (600-840), `expanded` (>=840). Drive column counts / layout
|
|
25
|
+
switches off it (see the responsive-grid example).
|
|
26
|
+
|
|
27
|
+
`env` and `capabilities` (both exported from `@solidrt/core`) are the two
|
|
28
|
+
objects that expose this. They are plain objects with REACTIVE GETTERS, not
|
|
29
|
+
functions - read them as `capabilities.windowSizeClass`, `env.displayScale`
|
|
30
|
+
(NOT `capabilities()`); the getter reads reactive state underneath, so a read
|
|
31
|
+
inside JSX / a memo / an effect re-runs when it changes. (`windowSize()` and
|
|
32
|
+
`safeArea()` from `./window` ARE functions - call those.)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { env, capabilities } from "@solidrt/core"
|
|
36
|
+
|
|
37
|
+
env.windowSize // { width, height } (same value as windowSize())
|
|
38
|
+
env.displayScale // device pixel ratio (hi-DPI factor)
|
|
39
|
+
env.safeArea // inset distances from each edge
|
|
40
|
+
env.systemTheme // "dark" | "light" | "unknown" (resolves after startup)
|
|
41
|
+
env.orientation // "portrait" | "landscape" | ... | "unknown"
|
|
42
|
+
|
|
43
|
+
capabilities.windowSizeClass // "compact" | "medium" | "expanded"
|
|
44
|
+
capabilities.hover // a pointer can rest over content (mouse/trackpad)
|
|
45
|
+
capabilities.touch // direct touch input present
|
|
46
|
+
capabilities.precisePointer // pixel-precise pointing (mouse/trackpad)
|
|
47
|
+
capabilities.keyboardNav // hardware-key navigation available
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Read behavior decisions through `capabilities`; read `env` directly only when
|
|
51
|
+
you need the raw fact (e.g. `env.displayScale` for asset sizing below).
|
|
52
|
+
|
|
53
|
+
### Vectors vs raster, and hi-DPI
|
|
54
|
+
|
|
55
|
+
Because the drawn size is fluid and the display DPI varies, asset format is a
|
|
56
|
+
real design decision, not an afterthought:
|
|
57
|
+
|
|
58
|
+
- Prefer VECTORS (`<svg>`, `<d-path>`) whenever the render size is fluid or DPI
|
|
59
|
+
varies - they stay crisp at any size x `displayScale()`.
|
|
60
|
+
- RASTER (`<texture>` / `createImage`) needs source resolution >= displayed size
|
|
61
|
+
x `env.displayScale`, or it softens on hi-DPI. Author raster at 2-3x the
|
|
62
|
+
largest size you will ever draw it. A 256px PNG blown up large on a hi-DPI
|
|
63
|
+
tablet will look soft; the same art as SVG will not.
|
|
64
|
+
|
|
10
65
|
## Setup
|
|
11
66
|
|
|
12
67
|
```sh
|
package/examples/README.md
CHANGED
|
@@ -16,18 +16,26 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
16
16
|
- `frame-animation.tsx` - `onFrame` driving a transform animation each frame.
|
|
17
17
|
- `on-layout-connect.tsx` - `onLayout` + `getBoundingBox` connecting laid-out boxes with a `d-path`.
|
|
18
18
|
|
|
19
|
+
## Performance
|
|
20
|
+
- `repaint-boundary.tsx` - `repaintBoundary` on a `<view>` to keep static content from rebuilding while a neighbor animates: `{true}` retains the recorded draw list, `"snapshot"` also retains the rasterized pixels as a GPU texture (for raster-expensive, screen-aligned, static subtrees).
|
|
21
|
+
|
|
22
|
+
## Scrolling
|
|
23
|
+
- `scroll.tsx` - `createScroll`, the headless scroll primitive: it owns only the clamped offset (re-clamped on layout); you supply the viewport/content nodes via refs, apply the offset to `scrollX`/`scrollY`, and wire input (e.g. `onWheel`) to `scrollBy` yourself.
|
|
24
|
+
|
|
19
25
|
## Window state
|
|
20
26
|
- `window-signals.tsx` - reactive `windowSize()` / `safeArea()` accessors (prefer over `onResize`).
|
|
27
|
+
- `responsive-grid.tsx` - one app across phone/tablet/desktop: `capabilities.windowSizeClass` (Material 3 breakpoints, a reactive getter) drives the column count and `windowSize()` sizes each card; reflows on resize.
|
|
21
28
|
|
|
22
29
|
## Overlays
|
|
23
30
|
- `portal.tsx` - `createPortal` relocating content to the window root to escape clipping.
|
|
24
31
|
|
|
25
32
|
## Images and GPU
|
|
26
33
|
- `image.tsx` - `createImage` (async value: fetch + decode + upload) read inside a `<Loading>` boundary and shown with `<texture>`.
|
|
27
|
-
- `
|
|
34
|
+
- `inline-image.tsx` - bytes already in memory: `decodeImage` + `createTexture` (both synchronous) show an image with no `<Loading>` boundary. The sync counterpart to `image.tsx`.
|
|
35
|
+
- `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture, animated by driving its `iTime` uniform declaratively through the `<texture params={{...}}>` prop.
|
|
28
36
|
|
|
29
37
|
## Vector graphics
|
|
30
38
|
- `svg.tsx` - `<svg src={...}>` draws a whole SVG *document string* (not HTML/JSX children); multi-color fills vs a `currentColor` icon recolored by the `color` prop. This is how to use existing icon libraries (Lucide, Heroicons, etc.) - hand their SVG source to `src`.
|
|
31
39
|
|
|
32
40
|
## Bundling assets
|
|
33
|
-
- `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (
|
|
41
|
+
- `binary-import.tsx` - `import bytes from "./file" with { type: "binary" }` inlines a file's bytes into the bundle as a `Uint8Array` (the bytes are in memory, so `inline-image.tsx` displays them with the synchronous `decodeImage` + `createTexture` path). `with { type: "text" }` works the same way for a string.
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
//
|
|
7
7
|
// This example shows only the binary import itself: it reports the imported
|
|
8
8
|
// file's length and leading bytes. What you do with the bytes afterwards is a
|
|
9
|
-
// separate concern
|
|
10
|
-
//
|
|
9
|
+
// separate concern. Since inlined bytes are already in memory, the synchronous
|
|
10
|
+
// path fits: decodeImage(bytes) + createTexture (no fetch, no <Loading>) - see
|
|
11
|
+
// inline-image.tsx. Reach for createImage instead when the source is a string
|
|
12
|
+
// URL loaded at runtime, or when you want the source to swap reactively.
|
|
11
13
|
import { render } from "@solidrt/core"
|
|
12
14
|
import bytes from "./logo.png" with { type: "binary" }
|
|
13
15
|
|
package/examples/gpu-shader.tsx
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
// disposed.
|
|
7
7
|
//
|
|
8
8
|
// iResolution is filled in for you, but iTime is NOT - drive it (and any other
|
|
9
|
-
// uniform)
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
import {
|
|
9
|
+
// uniform) declaratively via the <texture> element's params prop; it applies at
|
|
10
|
+
// the next repaint, so a signal updated every frame stays paced to actual frames.
|
|
11
|
+
// The shader's size is baked in at creation.
|
|
12
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
13
|
+
import { createShader } from "@solidrt/core/gpu"
|
|
13
14
|
|
|
14
15
|
let FRAGMENT = `
|
|
15
16
|
void main() {
|
|
@@ -24,11 +25,12 @@ void main() {
|
|
|
24
25
|
|
|
25
26
|
function App() {
|
|
26
27
|
let id = createShader(FRAGMENT, 512, 512, { iTime: 0 })
|
|
27
|
-
|
|
28
|
+
let [time, setTime] = createSignal(0)
|
|
29
|
+
onFrame((tick) => setTime(tick / 1000))
|
|
28
30
|
|
|
29
31
|
return (
|
|
30
32
|
<window alignItems="center" justifyContent="center">
|
|
31
|
-
<texture src={id} width={400} height={400} />
|
|
33
|
+
<texture src={id} params={{ iTime: time() }} width={400} height={400} />
|
|
32
34
|
</window>
|
|
33
35
|
)
|
|
34
36
|
}
|
package/examples/image.tsx
CHANGED
|
@@ -8,8 +8,19 @@
|
|
|
8
8
|
// (this is the 2.0 async mechanic, not a manual undefined-signal + <Show>); a
|
|
9
9
|
// load failure surfaces to <Errored>. Pass an accessor (createImage(() => src()))
|
|
10
10
|
// to make the source reactive - the image reloads and the old texture is freed.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
//
|
|
12
|
+
// The rule: createImage always suspends (needs <Loading>), because it is an
|
|
13
|
+
// async value. For bytes you already hold - a `with { type: "binary" }` import
|
|
14
|
+
// or anything in memory - skip it: decodeImage(bytes) + createTexture (both
|
|
15
|
+
// synchronous) build the texture with no boundary at all. See inline-image.tsx.
|
|
16
|
+
// createImage earns its async only when it fetches a string URL or swaps source
|
|
17
|
+
// reactively; decodeImage + createTexture are the primitives underneath.
|
|
18
|
+
//
|
|
19
|
+
// Raster vs vector: a texture has a fixed source resolution, so it softens when
|
|
20
|
+
// drawn larger than (displayed size x env.displayScale) on a hi-DPI display.
|
|
21
|
+
// Author raster at 2-3x the largest size you will draw it. When the render size
|
|
22
|
+
// is fluid or DPI varies, prefer a vector (svg.tsx / <d-path>) instead - it
|
|
23
|
+
// stays crisp at any size.
|
|
13
24
|
import { render, createImage, Loading } from "@solidrt/core"
|
|
14
25
|
|
|
15
26
|
function App() {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Displaying an image whose bytes are already in memory - here a
|
|
2
|
+
// `with { type: "binary" }` import that inlines the file into the bundle. Because
|
|
3
|
+
// the bytes are on hand, the whole path is synchronous: decodeImage(bytes) turns
|
|
4
|
+
// the encoded PNG into raw RGBA8 pixels, and createTexture uploads them to the
|
|
5
|
+
// GPU and returns an id. No fetch, no async value, so no <Loading> boundary - the
|
|
6
|
+
// texture is ready in the same tick the component builds.
|
|
7
|
+
//
|
|
8
|
+
// Contrast image.tsx, which uses createImage for a string URL: that fetches at
|
|
9
|
+
// runtime, so it suspends and must be read inside <Loading>. The rule: createImage
|
|
10
|
+
// (async) for URLs or reactive sources; decodeImage + createTexture (sync) for
|
|
11
|
+
// bytes you already hold. Called here in the component body, the texture is freed
|
|
12
|
+
// automatically when the owner is disposed.
|
|
13
|
+
import { render, decodeImage, createTexture } from "@solidrt/core"
|
|
14
|
+
import bytes from "./logo.png" with { type: "binary" }
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
let { data, width, height } = decodeImage(bytes)
|
|
18
|
+
let id = createTexture(data, width, height)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<window alignItems="center" justifyContent="center">
|
|
22
|
+
<texture src={id} />
|
|
23
|
+
</window>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render(() => <App />)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// repaintBoundary marks a <view> subtree as its own retained cache, so nearby
|
|
2
|
+
// frequently changing content does not force it to rebuild every frame. It has
|
|
3
|
+
// two modes - both shown here, next to a square that spins every frame:
|
|
4
|
+
//
|
|
5
|
+
// - repaintBoundary={true} retains the recorded DRAW LIST: the subtree is
|
|
6
|
+
// recorded once and the commands are replayed until something inside changes.
|
|
7
|
+
// Skips re-recording. The default choice for static content next to animation.
|
|
8
|
+
//
|
|
9
|
+
// - repaintBoundary="snapshot" additionally retains the RASTERIZED PIXELS as a
|
|
10
|
+
// GPU texture, so replay skips rasterization too. Worth the texture memory
|
|
11
|
+
// only for raster-expensive static content (many glyphs, blurs, dense vector).
|
|
12
|
+
// It re-rasters on layout-size / display-scale changes, crops anything painted
|
|
13
|
+
// outside the layout box, and an ancestor scale animation smears the bitmap -
|
|
14
|
+
// so reach for it only for screen-aligned, static, raster-heavy subtrees.
|
|
15
|
+
//
|
|
16
|
+
// Rule of thumb: start with {true}; upgrade to "snapshot" only when the cached
|
|
17
|
+
// content is expensive to rasterize and stays screen-aligned and static.
|
|
18
|
+
import { render, onFrame, createSignal, For } from "@solidrt/core"
|
|
19
|
+
|
|
20
|
+
// Raster-expensive static content: a dense grid of rects. Cheap to replay from a
|
|
21
|
+
// draw list, but re-rasterizing it every frame would be wasteful - the case
|
|
22
|
+
// "snapshot" is built for.
|
|
23
|
+
function Grid(props: { boundary: true | "snapshot" }) {
|
|
24
|
+
let cells = Array.from({ length: 64 }, (_, i) => i)
|
|
25
|
+
return (
|
|
26
|
+
<view
|
|
27
|
+
repaintBoundary={props.boundary}
|
|
28
|
+
width={200}
|
|
29
|
+
height={200}
|
|
30
|
+
flexDirection="row"
|
|
31
|
+
flexWrap="wrap"
|
|
32
|
+
gap={3}
|
|
33
|
+
>
|
|
34
|
+
<For each={cells}>
|
|
35
|
+
{(i) => <rect width={22} height={22} radius={3} color={i % 2 ? "#2a3f5f" : "#3366b3"} />}
|
|
36
|
+
</For>
|
|
37
|
+
</view>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
let [angle, setAngle] = createSignal(0)
|
|
43
|
+
onFrame((tick) => setAngle(tick / 1000))
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<window flexDirection="row" alignItems="center" justifyContent="center" gap={32}>
|
|
47
|
+
<Grid boundary={true} />
|
|
48
|
+
<Grid boundary="snapshot" />
|
|
49
|
+
<view width={120} height={120} rotate={angle()}>
|
|
50
|
+
<rect width={120} height={120} radius={16} color="#e0245e" />
|
|
51
|
+
</view>
|
|
52
|
+
</window>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render(() => <App />)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Responsive layout: the SAME app on a phone, tablet, or desktop. The window is
|
|
2
|
+
// host-sized and resizable, so drive the layout from the live window instead of
|
|
3
|
+
// hardcoding pixels. Here the column count comes from
|
|
4
|
+
// `capabilities.windowSizeClass` (Material 3 width breakpoints: compact <600,
|
|
5
|
+
// medium 600-840, expanded >=840). Resize the window and the grid reflows.
|
|
6
|
+
//
|
|
7
|
+
// `capabilities` and `env` are plain objects with reactive GETTERS, not
|
|
8
|
+
// functions - read `capabilities.windowSizeClass` (no call). Reading it inside
|
|
9
|
+
// JSX tracks, so the memo below re-runs on every resize. `windowSize()` IS a
|
|
10
|
+
// function (call it) - we read its width to size each card exactly.
|
|
11
|
+
import { render, capabilities, windowSize, createMemo, For } from "@solidrt/core"
|
|
12
|
+
|
|
13
|
+
const GAP = 16
|
|
14
|
+
const PAD = 24
|
|
15
|
+
const COLORS = ["#1f6feb", "#3fb950", "#db6d28", "#a371f7", "#e3b341", "#f778ba", "#2dd4bf", "#f85149"]
|
|
16
|
+
|
|
17
|
+
function App() {
|
|
18
|
+
// Column count from the size class; card width derived so N fit per row.
|
|
19
|
+
let cols = createMemo(() => (capabilities.windowSizeClass === "expanded" ? 3 : capabilities.windowSizeClass === "medium" ? 2 : 1))
|
|
20
|
+
let cardWidth = createMemo(() => (windowSize().width - PAD * 2 - GAP * (cols() - 1)) / cols())
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<window>
|
|
24
|
+
<d-rect color="#0b0f17" />
|
|
25
|
+
<view flex={1} flexDirection="column" gap={GAP} padding={PAD}>
|
|
26
|
+
<text color="#e6e6e6" fontSize={18}>{capabilities.windowSizeClass} - {cols()} column{cols() === 1 ? "" : "s"}</text>
|
|
27
|
+
<view flexDirection="row" flexWrap="wrap" gap={GAP}>
|
|
28
|
+
<For each={COLORS}>
|
|
29
|
+
{(c) => (
|
|
30
|
+
<view width={cardWidth()} height={96} alignItems="center" justifyContent="center">
|
|
31
|
+
<d-rect color={c} radius={12} />
|
|
32
|
+
<text color="#0b0f17" fontSize={16} fontWeight={700}>card</text>
|
|
33
|
+
</view>
|
|
34
|
+
)}
|
|
35
|
+
</For>
|
|
36
|
+
</view>
|
|
37
|
+
</view>
|
|
38
|
+
</window>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(() => <App />)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// createScroll is a HEADLESS scroll primitive: it owns only the geometry - the
|
|
2
|
+
// clamped offset, re-clamped in onLayout against the measured content-vs-viewport
|
|
3
|
+
// overflow - and nothing with a UI opinion. Input handling, momentum, and
|
|
4
|
+
// scrollbars are policy you supply. (@solidrt/components ScrollView is one such
|
|
5
|
+
// skin built on top of this.)
|
|
6
|
+
//
|
|
7
|
+
// The shape it expects:
|
|
8
|
+
// - a VIEWPORT node: the clipping box, overflow="hidden".
|
|
9
|
+
// - a CONTENT node inside it: an inner wrapper that takes the children's natural
|
|
10
|
+
// size (flexShrink={0} so it can exceed the viewport).
|
|
11
|
+
// Capture both with refs, pass their accessors to createScroll, then apply the
|
|
12
|
+
// returned offset to the viewport's scrollX/scrollY. createScroll does no input,
|
|
13
|
+
// so wire an event (here onWheel) to scroll.scrollBy - positive dy moves content
|
|
14
|
+
// up. scrollTo(x, y) jumps to an absolute, clamped offset.
|
|
15
|
+
import { render, For, createScroll } from "@solidrt/core"
|
|
16
|
+
import type { WheelEvent } from "@solidrt/core"
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
let viewport: { id: number } | undefined
|
|
20
|
+
let content: { id: number } | undefined
|
|
21
|
+
|
|
22
|
+
// Default axis is "vertical"; pass { axis: "horizontal" } or "both" for others.
|
|
23
|
+
let scroll = createScroll(() => viewport, () => content)
|
|
24
|
+
|
|
25
|
+
let onWheel = (e: WheelEvent) => scroll.scrollBy(e.deltaX, e.deltaY)
|
|
26
|
+
|
|
27
|
+
let rows = Array.from({ length: 30 }, (_, i) => i)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<window alignItems="center" justifyContent="center">
|
|
31
|
+
<view
|
|
32
|
+
ref={(n: { id: number }) => (viewport = n)}
|
|
33
|
+
width={220}
|
|
34
|
+
height={280}
|
|
35
|
+
overflow="hidden"
|
|
36
|
+
clipRadius={12}
|
|
37
|
+
scrollY={scroll.offset().y}
|
|
38
|
+
onWheel={onWheel}
|
|
39
|
+
>
|
|
40
|
+
<d-rect color="#1a2233" radius={12} />
|
|
41
|
+
<view ref={(n: { id: number }) => (content = n)} flexShrink={0} padding={12} gap={8}>
|
|
42
|
+
<For each={rows}>
|
|
43
|
+
{(i) => (
|
|
44
|
+
<view height={40} justifyContent="center" paddingLeft={12}>
|
|
45
|
+
<rect height={40} radius={6} color={i % 2 ? "#2a3f5f" : "#3366b3"} />
|
|
46
|
+
<text color="#ffffff">Row {i}</text>
|
|
47
|
+
</view>
|
|
48
|
+
)}
|
|
49
|
+
</For>
|
|
50
|
+
</view>
|
|
51
|
+
</view>
|
|
52
|
+
</window>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render(() => <App />)
|
package/examples/svg.tsx
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// "currentColor" is recolored by the host `color` prop. For per-shape authored
|
|
11
11
|
// or animated vector art, compose <d-path> instead of this document layer.
|
|
12
12
|
//
|
|
13
|
+
// Being a vector, an <svg> is resolution-independent: it stays crisp at any
|
|
14
|
+
// drawn size x displayScale(). Prefer it over a raster <texture> (image.tsx)
|
|
15
|
+
// whenever the render size is fluid or the display DPI varies.
|
|
16
|
+
//
|
|
13
17
|
// This is how you use an existing icon library (Lucide, Heroicons, Feather,
|
|
14
18
|
// Material, etc.): those ship SVG source, so import/inline the icon string and
|
|
15
19
|
// hand it to `src`. The `currentColor` convention they follow means the `color`
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -23,26 +23,30 @@ export namespace JSX {
|
|
|
23
23
|
type RefCallback<T> = (el: T) => unknown
|
|
24
24
|
type Ref<T> = T | RefCallback<T>
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// ref lives on the element prop types (intersected into every entry below)
|
|
27
|
+
// rather than in IntrinsicAttributes: under our config, declaring it only in
|
|
28
|
+
// IntrinsicAttributes did not make it reach intrinsic elements, so `ref` on a
|
|
29
|
+
// host element was reported as an excess property.
|
|
30
|
+
interface ElementRef {
|
|
27
31
|
ref?: Ref<{ id: number }> | undefined
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
interface IntrinsicElements {
|
|
31
|
-
window: WindowProps
|
|
32
|
-
view: ViewProps
|
|
33
|
-
text: TextProps & LayoutProps
|
|
34
|
-
rect: RectProps & LayoutProps
|
|
35
|
-
oval: OvalProps & LayoutProps
|
|
36
|
-
path: PathProps & LayoutProps
|
|
37
|
-
svg: SvgProps & LayoutProps
|
|
38
|
-
texture: TextureProps & LayoutProps
|
|
39
|
-
audio: AudioProps
|
|
40
|
-
"d-view": ViewProps
|
|
41
|
-
"d-rect": RectProps
|
|
42
|
-
"d-oval": OvalProps
|
|
43
|
-
"d-path": PathProps
|
|
44
|
-
"d-svg": SvgProps
|
|
45
|
-
"d-texture": TextureProps
|
|
46
|
-
"d-text": TextProps
|
|
35
|
+
window: WindowProps & ElementRef
|
|
36
|
+
view: ViewProps & ElementRef
|
|
37
|
+
text: TextProps & LayoutProps & ElementRef
|
|
38
|
+
rect: RectProps & LayoutProps & ElementRef
|
|
39
|
+
oval: OvalProps & LayoutProps & ElementRef
|
|
40
|
+
path: PathProps & LayoutProps & ElementRef
|
|
41
|
+
svg: SvgProps & LayoutProps & ElementRef
|
|
42
|
+
texture: TextureProps & LayoutProps & ElementRef
|
|
43
|
+
audio: AudioProps & ElementRef
|
|
44
|
+
"d-view": ViewProps & ElementRef
|
|
45
|
+
"d-rect": RectProps & ElementRef
|
|
46
|
+
"d-oval": OvalProps & ElementRef
|
|
47
|
+
"d-path": PathProps & ElementRef
|
|
48
|
+
"d-svg": SvgProps & ElementRef
|
|
49
|
+
"d-texture": TextureProps & ElementRef
|
|
50
|
+
"d-text": TextProps & ElementRef
|
|
47
51
|
}
|
|
48
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"./gpu": "./src/gpu.ts",
|
|
12
12
|
"./image": "./src/image.ts",
|
|
13
13
|
"./microphone": "./src/microphone.ts",
|
|
14
|
-
"./scroll": "./src/scroll.ts",
|
|
15
14
|
"./speech-recognition": "./src/speech-recognition.ts",
|
|
16
15
|
"./text-input": "./src/text-input.ts",
|
|
17
16
|
"./jsx-runtime": "./jsx-runtime.d.ts",
|
|
@@ -27,7 +26,7 @@
|
|
|
27
26
|
"colord": "^2.9.3"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
29
|
+
"@solidrt/flux-types": "0.0.23"
|
|
31
30
|
},
|
|
32
31
|
"peerDependencies": {
|
|
33
32
|
"@solidjs/signals": "2.0.0-beta.15",
|
package/src/gpu.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
// GPU textures and shaders, reactive (SolidJS) layer: the create* helpers free
|
|
2
|
-
// their texture automatically when the reactive owner is disposed.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// their texture automatically when the reactive owner is disposed. Drive a
|
|
3
|
+
// shader's uniforms declaratively with `<texture src={id} params={{...}} />`
|
|
4
|
+
// (see TextureProps) - the preferred way, deferred to the next real repaint so
|
|
5
|
+
// a fast-changing signal stays paced to actual frames. setShaderParams is the
|
|
6
|
+
// imperative exception: reach for it only when there is no `<texture>` element
|
|
7
|
+
// to hold a params prop, e.g. a shader that only feeds another shader as a
|
|
8
|
+
// sampler2D input. The imperative primitives (uploadTexture, setShaderParams,
|
|
9
|
+
// destroyTexture, ...) live in the `flux:gpu` module.
|
|
5
10
|
|
|
6
11
|
import { getOwner, onCleanup } from "@solidjs/signals"
|
|
7
12
|
import * as gpu from "flux:gpu"
|
|
8
13
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// a
|
|
14
|
+
// Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
|
|
15
|
+
// -- need not import flux directly: destroyTexture for the manual-cleanup path
|
|
16
|
+
// (textures made outside a reactive scope, e.g. after an await, are not
|
|
17
|
+
// auto-freed), uploadTexture to push new pixels into a mutable texture, and
|
|
18
|
+
// setShaderParams as the non-reactive exception described above - prefer
|
|
19
|
+
// `<texture params={...}>` when a `<texture>` element is already in the tree.
|
|
15
20
|
export { destroyTexture, setShaderParams, uploadTexture } from "flux:gpu"
|
|
16
21
|
|
|
17
22
|
/**
|
|
@@ -48,12 +53,14 @@ export function createMutableTexture(data: Uint8Array, width: number, height: nu
|
|
|
48
53
|
* Compiles a GLSL ES 3.00 fragment shader and renders it into a texture,
|
|
49
54
|
* returning the texture id (usable anywhere a normal texture id is, e.g.
|
|
50
55
|
* `<texture src>`). The fragment body may reference `vUV` (0..1, top-left
|
|
51
|
-
* origin), `iResolution`, `iTime`, and any `uniform float` it declares;
|
|
52
|
-
* their values
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* origin), `iResolution`, `iTime`, and any `uniform float` it declares; drive
|
|
57
|
+
* their values with `<texture src={id} params={{...}} />` (preferred) or, when
|
|
58
|
+
* there is no `<texture>` element for it, imperatively with `setShaderParams`.
|
|
59
|
+
* `textures` binds each declared `uniform sampler2D` to an existing texture id
|
|
60
|
+
* (e.g. a camera or decoded image) so the shader can read it; those inputs are
|
|
61
|
+
* re-sampled on every params update, so live sources stay current. Frees the
|
|
62
|
+
* texture and shader program when the reactive owner is disposed; create
|
|
63
|
+
* outside any reactive scope for app-lifetime shaders.
|
|
57
64
|
*/
|
|
58
65
|
export function createShader(
|
|
59
66
|
fragmentSrc: string,
|
package/src/image.ts
CHANGED
|
@@ -34,6 +34,12 @@ export type ImageSource = string | Uint8Array
|
|
|
34
34
|
* current texture is freed when the owner is disposed. Display it with
|
|
35
35
|
* `<texture src={id()} />`; the texture carries its own pixel size, so no
|
|
36
36
|
* width/height is needed unless you want to scale it.
|
|
37
|
+
*
|
|
38
|
+
* For bytes you already hold (a `with { type: "binary" }` import, or anything in
|
|
39
|
+
* memory) this suspends needlessly: `decodeImage` + `createTexture` are both
|
|
40
|
+
* synchronous, so reach for them directly and skip the `<Loading>` boundary.
|
|
41
|
+
* `createImage` earns its async only for a fetched string URL or a reactive
|
|
42
|
+
* source.
|
|
37
43
|
*/
|
|
38
44
|
export function createImage(src: ImageSource | (() => ImageSource)): () => number {
|
|
39
45
|
let getSrc = typeof src === "function" ? src : () => src
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type { Capabilities, WindowSizeClass } from "./capabilities"
|
|
|
13
13
|
export { createTexture } from "./gpu"
|
|
14
14
|
export { createImage, decodeImage } from "./image"
|
|
15
15
|
export type { DecodedImage, ImageSource } from "./image"
|
|
16
|
+
export { createScroll } from "./scroll"
|
|
17
|
+
export type { Scroll, ScrollAxis, ScrollOffset, ScrollOptions } from "./scroll"
|
|
16
18
|
export type {
|
|
17
19
|
LayoutProps,
|
|
18
20
|
TransformProps,
|
package/src/types.d.ts
CHANGED
|
@@ -37,10 +37,17 @@ export interface ElementChildrenAttribute {
|
|
|
37
37
|
|
|
38
38
|
type Children = Element
|
|
39
39
|
|
|
40
|
+
// Doc-comment policy for this file: JSX props mostly mirror CSS/DOM, which any
|
|
41
|
+
// developer or agent already knows, so a comment restating standard semantics
|
|
42
|
+
// is noise. Add one only where a prop deviates from that standard, is bespoke
|
|
43
|
+
// to this engine, or has an interaction that isn't decidable from the type
|
|
44
|
+
// alone (e.g. shorthand-vs-longhand precedence, a subsetted value range).
|
|
45
|
+
|
|
40
46
|
interface FlexboxProps {
|
|
41
47
|
gap?: number
|
|
42
48
|
rowGap?: number
|
|
43
49
|
columnGap?: number
|
|
50
|
+
/** Shorthand; overridden by flexGrow/flexShrink/flexBasis when they're also set. */
|
|
44
51
|
flex?: number | "none" | "auto" | (string & {})
|
|
45
52
|
flexGrow?: number
|
|
46
53
|
flexShrink?: number
|
|
@@ -54,6 +61,7 @@ interface FlexboxProps {
|
|
|
54
61
|
justifyContent?: "start" | "end" | "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-evenly" | "space-around"
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
/** CSS grid subset: line-based placement only, no named lines, no grid-template-areas, and auto tracks take a fixed size (no minmax/fr/keyword). */
|
|
57
65
|
interface GridProps {
|
|
58
66
|
gridAutoFlow?: "row" | "column" | "row-dense" | "column-dense"
|
|
59
67
|
gridAutoColumns?: number
|
|
@@ -66,10 +74,17 @@ interface GridProps {
|
|
|
66
74
|
gridTemplateRows?: string
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
/** A layout length: a bare number is pixels, plus "auto" and percent strings. */
|
|
69
78
|
type Dimension = number | "auto" | `${number}%`
|
|
70
79
|
|
|
71
80
|
export interface LayoutProps extends FlexboxProps, GridProps {
|
|
72
81
|
display?: "block" | "flex" | "grid" | "none"
|
|
82
|
+
/**
|
|
83
|
+
* No "fixed" or "sticky". Unlike CSS, `absolute` does not itself become a
|
|
84
|
+
* containing block: an absolute element resolves against the nearest
|
|
85
|
+
* ancestor with `position: relative`, so a chain of absolute elements
|
|
86
|
+
* resolves against whatever relative element is above all of them.
|
|
87
|
+
*/
|
|
73
88
|
position?: "relative" | "absolute"
|
|
74
89
|
|
|
75
90
|
top?: Dimension
|
|
@@ -201,8 +216,6 @@ export interface WindowProps extends LayoutProps {
|
|
|
201
216
|
children?: Children
|
|
202
217
|
title?: string
|
|
203
218
|
fullscreen?: boolean
|
|
204
|
-
vsync?: boolean
|
|
205
|
-
fps?: boolean
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
|
|
@@ -229,6 +242,11 @@ export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
|
|
|
229
242
|
repaintBoundary?: boolean | "snapshot"
|
|
230
243
|
}
|
|
231
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Not implemented: there is no native `audio` element kind yet, so using
|
|
247
|
+
* `<audio>` panics ("unknown node kind: audio"). Typed ahead of the backing
|
|
248
|
+
* work; treat this interface as a plan, not a working API.
|
|
249
|
+
*/
|
|
232
250
|
export interface AudioProps {
|
|
233
251
|
src?: Uint8Array
|
|
234
252
|
play?: number
|
|
@@ -245,7 +263,9 @@ export interface RectProps extends Position, PaintProps, PointerProps {
|
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
export interface OvalProps extends Position, PaintProps, PointerProps {
|
|
266
|
+
/** Bounding box width of the ellipse (not a radius); defaults to the layout box. */
|
|
248
267
|
w?: number
|
|
268
|
+
/** Bounding box height of the ellipse (not a radius); defaults to the layout box. */
|
|
249
269
|
h?: number
|
|
250
270
|
}
|
|
251
271
|
|
|
@@ -254,7 +274,9 @@ export interface LineProps extends PaintProps, PointerProps {
|
|
|
254
274
|
y1?: number
|
|
255
275
|
x2?: number
|
|
256
276
|
y2?: number
|
|
277
|
+
/** Dash pattern in local units: the drawn segment length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
|
|
257
278
|
onLength?: number
|
|
279
|
+
/** Dash pattern in local units: the gap length. Both onLength and offLength must be set to dash; with either unset the line is solid. */
|
|
258
280
|
offLength?: number
|
|
259
281
|
}
|
|
260
282
|
|
|
@@ -289,11 +311,14 @@ export interface TextProps extends Position, PaintProps, PointerProps {
|
|
|
289
311
|
|
|
290
312
|
export interface TextureProps extends Position {
|
|
291
313
|
src?: number
|
|
292
|
-
|
|
293
|
-
|
|
314
|
+
w?: number
|
|
315
|
+
h?: number
|
|
294
316
|
srcX?: number
|
|
295
317
|
srcY?: number
|
|
296
318
|
srcW?: number
|
|
297
319
|
srcH?: number
|
|
320
|
+
// Shader uniform values, when src names a shader texture. Applied at the
|
|
321
|
+
// next repaint (not synchronously), so a fast-changing signal stays paced
|
|
322
|
+
// to real frames rather than triggering a GL render pass per write.
|
|
298
323
|
params?: Record<string, number>
|
|
299
324
|
}
|