@solidrt/core 0.0.65 → 0.0.67
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/AGENTS.md +3 -3
- package/README.md +1 -1
- package/agents/performance.md +27 -25
- package/docs/index.md +8 -4
- package/package.json +3 -3
- package/src/camera.ts +1 -1
- package/src/core.ts +5 -3
- package/src/double-tap.ts +9 -2
- package/src/environment.ts +3 -3
- package/src/gamepad.ts +1 -1
- package/src/gpu.ts +1 -1
- package/src/input-pointer.ts +12 -0
- package/src/long-press.ts +27 -5
- package/src/pan.ts +21 -3
- package/src/renderer.ts +8 -8
- package/src/runtime-modules.d.ts +15 -10
- package/src/swipe.ts +16 -0
- package/src/textures.ts +1 -1
- package/src/transform.ts +23 -3
- package/src/types.d.ts +14 -4
- package/src/window.ts +10 -4
package/AGENTS.md
CHANGED
|
@@ -92,7 +92,7 @@ real design decision, not an afterthought:
|
|
|
92
92
|
|
|
93
93
|
```sh
|
|
94
94
|
bun add @solidrt/core # the renderer
|
|
95
|
-
bun add -d @solidrt/cli # the `
|
|
95
|
+
bun add -d @solidrt/cli # the `sol` tool (see its AGENTS.md)
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
Core primitives alone are enough to build a full app. The optional extensions
|
|
@@ -320,7 +320,7 @@ that reads exactly like Solid fallout.
|
|
|
320
320
|
input happened. Time anything about input with it (a press length, the
|
|
321
321
|
gap between two taps, a speed from two positions), never with
|
|
322
322
|
`performance.now()` in the handler: that is when the handler ran, and
|
|
323
|
-
it does not follow the dev clock, `
|
|
323
|
+
it does not follow the dev clock, `sol render` or a test. The stamp is
|
|
324
324
|
the input's own time, not its delivery's: a move carries the time the
|
|
325
325
|
pointer was at the position it reports, however late the frame that
|
|
326
326
|
delivers it; a down, up, wheel or key the time the platform recorded.
|
|
@@ -616,7 +616,7 @@ that reads exactly like Solid fallout.
|
|
|
616
616
|
seldom changes the picture. `frameTime()` is the current frame's `tick`
|
|
617
617
|
read from anywhere (not reactive, requests nothing): the app's own
|
|
618
618
|
clock for code with no tick in hand, where `performance.now()` would
|
|
619
|
-
ignore the dev clock, `
|
|
619
|
+
ignore the dev clock, `sol render` and tests. A JS
|
|
620
620
|
tween loop or an animation library pushing interpolated values through
|
|
621
621
|
signals is the single most expensive mistake available here - read
|
|
622
622
|
agents/performance.md before writing either.
|
package/README.md
CHANGED
package/agents/performance.md
CHANGED
|
@@ -149,29 +149,31 @@ Rules, in order of leverage:
|
|
|
149
149
|
anything needing what is behind it does not - and is split from content
|
|
150
150
|
invalidation, so a params-only change re-runs the pass against the cached
|
|
151
151
|
snapshot instead of re-rasterizing. A window shader's output is invisible
|
|
152
|
-
to get_snapshot and every other MCP tool; `bun run
|
|
152
|
+
to get_snapshot and every other MCP tool; `bun run sol render` is the only
|
|
153
153
|
way to see it (see @solidrt/cli AGENTS.md).
|
|
154
|
-
8. `flux:wasm`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
154
|
+
8. `flux:wasm` compiles a module to machine code on devices that allow it
|
|
155
|
+
(`Flux.capabilities` lists `wasm-native`: desktop, 64-bit Android) and
|
|
156
|
+
interprets it elsewhere (iOS, 32-bit Android). Both lanes are the fast
|
|
157
|
+
lane for compute: measured on a desktop, an arm64 tablet and a 32-bit
|
|
158
|
+
TV, a numeric kernel (typed-array math, tight inner loops, no host calls
|
|
159
|
+
inside the loop) runs 18x to 960x faster than the same loop in
|
|
160
|
+
JavaScript when compiled and 4x to 88x when interpreted, float at the low
|
|
161
|
+
end, integer and SIMD-shaped loops at the high end. So when profiling
|
|
162
|
+
shows such a kernel is where the app spends its time, port it: measure
|
|
163
|
+
the JS loop, port the kernel, measure again, keep whichever wins. The
|
|
164
|
+
boundary is the cost, not the code: a host call out of the module costs
|
|
165
|
+
3x to 7x a plain JavaScript call, two to three hundred iterations of a
|
|
166
|
+
tight loop, so batch at the boundary - one call per frame over a byte
|
|
167
|
+
buffer, never a call per element. What wasm does not do is speed up
|
|
168
|
+
render-path work (rules 1-3 are that leverage). It is also the way to
|
|
169
|
+
ship one compiled module across every target with no native toolchain,
|
|
170
|
+
and it pairs with an isolate when a call runs long enough to block.
|
|
171
|
+
9. There is no native-library route from app code. A shared library is
|
|
172
|
+
compiled per platform, so it cannot travel in a portable app payload,
|
|
173
|
+
and Android loads only what arrived inside the APK. Binding one is a
|
|
174
|
+
custom runtime build: the runtime crates with the library registered
|
|
175
|
+
beside the built-in modules. For speed, everything above comes first;
|
|
176
|
+
for a proven library that has a wasm build, rule 8.
|
|
175
177
|
10. Text costs shaping, once per word per style, through a shared word cache;
|
|
176
178
|
line breaking is arithmetic over the shaped words. So re-breaking text
|
|
177
179
|
every frame with prepareText + layoutNextLine (a shape that breathes, an
|
|
@@ -188,7 +190,7 @@ Rules, in order of leverage:
|
|
|
188
190
|
## Isolates: heavy work off the JS thread
|
|
189
191
|
|
|
190
192
|
A long synchronous computation (a big parse, a simulation step, a blocking
|
|
191
|
-
`flux:
|
|
193
|
+
`flux:wasm` call) freezes rendering and input for its duration.
|
|
192
194
|
Move it into an isolate module: a file whose first statement is the
|
|
193
195
|
`"use isolate"` directive runs in a second runtime on its own thread, and
|
|
194
196
|
main calls its exports as async functions.
|
|
@@ -208,7 +210,7 @@ let n = await worker.crunch(bytes) // main keeps rendering meanwhil
|
|
|
208
210
|
```
|
|
209
211
|
|
|
210
212
|
The bundler builds each such module as its own bundle and ships it with the
|
|
211
|
-
app (dev pushes and `
|
|
213
|
+
app (dev pushes and `sol pack` alike). Rules: main may only `import type`
|
|
212
214
|
from an isolate module (a value import is a build error); arguments and
|
|
213
215
|
results are copies (numbers, strings, byte buffers, arrays, plain objects -
|
|
214
216
|
no functions, no class instances); the child has the non-gui `flux:*`
|
|
@@ -248,7 +250,7 @@ not infer it from the desktop number.
|
|
|
248
250
|
- **Some paints cost a frame each on a tiled mobile GPU.** Measured on a
|
|
249
251
|
mid-range 2020 tablet (Adreno 610, Impeller GLES) with ten panes sliding
|
|
250
252
|
and resizing on a layout transition, each row by subtraction from the
|
|
251
|
-
compositor's own present record (`
|
|
253
|
+
compositor's own present record (`sol android --census`), against a
|
|
252
254
|
frame that ran at 60 fps with a 13 ms GPU span:
|
|
253
255
|
- a 3-stop linear-gradient fill per pane: +15 ms, 20 fps. A gradient is
|
|
254
256
|
shaded per pixel per frame; the same tint as a flat `d-rect` costs
|
package/docs/index.md
CHANGED
|
@@ -91,9 +91,13 @@ ancestor with `position="relative"`.
|
|
|
91
91
|
## Input
|
|
92
92
|
|
|
93
93
|
Pointer, wheel, and key events are props on any element:
|
|
94
|
-
`onPointerDown`, `onPointerMove`, `onPointerUp`, `
|
|
95
|
-
`onPointerLeave`, `onWheel`, `onKeyDown`, `onKeyUp`.
|
|
96
|
-
hit leaf up to the root, and `stopPropagation()`
|
|
94
|
+
`onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerCancel`,
|
|
95
|
+
`onPointerEnter`, `onPointerLeave`, `onWheel`, `onKeyDown`, `onKeyUp`.
|
|
96
|
+
Events travel from the hit leaf up to the root, and `stopPropagation()`
|
|
97
|
+
ends the walk. A pointer ends with an up or a cancel, never both: the
|
|
98
|
+
cancel is the system taking the touch away (a back swipe, palm rejection),
|
|
99
|
+
delivered along the pressed path with the pointer's last position, so a
|
|
100
|
+
press ends without firing and a drag ends without a fling. Key events
|
|
97
101
|
bubble the same way, starting at the focused node - or at the window root
|
|
98
102
|
when nothing is focused, so `onKeyDown` on the window is where app-global
|
|
99
103
|
shortcuts live.
|
|
@@ -176,7 +180,7 @@ animation and audio, not the persistence moment.
|
|
|
176
180
|
A link from outside (a custom scheme link the OS routed to the app, or one
|
|
177
181
|
a dev tool sent) arrives raw: `env.launchLink` is the one this run of the
|
|
178
182
|
app was started with (null without; a hot reload starts at the location
|
|
179
|
-
the app last reported, see `reportLocation` in `
|
|
183
|
+
the app last reported, see `reportLocation` in `sol:dev`), `onLink` fires
|
|
180
184
|
for one arriving while the app runs. What a link names is the app's to decide, and it is untrusted input:
|
|
181
185
|
validate before acting on any part of it. A router turns links into screens
|
|
182
186
|
for you; without one, parse it yourself. The scheme a packed app answers to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.67",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"AGENTS.md"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@solidrt/flux-types": "0.0.
|
|
37
|
-
"@solidrt/test": "0.0.
|
|
36
|
+
"@solidrt/flux-types": "0.0.67",
|
|
37
|
+
"@solidrt/test": "0.0.67"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@solidjs/signals": "2.0.0-rc.13",
|
package/src/camera.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
10
10
|
import { listCameras, open } from "flux:camera"
|
|
11
11
|
import type { TextureId } from "flux:gpu"
|
|
12
|
-
import { on } from "
|
|
12
|
+
import { on } from "sol:events"
|
|
13
13
|
|
|
14
14
|
export type CameraFacing = "front" | "back" | "unknown"
|
|
15
15
|
|
package/src/core.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import * as tree from "flux:rendertree"
|
|
2
2
|
import type { TextureId } from "flux:gpu"
|
|
3
3
|
import { createSignal, getOwner, onCleanup } from "@solidjs/signals"
|
|
4
|
-
import { on } from "
|
|
4
|
+
import { on } from "sol:events"
|
|
5
5
|
|
|
6
6
|
let handlers = new Map<number, Map<string, Function>>()
|
|
7
7
|
|
|
8
8
|
// Pointer-handler presence mirrored into the render tree, so the runtime can
|
|
9
9
|
// skip building deliveries that would reach nobody (moves over static content
|
|
10
10
|
// are the flood case). Bits match alloy's EventInterest (rendertree/hit.rs);
|
|
11
|
-
// keep the two in sync. Down
|
|
12
|
-
// them: focus and gesture side effects hang off them regardless
|
|
11
|
+
// keep the two in sync. Down, up and cancel are recorded but the runtime
|
|
12
|
+
// never gates them: focus and gesture side effects hang off them regardless
|
|
13
|
+
// of handlers.
|
|
13
14
|
const MOVE_BIT = 1
|
|
14
15
|
const POINTER_INTEREST: Record<string, number> = {
|
|
15
16
|
onPointerMove: MOVE_BIT,
|
|
@@ -18,6 +19,7 @@ const POINTER_INTEREST: Record<string, number> = {
|
|
|
18
19
|
onPointerEnter: 8,
|
|
19
20
|
onPointerLeave: 16,
|
|
20
21
|
onWheel: 32,
|
|
22
|
+
onPointerCancel: 64,
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
let interests = new Map<number, number>()
|
package/src/double-tap.ts
CHANGED
|
@@ -48,8 +48,8 @@ export interface TapSequenceOptions {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** The tap bookkeeping under a double-tap: one first tap at a time,
|
|
51
|
-
* decided by the second down, by travel, by a long hold, by
|
|
52
|
-
* timer or by cancel(). */
|
|
51
|
+
* decided by the second down, by travel, by a long hold, by a cancelled
|
|
52
|
+
* pointer, by the window timer or by cancel(). */
|
|
53
53
|
export function createTapSequence(options: TapSequenceOptions) {
|
|
54
54
|
let first: { id: number; at: PointerPoint; downAt: number; upAt: number | null } | null = null
|
|
55
55
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -99,6 +99,9 @@ export function createTapSequence(options: TapSequenceOptions) {
|
|
|
99
99
|
settle(false)
|
|
100
100
|
}, DOUBLE_TAP_MS)
|
|
101
101
|
},
|
|
102
|
+
cancelled(e: PointerEvent) {
|
|
103
|
+
if (first && first.id === e.pointerId) settle(false)
|
|
104
|
+
},
|
|
102
105
|
cancel: () => settle(false),
|
|
103
106
|
}
|
|
104
107
|
}
|
|
@@ -145,6 +148,10 @@ export function createDoubleTap(options: DoubleTapOptions) {
|
|
|
145
148
|
if (second === e.pointerId) release()
|
|
146
149
|
taps.up(e)
|
|
147
150
|
},
|
|
151
|
+
onPointerCancel: (e: PointerEvent) => {
|
|
152
|
+
if (second === e.pointerId) release()
|
|
153
|
+
taps.cancelled(e)
|
|
154
|
+
},
|
|
148
155
|
}
|
|
149
156
|
return { handlers, cancel }
|
|
150
157
|
}
|
package/src/environment.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createSignal, runWithOwner } from "@solidjs/signals"
|
|
2
|
-
import { on } from "
|
|
2
|
+
import { on } from "sol:events"
|
|
3
3
|
import { onPointerMove } from "./core"
|
|
4
4
|
import { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
5
5
|
|
|
@@ -16,7 +16,7 @@ import { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } fro
|
|
|
16
16
|
// runs, which is usually inside a component or a computation (a memo's first
|
|
17
17
|
// read of env.inputDevices, a HUD effect's first read of pointerLocked()).
|
|
18
18
|
// Created there, the fact would belong to that reader: sticky events replay
|
|
19
|
-
// their cached value synchronously on subscribe (
|
|
19
|
+
// their cached value synchronously on subscribe (sol:events' on()), so the
|
|
20
20
|
// on() call writes the signal it just created inside the reader's owned
|
|
21
21
|
// scope - a render-time write, which the guard rejects - and a root or a
|
|
22
22
|
// subscription created there is disposed with the reader. The facts are
|
|
@@ -274,7 +274,7 @@ export let env = {
|
|
|
274
274
|
},
|
|
275
275
|
/**
|
|
276
276
|
* The link this process was started with, or null: a custom scheme link
|
|
277
|
-
* the OS routed to the app (Android intent data; `
|
|
277
|
+
* the OS routed to the app (Android intent data; `sol render --link`),
|
|
278
278
|
* raw, exactly as it arrived. What it names is the app's to decide, and
|
|
279
279
|
* it is untrusted input: validate before acting on any part of it. A link
|
|
280
280
|
* arriving while the app runs is an event instead: `onLink`. Fixed for the
|
package/src/gamepad.ts
CHANGED
package/src/gpu.ts
CHANGED
|
@@ -347,7 +347,7 @@ export function createMutableTexture(
|
|
|
347
347
|
* `packed` as a keyword, so `vec4 packed = texture(...)` fails with a syntax
|
|
348
348
|
* error that does not name the identifier - pick another name.
|
|
349
349
|
*
|
|
350
|
-
* A bad source is a runtime failure, not one `
|
|
350
|
+
* A bad source is a runtime failure, not one `sol check` catches: the compile
|
|
351
351
|
* throws at this call, so a shader created in a component body takes the app
|
|
352
352
|
* to the error window unless an `<Errored>` closer in the tree claims it.
|
|
353
353
|
*/
|
package/src/input-pointer.ts
CHANGED
|
@@ -133,6 +133,7 @@ export interface PointerFeed extends InputDevice {
|
|
|
133
133
|
onPointerDown(event: PointerEvent): void
|
|
134
134
|
onPointerMove(event: PointerEvent): void
|
|
135
135
|
onPointerUp(event: PointerEvent): void
|
|
136
|
+
onPointerCancel(event: PointerEvent): void
|
|
136
137
|
onPointerLeave(event: PointerEvent): void
|
|
137
138
|
onWheel(event: WheelEvent): void
|
|
138
139
|
}
|
|
@@ -481,6 +482,17 @@ export function createPointerFeed(options: PointerFeedOptions = {}): PointerFeed
|
|
|
481
482
|
opening = null
|
|
482
483
|
}
|
|
483
484
|
},
|
|
485
|
+
onPointerCancel(e) {
|
|
486
|
+
if (!downs.has(e.pointerId)) return
|
|
487
|
+
// The brackets close with no velocity and no swipe: the pointer
|
|
488
|
+
// ended without its lift.
|
|
489
|
+
transform.handlers.onPointerCancel(e)
|
|
490
|
+
released = null
|
|
491
|
+
lifted(e)
|
|
492
|
+
hold.cancelled(e)
|
|
493
|
+
taps.cancelled(e)
|
|
494
|
+
if (opening && opening.id === e.pointerId) opening = null
|
|
495
|
+
},
|
|
484
496
|
onPointerLeave() {},
|
|
485
497
|
onWheel(e) {
|
|
486
498
|
if (e.deltaY === 0) return
|
package/src/long-press.ts
CHANGED
|
@@ -36,7 +36,8 @@ export interface HoldTimerOptions {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** The arm / slop / timer machine under a long-press: one pointer at a
|
|
39
|
-
* time, disarmed by travel, by a lift, by a
|
|
39
|
+
* time, disarmed by travel, by a lift, by a cancelled pointer, by a second
|
|
40
|
+
* pointer or by cancel(). */
|
|
40
41
|
export function createHoldTimer(options: HoldTimerOptions) {
|
|
41
42
|
let armed: { id: number; at: PointerPoint; mods: { shiftKey: boolean; ctrlKey: boolean; altKey: boolean; metaKey: boolean; button?: number } } | null = null
|
|
42
43
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -69,6 +70,9 @@ export function createHoldTimer(options: HoldTimerOptions) {
|
|
|
69
70
|
up(e: PointerEvent) {
|
|
70
71
|
if (armed && armed.id === e.pointerId) disarm()
|
|
71
72
|
},
|
|
73
|
+
cancelled(e: PointerEvent) {
|
|
74
|
+
if (armed && armed.id === e.pointerId) disarm()
|
|
75
|
+
},
|
|
72
76
|
cancel: disarm,
|
|
73
77
|
}
|
|
74
78
|
}
|
|
@@ -84,6 +88,10 @@ export interface LongPressOptions {
|
|
|
84
88
|
onLongPressMove?: (dx: number, dy: number) => void
|
|
85
89
|
/** The lift after a fire. */
|
|
86
90
|
onLongPressEnd?: () => void
|
|
91
|
+
/** A fired long press ended without its lift: the system cancelled the
|
|
92
|
+
* pointer (onPointerCancel), or another recognizer took it in the
|
|
93
|
+
* arena. Fired instead of onLongPressEnd. */
|
|
94
|
+
onLongPressCancel?: () => void
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
export function createLongPress(options: LongPressOptions) {
|
|
@@ -99,16 +107,26 @@ export function createLongPress(options: LongPressOptions) {
|
|
|
99
107
|
options.onLongPress?.(at)
|
|
100
108
|
},
|
|
101
109
|
})
|
|
102
|
-
let
|
|
103
|
-
hold.cancel()
|
|
110
|
+
let release = () => {
|
|
104
111
|
if (fired !== null) {
|
|
105
112
|
arena.release(fired, owner)
|
|
106
113
|
fired = null
|
|
107
114
|
last = null
|
|
108
115
|
}
|
|
109
116
|
}
|
|
117
|
+
// The press ends without its lift, whatever took the pointer: the arena
|
|
118
|
+
// (cancel) or the system (onPointerCancel). An unmount releases alone.
|
|
119
|
+
let cancel = () => {
|
|
120
|
+
hold.cancel()
|
|
121
|
+
let won = fired !== null
|
|
122
|
+
release()
|
|
123
|
+
if (won) options.onLongPressCancel?.()
|
|
124
|
+
}
|
|
110
125
|
let owner: ArenaOwner = { cancel }
|
|
111
|
-
onSettled(() =>
|
|
126
|
+
onSettled(() => () => {
|
|
127
|
+
hold.cancel()
|
|
128
|
+
release()
|
|
129
|
+
})
|
|
112
130
|
|
|
113
131
|
let handlers = {
|
|
114
132
|
onPointerDown: (e: PointerEvent) => {
|
|
@@ -126,12 +144,16 @@ export function createLongPress(options: LongPressOptions) {
|
|
|
126
144
|
},
|
|
127
145
|
onPointerUp: (e: PointerEvent) => {
|
|
128
146
|
if (fired === e.pointerId) {
|
|
129
|
-
|
|
147
|
+
release()
|
|
130
148
|
options.onLongPressEnd?.()
|
|
131
149
|
return
|
|
132
150
|
}
|
|
133
151
|
hold.up(e)
|
|
134
152
|
},
|
|
153
|
+
onPointerCancel: (e: PointerEvent) => {
|
|
154
|
+
if (fired === e.pointerId) cancel()
|
|
155
|
+
else hold.cancelled(e)
|
|
156
|
+
},
|
|
135
157
|
}
|
|
136
158
|
return { handlers, cancel }
|
|
137
159
|
}
|
package/src/pan.ts
CHANGED
|
@@ -35,6 +35,13 @@ export interface PanOptions {
|
|
|
35
35
|
* finger rested before lifting or moved under FLING_MIN_VELOCITY.
|
|
36
36
|
*/
|
|
37
37
|
onPanEnd?: (velocity: Velocity) => void
|
|
38
|
+
/**
|
|
39
|
+
* The pan ended without its result: the system cancelled the pointer
|
|
40
|
+
* (onPointerCancel), or another recognizer took it in the arena. Fired
|
|
41
|
+
* instead of onPanEnd, only after onPanStart; whoever animates on
|
|
42
|
+
* settles here, nothing flings.
|
|
43
|
+
*/
|
|
44
|
+
onPanCancel?: () => void
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
// The pan recognizer: turns a drag into a movement-delta stream. On a down it
|
|
@@ -49,8 +56,9 @@ export interface PanOptions {
|
|
|
49
56
|
// fling minimum) for momentum. Moves and the up arrive on the frozen down
|
|
50
57
|
// path, so an active pan keeps streaming when the pointer leaves the node
|
|
51
58
|
// or the window.
|
|
52
|
-
// cancel() is the external-cancel hook; it
|
|
53
|
-
//
|
|
59
|
+
// cancel() is the external-cancel hook, the arena's; it and a pointer cancel
|
|
60
|
+
// end an active pan with onPanCancel instead of onPanEnd. Options are read
|
|
61
|
+
// at event time. Single-pointer by design; for
|
|
54
62
|
// multi-pointer pinch/rotate compose createTransform instead.
|
|
55
63
|
//
|
|
56
64
|
// Frames: the slop is finger travel, so it is measured in window pixels
|
|
@@ -87,7 +95,13 @@ export function createPan(options: PanOptions) {
|
|
|
87
95
|
armed = null
|
|
88
96
|
origin = null
|
|
89
97
|
}
|
|
90
|
-
|
|
98
|
+
// The pan ends without its result, whatever took the pointer: the arena
|
|
99
|
+
// (cancel) or the system (onPointerCancel). An unmount resets alone.
|
|
100
|
+
let cancel = () => {
|
|
101
|
+
let started = active != null
|
|
102
|
+
reset()
|
|
103
|
+
if (started) options.onPanCancel?.()
|
|
104
|
+
}
|
|
91
105
|
let owner = { cancel }
|
|
92
106
|
|
|
93
107
|
// An unmount mid-drag must not leave a resolved claim behind.
|
|
@@ -134,6 +148,10 @@ export function createPan(options: PanOptions) {
|
|
|
134
148
|
reset()
|
|
135
149
|
}
|
|
136
150
|
},
|
|
151
|
+
onPointerCancel: (e: PointerEvent) => {
|
|
152
|
+
if (active === e.pointerId) cancel()
|
|
153
|
+
else if (armed === e.pointerId) reset()
|
|
154
|
+
},
|
|
137
155
|
}
|
|
138
156
|
|
|
139
157
|
return { handlers, cancel }
|
package/src/renderer.ts
CHANGED
|
@@ -86,7 +86,7 @@ function flushDestroy(): void {
|
|
|
86
86
|
function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
87
87
|
if (!node || !parent) return
|
|
88
88
|
|
|
89
|
-
// console.debug("[
|
|
89
|
+
// console.debug("[sol] removeNode", parent.id, node.id)
|
|
90
90
|
|
|
91
91
|
// Update JS tree references
|
|
92
92
|
let index = parent.children.indexOf(node)
|
|
@@ -113,7 +113,7 @@ function removeNode(parent: ProxyNode, node: ProxyNode): void {
|
|
|
113
113
|
// bookkeeping on the hot create/insert paths, orphans are derived from the
|
|
114
114
|
// proxy map itself: parentless, not the window root, and not awaiting the
|
|
115
115
|
// destroy sweep. window.ts runs the scan on a rendered frame every few
|
|
116
|
-
// seconds; dev bundles only (
|
|
116
|
+
// seconds; dev bundles only (sol always defines import.meta.env.DEV, so a
|
|
117
117
|
// production bundle folds the check into a constant early return).
|
|
118
118
|
const SENTINEL_INTERVAL_MS = 5000
|
|
119
119
|
let sentinelDue = 0
|
|
@@ -208,7 +208,7 @@ function routeFor(name: string): number {
|
|
|
208
208
|
function applyProp<T>(node: ProxyNode, name: string, value: T): void {
|
|
209
209
|
if (!node) return
|
|
210
210
|
|
|
211
|
-
// console.debug("[
|
|
211
|
+
// console.debug("[sol] applyProp", node.id, name, value)
|
|
212
212
|
|
|
213
213
|
switch (routeFor(name)) {
|
|
214
214
|
case ROUTE_EVENT:
|
|
@@ -234,7 +234,7 @@ let renderer = createRenderer<ProxyNode>({
|
|
|
234
234
|
createElement: (elementType: string, props?: Record<string, any>): ProxyNode => {
|
|
235
235
|
let proxy = createProxyNode(elementType)
|
|
236
236
|
|
|
237
|
-
// console.debug("[
|
|
237
|
+
// console.debug("[sol] createElement", proxy.id, elementType)
|
|
238
238
|
|
|
239
239
|
if (elementType === "window") tree.createRoot(proxy.id)
|
|
240
240
|
else tree.createNode(proxy.id, elementType)
|
|
@@ -257,20 +257,20 @@ let renderer = createRenderer<ProxyNode>({
|
|
|
257
257
|
// no element form of its own (the DOM's "#text" node name).
|
|
258
258
|
createTextNode: (value: string): ProxyNode => {
|
|
259
259
|
let proxy = createProxyNode("#text")
|
|
260
|
-
// console.debug("[
|
|
260
|
+
// console.debug("[sol] createTextNode", proxy.id, value)
|
|
261
261
|
tree.createNode(proxy.id, "#text")
|
|
262
262
|
tree.setProperty(proxy.id, "text", "" + value)
|
|
263
263
|
return proxy
|
|
264
264
|
},
|
|
265
265
|
|
|
266
266
|
replaceText: (node: ProxyNode, value: string): void => {
|
|
267
|
-
// console.debug("[
|
|
267
|
+
// console.debug("[sol] replaceText", node.id, value)
|
|
268
268
|
tree.setProperty(node.id, "text", "" + value)
|
|
269
269
|
},
|
|
270
270
|
|
|
271
271
|
isTextNode: (node: ProxyNode): boolean => node?.elementType === "#text",
|
|
272
272
|
setProperty: <T>(node: ProxyNode, name: string, value: T): void => {
|
|
273
|
-
// console.debug("[
|
|
273
|
+
// console.debug("[sol] setProperty", node.id, name, value)
|
|
274
274
|
applyProp(node, name, value)
|
|
275
275
|
},
|
|
276
276
|
|
|
@@ -294,7 +294,7 @@ let renderer = createRenderer<ProxyNode>({
|
|
|
294
294
|
pendingDestroy.delete(node.id)
|
|
295
295
|
|
|
296
296
|
if (parent) {
|
|
297
|
-
// console.debug("[
|
|
297
|
+
// console.debug("[sol] insertNode", parent.id, node.id, anchor?.id ?? "")
|
|
298
298
|
|
|
299
299
|
// Native first: the tree refuses a laid-out element under a d-* parent
|
|
300
300
|
// (it throws, naming both tags), and the mirror must not record a child
|
package/src/runtime-modules.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Lattice runner builtin modules (the "
|
|
1
|
+
// Lattice runner builtin modules (the "sol:*" surface). These are ambient
|
|
2
2
|
// module declarations, so they live in this global-script .d.ts (no top-level
|
|
3
3
|
// import/export) rather than in types.d.ts: an ambient `declare module` only
|
|
4
4
|
// becomes globally visible to consumers from a non-module declaration file.
|
|
@@ -26,7 +26,8 @@ declare module "*.frag" {
|
|
|
26
26
|
|
|
27
27
|
// Binary asset imports: `import data from "./pic.png" with { type: "binary" }`.
|
|
28
28
|
// The bundler inlines the file's bytes as a Uint8Array (see packages/cli
|
|
29
|
-
// bundler `inlineImport`); feed it straight into createImage/decodeImage
|
|
29
|
+
// bundler `inlineImport`); feed it straight into createImage/decodeImage, or a
|
|
30
|
+
// `.wasm` into `new Module` from flux:wasm.
|
|
30
31
|
declare module "*.png" {
|
|
31
32
|
const bytes: Uint8Array
|
|
32
33
|
export default bytes
|
|
@@ -51,7 +52,11 @@ declare module "*.glb" {
|
|
|
51
52
|
const bytes: Uint8Array
|
|
52
53
|
export default bytes
|
|
53
54
|
}
|
|
54
|
-
declare module "*.
|
|
55
|
+
declare module "*.sol3m" {
|
|
56
|
+
const bytes: Uint8Array
|
|
57
|
+
export default bytes
|
|
58
|
+
}
|
|
59
|
+
declare module "*.wasm" {
|
|
55
60
|
const bytes: Uint8Array
|
|
56
61
|
export default bytes
|
|
57
62
|
}
|
|
@@ -63,13 +68,13 @@ declare module "*.srtm" {
|
|
|
63
68
|
// pointer moves have dispatched, every pointer the same age - multi-pointer
|
|
64
69
|
// recognizers measure there; its `timeStamp` is theirs), and "render" (the
|
|
65
70
|
// per-frame signal).
|
|
66
|
-
declare module "
|
|
71
|
+
declare module "sol:events" {
|
|
67
72
|
export function on(event: string, callback: (data: any) => void): () => void
|
|
68
73
|
export function once(event: string, callback: (data: any) => void): () => void
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
// The running application's own surface (lattice), present in every build.
|
|
72
|
-
declare module "
|
|
77
|
+
declare module "sol:app" {
|
|
73
78
|
/**
|
|
74
79
|
* End the current app now, without running the quit hooks: back to the
|
|
75
80
|
* player in a dev client, quit when standalone or at the player root (on
|
|
@@ -100,7 +105,7 @@ declare module "srt:app" {
|
|
|
100
105
|
|
|
101
106
|
// Dev-server control surface (lattice). Present only in dev/go builds; in other
|
|
102
107
|
// builds `available` is false and the functions are no-ops.
|
|
103
|
-
declare module "
|
|
108
|
+
declare module "sol:dev" {
|
|
104
109
|
export const available: boolean
|
|
105
110
|
export const canDiscover: boolean
|
|
106
111
|
export const recents: string[]
|
|
@@ -139,7 +144,7 @@ declare module "srt:dev" {
|
|
|
139
144
|
// Installed-app management (lattice), the player's surface over the client's
|
|
140
145
|
// version store. Present only in go/dev client builds; elsewhere `available`
|
|
141
146
|
// is false, `list` returns [], and launch/remove are no-ops.
|
|
142
|
-
declare module "
|
|
147
|
+
declare module "sol:apps" {
|
|
143
148
|
export const available: boolean
|
|
144
149
|
/**
|
|
145
150
|
* An installed app: id, display name (the installed manifest's displayName,
|
|
@@ -233,16 +238,16 @@ declare module "srt:apps" {
|
|
|
233
238
|
// submits the display list. To schedule a future frame instead, use
|
|
234
239
|
// requestFrame() from "flux:rendertree". The tree-building surface itself is
|
|
235
240
|
// "flux:rendertree" (from @solidrt/flux-types).
|
|
236
|
-
declare module "
|
|
241
|
+
declare module "sol:render" {
|
|
237
242
|
export function renderFrame(): void
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
// The engine verbs of an app test (lattice, dev client only), which
|
|
241
246
|
// @solidrt/test builds its surface on; tests import that, not this.
|
|
242
|
-
// The verbs work only in an engine a test host built (`
|
|
247
|
+
// The verbs work only in an engine a test host built (`sol test`) and throw
|
|
243
248
|
// anywhere else. App time there is frame / frameRate and nothing else: no
|
|
244
249
|
// frame runs unless one is asked for.
|
|
245
|
-
declare module "
|
|
250
|
+
declare module "sol:test" {
|
|
246
251
|
/**
|
|
247
252
|
* Runs one frame: timers due by its time fire, then frame callbacks and
|
|
248
253
|
* the flush, then the draw if anything demanded one. Fulfills once the
|
package/src/swipe.ts
CHANGED
|
@@ -67,6 +67,10 @@ export interface SwipeOptions {
|
|
|
67
67
|
onSwipe?: (direction: SwipeDirection, velocity: Velocity) => void
|
|
68
68
|
/** Every lift of an active swipe, qualified or not. */
|
|
69
69
|
onSwipeEnd?: (velocity: Velocity) => void
|
|
70
|
+
/** An active swipe ended without its lift: the system cancelled the
|
|
71
|
+
* pointer (onPointerCancel), or another recognizer took it in the
|
|
72
|
+
* arena. Fired instead of onSwipe and onSwipeEnd. */
|
|
73
|
+
onSwipeCancel?: () => void
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
export function createSwipe(options: SwipeOptions) {
|
|
@@ -97,6 +101,11 @@ export function createSwipe(options: SwipeOptions) {
|
|
|
97
101
|
if (direction) options.onSwipe?.(direction, velocity)
|
|
98
102
|
options.onSwipeEnd?.(velocity)
|
|
99
103
|
},
|
|
104
|
+
onPanCancel: () => {
|
|
105
|
+
down = null
|
|
106
|
+
lift = null
|
|
107
|
+
options.onSwipeCancel?.()
|
|
108
|
+
},
|
|
100
109
|
})
|
|
101
110
|
|
|
102
111
|
let handlers = {
|
|
@@ -120,6 +129,13 @@ export function createSwipe(options: SwipeOptions) {
|
|
|
120
129
|
lift = null
|
|
121
130
|
}
|
|
122
131
|
},
|
|
132
|
+
onPointerCancel: (e: PointerEvent) => {
|
|
133
|
+
pan.handlers.onPointerCancel(e)
|
|
134
|
+
if (down && down.id === e.pointerId) {
|
|
135
|
+
down = null
|
|
136
|
+
lift = null
|
|
137
|
+
}
|
|
138
|
+
},
|
|
123
139
|
}
|
|
124
140
|
let cancel = () => {
|
|
125
141
|
pan.cancel()
|
package/src/textures.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the runtime: this entry imports nothing the runtime provides, so a bake
|
|
3
3
|
// script under bun and an extension's pure modules use it as it is
|
|
4
4
|
// (tests/textures.test.ts imports it on the bare flux binary and fails the
|
|
5
|
-
// moment a gui or
|
|
5
|
+
// moment a gui or sol: import creeps in). Any extension that bakes or loads textures
|
|
6
6
|
// builds on this, the first-party 3d one included.
|
|
7
7
|
//
|
|
8
8
|
// isKtx2 tells a compressed texture file from a PNG or JPEG.
|
package/src/transform.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { onSettled } from "@solidjs/signals"
|
|
2
|
-
import { on } from "
|
|
2
|
+
import { on } from "sol:events"
|
|
3
3
|
import type { PointerEvent } from "./types"
|
|
4
4
|
import { arena, type ArenaOwner } from "./arena"
|
|
5
5
|
import { createVelocityTracker, flingVelocity } from "./velocity"
|
|
@@ -59,6 +59,10 @@ export interface TransformOptions {
|
|
|
59
59
|
* parent-frame pixels per second (velocity.ts; zero after a rest or
|
|
60
60
|
* under the fling minimum). */
|
|
61
61
|
onTransformEnd?: (velocity: Velocity) => void
|
|
62
|
+
/** The gesture ended without its result: the system cancelled a finger
|
|
63
|
+
* (onPointerCancel), or another recognizer took the pointers in the
|
|
64
|
+
* arena. Fired instead of onTransformEnd, only after onTransformStart. */
|
|
65
|
+
onTransformCancel?: () => void
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
// The merged transform recognizer: pan + pinch + rotate as ONE gesture over
|
|
@@ -116,7 +120,8 @@ export interface TransformOptions {
|
|
|
116
120
|
// configuration, so the change itself never emits a jump delta. The gesture
|
|
117
121
|
// ends when the last finger lifts. Moves and ups arrive on the frozen down
|
|
118
122
|
// path, so an active transform survives leaving the node or the window.
|
|
119
|
-
// cancel() is the external-cancel hook; it
|
|
123
|
+
// cancel() is the external-cancel hook, the arena's; it and a cancelled
|
|
124
|
+
// finger end an active gesture with onTransformCancel instead of
|
|
120
125
|
// onTransformEnd. Options are read at event time.
|
|
121
126
|
//
|
|
122
127
|
// Frames: the slop, the span filter and its rate gates are finger-travel
|
|
@@ -206,7 +211,11 @@ export function createTransform(options: TransformOptions) {
|
|
|
206
211
|
rebase = false
|
|
207
212
|
tracker.reset()
|
|
208
213
|
}
|
|
209
|
-
let cancel =
|
|
214
|
+
let cancel = () => {
|
|
215
|
+
let started = active
|
|
216
|
+
reset()
|
|
217
|
+
if (started) options.onTransformCancel?.()
|
|
218
|
+
}
|
|
210
219
|
let owner: ArenaOwner = { cancel }
|
|
211
220
|
|
|
212
221
|
// The per-frame measure point: runs at the pointerFrame terminator, when
|
|
@@ -355,6 +364,17 @@ export function createTransform(options: TransformOptions) {
|
|
|
355
364
|
pointers.delete(e.pointerId)
|
|
356
365
|
ref = pointers.size > 0 ? measure() : null
|
|
357
366
|
},
|
|
367
|
+
onPointerCancel: (e: PointerEvent) => {
|
|
368
|
+
if (!pointers.has(e.pointerId)) return
|
|
369
|
+
// The system cancels the gesture, not a finger: every platform
|
|
370
|
+
// cancels all of them together, so the whole gesture ends.
|
|
371
|
+
if (active) {
|
|
372
|
+
cancel()
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
pointers.delete(e.pointerId)
|
|
376
|
+
ref = pointers.size > 0 ? measure() : null
|
|
377
|
+
},
|
|
358
378
|
}
|
|
359
379
|
|
|
360
380
|
return { handlers, cancel }
|
package/src/types.d.ts
CHANGED
|
@@ -9,14 +9,14 @@ import type { Element } from "solid-js"
|
|
|
9
9
|
|
|
10
10
|
export type { TextInputHints }
|
|
11
11
|
|
|
12
|
-
// The "
|
|
12
|
+
// The "sol:*" lattice runner modules are declared in ./runtime-modules.d.ts
|
|
13
13
|
// (referenced above) - ambient `declare module` only reaches consumers from a
|
|
14
14
|
// non-module declaration file, and this file is a module.
|
|
15
15
|
|
|
16
16
|
declare global {
|
|
17
17
|
interface ImportMeta {
|
|
18
18
|
/**
|
|
19
|
-
* Build-mode constants, substituted textually by the
|
|
19
|
+
* Build-mode constants, substituted textually by the sol bundler (Vite
|
|
20
20
|
* vocabulary). `DEV` is true in dev bundles and false in production
|
|
21
21
|
* bundles, where the substituted constant lets the minifier fold
|
|
22
22
|
* dev-only code away entirely.
|
|
@@ -278,7 +278,8 @@ export interface TransformProps {
|
|
|
278
278
|
|
|
279
279
|
// Window-relative pointer coordinates are reported as clientX/clientY (matching
|
|
280
280
|
// the DOM MouseEvent). pointerType distinguishes mouse from touch; button is the
|
|
281
|
-
// pressed button on down/up (0 = primary)
|
|
281
|
+
// pressed button on down/up (0 = primary) and absent on a cancel; the modifier
|
|
282
|
+
// flags mirror the DOM.
|
|
282
283
|
export interface PointerEvent {
|
|
283
284
|
/**
|
|
284
285
|
* When the event happened, in milliseconds. Every input event carries one,
|
|
@@ -291,7 +292,7 @@ export interface PointerEvent {
|
|
|
291
292
|
* pointer, so the real move after it can carry an earlier time. Use it
|
|
292
293
|
* where you would reach for `performance.now()` in a handler: that one
|
|
293
294
|
* measures when the handler ran, not when the input happened, and it does
|
|
294
|
-
* not follow the dev clock, `
|
|
295
|
+
* not follow the dev clock, `sol render` or a test. The origin is
|
|
295
296
|
* arbitrary (compare stamps with each other, not with `performance.now()`,
|
|
296
297
|
* a timer or the `onFrame` tick).
|
|
297
298
|
*/
|
|
@@ -408,6 +409,15 @@ export interface NodeProps {
|
|
|
408
409
|
export interface PointerProps extends NodeProps {
|
|
409
410
|
onPointerDown?: (event: PointerEvent) => void
|
|
410
411
|
onPointerUp?: (event: PointerEvent) => void
|
|
412
|
+
/**
|
|
413
|
+
* The system took the pointer away: a system gesture (the back swipe,
|
|
414
|
+
* the notification shade), palm rejection, the window losing the touch.
|
|
415
|
+
* The last event for that pointer, and no up follows it, so a press
|
|
416
|
+
* ends without firing and a drag ends without a fling. Routed along the
|
|
417
|
+
* pressed path like an up, with the pointer's last known position and
|
|
418
|
+
* no `button`.
|
|
419
|
+
*/
|
|
420
|
+
onPointerCancel?: (event: PointerEvent) => void
|
|
411
421
|
onPointerMove?: (event: PointerEvent) => void
|
|
412
422
|
onPointerEnter?: (event: PointerEvent) => void
|
|
413
423
|
onPointerLeave?: (event: PointerEvent) => void
|
package/src/window.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, flush } from "@solidjs/signals"
|
|
2
2
|
import { requestFrame, setPointerLock } from "flux:rendertree"
|
|
3
|
-
import { renderFrame } from "
|
|
4
|
-
import { on, once } from "
|
|
5
|
-
import { exit as nativeExit, background as nativeBackground, registerProtocolHandler as nativeRegisterProtocolHandler } from "
|
|
3
|
+
import { renderFrame } from "sol:render"
|
|
4
|
+
import { on, once } from "sol:events"
|
|
5
|
+
import { exit as nativeExit, background as nativeBackground, registerProtocolHandler as nativeRegisterProtocolHandler } from "sol:app"
|
|
6
6
|
import { platform } from "flux:process"
|
|
7
7
|
import { getEventHandler, focusedNode, setFocus, activateTextInput, setInterestRoot } from "./core"
|
|
8
8
|
import { scanForOrphans, getNodePath } from "./renderer"
|
|
@@ -269,7 +269,7 @@ export function onFrame(fn: (tick: number, frame: number, rate: number) => void,
|
|
|
269
269
|
* between frames (an event handler, a timer) it is the latest frame's. It
|
|
270
270
|
* is 0 before the first frame. Not reactive, and reading it requests no
|
|
271
271
|
* frame. Use it to time things against the app's own clock from code that
|
|
272
|
-
* has no tick in hand: it follows the dev tools' clock control, `
|
|
272
|
+
* has no tick in hand: it follows the dev tools' clock control, `sol
|
|
273
273
|
* render` and a test, where performance.now() does not.
|
|
274
274
|
*/
|
|
275
275
|
export function frameTime(): number {
|
|
@@ -553,6 +553,7 @@ export function attachWindow(nodeId: number) {
|
|
|
553
553
|
let unsubscribe: () => void = null!
|
|
554
554
|
let unsubDown: () => void = null!
|
|
555
555
|
let unsubUp: () => void = null!
|
|
556
|
+
let unsubCancel: () => void = null!
|
|
556
557
|
let unsubMove: () => void = null!
|
|
557
558
|
let unsubEnter: () => void = null!
|
|
558
559
|
let unsubLeave: () => void = null!
|
|
@@ -664,6 +665,10 @@ export function attachWindow(nodeId: number) {
|
|
|
664
665
|
bubble(raw, "onPointerUp")
|
|
665
666
|
})
|
|
666
667
|
|
|
668
|
+
unsubCancel = on("pointerCancel", (raw: RawPointer) => {
|
|
669
|
+
bubble(raw, "onPointerCancel")
|
|
670
|
+
})
|
|
671
|
+
|
|
667
672
|
unsubMove = on("pointerMove", (raw: RawPointer) => {
|
|
668
673
|
bubble(raw, "onPointerMove")
|
|
669
674
|
})
|
|
@@ -761,6 +766,7 @@ export function attachWindow(nodeId: number) {
|
|
|
761
766
|
if (unsubscribe) unsubscribe()
|
|
762
767
|
if (unsubDown) unsubDown()
|
|
763
768
|
if (unsubUp) unsubUp()
|
|
769
|
+
if (unsubCancel) unsubCancel()
|
|
764
770
|
if (unsubMove) unsubMove()
|
|
765
771
|
if (unsubEnter) unsubEnter()
|
|
766
772
|
if (unsubLeave) unsubLeave()
|