@solidrt/core 0.0.37 → 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 +1 -1
- package/examples/README.md +7 -1
- package/examples/pointer-local-coords.tsx +53 -0
- package/examples/window-shader-history.tsx +62 -0
- package/examples/window-shader.tsx +71 -0
- package/package.json +5 -5
- package/src/environment.ts +29 -0
- package/src/gamepad.ts +6 -0
- package/src/gpu.ts +50 -0
- package/src/index.ts +2 -1
- package/src/renderer.ts +2 -2
- package/src/runtime-modules.d.ts +31 -3
- package/src/text-input.ts +1 -1
- package/src/types.d.ts +96 -2
- package/src/window.ts +86 -77
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.
|
|
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
|
|
package/examples/README.md
CHANGED
|
@@ -16,8 +16,11 @@ 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
|
+
## Pointer input
|
|
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
|
+
|
|
19
22
|
## 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).
|
|
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.
|
|
21
24
|
|
|
22
25
|
## Scrolling
|
|
23
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.
|
|
@@ -33,6 +36,9 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
33
36
|
- `image.tsx` - `createImage` (async value: fetch + decode + upload) read inside a `<Loading>` boundary and shown with `<texture>`.
|
|
34
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`.
|
|
35
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.
|
|
36
42
|
|
|
37
43
|
## Sound
|
|
38
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,53 @@
|
|
|
1
|
+
// Pointer events carry the pointer position in three coordinate frames,
|
|
2
|
+
// resolved per node as the event bubbles:
|
|
3
|
+
// - clientX/clientY - the window frame.
|
|
4
|
+
// - localX/localY - the frame of the node whose handler is running, its whole
|
|
5
|
+
// transform chain undone. Exact even when the pointer is not over the node:
|
|
6
|
+
// after a pointer down, moves route along the frozen down path and keep
|
|
7
|
+
// reporting true locals (a fast drag cannot escape the chip below).
|
|
8
|
+
// - parentX/parentY - the frame of the node's path parent, which is the frame
|
|
9
|
+
// the node's own x/y props live in.
|
|
10
|
+
// The drag idiom needs no transform math in the app: take the grab offset from
|
|
11
|
+
// localX/localY at pointer down, place with parentX/parentY - offset during
|
|
12
|
+
// moves. The surface here is rotated and scaled to prove the point - the chip
|
|
13
|
+
// still tracks the pointer exactly. Keying the grab by pointerId keeps
|
|
14
|
+
// concurrent touches (each routed along its own down path) independent.
|
|
15
|
+
import { render, createSignal } from "@solidrt/core"
|
|
16
|
+
|
|
17
|
+
function App() {
|
|
18
|
+
let [pos, setPos] = createSignal({ x: 40, y: 40 })
|
|
19
|
+
let grab: { pointer: number; dx: number; dy: number } | null = null
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<window alignItems="center" justifyContent="center">
|
|
23
|
+
<view width={360} height={240} rotate={0.3} scale={1.2}>
|
|
24
|
+
<d-rect radius={16} color="#2a2f3a" />
|
|
25
|
+
<view
|
|
26
|
+
position="absolute"
|
|
27
|
+
width={100}
|
|
28
|
+
height={64}
|
|
29
|
+
x={pos().x}
|
|
30
|
+
y={pos().y}
|
|
31
|
+
justifyContent="center"
|
|
32
|
+
alignItems="center"
|
|
33
|
+
onPointerDown={(e) => {
|
|
34
|
+
if (grab) return
|
|
35
|
+
grab = { pointer: e.pointerId, dx: e.localX, dy: e.localY }
|
|
36
|
+
}}
|
|
37
|
+
onPointerMove={(e) => {
|
|
38
|
+
if (!grab || e.pointerId !== grab.pointer) return
|
|
39
|
+
setPos({ x: e.parentX - grab.dx, y: e.parentY - grab.dy })
|
|
40
|
+
}}
|
|
41
|
+
onPointerUp={() => {
|
|
42
|
+
grab = null
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<d-rect radius={12} color="#3366b3" />
|
|
46
|
+
<text color="white">drag me</text>
|
|
47
|
+
</view>
|
|
48
|
+
</view>
|
|
49
|
+
</window>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render(() => <App />)
|
|
@@ -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.
|
|
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.
|
|
30
|
+
"@solidrt/flux-types": "0.0.39"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@solidjs/signals": "2.0.0-beta.
|
|
34
|
-
"@solidjs/universal": "2.0.0-beta.
|
|
35
|
-
"solid-js": "2.0.0-beta.
|
|
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/environment.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface InputDevices {
|
|
|
28
28
|
|
|
29
29
|
export type SystemTheme = "dark" | "light" | "unknown"
|
|
30
30
|
|
|
31
|
+
export type Visibility = "visible" | "hidden"
|
|
32
|
+
|
|
31
33
|
export type Orientation = "portrait" | "portraitFlipped" | "landscape" | "landscapeFlipped" | "unknown"
|
|
32
34
|
|
|
33
35
|
let devicesAccessor: (() => InputDevices | undefined) | undefined
|
|
@@ -52,6 +54,18 @@ function ensureSystemThemeState() {
|
|
|
52
54
|
systemThemeAccessor = theme
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
let visibilityAccessor: (() => Visibility) | undefined
|
|
58
|
+
|
|
59
|
+
function ensureVisibilityState() {
|
|
60
|
+
if (visibilityAccessor) return
|
|
61
|
+
let [visibility, setVisibility] = createSignal<Visibility>("visible", { ownedWrite: true })
|
|
62
|
+
// Sticky. The runtime may report the same state through several platform
|
|
63
|
+
// paths; the signal's equality check turns repeats into no-ops for
|
|
64
|
+
// reactive consumers.
|
|
65
|
+
on("visibility", (e: { state?: Visibility }) => setVisibility(e.state === "hidden" ? "hidden" : "visible"))
|
|
66
|
+
visibilityAccessor = visibility
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
let orientationAccessor: (() => Orientation) | undefined
|
|
56
70
|
|
|
57
71
|
function ensureOrientationState() {
|
|
@@ -167,6 +181,21 @@ export let env = {
|
|
|
167
181
|
ensureTextScaleState()
|
|
168
182
|
return textScaleAccessor!()
|
|
169
183
|
},
|
|
184
|
+
/**
|
|
185
|
+
* Whether the app is on screen: "hidden" while backgrounded (Android) or
|
|
186
|
+
* minimized (desktop), "visible" again on return. The web's
|
|
187
|
+
* `visibilityState` vocabulary without the `document` machinery - react
|
|
188
|
+
* to it in a tracked scope (JSX, memo, effect).
|
|
189
|
+
*
|
|
190
|
+
* This is the persistence moment: there is no close event on any
|
|
191
|
+
* platform (Android gives no time, desktop window close never enters
|
|
192
|
+
* JS), so save state when this goes "hidden". While hidden, timers keep
|
|
193
|
+
* running but no frames are produced.
|
|
194
|
+
*/
|
|
195
|
+
get visibility(): Visibility {
|
|
196
|
+
ensureVisibilityState()
|
|
197
|
+
return visibilityAccessor!()
|
|
198
|
+
},
|
|
170
199
|
/** Orientation of the display the window is on. */
|
|
171
200
|
get orientation(): Orientation {
|
|
172
201
|
ensureOrientationState()
|
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
|
@@ -7,7 +7,7 @@ export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, exit
|
|
|
7
7
|
export type { BackEvent } from "./window"
|
|
8
8
|
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
9
9
|
export { env } from "./environment"
|
|
10
|
-
export type { InputDevices, SystemTheme, Orientation } from "./environment"
|
|
10
|
+
export type { InputDevices, SystemTheme, Orientation, Visibility } from "./environment"
|
|
11
11
|
export { gamepads } from "./gamepad"
|
|
12
12
|
export type { GamepadState } from "./gamepad"
|
|
13
13
|
export { capabilities } from "./capabilities"
|
|
@@ -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,
|
package/src/renderer.ts
CHANGED
|
@@ -97,14 +97,14 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
|
97
97
|
// bookkeeping on the hot create/insert paths, orphans are derived from the
|
|
98
98
|
// proxy map itself: parentless, not the window root, and not awaiting the
|
|
99
99
|
// destroy sweep. window.ts runs the scan on a rendered frame every few
|
|
100
|
-
// seconds; dev bundles only (srt always defines
|
|
100
|
+
// seconds; dev bundles only (srt always defines import.meta.env.DEV, so a
|
|
101
101
|
// production bundle folds the check into a constant early return).
|
|
102
102
|
const SENTINEL_INTERVAL_MS = 5000
|
|
103
103
|
let sentinelDue = 0
|
|
104
104
|
let warnedLeakTypes = new Set<string>()
|
|
105
105
|
|
|
106
106
|
export function scanForOrphans(now: number): void {
|
|
107
|
-
if (
|
|
107
|
+
if (!import.meta.env.DEV) return
|
|
108
108
|
if (now < sentinelDue) return
|
|
109
109
|
sentinelDue = now + SENTINEL_INTERVAL_MS
|
|
110
110
|
let counts = new Map<string, number>()
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -23,6 +23,14 @@ declare module "*.jpeg" {
|
|
|
23
23
|
const bytes: Uint8Array
|
|
24
24
|
export default bytes
|
|
25
25
|
}
|
|
26
|
+
declare module "*.wav" {
|
|
27
|
+
const bytes: Uint8Array
|
|
28
|
+
export default bytes
|
|
29
|
+
}
|
|
30
|
+
declare module "*.ogg" {
|
|
31
|
+
const bytes: Uint8Array
|
|
32
|
+
export default bytes
|
|
33
|
+
}
|
|
26
34
|
|
|
27
35
|
// UI event bus (lattice), provided by the runtime as a builtin module.
|
|
28
36
|
// on/once return an unsubscribe function.
|
|
@@ -48,6 +56,12 @@ declare module "srt:dev" {
|
|
|
48
56
|
export const available: boolean
|
|
49
57
|
export const canDiscover: boolean
|
|
50
58
|
export const recents: string[]
|
|
59
|
+
/**
|
|
60
|
+
* The dev-server address the client was launched with (so the launcher can
|
|
61
|
+
* auto-connect without on-device interaction), or null when launched without
|
|
62
|
+
* one.
|
|
63
|
+
*/
|
|
64
|
+
export const launchAddress: string | null
|
|
51
65
|
export function connect(address: string): void
|
|
52
66
|
export function discover(): void
|
|
53
67
|
export function stop(): void
|
|
@@ -69,10 +83,24 @@ declare module "srt:apps" {
|
|
|
69
83
|
export const available: boolean
|
|
70
84
|
/**
|
|
71
85
|
* An installed app: id, display name (the installed manifest's displayName,
|
|
72
|
-
* 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).
|
|
73
94
|
*/
|
|
74
|
-
export type InstalledApp = {
|
|
75
|
-
|
|
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. */
|
|
76
104
|
export function list(): InstalledApp[]
|
|
77
105
|
/**
|
|
78
106
|
* A stored version: id (manifest hash), bytes on disk, whether it is the
|
package/src/text-input.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Ordered selection bounds [start, end).
|
|
85
|
-
let range = () => {
|
|
85
|
+
let range = (): [number, number] => {
|
|
86
86
|
let { anchor, focus } = selection()
|
|
87
87
|
return anchor <= focus ? [anchor, focus] : [focus, anchor]
|
|
88
88
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ import type { Element } from "solid-js"
|
|
|
9
9
|
// non-module declaration file, and this file is a module.
|
|
10
10
|
|
|
11
11
|
declare global {
|
|
12
|
+
interface ImportMeta {
|
|
13
|
+
/**
|
|
14
|
+
* Build-mode constants, substituted textually by the srt bundler (Vite
|
|
15
|
+
* vocabulary). `DEV` is true in dev bundles and false in production
|
|
16
|
+
* bundles, where the substituted constant lets the minifier fold
|
|
17
|
+
* dev-only code away entirely.
|
|
18
|
+
*/
|
|
19
|
+
readonly env: { readonly DEV: boolean }
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
let image: {
|
|
13
23
|
decodeImage(bytes: Uint8Array): { data: Uint8Array, width: number, height: number }
|
|
14
24
|
}
|
|
@@ -180,6 +190,29 @@ export interface TransformProps {
|
|
|
180
190
|
export interface PointerEvent {
|
|
181
191
|
clientX: number
|
|
182
192
|
clientY: number
|
|
193
|
+
/**
|
|
194
|
+
* Pointer position in the coordinate frame of the node whose handler is
|
|
195
|
+
* running (its transform chain undone), so it differs per node as the event
|
|
196
|
+
* bubbles. Exact even when the pointer is not over the node: a drag routed
|
|
197
|
+
* along the frozen down-path keeps reporting true local coordinates after
|
|
198
|
+
* leaving it.
|
|
199
|
+
*/
|
|
200
|
+
localX: number
|
|
201
|
+
localY: number
|
|
202
|
+
/**
|
|
203
|
+
* Pointer position in the frame the running node's own x/y coordinates live
|
|
204
|
+
* in: its parent on the hit path (the window for the root). The drag idiom
|
|
205
|
+
* is `x = parentX - grab offset`, with the grab offset taken from
|
|
206
|
+
* localX/localY at pointer down. The path parent skips
|
|
207
|
+
* pointerEvents="none" ancestors, so it is the layout parent in ordinary
|
|
208
|
+
* trees.
|
|
209
|
+
*/
|
|
210
|
+
parentX: number
|
|
211
|
+
parentY: number
|
|
212
|
+
/** Node id whose handler is currently running (bubbling changes it per call). */
|
|
213
|
+
currentTarget: number
|
|
214
|
+
/** Deepest node id of the event's path (the hit leaf). */
|
|
215
|
+
target: number
|
|
183
216
|
pointerId: number
|
|
184
217
|
pointerType: "mouse" | "touch" | "pen" | (string & {})
|
|
185
218
|
button?: number
|
|
@@ -242,6 +275,45 @@ export interface WindowProps extends LayoutProps, PointerProps {
|
|
|
242
275
|
children?: Children
|
|
243
276
|
title?: string
|
|
244
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
|
|
245
317
|
}
|
|
246
318
|
|
|
247
319
|
export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
|
|
@@ -264,8 +336,13 @@ export interface ViewProps extends LayoutProps, TransformProps, PointerProps {
|
|
|
264
336
|
* on layout-size or display-scale changes. Content painted outside the
|
|
265
337
|
* element's layout box is cropped, and ancestor scale animations smear the
|
|
266
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.
|
|
267
344
|
*/
|
|
268
|
-
repaintBoundary?: boolean | "snapshot"
|
|
345
|
+
repaintBoundary?: boolean | "snapshot" | "snapshot-no-aa"
|
|
269
346
|
}
|
|
270
347
|
|
|
271
348
|
// draw primitives
|
|
@@ -330,8 +407,25 @@ export interface TextProps extends Position, PaintProps, PointerProps {
|
|
|
330
407
|
maxLines?: number
|
|
331
408
|
}
|
|
332
409
|
|
|
333
|
-
|
|
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 {
|
|
334
420
|
src?: number
|
|
421
|
+
/**
|
|
422
|
+
* How the texture's pixels map to the element box (CSS object-fit).
|
|
423
|
+
* "fill" (default) stretches; "cover" and "none" crop; "contain" and
|
|
424
|
+
* "scale-down" letterbox. Everything centers - there is no object-position.
|
|
425
|
+
* Paint-only: the element box itself is unaffected, so "contain" letterbox
|
|
426
|
+
* bars and "cover" cropped edges still hit-test as part of the element.
|
|
427
|
+
*/
|
|
428
|
+
fit?: "fill" | "cover" | "contain" | "none" | "scale-down"
|
|
335
429
|
w?: number
|
|
336
430
|
h?: number
|
|
337
431
|
srcX?: number
|
package/src/window.ts
CHANGED
|
@@ -17,15 +17,12 @@ export { exit }
|
|
|
17
17
|
|
|
18
18
|
// ------ Pointer routing -----------------
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// gesture ownership is claim-based, above this layer. Enter/leave stay
|
|
27
|
-
// hover-driven, and moves with no active down follow the live hit path.
|
|
28
|
-
let downPaths = new Map<number, number[]>()
|
|
20
|
+
// Routing lives in the engine: the runtime freezes each pointer's hit path at
|
|
21
|
+
// pointerDown and delivers every event with its exact targets plus per-node
|
|
22
|
+
// local/parent-frame coordinate arrays (see PointerEvent in types.d.ts). This
|
|
23
|
+
// side only walks the delivered path, resolving the per-node scalars before
|
|
24
|
+
// each handler. There is no exclusive pointer capture; gesture ownership is
|
|
25
|
+
// claim-based, above this layer.
|
|
29
26
|
|
|
30
27
|
// ------ Animation frames ----------------
|
|
31
28
|
|
|
@@ -193,22 +190,38 @@ export function onWindowBlur(fn: () => void) {
|
|
|
193
190
|
|
|
194
191
|
export type BackEvent = { preventDefault: () => void }
|
|
195
192
|
|
|
196
|
-
// App handlers for the window-level back event,
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
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)[] = []
|
|
200
202
|
|
|
201
203
|
/**
|
|
202
204
|
* Calls `fn` on the user's back intent (Android back button/gesture, the
|
|
203
205
|
* desktop dev chord). Call `e.preventDefault()` when back means in-app
|
|
204
206
|
* navigation right now (close a modal, previous screen); unprevented, the
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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
|
+
*
|
|
207
217
|
* Returns a cleanup function; also auto-cleans within a reactive scope.
|
|
208
218
|
*/
|
|
209
219
|
export function onBack(fn: (e: BackEvent) => void) {
|
|
210
|
-
backHandlers.
|
|
211
|
-
let cleanup = () =>
|
|
220
|
+
backHandlers.push(fn)
|
|
221
|
+
let cleanup = () => {
|
|
222
|
+
let i = backHandlers.lastIndexOf(fn)
|
|
223
|
+
if (i >= 0) backHandlers.splice(i, 1)
|
|
224
|
+
}
|
|
212
225
|
onCleanup(cleanup)
|
|
213
226
|
return cleanup
|
|
214
227
|
}
|
|
@@ -252,77 +265,71 @@ export function attachWindow(_nodeId: number) {
|
|
|
252
265
|
runFrame(time * 1000, frame)
|
|
253
266
|
})
|
|
254
267
|
|
|
255
|
-
// Dispatch an event to every node on the
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
268
|
+
// Dispatch an event to every node on the delivered path, resolving the
|
|
269
|
+
// per-node fields from the parallel wire arrays before each handler:
|
|
270
|
+
// localX/localY is the pointer in that node's own frame, parentX/parentY
|
|
271
|
+
// in its path-parent's frame (the frame the node's x/y live in), and
|
|
272
|
+
// currentTarget the node whose handler is running. `reverse` walks the
|
|
273
|
+
// root->leaf array leaf-first (bubbling), so a child handler can call
|
|
274
|
+
// e.stopPropagation() to keep the event from reaching its ancestors;
|
|
275
|
+
// enter/leave arrive pre-ordered and walk forward.
|
|
276
|
+
interface RawPointer {
|
|
277
|
+
targets: number[]
|
|
278
|
+
localX: number[]
|
|
279
|
+
localY: number[]
|
|
280
|
+
parentX: number[]
|
|
281
|
+
parentY: number[]
|
|
282
|
+
[k: string]: any
|
|
283
|
+
}
|
|
284
|
+
let dispatchPath = (raw: RawPointer, handler: string, reverse: boolean) => {
|
|
285
|
+
let { targets, localX, localY, parentX, parentY, ...e } = raw
|
|
259
286
|
let stopped = false
|
|
260
287
|
e.stopPropagation = () => {
|
|
261
288
|
stopped = true
|
|
262
289
|
}
|
|
263
|
-
|
|
290
|
+
let n = targets.length
|
|
291
|
+
for (let k = 0; k < n; k++) {
|
|
292
|
+
let i = reverse ? n - 1 - k : k
|
|
293
|
+
e.currentTarget = targets[i]!
|
|
294
|
+
e.localX = localX[i]!
|
|
295
|
+
e.localY = localY[i]!
|
|
296
|
+
e.parentX = parentX[i]!
|
|
297
|
+
e.parentY = parentY[i]!
|
|
264
298
|
getEventHandler(targets[i]!, handler)?.(e)
|
|
265
299
|
if (stopped) break
|
|
266
300
|
}
|
|
267
301
|
}
|
|
302
|
+
let bubble = (raw: RawPointer, handler: string) => dispatchPath(raw, handler, true)
|
|
303
|
+
let dispatchOrdered = (raw: RawPointer, handler: string) => dispatchPath(raw, handler, false)
|
|
304
|
+
|
|
305
|
+
unsubDown = on("pointerDown", (raw: RawPointer) => {
|
|
306
|
+
bubble(raw, "onPointerDown")
|
|
307
|
+
// Outside-tap blur. Read focus AFTER per-node handlers so a tap that
|
|
308
|
+
// moves focus to a new node is not immediately blurred again.
|
|
309
|
+
let focused = getFocusedNodeId()
|
|
310
|
+
if (focused != null && !raw.targets.includes(focused)) {
|
|
311
|
+
setFocus(null)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
268
314
|
|
|
269
|
-
|
|
270
|
-
"
|
|
271
|
-
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
272
|
-
downPaths.set(e.pointerId, targets)
|
|
273
|
-
bubble(targets, "onPointerDown", e)
|
|
274
|
-
// Outside-tap blur. Read focus AFTER per-node handlers so a tap that
|
|
275
|
-
// moves focus to a new node is not immediately blurred again.
|
|
276
|
-
let focused = getFocusedNodeId()
|
|
277
|
-
if (focused != null && !targets.includes(focused)) {
|
|
278
|
-
setFocus(null)
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
unsubUp = on("pointerUp", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
284
|
-
let frozen = downPaths.get(e.pointerId)
|
|
285
|
-
downPaths.delete(e.pointerId)
|
|
286
|
-
bubble(frozen ?? targets, "onPointerUp", e)
|
|
315
|
+
unsubUp = on("pointerUp", (raw: RawPointer) => {
|
|
316
|
+
bubble(raw, "onPointerUp")
|
|
287
317
|
})
|
|
288
318
|
|
|
289
|
-
unsubMove = on(
|
|
290
|
-
"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
let stopped = false
|
|
301
|
-
e.stopPropagation = () => {
|
|
302
|
-
stopped = true
|
|
303
|
-
}
|
|
304
|
-
for (let nodeId of targets) {
|
|
305
|
-
getEventHandler(nodeId, handler)?.(e)
|
|
306
|
-
if (stopped) break
|
|
307
|
-
}
|
|
308
|
-
}
|
|
319
|
+
unsubMove = on("pointerMove", (raw: RawPointer) => {
|
|
320
|
+
bubble(raw, "onPointerMove")
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
unsubEnter = on("pointerEnter", (raw: RawPointer) => {
|
|
324
|
+
dispatchOrdered(raw, "onPointerEnter")
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
unsubLeave = on("pointerLeave", (raw: RawPointer) => {
|
|
328
|
+
dispatchOrdered(raw, "onPointerLeave")
|
|
329
|
+
})
|
|
309
330
|
|
|
310
|
-
|
|
311
|
-
"
|
|
312
|
-
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
313
|
-
dispatchOrdered(targets, "onPointerEnter", e)
|
|
314
|
-
},
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
unsubLeave = on(
|
|
318
|
-
"pointerLeave",
|
|
319
|
-
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
320
|
-
dispatchOrdered(targets, "onPointerLeave", e)
|
|
321
|
-
},
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
unsubWheel = on("wheel", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
325
|
-
bubble(targets, "onWheel", e)
|
|
331
|
+
unsubWheel = on("wheel", (raw: RawPointer) => {
|
|
332
|
+
bubble(raw, "onWheel")
|
|
326
333
|
})
|
|
327
334
|
|
|
328
335
|
unsubKeyDown = on("keydown", (e: any) => {
|
|
@@ -347,7 +354,9 @@ export function attachWindow(_nodeId: number) {
|
|
|
347
354
|
},
|
|
348
355
|
}
|
|
349
356
|
// Copy first: a handler may unregister (itself or others) mid-dispatch.
|
|
350
|
-
|
|
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)
|
|
351
360
|
if (!prevented) exit()
|
|
352
361
|
})
|
|
353
362
|
|