@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.
@@ -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.37",
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.37"
30
+ "@solidrt/flux-types": "0.0.38"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@solidjs/signals": "2.0.0-beta.20",
@@ -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 process.env.NODE_ENV, so a
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 (process.env.NODE_ENV === "production") return
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>()
@@ -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
- // Hit path frozen at pointerDown, pointerId -> targets. While a pointer has an
21
- // active down, its moves and up dispatch along this path (same leaf-to-root
22
- // bubble) no matter where the pointer currently is, so every node under the
23
- // original down observes the whole gesture: a drag keeps working off-element,
24
- // and an ancestor recognizer (a scroller's pan) sees the moves it needs to
25
- // take over mid-gesture via the arena. There is no exclusive pointer capture;
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 hit path, leaf->root (bubbling), so
256
- // a child handler can call e.stopPropagation() to keep the event from
257
- // reaching its ancestors. `targets` arrives root->leaf, hence the reverse.
258
- let bubble = (targets: number[], handler: string, e: any) => {
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
- for (let i = targets.length - 1; i >= 0; i--) {
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
- unsubDown = on(
270
- "pointerDown",
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
- "pointerMove",
291
- ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
292
- bubble(downPaths.get(e.pointerId) ?? targets, "onPointerMove", e)
293
- },
294
- )
295
-
296
- // Enter/leave keep their hover-diff order (already leaf->root for leave,
297
- // root->leaf for enter), but still honor stopPropagation for a consistent
298
- // event shape.
299
- let dispatchOrdered = (targets: number[], handler: string, e: any) => {
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
- unsubEnter = on(
311
- "pointerEnter",
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) => {