@solidrt/core 0.0.36 → 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 +3 -2
- package/src/renderer.ts +2 -2
- package/src/runtime-modules.d.ts +32 -2
- package/src/text-input.ts +1 -1
- package/src/types.d.ts +51 -0
- package/src/window.ts +107 -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
|
@@ -3,10 +3,11 @@ export { setFocus, getFocusedNodeId, measureText, getBoundingBox, getBoundingBox
|
|
|
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"
|
|
6
|
+
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, exit } from "./window"
|
|
7
|
+
export type { BackEvent } from "./window"
|
|
7
8
|
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
8
9
|
export { env } from "./environment"
|
|
9
|
-
export type { InputDevices, SystemTheme, Orientation } from "./environment"
|
|
10
|
+
export type { InputDevices, SystemTheme, Orientation, Visibility } from "./environment"
|
|
10
11
|
export { gamepads } from "./gamepad"
|
|
11
12
|
export type { GamepadState } from "./gamepad"
|
|
12
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.
|
|
@@ -31,12 +39,29 @@ declare module "srt:events" {
|
|
|
31
39
|
export function once(event: string, callback: (data: any) => void): () => void
|
|
32
40
|
}
|
|
33
41
|
|
|
42
|
+
// The running application's own surface (lattice), present in every build.
|
|
43
|
+
declare module "srt:app" {
|
|
44
|
+
/**
|
|
45
|
+
* Leave the current app, unconditionally: back to the launcher in a dev
|
|
46
|
+
* client, quit when standalone or at the launcher root (on Android the
|
|
47
|
+
* client backgrounds instead of dying). The default action of an
|
|
48
|
+
* unprevented `back` event; prefer the @solidrt/core re-export.
|
|
49
|
+
*/
|
|
50
|
+
export function exit(): void
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
// Dev-server control surface (lattice). Present only in dev/go builds; in other
|
|
35
54
|
// builds `available` is false and the functions are no-ops.
|
|
36
55
|
declare module "srt:dev" {
|
|
37
56
|
export const available: boolean
|
|
38
57
|
export const canDiscover: boolean
|
|
39
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
|
|
40
65
|
export function connect(address: string): void
|
|
41
66
|
export function discover(): void
|
|
42
67
|
export function stop(): void
|
|
@@ -63,8 +88,13 @@ declare module "srt:apps" {
|
|
|
63
88
|
export type InstalledApp = { id: string; name: string; version: string }
|
|
64
89
|
/** Installed apps, sorted by name. */
|
|
65
90
|
export function list(): InstalledApp[]
|
|
66
|
-
/**
|
|
67
|
-
|
|
91
|
+
/**
|
|
92
|
+
* A stored version: id (manifest hash), bytes on disk, whether it is the
|
|
93
|
+
* current one, and the SolidRT (CLI) release that built it per its
|
|
94
|
+
* manifest ("unknown" from an in-repo CLI or when the manifest predates
|
|
95
|
+
* the field).
|
|
96
|
+
*/
|
|
97
|
+
export type AppVersion = { id: string; size: number; current: boolean; solidrtVersion: string }
|
|
68
98
|
/** One file in a listing: a relative path and its size in bytes. */
|
|
69
99
|
export type AppFile = { path: string; size: number }
|
|
70
100
|
/**
|
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
|
|
@@ -198,8 +231,18 @@ export interface WheelEvent extends PointerEvent {
|
|
|
198
231
|
deltaY: number
|
|
199
232
|
}
|
|
200
233
|
|
|
234
|
+
// Key events use the W3C UI Events vocabulary: `key` is the logical,
|
|
235
|
+
// layout-dependent value ("a", "!", "Enter", "ArrowLeft"); `code` is the
|
|
236
|
+
// physical, layout-independent key position ("KeyA", "Digit1", "NumpadEnter").
|
|
237
|
+
// Printable characters for text entry arrive via onTextInput, not here.
|
|
201
238
|
export interface KeyEvent {
|
|
202
239
|
key: string
|
|
240
|
+
code: string
|
|
241
|
+
repeat: boolean
|
|
242
|
+
shiftKey: boolean
|
|
243
|
+
ctrlKey: boolean
|
|
244
|
+
altKey: boolean
|
|
245
|
+
metaKey: boolean
|
|
203
246
|
}
|
|
204
247
|
|
|
205
248
|
export interface TextEvent {
|
|
@@ -322,6 +365,14 @@ export interface TextProps extends Position, PaintProps, PointerProps {
|
|
|
322
365
|
|
|
323
366
|
export interface TextureProps extends Position, PointerProps {
|
|
324
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"
|
|
325
376
|
w?: number
|
|
326
377
|
h?: number
|
|
327
378
|
srcX?: number
|
package/src/window.ts
CHANGED
|
@@ -2,20 +2,27 @@ 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
|
|
|
9
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
export { exit }
|
|
17
|
+
|
|
8
18
|
// ------ Pointer routing -----------------
|
|
9
19
|
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// gesture ownership is claim-based, above this layer. Enter/leave stay
|
|
17
|
-
// hover-driven, and moves with no active down follow the live hit path.
|
|
18
|
-
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.
|
|
19
26
|
|
|
20
27
|
// ------ Animation frames ----------------
|
|
21
28
|
|
|
@@ -179,6 +186,30 @@ export function onWindowBlur(fn: () => void) {
|
|
|
179
186
|
return unsubscribe
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
// ------ Back ----------------
|
|
190
|
+
|
|
191
|
+
export type BackEvent = { preventDefault: () => void }
|
|
192
|
+
|
|
193
|
+
// App handlers for the window-level back event, run in registration order.
|
|
194
|
+
// Kept in a local registry (not per-handler bus subscriptions) so the default
|
|
195
|
+
// action runs exactly once, after every handler has had its say.
|
|
196
|
+
let backHandlers = new Set<(e: BackEvent) => void>()
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calls `fn` on the user's back intent (Android back button/gesture, the
|
|
200
|
+
* desktop dev chord). Call `e.preventDefault()` when back means in-app
|
|
201
|
+
* navigation right now (close a modal, previous screen); unprevented, the
|
|
202
|
+
* default action runs: exit(). Apps without a handler exit on back
|
|
203
|
+
* everywhere, which is the correct zero-effort default.
|
|
204
|
+
* Returns a cleanup function; also auto-cleans within a reactive scope.
|
|
205
|
+
*/
|
|
206
|
+
export function onBack(fn: (e: BackEvent) => void) {
|
|
207
|
+
backHandlers.add(fn)
|
|
208
|
+
let cleanup = () => backHandlers.delete(fn)
|
|
209
|
+
onCleanup(cleanup)
|
|
210
|
+
return cleanup
|
|
211
|
+
}
|
|
212
|
+
|
|
182
213
|
// ------ Window ----------------
|
|
183
214
|
|
|
184
215
|
export function attachWindow(_nodeId: number) {
|
|
@@ -191,6 +222,7 @@ export function attachWindow(_nodeId: number) {
|
|
|
191
222
|
let unsubWheel: () => void = null!
|
|
192
223
|
let unsubKeyDown: () => void = null!
|
|
193
224
|
let unsubKeyUp: () => void = null!
|
|
225
|
+
let unsubBack: () => void = null!
|
|
194
226
|
let unsubTextInput: () => void = null!
|
|
195
227
|
let unsubKeyboardVisibility: () => void = null!
|
|
196
228
|
let unsubRefreshRate: () => void = null!
|
|
@@ -217,77 +249,71 @@ export function attachWindow(_nodeId: number) {
|
|
|
217
249
|
runFrame(time * 1000, frame)
|
|
218
250
|
})
|
|
219
251
|
|
|
220
|
-
// Dispatch an event to every node on the
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
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
|
|
224
270
|
let stopped = false
|
|
225
271
|
e.stopPropagation = () => {
|
|
226
272
|
stopped = true
|
|
227
273
|
}
|
|
228
|
-
|
|
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]!
|
|
229
282
|
getEventHandler(targets[i]!, handler)?.(e)
|
|
230
283
|
if (stopped) break
|
|
231
284
|
}
|
|
232
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
|
+
})
|
|
233
298
|
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
237
|
-
downPaths.set(e.pointerId, targets)
|
|
238
|
-
bubble(targets, "onPointerDown", e)
|
|
239
|
-
// Outside-tap blur. Read focus AFTER per-node handlers so a tap that
|
|
240
|
-
// moves focus to a new node is not immediately blurred again.
|
|
241
|
-
let focused = getFocusedNodeId()
|
|
242
|
-
if (focused != null && !targets.includes(focused)) {
|
|
243
|
-
setFocus(null)
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
unsubUp = on("pointerUp", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
249
|
-
let frozen = downPaths.get(e.pointerId)
|
|
250
|
-
downPaths.delete(e.pointerId)
|
|
251
|
-
bubble(frozen ?? targets, "onPointerUp", e)
|
|
299
|
+
unsubUp = on("pointerUp", (raw: RawPointer) => {
|
|
300
|
+
bubble(raw, "onPointerUp")
|
|
252
301
|
})
|
|
253
302
|
|
|
254
|
-
unsubMove = on(
|
|
255
|
-
"
|
|
256
|
-
|
|
257
|
-
bubble(downPaths.get(e.pointerId) ?? targets, "onPointerMove", e)
|
|
258
|
-
},
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
// Enter/leave keep their hover-diff order (already leaf->root for leave,
|
|
262
|
-
// root->leaf for enter), but still honor stopPropagation for a consistent
|
|
263
|
-
// event shape.
|
|
264
|
-
let dispatchOrdered = (targets: number[], handler: string, e: any) => {
|
|
265
|
-
let stopped = false
|
|
266
|
-
e.stopPropagation = () => {
|
|
267
|
-
stopped = true
|
|
268
|
-
}
|
|
269
|
-
for (let nodeId of targets) {
|
|
270
|
-
getEventHandler(nodeId, handler)?.(e)
|
|
271
|
-
if (stopped) break
|
|
272
|
-
}
|
|
273
|
-
}
|
|
303
|
+
unsubMove = on("pointerMove", (raw: RawPointer) => {
|
|
304
|
+
bubble(raw, "onPointerMove")
|
|
305
|
+
})
|
|
274
306
|
|
|
275
|
-
unsubEnter = on(
|
|
276
|
-
"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
(
|
|
285
|
-
dispatchOrdered(targets, "onPointerLeave", e)
|
|
286
|
-
},
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
unsubWheel = on("wheel", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
290
|
-
bubble(targets, "onWheel", e)
|
|
307
|
+
unsubEnter = on("pointerEnter", (raw: RawPointer) => {
|
|
308
|
+
dispatchOrdered(raw, "onPointerEnter")
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
unsubLeave = on("pointerLeave", (raw: RawPointer) => {
|
|
312
|
+
dispatchOrdered(raw, "onPointerLeave")
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
unsubWheel = on("wheel", (raw: RawPointer) => {
|
|
316
|
+
bubble(raw, "onWheel")
|
|
291
317
|
})
|
|
292
318
|
|
|
293
319
|
unsubKeyDown = on("keydown", (e: any) => {
|
|
@@ -304,6 +330,18 @@ export function attachWindow(_nodeId: number) {
|
|
|
304
330
|
}
|
|
305
331
|
})
|
|
306
332
|
|
|
333
|
+
unsubBack = on("back", () => {
|
|
334
|
+
let prevented = false
|
|
335
|
+
let e: BackEvent = {
|
|
336
|
+
preventDefault: () => {
|
|
337
|
+
prevented = true
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
// Copy first: a handler may unregister (itself or others) mid-dispatch.
|
|
341
|
+
for (let fn of [...backHandlers]) fn(e)
|
|
342
|
+
if (!prevented) exit()
|
|
343
|
+
})
|
|
344
|
+
|
|
307
345
|
unsubTextInput = on("textInput", (e: any) => {
|
|
308
346
|
let id = getFocusedNodeId()
|
|
309
347
|
if (id != null) {
|
|
@@ -339,6 +377,7 @@ export function attachWindow(_nodeId: number) {
|
|
|
339
377
|
if (unsubWheel) unsubWheel()
|
|
340
378
|
if (unsubKeyDown) unsubKeyDown()
|
|
341
379
|
if (unsubKeyUp) unsubKeyUp()
|
|
380
|
+
if (unsubBack) unsubBack()
|
|
342
381
|
if (unsubTextInput) unsubTextInput()
|
|
343
382
|
if (unsubKeyboardVisibility) unsubKeyboardVisibility()
|
|
344
383
|
if (unsubRefreshRate) unsubRefreshRate()
|