@solidrt/core 0.0.35 → 0.0.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
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.35"
30
+ "@solidrt/flux-types": "0.0.37"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@solidjs/signals": "2.0.0-beta.20",
package/src/core.ts CHANGED
@@ -64,16 +64,27 @@ export interface BoundingBox {
64
64
  }
65
65
 
66
66
  /**
67
- * Returns the node's window-relative bounding box from the most recently
68
- * computed layout, or `null` if the node has no layout or has not been laid out
69
- * yet. This is a snapshot read, not reactive: call it inside `onLayout` (or an
70
- * event handler) to get values for the current frame. Phase 1 composes only
71
- * translations; x/y are wrong when a rotate/scale sits anywhere above the node.
67
+ * Returns the node's bounding box from the most recently computed layout,
68
+ * relative to its nearest positioning context (an ancestor with an explicit
69
+ * `position="relative"`, falling back to the window), or `null` if the node
70
+ * has no layout or has not been laid out yet. This is a snapshot read, not
71
+ * reactive: call it inside `onLayout` (or an event handler) to get values for
72
+ * the current frame. Transforms anywhere in the chain (including the node's
73
+ * own) compose fully; the box is the axis-aligned bounds of the transformed
74
+ * quad.
72
75
  */
73
76
  export function getBoundingBox(node: { id: number }): BoundingBox | null {
74
77
  return tree.getBoundingBox(node.id)
75
78
  }
76
79
 
80
+ /**
81
+ * Like getBoundingBox, but always window-relative (getBoundingClientRect
82
+ * semantics), the frame pointer event clientX/clientY are reported in.
83
+ */
84
+ export function getBoundingBoxViewport(node: { id: number }): BoundingBox | null {
85
+ return tree.getBoundingBoxViewport(node.id)
86
+ }
87
+
77
88
  /**
78
89
  * Measures the rendered size of `text` in layout pixels under the given font
79
90
  * options (family, size, weight, style, maxLines), without adding it to the
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export * from "./renderer"
2
- export { setFocus, getFocusedNodeId, measureText, getBoundingBox } from "./core"
2
+ export { setFocus, getFocusedNodeId, measureText, getBoundingBox, getBoundingBoxViewport } from "./core"
3
3
  export type { BoundingBox } from "./core"
4
4
  export { parseColor, mixColors, brightness, createLinearGradient, createRadialGradient } from "./color"
5
5
  export type { Gradient, GradientStop } from "./color"
6
- export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur } from "./window"
7
- export { setPointerCapture, releasePointerCapture } from "./window"
6
+ export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, exit } from "./window"
7
+ export type { BackEvent } from "./window"
8
8
  export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
9
9
  export { env } from "./environment"
10
10
  export type { InputDevices, SystemTheme, Orientation } from "./environment"
@@ -31,6 +31,17 @@ declare module "srt:events" {
31
31
  export function once(event: string, callback: (data: any) => void): () => void
32
32
  }
33
33
 
34
+ // The running application's own surface (lattice), present in every build.
35
+ declare module "srt:app" {
36
+ /**
37
+ * Leave the current app, unconditionally: back to the launcher in a dev
38
+ * client, quit when standalone or at the launcher root (on Android the
39
+ * client backgrounds instead of dying). The default action of an
40
+ * unprevented `back` event; prefer the @solidrt/core re-export.
41
+ */
42
+ export function exit(): void
43
+ }
44
+
34
45
  // Dev-server control surface (lattice). Present only in dev/go builds; in other
35
46
  // builds `available` is false and the functions are no-ops.
36
47
  declare module "srt:dev" {
@@ -63,18 +74,28 @@ declare module "srt:apps" {
63
74
  export type InstalledApp = { id: string; name: string; version: string }
64
75
  /** Installed apps, sorted by name. */
65
76
  export function list(): InstalledApp[]
66
- /** A stored version: id (manifest hash), bytes on disk, whether it is the current one. */
67
- export type AppVersion = { id: string; size: number; current: boolean }
77
+ /**
78
+ * A stored version: id (manifest hash), bytes on disk, whether it is the
79
+ * current one, and the SolidRT (CLI) release that built it per its
80
+ * manifest ("unknown" from an in-repo CLI or when the manifest predates
81
+ * the field).
82
+ */
83
+ export type AppVersion = { id: string; size: number; current: boolean; solidrtVersion: string }
68
84
  /** One file in a listing: a relative path and its size in bytes. */
69
85
  export type AppFile = { path: string; size: number }
86
+ /**
87
+ * One fetch-cache entry: the cached (resolved) url, the response content
88
+ * type (lowercased, parameters stripped; absent when the response had
89
+ * none) and the entry's size on disk.
90
+ */
91
+ export type AppCacheEntry = { url: string; type?: string; size: number }
70
92
  /**
71
93
  * Usage details for one installed app: total bytes of its stored versions
72
- * (assets shared between versions via hardlinks count in each) and of its
73
- * data sandbox, plus the stored versions (current first, then newest first)
74
- * and three file listings, each sorted by path. `assets` is the current
75
- * version's manifest claim (declared sizes); `files` and `data` are disk
76
- * walks of the current version dir and the data sandbox - the truth, so a
77
- * divergence from the manifest is visible.
94
+ * (assets shared between versions via hardlinks count in each), of its
95
+ * data sandbox and of its fetch cache, plus the stored versions (current
96
+ * first, then newest first) and three listings: `files` and `data` are
97
+ * disk walks of the current version dir and the data sandbox (sorted by
98
+ * path), `cache` is the fetch cache's entries (sorted by url).
78
99
  */
79
100
  export type AppInfo = {
80
101
  id: string
@@ -82,10 +103,11 @@ declare module "srt:apps" {
82
103
  version: string
83
104
  installSize: number
84
105
  dataSize: number
106
+ cacheSize: number
85
107
  versions: AppVersion[]
86
- assets: AppFile[]
87
108
  files: AppFile[]
88
109
  data: AppFile[]
110
+ cache: AppCacheEntry[]
89
111
  }
90
112
  /** Usage details for an installed app. Throws when the app is not installed. */
91
113
  export function info(id: string): AppInfo
@@ -100,6 +122,22 @@ declare module "srt:apps" {
100
122
  * the app is not installed.
101
123
  */
102
124
  export function remove(id: string): void
125
+ /**
126
+ * Delete the app's fetch cache. Clearing a missing or empty cache is a
127
+ * no-op; the id does not need to be installed, so a removed app's
128
+ * leftover cache is still clearable.
129
+ */
130
+ export function clearCache(id: string): void
131
+ /**
132
+ * Build identity of this runtime, for the launcher's settings screen. Not
133
+ * app-specific, but surfaced here since the launcher already imports this
134
+ * module. `version` is the release version (git describe; "0.0.0-dev" in a
135
+ * plain build), `profile` is "debug" or "release", `platform` is the OS
136
+ * (std::env::consts::OS, e.g. "linux", "android", "windows", "macos").
137
+ */
138
+ export const version: string
139
+ export const profile: string
140
+ export const platform: string
103
141
  }
104
142
 
105
143
  // Frame draw (lattice runner). renderFrame() synchronously renders the current
package/src/types.d.ts CHANGED
@@ -198,8 +198,18 @@ export interface WheelEvent extends PointerEvent {
198
198
  deltaY: number
199
199
  }
200
200
 
201
+ // Key events use the W3C UI Events vocabulary: `key` is the logical,
202
+ // layout-dependent value ("a", "!", "Enter", "ArrowLeft"); `code` is the
203
+ // physical, layout-independent key position ("KeyA", "Digit1", "NumpadEnter").
204
+ // Printable characters for text entry arrive via onTextInput, not here.
201
205
  export interface KeyEvent {
202
206
  key: string
207
+ code: string
208
+ repeat: boolean
209
+ shiftKey: boolean
210
+ ctrlKey: boolean
211
+ altKey: boolean
212
+ metaKey: boolean
203
213
  }
204
214
 
205
215
  export interface TextEvent {
package/src/window.ts CHANGED
@@ -2,30 +2,30 @@ import { createSignal, onCleanup, onSettled, flush } from "@solidjs/signals"
2
2
  import { requestFrame } from "flux:rendertree"
3
3
  import { renderFrame } from "srt:render"
4
4
  import { on, once } from "srt:events"
5
+ import { exit } from "srt:app"
5
6
  import { getEventHandler, getFocusedNodeId, setFocus } from "./core"
6
7
  import { scanForOrphans } from "./renderer"
7
8
 
8
- // ------ Pointer capture -----------------
9
-
10
- // Active pointer captures, pointerId -> nodeId. While a pointer is captured, its
11
- // move/up events dispatch straight to the captured node (bypassing hit testing),
12
- // so a drag keeps working when the pointer drifts off the element. Modeled on the
13
- // web setPointerCapture; capture auto-releases on pointerUp.
14
- let pointerCaptures = new Map<number, number>()
15
-
16
9
  /**
17
- * Routes all further move/up events for `pointerId` to `nodeId` until release,
18
- * regardless of what is under the pointer. Call from a pointerdown handler to own
19
- * a drag (e.g. a slider). Auto-releases on the matching pointerup.
10
+ * Leaves the current app, unconditionally: back to the launcher in a dev
11
+ * client, quitting when standalone or at the launcher itself (on Android the
12
+ * client backgrounds instead of dying). The default action of an unprevented
13
+ * `back` event; call it directly to exit programmatically, e.g. after
14
+ * intercepting back for an unsaved-changes dialog.
20
15
  */
21
- export function setPointerCapture(nodeId: number, pointerId: number) {
22
- pointerCaptures.set(pointerId, nodeId)
23
- }
16
+ export { exit }
24
17
 
25
- /** Ends a capture early. Not needed for the common case (pointerup releases it). */
26
- export function releasePointerCapture(pointerId: number) {
27
- pointerCaptures.delete(pointerId)
28
- }
18
+ // ------ Pointer routing -----------------
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[]>()
29
29
 
30
30
  // ------ Animation frames ----------------
31
31
 
@@ -189,6 +189,30 @@ export function onWindowBlur(fn: () => void) {
189
189
  return unsubscribe
190
190
  }
191
191
 
192
+ // ------ Back ----------------
193
+
194
+ export type BackEvent = { preventDefault: () => void }
195
+
196
+ // App handlers for the window-level back event, run in registration order.
197
+ // Kept in a local registry (not per-handler bus subscriptions) so the default
198
+ // action runs exactly once, after every handler has had its say.
199
+ let backHandlers = new Set<(e: BackEvent) => void>()
200
+
201
+ /**
202
+ * Calls `fn` on the user's back intent (Android back button/gesture, the
203
+ * desktop dev chord). Call `e.preventDefault()` when back means in-app
204
+ * navigation right now (close a modal, previous screen); unprevented, the
205
+ * default action runs: exit(). Apps without a handler exit on back
206
+ * everywhere, which is the correct zero-effort default.
207
+ * Returns a cleanup function; also auto-cleans within a reactive scope.
208
+ */
209
+ export function onBack(fn: (e: BackEvent) => void) {
210
+ backHandlers.add(fn)
211
+ let cleanup = () => backHandlers.delete(fn)
212
+ onCleanup(cleanup)
213
+ return cleanup
214
+ }
215
+
192
216
  // ------ Window ----------------
193
217
 
194
218
  export function attachWindow(_nodeId: number) {
@@ -201,6 +225,7 @@ export function attachWindow(_nodeId: number) {
201
225
  let unsubWheel: () => void = null!
202
226
  let unsubKeyDown: () => void = null!
203
227
  let unsubKeyUp: () => void = null!
228
+ let unsubBack: () => void = null!
204
229
  let unsubTextInput: () => void = null!
205
230
  let unsubKeyboardVisibility: () => void = null!
206
231
  let unsubRefreshRate: () => void = null!
@@ -244,6 +269,7 @@ export function attachWindow(_nodeId: number) {
244
269
  unsubDown = on(
245
270
  "pointerDown",
246
271
  ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
272
+ downPaths.set(e.pointerId, targets)
247
273
  bubble(targets, "onPointerDown", e)
248
274
  // Outside-tap blur. Read focus AFTER per-node handlers so a tap that
249
275
  // moves focus to a new node is not immediately blurred again.
@@ -255,28 +281,15 @@ export function attachWindow(_nodeId: number) {
255
281
  )
256
282
 
257
283
  unsubUp = on("pointerUp", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
258
- let captured = pointerCaptures.get(e.pointerId)
259
- if (captured != null) {
260
- // Deliver to the captured node, then release (web auto-release on up).
261
- e.stopPropagation = () => {}
262
- getEventHandler(captured, "onPointerUp")?.(e)
263
- pointerCaptures.delete(e.pointerId)
264
- return
265
- }
266
- bubble(targets, "onPointerUp", e)
284
+ let frozen = downPaths.get(e.pointerId)
285
+ downPaths.delete(e.pointerId)
286
+ bubble(frozen ?? targets, "onPointerUp", e)
267
287
  })
268
288
 
269
289
  unsubMove = on(
270
290
  "pointerMove",
271
291
  ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
272
- let captured = pointerCaptures.get(e.pointerId)
273
- if (captured != null) {
274
- // Captured drags get moves anywhere over the window, bypassing hit test.
275
- e.stopPropagation = () => {}
276
- getEventHandler(captured, "onPointerMove")?.(e)
277
- return
278
- }
279
- bubble(targets, "onPointerMove", e)
292
+ bubble(downPaths.get(e.pointerId) ?? targets, "onPointerMove", e)
280
293
  },
281
294
  )
282
295
 
@@ -326,6 +339,18 @@ export function attachWindow(_nodeId: number) {
326
339
  }
327
340
  })
328
341
 
342
+ unsubBack = on("back", () => {
343
+ let prevented = false
344
+ let e: BackEvent = {
345
+ preventDefault: () => {
346
+ prevented = true
347
+ },
348
+ }
349
+ // Copy first: a handler may unregister (itself or others) mid-dispatch.
350
+ for (let fn of [...backHandlers]) fn(e)
351
+ if (!prevented) exit()
352
+ })
353
+
329
354
  unsubTextInput = on("textInput", (e: any) => {
330
355
  let id = getFocusedNodeId()
331
356
  if (id != null) {
@@ -361,6 +386,7 @@ export function attachWindow(_nodeId: number) {
361
386
  if (unsubWheel) unsubWheel()
362
387
  if (unsubKeyDown) unsubKeyDown()
363
388
  if (unsubKeyUp) unsubKeyUp()
389
+ if (unsubBack) unsubBack()
364
390
  if (unsubTextInput) unsubTextInput()
365
391
  if (unsubKeyboardVisibility) unsubKeyboardVisibility()
366
392
  if (unsubRefreshRate) unsubRefreshRate()