@solidrt/core 0.0.37 → 0.0.38
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/examples/README.md +3 -0
- package/examples/pointer-local-coords.tsx +53 -0
- package/package.json +2 -2
- package/src/environment.ts +29 -0
- package/src/index.ts +1 -1
- package/src/renderer.ts +2 -2
- package/src/runtime-modules.d.ts +14 -0
- package/src/text-input.ts +1 -1
- package/src/types.d.ts +41 -0
- package/src/window.ts +59 -68
package/examples/README.md
CHANGED
|
@@ -16,6 +16,9 @@ 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
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).
|
|
21
24
|
|
|
@@ -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 />)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"colord": "^2.9.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
30
|
+
"@solidrt/flux-types": "0.0.38"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@solidjs/signals": "2.0.0-beta.20",
|
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/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"
|
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
|
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
|
|
@@ -332,6 +365,14 @@ export interface TextProps extends Position, PaintProps, PointerProps {
|
|
|
332
365
|
|
|
333
366
|
export interface TextureProps extends Position, PointerProps {
|
|
334
367
|
src?: number
|
|
368
|
+
/**
|
|
369
|
+
* How the texture's pixels map to the element box (CSS object-fit).
|
|
370
|
+
* "fill" (default) stretches; "cover" and "none" crop; "contain" and
|
|
371
|
+
* "scale-down" letterbox. Everything centers - there is no object-position.
|
|
372
|
+
* Paint-only: the element box itself is unaffected, so "contain" letterbox
|
|
373
|
+
* bars and "cover" cropped edges still hit-test as part of the element.
|
|
374
|
+
*/
|
|
375
|
+
fit?: "fill" | "cover" | "contain" | "none" | "scale-down"
|
|
335
376
|
w?: number
|
|
336
377
|
h?: number
|
|
337
378
|
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
|
|
|
@@ -252,77 +249,71 @@ export function attachWindow(_nodeId: number) {
|
|
|
252
249
|
runFrame(time * 1000, frame)
|
|
253
250
|
})
|
|
254
251
|
|
|
255
|
-
// Dispatch an event to every node on the
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
252
|
+
// Dispatch an event to every node on the delivered path, resolving the
|
|
253
|
+
// per-node fields from the parallel wire arrays before each handler:
|
|
254
|
+
// localX/localY is the pointer in that node's own frame, parentX/parentY
|
|
255
|
+
// in its path-parent's frame (the frame the node's x/y live in), and
|
|
256
|
+
// currentTarget the node whose handler is running. `reverse` walks the
|
|
257
|
+
// root->leaf array leaf-first (bubbling), so a child handler can call
|
|
258
|
+
// e.stopPropagation() to keep the event from reaching its ancestors;
|
|
259
|
+
// enter/leave arrive pre-ordered and walk forward.
|
|
260
|
+
interface RawPointer {
|
|
261
|
+
targets: number[]
|
|
262
|
+
localX: number[]
|
|
263
|
+
localY: number[]
|
|
264
|
+
parentX: number[]
|
|
265
|
+
parentY: number[]
|
|
266
|
+
[k: string]: any
|
|
267
|
+
}
|
|
268
|
+
let dispatchPath = (raw: RawPointer, handler: string, reverse: boolean) => {
|
|
269
|
+
let { targets, localX, localY, parentX, parentY, ...e } = raw
|
|
259
270
|
let stopped = false
|
|
260
271
|
e.stopPropagation = () => {
|
|
261
272
|
stopped = true
|
|
262
273
|
}
|
|
263
|
-
|
|
274
|
+
let n = targets.length
|
|
275
|
+
for (let k = 0; k < n; k++) {
|
|
276
|
+
let i = reverse ? n - 1 - k : k
|
|
277
|
+
e.currentTarget = targets[i]!
|
|
278
|
+
e.localX = localX[i]!
|
|
279
|
+
e.localY = localY[i]!
|
|
280
|
+
e.parentX = parentX[i]!
|
|
281
|
+
e.parentY = parentY[i]!
|
|
264
282
|
getEventHandler(targets[i]!, handler)?.(e)
|
|
265
283
|
if (stopped) break
|
|
266
284
|
}
|
|
267
285
|
}
|
|
286
|
+
let bubble = (raw: RawPointer, handler: string) => dispatchPath(raw, handler, true)
|
|
287
|
+
let dispatchOrdered = (raw: RawPointer, handler: string) => dispatchPath(raw, handler, false)
|
|
288
|
+
|
|
289
|
+
unsubDown = on("pointerDown", (raw: RawPointer) => {
|
|
290
|
+
bubble(raw, "onPointerDown")
|
|
291
|
+
// Outside-tap blur. Read focus AFTER per-node handlers so a tap that
|
|
292
|
+
// moves focus to a new node is not immediately blurred again.
|
|
293
|
+
let focused = getFocusedNodeId()
|
|
294
|
+
if (focused != null && !raw.targets.includes(focused)) {
|
|
295
|
+
setFocus(null)
|
|
296
|
+
}
|
|
297
|
+
})
|
|
268
298
|
|
|
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)
|
|
299
|
+
unsubUp = on("pointerUp", (raw: RawPointer) => {
|
|
300
|
+
bubble(raw, "onPointerUp")
|
|
287
301
|
})
|
|
288
302
|
|
|
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
|
-
}
|
|
303
|
+
unsubMove = on("pointerMove", (raw: RawPointer) => {
|
|
304
|
+
bubble(raw, "onPointerMove")
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
unsubEnter = on("pointerEnter", (raw: RawPointer) => {
|
|
308
|
+
dispatchOrdered(raw, "onPointerEnter")
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
unsubLeave = on("pointerLeave", (raw: RawPointer) => {
|
|
312
|
+
dispatchOrdered(raw, "onPointerLeave")
|
|
313
|
+
})
|
|
309
314
|
|
|
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)
|
|
315
|
+
unsubWheel = on("wheel", (raw: RawPointer) => {
|
|
316
|
+
bubble(raw, "onWheel")
|
|
326
317
|
})
|
|
327
318
|
|
|
328
319
|
unsubKeyDown = on("keydown", (e: any) => {
|