@solidrt/core 0.0.18 → 0.0.20
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 +2 -2
- package/src/capabilities.ts +48 -0
- package/src/environment.ts +155 -0
- package/src/index.ts +5 -0
- package/src/renderer.ts +45 -12
- package/src/types.d.ts +10 -1
- package/src/window.ts +69 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
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.20"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@solidjs/signals": "2.0.0-beta.15",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { env } from "./environment"
|
|
2
|
+
|
|
3
|
+
// Capabilities: what interactions are possible in the current environment.
|
|
4
|
+
// Derived from Environment State on every read - computed, never stored - so
|
|
5
|
+
// they are reactive wherever env is. Capabilities describe what is possible,
|
|
6
|
+
// not how the UI should behave; behavior lives in the policy layer on top.
|
|
7
|
+
//
|
|
8
|
+
// Device presence wins when the runtime reports it (so unplugging the mouse
|
|
9
|
+
// drops hover); the traffic-inferred seen-flags are the fallback for runtimes
|
|
10
|
+
// without device enumeration.
|
|
11
|
+
|
|
12
|
+
export type WindowSizeClass = "compact" | "medium" | "expanded"
|
|
13
|
+
|
|
14
|
+
export type Capabilities = {
|
|
15
|
+
/** A pointer can rest over content without pressing (mouse/trackpad). */
|
|
16
|
+
hover: boolean
|
|
17
|
+
/** Pixel-precise pointing (mouse/trackpad), as opposed to a finger. */
|
|
18
|
+
precisePointer: boolean
|
|
19
|
+
/** Direct touch input. */
|
|
20
|
+
touch: boolean
|
|
21
|
+
/** Hardware-key navigation (tab/arrow traversal, shortcuts). */
|
|
22
|
+
keyboardNav: boolean
|
|
23
|
+
/** How much horizontal room the window offers (Material breakpoints). */
|
|
24
|
+
windowSizeClass: WindowSizeClass
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Material 3 width breakpoints, in logical pixels.
|
|
28
|
+
const MEDIUM_MIN_WIDTH = 600
|
|
29
|
+
const EXPANDED_MIN_WIDTH = 840
|
|
30
|
+
|
|
31
|
+
export let capabilities: Capabilities = {
|
|
32
|
+
get hover() {
|
|
33
|
+
return env.inputDevices?.mouse ?? env.mouseSeen
|
|
34
|
+
},
|
|
35
|
+
get precisePointer() {
|
|
36
|
+
return env.inputDevices?.mouse ?? env.mouseSeen
|
|
37
|
+
},
|
|
38
|
+
get touch() {
|
|
39
|
+
return env.inputDevices?.touch ?? env.touchSeen
|
|
40
|
+
},
|
|
41
|
+
get keyboardNav() {
|
|
42
|
+
return env.inputDevices?.keyboard ?? env.keyboardSeen
|
|
43
|
+
},
|
|
44
|
+
get windowSizeClass(): WindowSizeClass {
|
|
45
|
+
let w = env.windowSize.width
|
|
46
|
+
return w >= EXPANDED_MIN_WIDTH ? "expanded" : w >= MEDIUM_MIN_WIDTH ? "medium" : "compact"
|
|
47
|
+
},
|
|
48
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createSignal } from "@solidjs/signals"
|
|
2
|
+
import { on } from "srt:events"
|
|
3
|
+
import { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
4
|
+
|
|
5
|
+
// Environment State: reactive facts about the current execution environment.
|
|
6
|
+
//
|
|
7
|
+
// Device presence comes from the runtime's sticky "inputDevices" event (init +
|
|
8
|
+
// hotplug). Runtimes that do not emit it leave `inputDevices` undefined, and
|
|
9
|
+
// the seen-flags below act as the fallback: a pointer type or key press has
|
|
10
|
+
// been seen this session. Seen-flags only ever go from false to true, so a
|
|
11
|
+
// capability derived from them can appear mid-session (e.g. the first mouse
|
|
12
|
+
// move) but never flickers away.
|
|
13
|
+
|
|
14
|
+
/** Connected input device classes, as reported by the runtime. */
|
|
15
|
+
export interface InputDevices {
|
|
16
|
+
keyboard: boolean
|
|
17
|
+
mouse: boolean
|
|
18
|
+
touch: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SystemTheme = "dark" | "light" | "unknown"
|
|
22
|
+
|
|
23
|
+
export type Orientation = "portrait" | "portraitFlipped" | "landscape" | "landscapeFlipped" | "unknown"
|
|
24
|
+
|
|
25
|
+
let devicesAccessor: (() => InputDevices | undefined) | undefined
|
|
26
|
+
|
|
27
|
+
function ensureDevicesState() {
|
|
28
|
+
if (devicesAccessor) return
|
|
29
|
+
let [devices, setDevices] = createSignal<InputDevices | undefined>(undefined)
|
|
30
|
+
// Sticky: the current state replays on subscribe, so the first read already
|
|
31
|
+
// sees it on runtimes that report devices.
|
|
32
|
+
on("inputDevices", (d: InputDevices) => {
|
|
33
|
+
setDevices({ keyboard: !!d.keyboard, mouse: !!d.mouse, touch: !!d.touch })
|
|
34
|
+
})
|
|
35
|
+
devicesAccessor = devices
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let systemThemeAccessor: (() => SystemTheme) | undefined
|
|
39
|
+
|
|
40
|
+
function ensureSystemThemeState() {
|
|
41
|
+
if (systemThemeAccessor) return
|
|
42
|
+
let [theme, setTheme] = createSignal<SystemTheme>("unknown")
|
|
43
|
+
on("systemTheme", (e: { theme?: SystemTheme }) => setTheme(e.theme ?? "unknown"))
|
|
44
|
+
systemThemeAccessor = theme
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let orientationAccessor: (() => Orientation) | undefined
|
|
48
|
+
|
|
49
|
+
function ensureOrientationState() {
|
|
50
|
+
if (orientationAccessor) return
|
|
51
|
+
let [orientation, setOrientation] = createSignal<Orientation>("unknown")
|
|
52
|
+
on("displayOrientation", (e: { orientation?: Orientation }) => {
|
|
53
|
+
setOrientation(e.orientation ?? "unknown")
|
|
54
|
+
})
|
|
55
|
+
orientationAccessor = orientation
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let mouseSeenAccessor: (() => boolean) | undefined
|
|
59
|
+
let touchSeenAccessor: (() => boolean) | undefined
|
|
60
|
+
|
|
61
|
+
function ensurePointerState() {
|
|
62
|
+
if (mouseSeenAccessor) return
|
|
63
|
+
let [mouse, setMouse] = createSignal(false)
|
|
64
|
+
let [touch, setTouch] = createSignal(false)
|
|
65
|
+
let sawMouse = false
|
|
66
|
+
let sawTouch = false
|
|
67
|
+
let unsubs: (() => void)[] = []
|
|
68
|
+
let note = (e: { pointerType?: string }) => {
|
|
69
|
+
if (e.pointerType === "mouse" && !sawMouse) {
|
|
70
|
+
sawMouse = true
|
|
71
|
+
setMouse(true)
|
|
72
|
+
} else if (e.pointerType === "touch" && !sawTouch) {
|
|
73
|
+
sawTouch = true
|
|
74
|
+
setTouch(true)
|
|
75
|
+
}
|
|
76
|
+
// Both types observed: nothing left to learn, stop listening.
|
|
77
|
+
if (sawMouse && sawTouch) for (let u of unsubs) u()
|
|
78
|
+
}
|
|
79
|
+
unsubs.push(on("pointerMove", note), on("pointerDown", note))
|
|
80
|
+
mouseSeenAccessor = mouse
|
|
81
|
+
touchSeenAccessor = touch
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let keyboardSeenAccessor: (() => boolean) | undefined
|
|
85
|
+
|
|
86
|
+
function ensureKeyboardState() {
|
|
87
|
+
if (keyboardSeenAccessor) return
|
|
88
|
+
let [keyboard, setKeyboard] = createSignal(false)
|
|
89
|
+
// Soft keyboards also deliver some keydowns (Backspace, Return), so this can
|
|
90
|
+
// read true on a touch-only device once the user types in a field. Only a
|
|
91
|
+
// fallback: capabilities prefer runtime-reported device presence.
|
|
92
|
+
let unsub = on("keydown", () => {
|
|
93
|
+
setKeyboard(true)
|
|
94
|
+
unsub()
|
|
95
|
+
})
|
|
96
|
+
keyboardSeenAccessor = keyboard
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reactive Environment State: what the framework observes about where it is
|
|
101
|
+
* running. Read properties inside a tracked scope (JSX, memo, effect) to re-run
|
|
102
|
+
* when they change. Behavior decisions should go through `capabilities` and the
|
|
103
|
+
* policy layer; read `env` directly only when the raw fact itself is needed.
|
|
104
|
+
*/
|
|
105
|
+
export let env = {
|
|
106
|
+
get windowSize() {
|
|
107
|
+
return windowSize()
|
|
108
|
+
},
|
|
109
|
+
get safeArea() {
|
|
110
|
+
return safeArea()
|
|
111
|
+
},
|
|
112
|
+
get displayScale() {
|
|
113
|
+
return displayScale()
|
|
114
|
+
},
|
|
115
|
+
get windowFocused() {
|
|
116
|
+
return windowFocused()
|
|
117
|
+
},
|
|
118
|
+
get keyboardHeight() {
|
|
119
|
+
return keyboardHeight()
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* Connected input device classes, or undefined until the runtime reports
|
|
123
|
+
* them (it does so at startup, so undefined normally means the runtime has
|
|
124
|
+
* no device enumeration).
|
|
125
|
+
*/
|
|
126
|
+
get inputDevices(): InputDevices | undefined {
|
|
127
|
+
ensureDevicesState()
|
|
128
|
+
return devicesAccessor!()
|
|
129
|
+
},
|
|
130
|
+
/** The OS-level dark/light preference. */
|
|
131
|
+
get systemTheme(): SystemTheme {
|
|
132
|
+
ensureSystemThemeState()
|
|
133
|
+
return systemThemeAccessor!()
|
|
134
|
+
},
|
|
135
|
+
/** Orientation of the display the window is on. */
|
|
136
|
+
get orientation(): Orientation {
|
|
137
|
+
ensureOrientationState()
|
|
138
|
+
return orientationAccessor!()
|
|
139
|
+
},
|
|
140
|
+
/** A mouse (or trackpad) pointer has produced events this session. */
|
|
141
|
+
get mouseSeen(): boolean {
|
|
142
|
+
ensurePointerState()
|
|
143
|
+
return mouseSeenAccessor!()
|
|
144
|
+
},
|
|
145
|
+
/** A touch pointer has produced events this session. */
|
|
146
|
+
get touchSeen(): boolean {
|
|
147
|
+
ensurePointerState()
|
|
148
|
+
return touchSeenAccessor!()
|
|
149
|
+
},
|
|
150
|
+
/** A key press has been delivered this session. */
|
|
151
|
+
get keyboardSeen(): boolean {
|
|
152
|
+
ensureKeyboardState()
|
|
153
|
+
return keyboardSeenAccessor!()
|
|
154
|
+
},
|
|
155
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,12 @@ export type { BoundingBox } from "./core"
|
|
|
4
4
|
export { parseColor, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
6
6
|
export { onFrame, onLayout, onResize, onWindowFocus, onWindowBlur } from "./window"
|
|
7
|
+
export { setPointerCapture, releasePointerCapture } from "./window"
|
|
7
8
|
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
9
|
+
export { env } from "./environment"
|
|
10
|
+
export type { InputDevices, SystemTheme, Orientation } from "./environment"
|
|
11
|
+
export { capabilities } from "./capabilities"
|
|
12
|
+
export type { Capabilities, WindowSizeClass } from "./capabilities"
|
|
8
13
|
export { createTexture } from "./gpu"
|
|
9
14
|
export { createImage, decodeImage } from "./image"
|
|
10
15
|
export type { DecodedImage, ImageSource } from "./image"
|
package/src/renderer.ts
CHANGED
|
@@ -30,9 +30,42 @@ function createProxyNode(elementType: ElementType): ProxyNode {
|
|
|
30
30
|
return node
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
33
|
+
// Nodes detached this tick and awaiting the destroy sweep, keyed by id so a
|
|
34
|
+
// re-insert can cancel one. See removeNode / flushDestroy.
|
|
35
|
+
let pendingDestroy = new Map<number, ProxyNode>()
|
|
36
|
+
let destroyScheduled = false
|
|
37
|
+
|
|
38
|
+
// Frees a detached node and its subtree on both sides. Descendants that were
|
|
39
|
+
// moved out (their parent no longer points here) are left alone. Clear focus
|
|
40
|
+
// before dropping handlers so onBlur still fires for a focused descendant.
|
|
41
|
+
function destroyNode(node: ProxyNode): void {
|
|
42
|
+
tree.destroyNode(node.id)
|
|
43
|
+
let cleanup = (n: ProxyNode) => {
|
|
44
|
+
for (let child of n.children) if (child.parent === n) cleanup(child)
|
|
45
|
+
if (n.id === getFocusedNodeId()) setFocus(null)
|
|
46
|
+
nodes.delete(n.id)
|
|
47
|
+
cleanupNodeHandlers(n.id)
|
|
48
|
+
}
|
|
49
|
+
cleanup(node)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// End-of-tick sweep: destroy every still-detached pending node. A node that was
|
|
53
|
+
// re-inserted this tick had its entry removed by insertNode (it moved, not
|
|
54
|
+
// died); one still parentless here is genuinely gone.
|
|
55
|
+
function flushDestroy(): void {
|
|
56
|
+
destroyScheduled = false
|
|
57
|
+
let batch = pendingDestroy
|
|
58
|
+
pendingDestroy = new Map()
|
|
59
|
+
for (let node of batch.values()) {
|
|
60
|
+
if (node.parent === undefined) destroyNode(node)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Detaches `node` from `parent`, keeping the subtree alive so it can be
|
|
65
|
+
// re-inserted elsewhere (a move) - matching DOM removeChild. Destruction is
|
|
66
|
+
// deferred to an end-of-tick sweep; if nothing re-attaches the node by then it
|
|
67
|
+
// is freed. Hoisted out of the renderer config so createPortal can reuse it
|
|
68
|
+
// (createRenderer does not return its removeNode hook).
|
|
36
69
|
function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
37
70
|
if (!node || !parent) return
|
|
38
71
|
|
|
@@ -45,17 +78,13 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
|
45
78
|
}
|
|
46
79
|
node.parent = undefined
|
|
47
80
|
|
|
48
|
-
tree.
|
|
81
|
+
tree.detachNode(parent.id, node.id)
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (n.id === getFocusedNodeId()) setFocus(null)
|
|
55
|
-
nodes.delete(n.id)
|
|
56
|
-
cleanupNodeHandlers(n.id)
|
|
83
|
+
pendingDestroy.set(node.id, node)
|
|
84
|
+
if (!destroyScheduled) {
|
|
85
|
+
destroyScheduled = true
|
|
86
|
+
Promise.resolve().then(flushDestroy)
|
|
57
87
|
}
|
|
58
|
-
cleanup(node)
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
export let {
|
|
@@ -123,6 +152,10 @@ export let {
|
|
|
123
152
|
insertNode: (parent: ProxyNode, node: ProxyNode, anchor?: ProxyNode): void => {
|
|
124
153
|
if (!node) return
|
|
125
154
|
|
|
155
|
+
// A re-inserted node is being moved, not destroyed: cancel its pending
|
|
156
|
+
// destroy so the end-of-tick sweep leaves it (and its subtree) alone.
|
|
157
|
+
pendingDestroy.delete(node.id)
|
|
158
|
+
|
|
126
159
|
if (parent) {
|
|
127
160
|
node.parent = parent
|
|
128
161
|
|
package/src/types.d.ts
CHANGED
|
@@ -153,6 +153,10 @@ export interface PointerEvent {
|
|
|
153
153
|
ctrlKey: boolean
|
|
154
154
|
altKey: boolean
|
|
155
155
|
metaKey: boolean
|
|
156
|
+
// Stops the event from reaching ancestor handlers. Events dispatch leaf->root
|
|
157
|
+
// (bubbling), so calling this in a child prevents the enclosing node from
|
|
158
|
+
// seeing it (e.g. a slider claiming a drag so an ancestor scroller ignores it).
|
|
159
|
+
stopPropagation: () => void
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
export interface WheelEvent extends PointerEvent {
|
|
@@ -264,8 +268,13 @@ export interface SvgProps extends Position, PointerProps {
|
|
|
264
268
|
color?: Color
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
export interface TextProps extends PaintProps, PointerProps {
|
|
271
|
+
export interface TextProps extends Position, PaintProps, PointerProps {
|
|
268
272
|
children?: Children
|
|
273
|
+
// Shaping (wrap) width. Detached text wraps at the inherited ancestor size
|
|
274
|
+
// by default; set w for an unwrapped natural line or an explicit wrap width.
|
|
275
|
+
w?: number
|
|
276
|
+
// Reported-bounds height only; paragraph height always falls out of the text.
|
|
277
|
+
h?: number
|
|
269
278
|
fontFamily?: "sans" | "mono" | (string & {})
|
|
270
279
|
fontSize?: number
|
|
271
280
|
lineHeight?: number
|
package/src/window.ts
CHANGED
|
@@ -4,6 +4,28 @@ import { renderFrame } from "srt:render"
|
|
|
4
4
|
import { on, once } from "srt:events"
|
|
5
5
|
import { getEventHandler, getFocusedNodeId, setFocus } from "./core"
|
|
6
6
|
|
|
7
|
+
// ------ Pointer capture -----------------
|
|
8
|
+
|
|
9
|
+
// Active pointer captures, pointerId -> nodeId. While a pointer is captured, its
|
|
10
|
+
// move/up events dispatch straight to the captured node (bypassing hit testing),
|
|
11
|
+
// so a drag keeps working when the pointer drifts off the element. Modeled on the
|
|
12
|
+
// web setPointerCapture; capture auto-releases on pointerUp.
|
|
13
|
+
let pointerCaptures = new Map<number, number>()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Routes all further move/up events for `pointerId` to `nodeId` until release,
|
|
17
|
+
* regardless of what is under the pointer. Call from a pointerdown handler to own
|
|
18
|
+
* a drag (e.g. a slider). Auto-releases on the matching pointerup.
|
|
19
|
+
*/
|
|
20
|
+
export function setPointerCapture(nodeId: number, pointerId: number) {
|
|
21
|
+
pointerCaptures.set(pointerId, nodeId)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Ends a capture early. Not needed for the common case (pointerup releases it). */
|
|
25
|
+
export function releasePointerCapture(pointerId: number) {
|
|
26
|
+
pointerCaptures.delete(pointerId)
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
// ------ Animation frames ----------------
|
|
8
30
|
|
|
9
31
|
let nextFrameId = 1
|
|
@@ -201,12 +223,24 @@ export function attachWindow(_nodeId: number) {
|
|
|
201
223
|
runFrame(time * 1000, frame)
|
|
202
224
|
})
|
|
203
225
|
|
|
226
|
+
// Dispatch an event to every node on the hit path, leaf->root (bubbling), so
|
|
227
|
+
// a child handler can call e.stopPropagation() to keep the event from
|
|
228
|
+
// reaching its ancestors. `targets` arrives root->leaf, hence the reverse.
|
|
229
|
+
let bubble = (targets: number[], handler: string, e: any) => {
|
|
230
|
+
let stopped = false
|
|
231
|
+
e.stopPropagation = () => {
|
|
232
|
+
stopped = true
|
|
233
|
+
}
|
|
234
|
+
for (let i = targets.length - 1; i >= 0; i--) {
|
|
235
|
+
getEventHandler(targets[i]!, handler)?.(e)
|
|
236
|
+
if (stopped) break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
204
240
|
unsubDown = on(
|
|
205
241
|
"pointerDown",
|
|
206
242
|
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
207
|
-
|
|
208
|
-
getEventHandler(nodeId, "onPointerDown")?.(e)
|
|
209
|
-
}
|
|
243
|
+
bubble(targets, "onPointerDown", e)
|
|
210
244
|
// Outside-tap blur. Read focus AFTER per-node handlers so a tap that
|
|
211
245
|
// moves focus to a new node is not immediately blurred again.
|
|
212
246
|
let focused = getFocusedNodeId()
|
|
@@ -217,42 +251,61 @@ export function attachWindow(_nodeId: number) {
|
|
|
217
251
|
)
|
|
218
252
|
|
|
219
253
|
unsubUp = on("pointerUp", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
220
|
-
|
|
221
|
-
|
|
254
|
+
let captured = pointerCaptures.get(e.pointerId)
|
|
255
|
+
if (captured != null) {
|
|
256
|
+
// Deliver to the captured node, then release (web auto-release on up).
|
|
257
|
+
e.stopPropagation = () => {}
|
|
258
|
+
getEventHandler(captured, "onPointerUp")?.(e)
|
|
259
|
+
pointerCaptures.delete(e.pointerId)
|
|
260
|
+
return
|
|
222
261
|
}
|
|
262
|
+
bubble(targets, "onPointerUp", e)
|
|
223
263
|
})
|
|
224
264
|
|
|
225
265
|
unsubMove = on(
|
|
226
266
|
"pointerMove",
|
|
227
267
|
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
228
|
-
|
|
229
|
-
|
|
268
|
+
let captured = pointerCaptures.get(e.pointerId)
|
|
269
|
+
if (captured != null) {
|
|
270
|
+
// Captured drags get moves anywhere over the window, bypassing hit test.
|
|
271
|
+
e.stopPropagation = () => {}
|
|
272
|
+
getEventHandler(captured, "onPointerMove")?.(e)
|
|
273
|
+
return
|
|
230
274
|
}
|
|
275
|
+
bubble(targets, "onPointerMove", e)
|
|
231
276
|
},
|
|
232
277
|
)
|
|
233
278
|
|
|
279
|
+
// Enter/leave keep their hover-diff order (already leaf->root for leave,
|
|
280
|
+
// root->leaf for enter), but still honor stopPropagation for a consistent
|
|
281
|
+
// event shape.
|
|
282
|
+
let dispatchOrdered = (targets: number[], handler: string, e: any) => {
|
|
283
|
+
let stopped = false
|
|
284
|
+
e.stopPropagation = () => {
|
|
285
|
+
stopped = true
|
|
286
|
+
}
|
|
287
|
+
for (let nodeId of targets) {
|
|
288
|
+
getEventHandler(nodeId, handler)?.(e)
|
|
289
|
+
if (stopped) break
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
234
293
|
unsubEnter = on(
|
|
235
294
|
"pointerEnter",
|
|
236
295
|
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
237
|
-
|
|
238
|
-
getEventHandler(nodeId, "onPointerEnter")?.(e)
|
|
239
|
-
}
|
|
296
|
+
dispatchOrdered(targets, "onPointerEnter", e)
|
|
240
297
|
},
|
|
241
298
|
)
|
|
242
299
|
|
|
243
300
|
unsubLeave = on(
|
|
244
301
|
"pointerLeave",
|
|
245
302
|
({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
246
|
-
|
|
247
|
-
getEventHandler(nodeId, "onPointerLeave")?.(e)
|
|
248
|
-
}
|
|
303
|
+
dispatchOrdered(targets, "onPointerLeave", e)
|
|
249
304
|
},
|
|
250
305
|
)
|
|
251
306
|
|
|
252
307
|
unsubWheel = on("wheel", ({ targets, ...e }: { targets: number[]; [k: string]: any }) => {
|
|
253
|
-
|
|
254
|
-
getEventHandler(nodeId, "onWheel")?.(e)
|
|
255
|
-
}
|
|
308
|
+
bubble(targets, "onWheel", e)
|
|
256
309
|
})
|
|
257
310
|
|
|
258
311
|
unsubKeyDown = on("keydown", (e: any) => {
|