@solidrt/core 0.0.38 → 0.0.39

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 CHANGED
@@ -84,7 +84,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
84
84
  ```
85
85
 
86
86
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
87
- 2.0.0-beta.20); bun resolves them from peerDependencies.
87
+ 2.0.0-beta.26); bun resolves them from peerDependencies.
88
88
 
89
89
  ## Element model (the parts that are easy to get wrong)
90
90
 
@@ -20,7 +20,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
20
20
  - `pointer-local-coords.tsx` - the three pointer coordinate frames (`clientX` window, `localX` the handling node's own frame, `parentX` its path-parent's frame - where the node's x/y live) and the transform-proof drag idiom: grab offset from `localX` at down, place with `parentX - offset` on moves. Exact inside rotated/scaled ancestors and when the pointer leaves the node mid-drag.
21
21
 
22
22
  ## Performance
23
- - `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).
23
+ - `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). `"snapshot-no-aa"` rasterizes without anti-aliasing: cheaper, fine for text and axis-aligned rects, hard-edged on vector content.
24
24
 
25
25
  ## Scrolling
26
26
  - `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.
@@ -36,6 +36,9 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
36
36
  - `image.tsx` - `createImage` (async value: fetch + decode + upload) read inside a `<Loading>` boundary and shown with `<texture>`.
37
37
  - `inline-image.tsx` - bytes already in memory: `decodeImage` + `createTexture` (both synchronous) show an image with no `<Loading>` boundary. The sync counterpart to `image.tsx`.
38
38
  - `gpu-shader.tsx` - a GLSL fragment shader rendered to a texture, animated by driving its `iTime` uniform declaratively through the `<texture params={{...}}>` prop.
39
+ - `gpu-raw-program.tsx` - the raw shading layer: compileShader/linkProgram/createShaderTarget, one vertex stage shared by two programs, with and without the standard header.
40
+ - `window-shader.tsx` - the `shader` prop on `<window>`: the finished frame drawn through a raw-linked warp program before present, click to toggle between warp and identity.
41
+ - `window-shader-history.tsx` - the window shader's frame history: `previous` binds last frame as uPrevious, drawn as a one-frame motion echo behind an orbiting square; click toggles the echo term.
39
42
 
40
43
  ## Sound
41
44
  - `sound.tsx` - `createSound`: decode a clip once from bytes (here a binary import), replay cheaply; `overlap` stacking vs single-voice, `playing()` signal, release on unmount. Points to `createSoundStream` for long tracks streamed from a path.
@@ -0,0 +1,62 @@
1
+ // `previous: true` on the window shader retains the last resolved frame as a
2
+ // second layer the program samples as uPrevious, rotated each frame - a
3
+ // one-frame history. Here it draws a motion echo behind the orbiting square;
4
+ // click to toggle the echo term off and compare with the plain frame.
5
+ import { render, onFrame, createSignal } from "@solidrt/core"
6
+ import { compileShader, destroyShader, linkProgram } from "@solidrt/core/gpu"
7
+
8
+ let VERTEX = `#version 300 es
9
+ precision highp float;
10
+ out vec2 vUV;
11
+ void main() {
12
+ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
13
+ // uSource/uPrevious are top-left origin; flip v so the frame lands upright.
14
+ vUV = vec2(p.x, 1.0 - p.y);
15
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
16
+ }
17
+ `
18
+
19
+ let ECHO = `
20
+ uniform sampler2D uSource;
21
+ uniform sampler2D uPrevious;
22
+ uniform float uEcho;
23
+ in vec2 vUV;
24
+ void main() {
25
+ vec4 cur = texture(uSource, vUV);
26
+ vec4 prev = texture(uPrevious, vUV);
27
+ // Brightest of the current frame and the decayed previous one: motion
28
+ // leaves a one-frame ghost trailing it.
29
+ fragColor = max(cur, prev * uEcho);
30
+ }
31
+ `
32
+
33
+ function App() {
34
+ let vs = compileShader("vertex", VERTEX)
35
+ let fs = compileShader("fragment", ECHO, { header: true })
36
+ let echoProgram = linkProgram(vs, fs)
37
+ destroyShader(vs)
38
+ destroyShader(fs)
39
+
40
+ let [angle, setAngle] = createSignal(0)
41
+ let [echo, setEcho] = createSignal(0.65)
42
+ onFrame(tick => setAngle(tick / 350))
43
+
44
+ return (
45
+ <window
46
+ shader={{ program: echoProgram, params: { uEcho: echo() }, previous: true }}
47
+ onPointerDown={() => setEcho(e => (e > 0 ? 0 : 0.65))}
48
+ alignItems="center"
49
+ justifyContent="center"
50
+ >
51
+ <rect position="absolute" top={0} right={0} bottom={0} left={0} color="#101826" />
52
+ <view width={70} height={70} x={Math.cos(angle()) * 150} y={Math.sin(angle()) * 150}>
53
+ <rect width={70} height={70} radius={16} color="#7ad0ff" />
54
+ </view>
55
+ <text position="absolute" bottom={24} fontSize={14} color="#99aabb">
56
+ Click to toggle the uPrevious echo
57
+ </text>
58
+ </window>
59
+ )
60
+ }
61
+
62
+ render(() => <App />)
@@ -0,0 +1,71 @@
1
+ // The window shader: the finished frame renders into a runtime-owned layer
2
+ // texture and a linked program draws over it into the window, as the last
3
+ // step before present. The program samples the frame as uSource (top-left
4
+ // origin - the vertex stage flips v when mapping onto the window), gets
5
+ // iResolution in physical pixels, and draws attributeless at vertexCount
6
+ // (default 3, the covering triangle).
7
+ //
8
+ // Click anywhere to toggle the warp amount between 0 and 1: at 0 the program
9
+ // is an identity pass, which must be indistinguishable from no shader at all
10
+ // (the orientation/half-pixel regression check from the plan).
11
+ import { render, onFrame, createSignal } from "@solidrt/core"
12
+ import { compileShader, destroyShader, linkProgram } from "@solidrt/core/gpu"
13
+
14
+ let VERTEX = `#version 300 es
15
+ precision highp float;
16
+ out vec2 vUV;
17
+ void main() {
18
+ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
19
+ // uSource is top-left origin; flip v so the frame lands upright on the
20
+ // window (the one flip of the frame path, done here in the vertex stage).
21
+ vUV = vec2(p.x, 1.0 - p.y);
22
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
23
+ }
24
+ `
25
+
26
+ // { header: true } declares #version, precision, iResolution/iTime and
27
+ // fragColor; uSource, vUV, and the app's own uniforms are declared here.
28
+ let WARP = `
29
+ uniform sampler2D uSource;
30
+ uniform float uAmount;
31
+ in vec2 vUV;
32
+ void main() {
33
+ vec2 uv = vUV;
34
+ uv.x += sin(uv.y * 24.0 + iTime * 3.0) * 0.012 * uAmount;
35
+ uv.y += sin(uv.x * 18.0 - iTime * 2.0) * 0.012 * uAmount;
36
+ fragColor = texture(uSource, uv);
37
+ }
38
+ `
39
+
40
+ function App() {
41
+ let vs = compileShader("vertex", VERTEX)
42
+ let fs = compileShader("fragment", WARP, { header: true })
43
+ let warp = linkProgram(vs, fs)
44
+ destroyShader(vs)
45
+ destroyShader(fs)
46
+
47
+ let [time, setTime] = createSignal(0)
48
+ let [amount, setAmount] = createSignal(1)
49
+ onFrame(tick => setTime(tick / 1000))
50
+
51
+ return (
52
+ <window
53
+ shader={{ program: warp, params: { iTime: time(), uAmount: amount() } }}
54
+ onPointerDown={() => setAmount(a => (a > 0 ? 0 : 1))}
55
+ flexDirection="column"
56
+ gap={12}
57
+ alignItems="center"
58
+ justifyContent="center"
59
+ >
60
+ <text fontSize={28} color="#222">Window shader</text>
61
+ <view flexDirection="row" gap={12}>
62
+ <rect w={90} h={90} radius={12} color="#0077ff" />
63
+ <rect w={90} h={90} radius={12} color="#ff6a00" />
64
+ <rect w={90} h={90} radius={12} color="#00c46a" />
65
+ </view>
66
+ <text fontSize={14} color="#666">Click to toggle warp (identity at 0)</text>
67
+ </window>
68
+ )
69
+ }
70
+
71
+ render(() => <App />)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  "colord": "^2.9.3"
28
28
  },
29
29
  "devDependencies": {
30
- "@solidrt/flux-types": "0.0.38"
30
+ "@solidrt/flux-types": "0.0.39"
31
31
  },
32
32
  "peerDependencies": {
33
- "@solidjs/signals": "2.0.0-beta.20",
34
- "@solidjs/universal": "2.0.0-beta.20",
35
- "solid-js": "2.0.0-beta.20"
33
+ "@solidjs/signals": "2.0.0-beta.26",
34
+ "@solidjs/universal": "2.0.0-beta.26",
35
+ "solid-js": "2.0.0-beta.26"
36
36
  }
37
37
  }
package/src/gamepad.ts CHANGED
@@ -15,6 +15,12 @@ import { on } from "srt:events"
15
15
  * "back", "guide", "leftShoulder", "rightShoulder", "leftStick",
16
16
  * "rightStick"). `axes` has sticks ("leftX", "leftY", "rightX", "rightY") in
17
17
  * -1..1 and triggers ("leftTrigger", "rightTrigger") in 0..1.
18
+ *
19
+ * The snapshot is a faithful report. Note that pressing "back" (select) on a
20
+ * mapped pad ALSO emits the `back` event (see onBack) - it is the pad-side
21
+ * sibling of Android's system back, the runtime's exit-to-launcher gesture.
22
+ * Apps that bind "back" for their own controls should preventDefault that
23
+ * event.
18
24
  */
19
25
  export interface GamepadState {
20
26
  /** Runtime instance id: unique per connection, not stable across reconnects. */
package/src/gpu.ts CHANGED
@@ -44,11 +44,30 @@ export {
44
44
  export { destroyBuffer, setDrawCount } from "flux:gpu"
45
45
  export type { Topology, VertexAttribute } from "flux:gpu"
46
46
 
47
+ // The raw shading layer, re-exported as-is - no reactive wrapper, the app
48
+ // owns these lifetimes. compileShader compiles one stage from complete GLSL
49
+ // ES (or with the standard header via { header: true }); linkProgram links a
50
+ // vertex and a fragment stage into a program handle that backs any number of
51
+ // createShaderTarget calls (and compiles nothing per target); destroyShader /
52
+ // destroyProgram free by id space, either order safe against live targets.
53
+ // createShader/createPipeline remain the fused conveniences on top.
54
+ export { compileShader, destroyProgram, destroyShader, linkProgram } from "flux:gpu"
55
+
47
56
  // captureSnapshot renders a node to a texture and readTexture reads any
48
57
  // texture's bytes back. Re-exported raw (no reactive auto-cleanup wrapper):
49
58
  // captureSnapshot resolves asynchronously, by which point the reactive owner is
50
59
  // no longer current, so the caller owns the returned id and frees it with
51
60
  // destroyTexture (as with any texture created after an await).
61
+ //
62
+ // Together they are the one-shot bake path: draw something only the engine can
63
+ // produce (shaped text, an SVG, a themed view), capture it, read the pixels and
64
+ // process them on the CPU - baking a glyph atlas is the worked example. Not a
65
+ // rendering path: a capture rasterizes the subtree offscreen, reads it back to
66
+ // the CPU and re-uploads it, costing a full GPU -> CPU -> GPU round trip and a
67
+ // paint pass of latency every call. Batch captures (one paint pass services
68
+ // many), never run them per frame, and do not use them to feed live screen
69
+ // content into a shader - for that the source has to update in place (another
70
+ // pipeline's target, a camera texture).
52
71
  export { captureSnapshot, readTexture } from "flux:gpu"
53
72
 
54
73
  /**
@@ -111,6 +130,37 @@ export function createShader(
111
130
  return id
112
131
  }
113
132
 
133
+ /**
134
+ * Creates a render target over a program from `linkProgram` and renders it
135
+ * once, returning the texture id (usable anywhere a normal texture id is,
136
+ * e.g. `<texture src>`; resize with `setShaderSize`, drive uniforms with
137
+ * `<texture params>` or `setShaderParams`). Many targets may share one
138
+ * program, and creating a target compiles nothing. The mesh options mirror
139
+ * `createPipeline`: a raw-linked program carries its own vertex stage, so a
140
+ * fullscreen pass is `{ vertexCount: 3 }` over a covering-triangle vertex
141
+ * stage. Frees the target when the reactive owner is disposed (opt out with
142
+ * `opts.manual`); the program is yours and outlives it.
143
+ */
144
+ export function createShaderTarget(
145
+ program: number,
146
+ width: number,
147
+ height: number,
148
+ opts?: {
149
+ params?: Record<string, number>
150
+ textures?: Record<string, number>
151
+ attributes?: gpu.VertexAttribute[]
152
+ buffer?: number
153
+ topology?: gpu.Topology
154
+ vertexCount?: number
155
+ depth?: boolean
156
+ clearColor?: [number, number, number, number]
157
+ } & CreateOptions,
158
+ ): number {
159
+ let id = gpu.createShaderTarget(program, width, height, opts)
160
+ if (!opts?.manual && getOwner()) onCleanup(() => gpu.destroyTexture(id))
161
+ return id
162
+ }
163
+
114
164
  /** The reactive shader description `createShaderMemo` builds from. */
115
165
  export type ShaderSpec = {
116
166
  fragmentSrc: string
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export type {
27
27
  TextEvent,
28
28
  PaintProps,
29
29
  WindowProps,
30
+ WindowShaderProps,
30
31
  ViewProps,
31
32
  RectProps,
32
33
  OvalProps,
@@ -83,10 +83,24 @@ declare module "srt:apps" {
83
83
  export const available: boolean
84
84
  /**
85
85
  * An installed app: id, display name (the installed manifest's displayName,
86
- * defaulting to the id) and current version id (manifest hash).
86
+ * defaulting to the id) and current version id (manifest hash). `updated` is
87
+ * when that version became current, in milliseconds since the epoch (0 when
88
+ * the store's timestamp is unreadable); a repush of an identical manifest
89
+ * installs nothing and leaves it alone. `size` is the version's
90
+ * manifest-declared size (bundle plus assets) - claimed rather than walked,
91
+ * so that listing stays cheap; `info()` reports what is actually on disk.
92
+ * `icon` is the manifest-declared icon's SVG source, ready for an `<svg>`
93
+ * src; absent when the app declares none (or the file is unreadable).
87
94
  */
88
- export type InstalledApp = { id: string; name: string; version: string }
89
- /** Installed apps, sorted by name. */
95
+ export type InstalledApp = {
96
+ id: string
97
+ name: string
98
+ icon?: string
99
+ version: string
100
+ updated: number
101
+ size: number
102
+ }
103
+ /** Installed apps, most recently updated first. */
90
104
  export function list(): InstalledApp[]
91
105
  /**
92
106
  * A stored version: id (manifest hash), bytes on disk, whether it is the
package/src/types.d.ts CHANGED
@@ -275,6 +275,45 @@ export interface WindowProps extends LayoutProps, PointerProps {
275
275
  children?: Children
276
276
  title?: string
277
277
  fullscreen?: boolean
278
+ /**
279
+ * Run the window's finished frame through a GPU program as the last step
280
+ * before it reaches the screen. While declared, the frame renders into a
281
+ * runtime-owned layer texture the program samples; removing the prop
282
+ * restores the direct path and frees the layer. Everything else about the
283
+ * program (compiling, linking, lifetime) is the raw shading layer's:
284
+ * see compileShader/linkProgram.
285
+ */
286
+ shader?: WindowShaderProps | null
287
+ }
288
+
289
+ /**
290
+ * A window shader declaration. The program reads the frame through
291
+ * `uniform sampler2D uSource` (top-left origin, like every sampled texture -
292
+ * so a vertex stage mapping it onto the window flips the v coordinate) and
293
+ * is drawn attributeless as triangles, `vertexCount` vertices fetched via
294
+ * gl_VertexID. `iResolution`, filled by name, is the window size in physical
295
+ * pixels (the pass covers exactly that). The window is cleared to opaque
296
+ * black first, so geometry that does not cover it still presents a defined
297
+ * frame.
298
+ */
299
+ export interface WindowShaderProps {
300
+ /** Linked program handle from linkProgram. */
301
+ program: number
302
+ /** Float uniforms filled by name, paced to the next real repaint. */
303
+ params?: Record<string, number>
304
+ /** Extra sampler2D inputs: uniform name to texture id. */
305
+ textures?: Record<string, number>
306
+ /** Vertices drawn (attributeless triangles). Default 3, the covering triangle. */
307
+ vertexCount?: number
308
+ /**
309
+ * Retain the last frame as a second layer the program samples as
310
+ * `uniform sampler2D uPrevious` (one-frame history: motion echo, frame
311
+ * differencing). Costs one extra window-sized texture while declared.
312
+ * Until a second frame exists uPrevious samples opaque black. Only declare
313
+ * the uPrevious uniform together with this flag - without it the uniform
314
+ * stays at unit 0 and aliases uSource. Default false.
315
+ */
316
+ previous?: boolean
278
317
  }
279
318
 
280
319
  export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
@@ -297,8 +336,13 @@ export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
297
336
  * on layout-size or display-scale changes. Content painted outside the
298
337
  * element's layout box is cropped, and ancestor scale animations smear the
299
338
  * bitmap; best for screen-aligned, static, raster-expensive content.
339
+ *
340
+ * "snapshot-no-aa" is "snapshot" rasterized without anti-aliasing: cheaper
341
+ * (no multisampled scratch, one render pass), but vector content - svg
342
+ * paths, rounded corners, rotated edges - comes out hard-edged. Text and
343
+ * axis-aligned rects look identical, so prefer it for plain UI panels.
300
344
  */
301
- repaintBoundary?: boolean | "snapshot"
345
+ repaintBoundary?: boolean | "snapshot" | "snapshot-no-aa"
302
346
  }
303
347
 
304
348
  // draw primitives
@@ -363,7 +407,16 @@ export interface TextProps extends Position, PaintProps, PointerProps {
363
407
  maxLines?: number
364
408
  }
365
409
 
366
- export interface TextureProps extends Position, PointerProps {
410
+ /**
411
+ * A raster draw uses only part of a paint. `blendMode` applies, which is how
412
+ * two GPU layers composite in the tree (a solid pass plus an additive pass)
413
+ * without a hand-written compositing shader. `color` contributes its alpha
414
+ * only, as an opacity multiplier; its RGB does not tint, and a gradient does
415
+ * not replace the texture. `drawStyle` and the stroke props have no effect.
416
+ * Texture alpha is premultiplied, so additive modes need no manual
417
+ * premultiplication.
418
+ */
419
+ export interface TextureProps extends Position, PaintProps, PointerProps {
367
420
  src?: number
368
421
  /**
369
422
  * How the texture's pixels map to the element box (CSS object-fit).
package/src/window.ts CHANGED
@@ -190,22 +190,38 @@ export function onWindowBlur(fn: () => void) {
190
190
 
191
191
  export type BackEvent = { preventDefault: () => void }
192
192
 
193
- // App handlers for the window-level back event, run in registration order.
194
- // Kept in a local registry (not per-handler bus subscriptions) so the default
195
- // action runs exactly once, after every handler has had its say.
196
- let backHandlers = new Set<(e: BackEvent) => void>()
193
+ // App handlers for the window-level back event, as a stack: the last one
194
+ // registered is offered the event first, and the first to prevent ends the
195
+ // dispatch. Back is a pop, so the thing most recently put on screen has to
196
+ // answer for it - a dialog that opens over a screen registers after it and must
197
+ // win, and registration order tracks mount order (a parent sets up before its
198
+ // children), so reverse order also reads as innermost-first. Kept in a local
199
+ // registry rather than per-handler bus subscriptions so the default action runs
200
+ // exactly once, after the handlers have had their say.
201
+ let backHandlers: ((e: BackEvent) => void)[] = []
197
202
 
198
203
  /**
199
204
  * Calls `fn` on the user's back intent (Android back button/gesture, the
200
205
  * desktop dev chord). Call `e.preventDefault()` when back means in-app
201
206
  * navigation right now (close a modal, previous screen); unprevented, the
202
- * default action runs: exit(). Apps without a handler exit on back
203
- * everywhere, which is the correct zero-effort default.
207
+ * event passes to the handler registered before this one, and if none of them
208
+ * prevents it either, to the default action: exit(). Apps without a handler
209
+ * exit on back everywhere, which is the correct zero-effort default.
210
+ *
211
+ * Handlers form a stack: the most recently registered runs first and the first
212
+ * to prevent ends the dispatch, so each screen or overlay owns one step of the
213
+ * back stack and none of them needs to know what the others are doing. A
214
+ * handler that does not prevent must not act either - the event is still on its
215
+ * way to whoever will handle it.
216
+ *
204
217
  * Returns a cleanup function; also auto-cleans within a reactive scope.
205
218
  */
206
219
  export function onBack(fn: (e: BackEvent) => void) {
207
- backHandlers.add(fn)
208
- let cleanup = () => backHandlers.delete(fn)
220
+ backHandlers.push(fn)
221
+ let cleanup = () => {
222
+ let i = backHandlers.lastIndexOf(fn)
223
+ if (i >= 0) backHandlers.splice(i, 1)
224
+ }
209
225
  onCleanup(cleanup)
210
226
  return cleanup
211
227
  }
@@ -338,7 +354,9 @@ export function attachWindow(_nodeId: number) {
338
354
  },
339
355
  }
340
356
  // Copy first: a handler may unregister (itself or others) mid-dispatch.
341
- for (let fn of [...backHandlers]) fn(e)
357
+ // Top of the stack down, stopping as soon as one takes the event.
358
+ let stack = [...backHandlers]
359
+ for (let i = stack.length - 1; i >= 0 && !prevented; i--) stack[i]!(e)
342
360
  if (!prevented) exit()
343
361
  })
344
362