@solidrt/core 0.0.66 → 0.0.68
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 +14 -4
- package/agents/performance.md +24 -22
- package/docs/index.md +7 -3
- package/package.json +3 -3
- package/src/core.ts +16 -2
- package/src/double-tap.ts +9 -2
- package/src/gpu.ts +9 -18
- package/src/index.ts +3 -3
- package/src/input-pointer.ts +12 -0
- package/src/long-press.ts +27 -5
- package/src/pan.ts +21 -3
- package/src/runtime-modules.d.ts +14 -1
- package/src/shaders.ts +82 -0
- package/src/swipe.ts +16 -0
- package/src/transform.ts +22 -2
- package/src/types.d.ts +31 -3
- package/src/window.ts +98 -1
package/AGENTS.md
CHANGED
|
@@ -138,9 +138,9 @@ that reads exactly like Solid fallout.
|
|
|
138
138
|
window (message, stack, a Reset button that retries the failed
|
|
139
139
|
computations) and logs `Uncaught error`. `<Errored>` gives a subtree its
|
|
140
140
|
own in-place fallback.
|
|
141
|
-
An `onFrame` callback is its own boundary,
|
|
142
|
-
is caught and logged (`Error in onFrame
|
|
143
|
-
frame), the subscription stays, and THE REST OF THAT CALLBACK IS
|
|
141
|
+
An `onFrame` (or `onBeforeRender`) callback is its own boundary,
|
|
142
|
+
event-listener style: a throw is caught and logged (`Error in onFrame
|
|
143
|
+
callback`, repeating every frame), the subscription stays, and THE REST OF THAT CALLBACK IS
|
|
144
144
|
ABANDONED - what it wrote before the throw lands, what came after never
|
|
145
145
|
does, frame after frame while the app keeps presenting. A partial frame
|
|
146
146
|
is therefore a live possibility: every sprite drawn after the throwing
|
|
@@ -613,7 +613,17 @@ that reads exactly like Solid fallout.
|
|
|
613
613
|
`onFrame(fn, { demand: false })` is the same hook without the standing
|
|
614
614
|
frame request: it runs on every frame the runtime delivers and the app
|
|
615
615
|
presents only when the body writes, for a loop that follows time but
|
|
616
|
-
seldom changes the picture. `
|
|
616
|
+
seldom changes the picture. `onBeforeRender(fn)` is the frame's late
|
|
617
|
+
update: it runs after every onFrame callback and the reactive flush,
|
|
618
|
+
when the frame's state is final, for what must see everything the frame
|
|
619
|
+
wrote and write once more (a camera follow, a fit to a measured box;
|
|
620
|
+
Unity's LateUpdate); its signal writes are flushed before the paint
|
|
621
|
+
too, the way onLayout's are. `onBeforeRender(fn, {
|
|
622
|
+
publish: true })` runs after those, and again after a frame's onLayout
|
|
623
|
+
handlers: the pass where an extension hands the engine what the frame
|
|
624
|
+
wrote into its mirrors (a scene its record buffers and lights, a sprite
|
|
625
|
+
layer its style and count), so a write made anywhere in the frame's JS
|
|
626
|
+
is in that frame's picture, never the next. `frameTime()` is the current frame's `tick`
|
|
617
627
|
read from anywhere (not reactive, requests nothing): the app's own
|
|
618
628
|
clock for code with no tick in hand, where `performance.now()` would
|
|
619
629
|
ignore the dev clock, `sol render` and tests. A JS
|
package/agents/performance.md
CHANGED
|
@@ -151,27 +151,29 @@ Rules, in order of leverage:
|
|
|
151
151
|
snapshot instead of re-rasterizing. A window shader's output is invisible
|
|
152
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.
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.68",
|
|
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.68",
|
|
37
|
+
"@solidrt/test": "0.0.68"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@solidjs/signals": "2.0.0-rc.13",
|
package/src/core.ts
CHANGED
|
@@ -8,8 +8,9 @@ let handlers = new Map<number, Map<string, Function>>()
|
|
|
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>()
|
|
@@ -388,6 +390,18 @@ export function prepareText(text: string, options?: tree.MeasureTextOptions): tr
|
|
|
388
390
|
return tree.prepareText(text, options)
|
|
389
391
|
}
|
|
390
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Makes the glyph cells of printable ASCII for these font styles ahead of
|
|
395
|
+
* their first use, off the frame: call it at startup with the type scale
|
|
396
|
+
* the app draws in (the components' Window does it for the theme's), so a
|
|
397
|
+
* screen's first paint in any of them never waits for cells. Glyphs outside
|
|
398
|
+
* ASCII are made at first use. Each entry names a font as a <text> would;
|
|
399
|
+
* line height and maxLines are ignored.
|
|
400
|
+
*/
|
|
401
|
+
export function warmText(styles: tree.MeasureTextOptions[]): void {
|
|
402
|
+
tree.warmText(styles)
|
|
403
|
+
}
|
|
404
|
+
|
|
391
405
|
/** One laid-out line from layoutNextLine. */
|
|
392
406
|
export type TextLine = {
|
|
393
407
|
/** Unit range [from, to) into prepared.units. */
|
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/gpu.ts
CHANGED
|
@@ -97,7 +97,7 @@ export type { TextureFormat } from "flux:gpu"
|
|
|
97
97
|
// runtime, distinct types to the checker, so a cross-space slip like
|
|
98
98
|
// destroyBuffer(textureId) fails to compile. Exported so apps can annotate
|
|
99
99
|
// storage (`let ids: TextureId[]`).
|
|
100
|
-
export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
|
|
100
|
+
export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId, TextureRect } from "flux:gpu"
|
|
101
101
|
|
|
102
102
|
// Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
|
|
103
103
|
// -- need not import flux directly: destroyTexture for the manual-cleanup path
|
|
@@ -196,22 +196,11 @@ export {
|
|
|
196
196
|
programAttributes,
|
|
197
197
|
} from "flux:gpu"
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
* grammars look for).
|
|
205
|
-
*
|
|
206
|
-
* Interpolated values are stringified verbatim, with no GLSL-aware formatting:
|
|
207
|
-
* `${2}` splices in the int literal `2`, which will not assign to a float. Pass
|
|
208
|
-
* anything that varies as a uniform instead of building it into the source.
|
|
209
|
-
*
|
|
210
|
-
* Raw semantics, so backslashes reach the compiler as written: the GLSL
|
|
211
|
-
* preprocessor continues a line with a trailing `\`, which a cooked template
|
|
212
|
-
* would reject as an invalid escape and silently pass through as `undefined`.
|
|
213
|
-
*/
|
|
214
|
-
export let glsl = String.raw
|
|
199
|
+
// The tag, and the screen-size clamp both sprite renderers share: the
|
|
200
|
+
// GLSL function a vertex stage pastes in, its JS twin for the picker, and
|
|
201
|
+
// the validation of a [minScreenPx, maxScreenPx] pair. Both live in the
|
|
202
|
+
// pure shaders.ts so headless tests and pure modules can reach them.
|
|
203
|
+
export { checkScreenSize, glsl, SCREEN_SIZE_GLSL, screenSizeScale } from "./shaders.ts"
|
|
215
204
|
|
|
216
205
|
// captureSnapshot renders a node to pixels and readTexture reads any
|
|
217
206
|
// texture's bytes back; both resolve the same { width, height, data } shape.
|
|
@@ -293,7 +282,9 @@ export function createCubeTexture(
|
|
|
293
282
|
|
|
294
283
|
/**
|
|
295
284
|
* Creates a GPU texture you intend to update over time: seed it with `data`,
|
|
296
|
-
* then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels
|
|
285
|
+
* then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels, or
|
|
286
|
+
* `uploadTexture(id, cell, { x, y, width, height })` to write one rect of it
|
|
287
|
+
* (a runtime atlas placing a cell without re-sending the sheet).
|
|
297
288
|
* `data` must hold at least `width * height` pixels at the declared format's
|
|
298
289
|
* size, in the view type matching the format (Uint8Array for "rgba8"/
|
|
299
290
|
* "rgba8-srgb"/"r8", Float32Array for "r32f"/"rgba32f"/"rgba16f"); it may
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, getLayoutBox, snapshotTexture, onPointerMove } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, warmText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, getLayoutBox, snapshotTexture, onPointerMove } from "./core"
|
|
3
3
|
export type { BoundingBox, GlobalPointerEvent, TextLine } from "./core"
|
|
4
4
|
export { parseColor, mixColors, withAlpha, brightness, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
6
|
-
export { onFrame, frameTime, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, onLink, onSuspend, onQuit, exit, background, registerProtocolHandler } from "./window"
|
|
7
|
-
export type { BackEvent, FrameOptions, LifecycleHandler } from "./window"
|
|
6
|
+
export { onFrame, frameTime, onBeforeRender, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, onLink, onSuspend, onQuit, exit, background, registerProtocolHandler } from "./window"
|
|
7
|
+
export type { BackEvent, BeforeRenderOptions, FrameOptions, LifecycleHandler } from "./window"
|
|
8
8
|
export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight, lockPointer, pointerLocked } from "./window"
|
|
9
9
|
export { env } from "./environment"
|
|
10
10
|
export type { InputDevices, SystemTheme, Orientation, Visibility, Launch } from "./environment"
|
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/runtime-modules.d.ts
CHANGED
|
@@ -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
|
|
@@ -55,6 +56,10 @@ declare module "*.sol3m" {
|
|
|
55
56
|
const bytes: Uint8Array
|
|
56
57
|
export default bytes
|
|
57
58
|
}
|
|
59
|
+
declare module "*.wasm" {
|
|
60
|
+
const bytes: Uint8Array
|
|
61
|
+
export default bytes
|
|
62
|
+
}
|
|
58
63
|
|
|
59
64
|
// UI event bus (lattice), provided by the runtime as a builtin module.
|
|
60
65
|
// on/once return an unsubscribe function. Notable events: the routed pointer
|
|
@@ -301,4 +306,12 @@ declare module "sol:test" {
|
|
|
301
306
|
* frame runs and no app time passes.
|
|
302
307
|
*/
|
|
303
308
|
export function capture(node: number): { width: number; height: number; data: Uint8Array }
|
|
309
|
+
/**
|
|
310
|
+
* Run one frame with a capture of `node` queued ahead of it, so the
|
|
311
|
+
* frame's own paint services it: fulfills, once the frame has run, with
|
|
312
|
+
* what that frame drew of the node (the shape of `capture`). Written
|
|
313
|
+
* and painted in one frame, where `capture` paints the tree afresh
|
|
314
|
+
* after the fact. One frame at a time, as `frame`.
|
|
315
|
+
*/
|
|
316
|
+
export function painted(node: number): Promise<{ width: number; height: number; data: Uint8Array }>
|
|
304
317
|
}
|
package/src/shaders.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The GPU-free shader helpers: the `glsl` tag every shader source in the
|
|
2
|
+
// repo is written with, and the GLSL shared between renderers with its JS
|
|
3
|
+
// twins. Pure - no GPU or GUI imports - so the tests run headless on flux
|
|
4
|
+
// and the extensions' pure modules can import from here; gpu.ts re-exports
|
|
5
|
+
// all of it for apps.
|
|
6
|
+
//
|
|
7
|
+
// The screen-size clamp, shared by every renderer that draws a world-sized
|
|
8
|
+
// quad and lets it hold a size on screen: @solidrt/2d's sprites (the
|
|
9
|
+
// camera zoom is the pixels per world unit) and @solidrt/3d's sprite
|
|
10
|
+
// material (the projection at the quad's depth is). A quad `w x h` world
|
|
11
|
+
// units draws at its world size times the pixels per unit, clamped to
|
|
12
|
+
// [minPx, maxPx] on its smaller axis as ONE uniform scale, so its aspect
|
|
13
|
+
// holds. No bounds is world size; a floor alone is the marker that stays
|
|
14
|
+
// findable at the overview; a ceiling alone is the label that must not
|
|
15
|
+
// balloon; equal bounds is a constant screen size (Three's
|
|
16
|
+
// `sizeAttenuation: false`), where w and h carry the aspect alone. A zero
|
|
17
|
+
// bound is off. The GLSL and the JS below are the same function twice,
|
|
18
|
+
// and every picker applies the JS one to the rect it tests, so a hit is
|
|
19
|
+
// where the pixel is (tests/shaders.test.ts pins the pair).
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tags an inline GLSL source, returning it unchanged. Shaders small enough to
|
|
23
|
+
* belong beside the code that uses them stay in the file; the tag is what makes
|
|
24
|
+
* them legible there, because editors highlight GLSL inside a template literal
|
|
25
|
+
* only when a known tag marks it (the name matters - `glsl` is the one the
|
|
26
|
+
* grammars look for).
|
|
27
|
+
*
|
|
28
|
+
* Interpolated values are stringified verbatim, with no GLSL-aware formatting:
|
|
29
|
+
* `${2}` splices in the int literal `2`, which will not assign to a float. Pass
|
|
30
|
+
* anything that varies as a uniform instead of building it into the source.
|
|
31
|
+
*
|
|
32
|
+
* Raw semantics, so backslashes reach the compiler as written: the GLSL
|
|
33
|
+
* preprocessor continues a line with a trailing `\`, which a cooked template
|
|
34
|
+
* would reject as an invalid escape and silently pass through as `undefined`.
|
|
35
|
+
*/
|
|
36
|
+
export let glsl = String.raw
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The GLSL half: `screenSizeScale(size, pxPerUnit, screenPx)` returns the
|
|
40
|
+
* uniform scale a `size` (world units) quad takes so its smaller axis
|
|
41
|
+
* covers at least `screenPx.x` and at most `screenPx.y` pixels when one
|
|
42
|
+
* world unit is `pxPerUnit` pixels; a zero bound is off, a collapsed quad
|
|
43
|
+
* (zero axis) stays collapsed. Paste it before `main`, multiply the quad's
|
|
44
|
+
* size by it.
|
|
45
|
+
*/
|
|
46
|
+
export const SCREEN_SIZE_GLSL = glsl`
|
|
47
|
+
float screenSizeScale(vec2 size, float pxPerUnit, vec2 screenPx) {
|
|
48
|
+
float smallest = min(abs(size.x), abs(size.y)) * pxPerUnit;
|
|
49
|
+
if (smallest <= 0.0) return 1.0;
|
|
50
|
+
float target = smallest;
|
|
51
|
+
if (screenPx.x > 0.0) target = max(target, screenPx.x);
|
|
52
|
+
if (screenPx.y > 0.0) target = min(target, screenPx.y);
|
|
53
|
+
return target / smallest;
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The JS half of SCREEN_SIZE_GLSL, for the picker: the uniform scale a
|
|
59
|
+
* `w x h` quad draws at when one world unit is `pxPerUnit` pixels and
|
|
60
|
+
* its smaller axis is clamped to [minPx, maxPx] on screen. 1 with both
|
|
61
|
+
* bounds off, when the quad already lies within them, or when it is
|
|
62
|
+
* collapsed.
|
|
63
|
+
*/
|
|
64
|
+
export function screenSizeScale(w: number, h: number, pxPerUnit: number, minPx: number, maxPx: number): number {
|
|
65
|
+
let smallest = Math.min(Math.abs(w), Math.abs(h)) * pxPerUnit
|
|
66
|
+
if (!(smallest > 0)) return 1
|
|
67
|
+
let target = smallest
|
|
68
|
+
if (minPx > 0) target = Math.max(target, minPx)
|
|
69
|
+
if (maxPx > 0) target = Math.min(target, maxPx)
|
|
70
|
+
return target / smallest
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate a screen-size pair (throws - the dev validation policy): each
|
|
75
|
+
* bound finite and >= 0 (0 = off), and a ceiling never below a floor that
|
|
76
|
+
* is on. `site` names the caller in the message.
|
|
77
|
+
*/
|
|
78
|
+
export function checkScreenSize(site: string, minPx: number, maxPx: number): void {
|
|
79
|
+
if (!(Number.isFinite(minPx) && minPx >= 0)) throw new Error(`${site}: minScreenPx must be a finite number >= 0, got ${minPx}`)
|
|
80
|
+
if (!(Number.isFinite(maxPx) && maxPx >= 0)) throw new Error(`${site}: maxScreenPx must be a finite number >= 0, got ${maxPx}`)
|
|
81
|
+
if (minPx > 0 && maxPx > 0 && maxPx < minPx) throw new Error(`${site}: maxScreenPx ${maxPx} is below minScreenPx ${minPx}`)
|
|
82
|
+
}
|
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/transform.ts
CHANGED
|
@@ -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
|
@@ -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,
|
|
@@ -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
|
|
@@ -1043,6 +1053,14 @@ export interface PathProps extends PaintProps, PointerProps, DashProps {
|
|
|
1043
1053
|
shadow?: ShadowProps
|
|
1044
1054
|
}
|
|
1045
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* A font role ("sans", "serif", "mono": the font the app packages for it,
|
|
1058
|
+
* or the runtime's own Noto) or a registered family name; an unknown
|
|
1059
|
+
* family falls back to sans. A character the family's font lacks is drawn
|
|
1060
|
+
* from the next registered font that has it, roles first; one no
|
|
1061
|
+
* registered font has draws as the font's missing-glyph box. Whitespace
|
|
1062
|
+
* and control characters never draw a box: a tab advances like a space.
|
|
1063
|
+
*/
|
|
1046
1064
|
export type FontFamily = "sans" | "serif" | "mono" | (string & {})
|
|
1047
1065
|
export type FontStyle = "normal" | "italic"
|
|
1048
1066
|
export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
|
|
@@ -1066,11 +1084,21 @@ export interface TextRunProps {
|
|
|
1066
1084
|
lineHeight?: number
|
|
1067
1085
|
fontStyle?: FontStyle
|
|
1068
1086
|
fontWeight?: FontWeight
|
|
1087
|
+
/**
|
|
1088
|
+
* Width as a percentage of normal (CSS font-stretch: 100 normal, 75
|
|
1089
|
+
* condensed, 125 expanded), set on the font's width axis and clamped to
|
|
1090
|
+
* its range; a font without one draws at its one width. Default 100.
|
|
1091
|
+
*/
|
|
1092
|
+
fontStretch?: number
|
|
1093
|
+
/**
|
|
1094
|
+
* Extra advance after every character (grapheme cluster) in pixels, the
|
|
1095
|
+
* last one included, as CSS letter-spacing; negative tightens. Default 0.
|
|
1096
|
+
*/
|
|
1097
|
+
letterSpacing?: number
|
|
1069
1098
|
/**
|
|
1070
1099
|
* Underline in the run's own color, drawn straight through descenders
|
|
1071
1100
|
* (no skip-ink). Position and thickness come from the font's own metrics
|
|
1072
|
-
* unless overridden
|
|
1073
|
-
* gets the shipped Noto values.
|
|
1101
|
+
* unless overridden.
|
|
1074
1102
|
*/
|
|
1075
1103
|
textDecoration?: TextDecoration
|
|
1076
1104
|
/** Pixels from the baseline to the top of the underline. */
|
package/src/window.ts
CHANGED
|
@@ -422,7 +422,9 @@ export function keyboardHeight(): number {
|
|
|
422
422
|
// write made by a handler would otherwise reach its node a frame late. The
|
|
423
423
|
// drain lives here, the way runFrame drains before renderFrame, so that no
|
|
424
424
|
// handler has to flush for itself. A throwing handler skips neither the
|
|
425
|
-
// handlers after it nor the flush.
|
|
425
|
+
// handlers after it nor the flush. The publish pass (onBeforeRender) runs
|
|
426
|
+
// once more after the flush, for the same reason: what a handler hands an
|
|
427
|
+
// extension lands in this frame's paint too.
|
|
426
428
|
let layoutHandlers: (() => void)[] = []
|
|
427
429
|
let layoutSubscribed = false
|
|
428
430
|
|
|
@@ -439,6 +441,7 @@ function runLayoutHandlers() {
|
|
|
439
441
|
} catch (err) {
|
|
440
442
|
console.error("Error in reactive flush:", err)
|
|
441
443
|
}
|
|
444
|
+
runPublish()
|
|
442
445
|
}
|
|
443
446
|
|
|
444
447
|
/**
|
|
@@ -462,6 +465,93 @@ export function onLayout(fn: () => void) {
|
|
|
462
465
|
return unsubscribe
|
|
463
466
|
}
|
|
464
467
|
|
|
468
|
+
// ------ Before render ----------------
|
|
469
|
+
|
|
470
|
+
// The last JS of a frame, in two passes. The plain handlers are the app's
|
|
471
|
+
// late update: they run once per frame after every onFrame callback and
|
|
472
|
+
// the reactive flush, so a follow or a fit reads the finished state of the
|
|
473
|
+
// frame (Unity's LateUpdate). The publish handlers run after them, and
|
|
474
|
+
// once more after the post-layout handlers' flush (the one other JS entry
|
|
475
|
+
// ahead of the paint): an extension hands its pending writes to the engine
|
|
476
|
+
// there - a scene's record bytes and lights, a layer's style and pose
|
|
477
|
+
// publish - so they are in the frame they were made in, the way the
|
|
478
|
+
// engine's own transform flush ahead of the paint is. Without this pass
|
|
479
|
+
// those writes land at the microtask after the whole frame closure, one
|
|
480
|
+
// frame after the paint that should have shown them. Each handler is its
|
|
481
|
+
// own boundary, listener style: a throw is logged and the rest still run.
|
|
482
|
+
let lateHandlers: (() => void)[] = []
|
|
483
|
+
let publishHandlers: (() => void)[] = []
|
|
484
|
+
|
|
485
|
+
function runHandlers(handlers: (() => void)[]) {
|
|
486
|
+
for (let fn of [...handlers]) {
|
|
487
|
+
try {
|
|
488
|
+
fn()
|
|
489
|
+
} catch (err) {
|
|
490
|
+
console.error("Error in onBeforeRender callback:", err)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Before renderFrame(): the late pass (skipped on the bootstrap frame, which
|
|
496
|
+
// runs no frame callbacks either), its signal writes flushed, then the
|
|
497
|
+
// publish pass - the same handlers-then-flush shape as the post-layout
|
|
498
|
+
// entry, so a late handler's reactive write reaches its node in this frame
|
|
499
|
+
// and the publish pass sees it. An empty flush is a flag check.
|
|
500
|
+
function runBeforeRender(bootstrap: boolean) {
|
|
501
|
+
if (!bootstrap) {
|
|
502
|
+
runHandlers(lateHandlers)
|
|
503
|
+
try {
|
|
504
|
+
flush()
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error("Error in reactive flush:", err)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
runHandlers(publishHandlers)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// After the post-layout handlers' flush: the publish pass alone.
|
|
513
|
+
function runPublish() {
|
|
514
|
+
runHandlers(publishHandlers)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export interface BeforeRenderOptions {
|
|
518
|
+
/**
|
|
519
|
+
* Run in the publish pass: after every plain handler, and again after
|
|
520
|
+
* the `onLayout` handlers of a frame that has any. For handing pending
|
|
521
|
+
* state to the engine, never for deciding it.
|
|
522
|
+
*/
|
|
523
|
+
publish?: boolean
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Calls `fn` right before every frame renders: after the frame's `onFrame`
|
|
528
|
+
* callbacks and the reactive flush, when the frame's state is final. The
|
|
529
|
+
* frame's late update: a camera follow, a fit to a measured box, anything
|
|
530
|
+
* that must see what every frame callback wrote and write once more.
|
|
531
|
+
* Writes made here, to the engine or through signals, are in this frame:
|
|
532
|
+
* the pending reactive writes are flushed once every handler has run.
|
|
533
|
+
*
|
|
534
|
+
* `{ publish: true }` registers for the publish pass instead: after every
|
|
535
|
+
* plain handler, and once more after the `onLayout` handlers of a frame
|
|
536
|
+
* that has any. It is where an extension hands the engine what the frame
|
|
537
|
+
* wrote into its mirrors (a scene syncs its record buffers and lights, a
|
|
538
|
+
* sprite layer its style and pose publish), so a write made anywhere in
|
|
539
|
+
* the frame's JS is in that frame's paint. A publish handler must be
|
|
540
|
+
* idempotent (it may run twice per frame) and must not write app state.
|
|
541
|
+
*
|
|
542
|
+
* Returns a cleanup function; also auto-cleans within an owned scope.
|
|
543
|
+
*/
|
|
544
|
+
export function onBeforeRender(fn: () => void, options?: BeforeRenderOptions) {
|
|
545
|
+
let handlers = options?.publish ? publishHandlers : lateHandlers
|
|
546
|
+
handlers.push(fn)
|
|
547
|
+
let unsubscribe = () => {
|
|
548
|
+
let i = handlers.indexOf(fn)
|
|
549
|
+
if (i >= 0) handlers.splice(i, 1)
|
|
550
|
+
}
|
|
551
|
+
if (getOwner()) onCleanup(unsubscribe)
|
|
552
|
+
return unsubscribe
|
|
553
|
+
}
|
|
554
|
+
|
|
465
555
|
export function onWindowFocus(fn: () => void) {
|
|
466
556
|
let unsubscribe = on("windowFocus", fn)
|
|
467
557
|
if (getOwner()) onCleanup(unsubscribe)
|
|
@@ -553,6 +643,7 @@ export function attachWindow(nodeId: number) {
|
|
|
553
643
|
let unsubscribe: () => void = null!
|
|
554
644
|
let unsubDown: () => void = null!
|
|
555
645
|
let unsubUp: () => void = null!
|
|
646
|
+
let unsubCancel: () => void = null!
|
|
556
647
|
let unsubMove: () => void = null!
|
|
557
648
|
let unsubEnter: () => void = null!
|
|
558
649
|
let unsubLeave: () => void = null!
|
|
@@ -588,6 +679,7 @@ export function attachWindow(nodeId: number) {
|
|
|
588
679
|
} catch (err) {
|
|
589
680
|
console.error("Error in reactive flush:", err)
|
|
590
681
|
}
|
|
682
|
+
runBeforeRender(bootstrap)
|
|
591
683
|
scanForOrphans(t)
|
|
592
684
|
renderFrame()
|
|
593
685
|
}
|
|
@@ -664,6 +756,10 @@ export function attachWindow(nodeId: number) {
|
|
|
664
756
|
bubble(raw, "onPointerUp")
|
|
665
757
|
})
|
|
666
758
|
|
|
759
|
+
unsubCancel = on("pointerCancel", (raw: RawPointer) => {
|
|
760
|
+
bubble(raw, "onPointerCancel")
|
|
761
|
+
})
|
|
762
|
+
|
|
667
763
|
unsubMove = on("pointerMove", (raw: RawPointer) => {
|
|
668
764
|
bubble(raw, "onPointerMove")
|
|
669
765
|
})
|
|
@@ -761,6 +857,7 @@ export function attachWindow(nodeId: number) {
|
|
|
761
857
|
if (unsubscribe) unsubscribe()
|
|
762
858
|
if (unsubDown) unsubDown()
|
|
763
859
|
if (unsubUp) unsubUp()
|
|
860
|
+
if (unsubCancel) unsubCancel()
|
|
764
861
|
if (unsubMove) unsubMove()
|
|
765
862
|
if (unsubEnter) unsubEnter()
|
|
766
863
|
if (unsubLeave) unsubLeave()
|