@solidrt/core 0.0.57 → 0.0.59
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 +42 -7
- package/docs/index.md +23 -0
- package/examples/README.md +1 -0
- package/examples/gpu-draw-list.tsx +7 -7
- package/examples/gpu-instancing.tsx +15 -11
- package/examples/gpu-particles.tsx +8 -4
- package/examples/gpu-pipeline.tsx +8 -4
- package/examples/gpu-shared-params.tsx +6 -6
- package/examples/gpu-sprites.tsx +11 -7
- package/examples/layout-slide.tsx +78 -0
- package/examples/stagger.tsx +35 -19
- package/package.json +5 -5
- package/src/environment.ts +34 -4
- package/src/gpu.ts +25 -35
- package/src/index.ts +4 -4
- package/src/input-chord.ts +44 -0
- package/src/input-keyboard.ts +10 -21
- package/src/input-map.ts +2 -1
- package/src/input-pointer.ts +153 -60
- package/src/renderer.ts +17 -2
- package/src/runtime-modules.d.ts +11 -4
- package/src/transform.ts +4 -1
- package/src/types.d.ts +70 -24
- package/src/video.ts +1 -1
- package/src/window.ts +146 -10
package/src/types.d.ts
CHANGED
|
@@ -461,6 +461,23 @@ export interface LineGeometryProps extends PositionProps {
|
|
|
461
461
|
/** A cubic-bezier timing curve: a CSS name or [x1, y1, x2, y2] control values. */
|
|
462
462
|
export type TransitionCurve = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | [number, number, number, number]
|
|
463
463
|
|
|
464
|
+
/**
|
|
465
|
+
* The object form of `from`/`exit`: the endpoint value plus that
|
|
466
|
+
* direction's own motion, so an ease-out enter pairs with an ease-in exit
|
|
467
|
+
* instead of playing the entry's curve backwards. Every field but `value`
|
|
468
|
+
* is optional and falls back to the entry's (`delay` included, so a
|
|
469
|
+
* staggered list needs no change), except that naming a `curve` or a
|
|
470
|
+
* `bounce` decides the kind outright: `{ value: 0, curve: "ease-in" }` on a
|
|
471
|
+
* spring entry is a tween of the entry's duration.
|
|
472
|
+
*/
|
|
473
|
+
export interface TransitionEndpoint {
|
|
474
|
+
value: number | string
|
|
475
|
+
duration?: number
|
|
476
|
+
curve?: TransitionCurve
|
|
477
|
+
bounce?: number
|
|
478
|
+
delay?: number
|
|
479
|
+
}
|
|
480
|
+
|
|
464
481
|
/**
|
|
465
482
|
* A perceptual spring - the default kind: a bare `{ duration }` is a
|
|
466
483
|
* critically damped spring. `duration` (ms) is the perceptual settling
|
|
@@ -479,20 +496,26 @@ export interface TransitionSpring {
|
|
|
479
496
|
* Mount-time enter animation: at the element's first attach the property
|
|
480
497
|
* snaps to this value and animates to the value it mounted with. Numbers
|
|
481
498
|
* for the scalar properties; the color property takes a CSS color string
|
|
482
|
-
* or packed number
|
|
483
|
-
*
|
|
499
|
+
* or packed number, or a {@link TransitionEndpoint} giving the enter its
|
|
500
|
+
* own curve, duration, bounce or delay. Per-property entries only (not
|
|
501
|
+
* under `all`); a later move or reorder re-runs nothing.
|
|
484
502
|
*/
|
|
485
|
-
from?: number | string
|
|
503
|
+
from?: number | string | TransitionEndpoint
|
|
486
504
|
/**
|
|
487
|
-
* Removal exit animation: an
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
505
|
+
* Removal exit animation: when an element unmounts, every `exit`
|
|
506
|
+
* declared on it or under it animates its property to this value
|
|
507
|
+
* (honoring `delay`), and the unmounted element stays visible, subtree
|
|
508
|
+
* and all, until the last of them settles, then frees. Same value forms
|
|
509
|
+
* as `from`, per-property only. A move never plays it, the exiting
|
|
510
|
+
* subtree is hit-test invisible, and no onTransitionEnd fires (the
|
|
511
|
+
* components are already disposed). An exiting element leaves the layout
|
|
512
|
+
* flow at once: its siblings reflow as if it were gone, and it is painted
|
|
513
|
+
* at its last box, which follows its parent's moves but not the parent's
|
|
514
|
+
* own reflow. The {@link TransitionEndpoint} form gives the exit its own
|
|
515
|
+
* motion: an ease-out enter reads wrong played backwards, so pair it with
|
|
516
|
+
* `exit: { value, curve: "ease-in" }`.
|
|
494
517
|
*/
|
|
495
|
-
exit?: number | string
|
|
518
|
+
exit?: number | string | TransitionEndpoint
|
|
496
519
|
}
|
|
497
520
|
|
|
498
521
|
/**
|
|
@@ -511,20 +534,26 @@ export interface TransitionTween {
|
|
|
511
534
|
* Mount-time enter animation: at the element's first attach the property
|
|
512
535
|
* snaps to this value and animates to the value it mounted with. Numbers
|
|
513
536
|
* for the scalar properties; the color property takes a CSS color string
|
|
514
|
-
* or packed number
|
|
515
|
-
*
|
|
537
|
+
* or packed number, or a {@link TransitionEndpoint} giving the enter its
|
|
538
|
+
* own curve, duration, bounce or delay. Per-property entries only (not
|
|
539
|
+
* under `all`); a later move or reorder re-runs nothing.
|
|
516
540
|
*/
|
|
517
|
-
from?: number | string
|
|
541
|
+
from?: number | string | TransitionEndpoint
|
|
518
542
|
/**
|
|
519
|
-
* Removal exit animation: an
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
543
|
+
* Removal exit animation: when an element unmounts, every `exit`
|
|
544
|
+
* declared on it or under it animates its property to this value
|
|
545
|
+
* (honoring `delay`), and the unmounted element stays visible, subtree
|
|
546
|
+
* and all, until the last of them settles, then frees. Same value forms
|
|
547
|
+
* as `from`, per-property only. A move never plays it, the exiting
|
|
548
|
+
* subtree is hit-test invisible, and no onTransitionEnd fires (the
|
|
549
|
+
* components are already disposed). An exiting element leaves the layout
|
|
550
|
+
* flow at once: its siblings reflow as if it were gone, and it is painted
|
|
551
|
+
* at its last box, which follows its parent's moves but not the parent's
|
|
552
|
+
* own reflow. The {@link TransitionEndpoint} form gives the exit its own
|
|
553
|
+
* motion: an ease-out enter reads wrong played backwards, so pair it with
|
|
554
|
+
* `exit: { value, curve: "ease-in" }`.
|
|
526
555
|
*/
|
|
527
|
-
exit?: number | string
|
|
556
|
+
exit?: number | string | TransitionEndpoint
|
|
528
557
|
}
|
|
529
558
|
|
|
530
559
|
/**
|
|
@@ -571,9 +600,12 @@ export type TransitionPropName =
|
|
|
571
600
|
| "radius"
|
|
572
601
|
| "color"
|
|
573
602
|
|
|
574
|
-
/**
|
|
603
|
+
/**
|
|
604
|
+
* Payload of onTransitionEnd: which animated property finished. `"layout"`
|
|
605
|
+
* is the layout slide settling on its box.
|
|
606
|
+
*/
|
|
575
607
|
export interface TransitionEndEvent {
|
|
576
|
-
property: TransitionPropName
|
|
608
|
+
property: TransitionPropName | "layout"
|
|
577
609
|
}
|
|
578
610
|
|
|
579
611
|
export interface TransitionProps {
|
|
@@ -610,6 +642,20 @@ export interface TransitionProps {
|
|
|
610
642
|
* lifecycle are unaffected. Adds on top of a per-entry `delay`.
|
|
611
643
|
*/
|
|
612
644
|
stagger?: number
|
|
645
|
+
/**
|
|
646
|
+
* Layout slide: when a layout moves this element (a row removed
|
|
647
|
+
* above it, one inserted, a `<For>` reorder, a resize), it slides
|
|
648
|
+
* from where it was painted to its new box on this motion instead
|
|
649
|
+
* of jumping, and is hit-tested where it is drawn. A reflow
|
|
650
|
+
* mid-slide retargets (a spring keeps its velocity). Its first
|
|
651
|
+
* layout, a reparent and a `display: none` toggle snap. The slide is
|
|
652
|
+
* parent-relative: a parent that moves carries its children at
|
|
653
|
+
* once, so declare it on every level that should glide. `true`
|
|
654
|
+
* borrows the `all` entry's motion (an error without one); `all`
|
|
655
|
+
* alone never slides. Position only today: a size change snaps.
|
|
656
|
+
* Settles with onTransitionEnd property `"layout"`.
|
|
657
|
+
*/
|
|
658
|
+
layout?: Omit<TransitionSpring, "from" | "exit"> | Omit<TransitionTween, "from" | "exit"> | TransitionShorthand | boolean
|
|
613
659
|
} & {
|
|
614
660
|
[P in TransitionPropName]?: Transition
|
|
615
661
|
})
|
package/src/video.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type VideoStream = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Opens a video file (MP4,
|
|
44
|
+
* Opens a video file (MP4, VP9 + AAC) and exposes it as reactive signals:
|
|
45
45
|
* read texture() in JSX and the frames appear once playback starts. Closes
|
|
46
46
|
* automatically when the reactive owner is disposed (e.g. the component
|
|
47
47
|
* unmounts). For imperative use, call open() from "flux:video" directly.
|
package/src/window.ts
CHANGED
|
@@ -2,18 +2,152 @@ import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, flush } fro
|
|
|
2
2
|
import { requestFrame, setPointerLock } from "flux:rendertree"
|
|
3
3
|
import { renderFrame } from "srt:render"
|
|
4
4
|
import { on, once } from "srt:events"
|
|
5
|
-
import { exit } from "srt:app"
|
|
5
|
+
import { exit as nativeExit, background as nativeBackground } from "srt:app"
|
|
6
|
+
import { platform } from "flux:process"
|
|
6
7
|
import { getEventHandler, focusedNode, setFocus, activateTextInput, setInterestRoot } from "./core"
|
|
7
8
|
import { scanForOrphans, getNodePath } from "./renderer"
|
|
8
9
|
|
|
10
|
+
// ------ Lifecycle hooks ----------------
|
|
11
|
+
|
|
12
|
+
// A suspend or quit handler; the runtime waits for a returned promise.
|
|
13
|
+
export type LifecycleHandler = () => void | Promise<void>
|
|
14
|
+
|
|
15
|
+
// How long exit() waits for the quit handlers before leaving anyway. The
|
|
16
|
+
// platform-initiated ends (window close, Android onDestroy) are bounded by the
|
|
17
|
+
// runtime's own deadline instead; this one only keeps a hung handler from
|
|
18
|
+
// pinning an app that asked to leave.
|
|
19
|
+
const EXIT_HOOK_DEADLINE_MS = 2000
|
|
20
|
+
|
|
21
|
+
let suspendHandlers: LifecycleHandler[] = []
|
|
22
|
+
let quitHandlers: LifecycleHandler[] = []
|
|
23
|
+
// The suspend dispatch still in flight, if any: a quit that follows a
|
|
24
|
+
// suspend (a finishing Android activity runs onPause, then onDestroy) waits
|
|
25
|
+
// for it before its own handlers, so the persist that suspend started lands
|
|
26
|
+
// before the process ends.
|
|
27
|
+
let pendingSuspend: Promise<void> | null = null
|
|
28
|
+
|
|
29
|
+
function registerLifecycle(list: LifecycleHandler[], fn: LifecycleHandler) {
|
|
30
|
+
list.push(fn)
|
|
31
|
+
let cleanup = () => {
|
|
32
|
+
let i = list.lastIndexOf(fn)
|
|
33
|
+
if (i >= 0) list.splice(i, 1)
|
|
34
|
+
}
|
|
35
|
+
if (getOwner()) onCleanup(cleanup)
|
|
36
|
+
return cleanup
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Runs every handler (a throw counts as a rejected promise) and settles once
|
|
40
|
+
// all of them have, logging the failures: one handler's error must not cut
|
|
41
|
+
// another's write short.
|
|
42
|
+
function settleHandlers(name: string, list: LifecycleHandler[]): Promise<void> {
|
|
43
|
+
let results = [...list].map((fn) => {
|
|
44
|
+
try {
|
|
45
|
+
return Promise.resolve(fn())
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return Promise.reject(err)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
return Promise.allSettled(results).then((settled) => {
|
|
51
|
+
for (let r of settled) {
|
|
52
|
+
if (r.status === "rejected") console.error(`Error in ${name} handler:`, r.reason)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dispatchSuspend(): Promise<void> {
|
|
58
|
+
let p = settleHandlers("onSuspend", suspendHandlers).finally(() => {
|
|
59
|
+
if (pendingSuspend === p) pendingSuspend = null
|
|
60
|
+
})
|
|
61
|
+
pendingSuspend = p
|
|
62
|
+
return p
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function dispatchQuit(): Promise<void> {
|
|
66
|
+
let inflight = pendingSuspend ?? Promise.resolve()
|
|
67
|
+
return inflight.then(() => settleHandlers("onQuit", quitHandlers))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The runtime asks through the event bus and waits for `done()`: the
|
|
71
|
+
// platform is held open (or its close budget spent) until then, so this must
|
|
72
|
+
// answer exactly once, whatever the handlers do. Subscribed at module load:
|
|
73
|
+
// core is always present, and a hook nobody listens to would leave the
|
|
74
|
+
// runtime waiting out its deadline on every close.
|
|
75
|
+
on("suspend", (e: { done: () => void }) => {
|
|
76
|
+
dispatchSuspend().then(e.done, e.done)
|
|
77
|
+
})
|
|
78
|
+
on("quit", (e: { done: () => void }) => {
|
|
79
|
+
dispatchQuit().then(e.done, e.done)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calls `fn` when the app is being suspended and may be killed without
|
|
84
|
+
* further notice: the user switches away on Android or iOS (the platform's
|
|
85
|
+
* "will enter background" moment). Never fires on desktop, where a minimized
|
|
86
|
+
* app is not killed. This is the moment to persist session state - a file, a
|
|
87
|
+
* database row, an HTTP request - so a later launch can pick the session up;
|
|
88
|
+
* the runtime holds the platform open and waits for a returned promise, up to
|
|
89
|
+
* its deadline (about ten seconds, platform-dependent), then lets the
|
|
90
|
+
* platform proceed. Nothing decides for you whether to restore afterwards;
|
|
91
|
+
* that is the app's call.
|
|
92
|
+
*
|
|
93
|
+
* Returns a cleanup function; also auto-cleans within an owned scope.
|
|
94
|
+
*/
|
|
95
|
+
export function onSuspend(fn: LifecycleHandler) {
|
|
96
|
+
return registerLifecycle(suspendHandlers, fn)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Calls `fn` when this app instance is ending: `exit()` (including the
|
|
101
|
+
* default action of an unprevented `back` on desktop), the desktop window
|
|
102
|
+
* closing, the Android activity finishing. Not when the app goes to the
|
|
103
|
+
* background (`background()`, or back at the root on Android). Not a last chance to save - on mobile the
|
|
104
|
+
* suspend hook has already run, and a user who quits usually does not want
|
|
105
|
+
* a session kept - but the place for work that needs a real close, and the
|
|
106
|
+
* runtime waits for a returned promise up to its deadline (well under a
|
|
107
|
+
* second on Android, where the platform's close budget is fixed; a couple of
|
|
108
|
+
* seconds on desktop). A force quit or a crash gives no signal on any
|
|
109
|
+
* platform.
|
|
110
|
+
*
|
|
111
|
+
* Returns a cleanup function; also auto-cleans within an owned scope.
|
|
112
|
+
*/
|
|
113
|
+
export function onQuit(fn: LifecycleHandler) {
|
|
114
|
+
return registerLifecycle(quitHandlers, fn)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ends the current app: back to the player in a dev client, quitting when
|
|
119
|
+
* standalone or at the player itself (on Android the activity finishes, so
|
|
120
|
+
* the next launch starts fresh). The default action of an unprevented `back`
|
|
121
|
+
* event on desktop; call it directly to exit programmatically, e.g. after
|
|
122
|
+
* intercepting back for an unsaved-changes dialog. Runs the `onQuit`
|
|
123
|
+
* handlers first and leaves once their promises settle (or the deadline
|
|
124
|
+
* passes); returns immediately.
|
|
125
|
+
*/
|
|
126
|
+
export function exit() {
|
|
127
|
+
let deadline = new Promise<void>((resolve) => setTimeout(resolve, EXIT_HOOK_DEADLINE_MS))
|
|
128
|
+
Promise.race([dispatchQuit(), deadline]).then(nativeExit, nativeExit)
|
|
129
|
+
}
|
|
130
|
+
|
|
9
131
|
/**
|
|
10
|
-
* Leaves the current app
|
|
11
|
-
* client
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
132
|
+
* Leaves the current app without ending it: back to the player in a dev
|
|
133
|
+
* client; otherwise the OS takes the app to the background - on Android the
|
|
134
|
+
* task moves behind the others with the process kept, on desktop the window
|
|
135
|
+
* minimizes. The default action of an unprevented `back` event on Android,
|
|
136
|
+
* where that is what the system itself does at the root of an app. No
|
|
137
|
+
* `onQuit` runs (the app is not ending); `onSuspend` fires on Android on the
|
|
138
|
+
* way, as for any background.
|
|
15
139
|
*/
|
|
16
|
-
export
|
|
140
|
+
export function background() {
|
|
141
|
+
nativeBackground()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The default action of an unprevented back event: what the platform itself
|
|
145
|
+
// does at the root of an app. Android backgrounds (since Android 12);
|
|
146
|
+
// everywhere else back at the root ends the app.
|
|
147
|
+
function backDefault() {
|
|
148
|
+
if (platform === "android") background()
|
|
149
|
+
else exit()
|
|
150
|
+
}
|
|
17
151
|
|
|
18
152
|
// ------ Pointer routing -----------------
|
|
19
153
|
|
|
@@ -301,8 +435,10 @@ let backHandlers: ((e: BackEvent) => void)[] = []
|
|
|
301
435
|
* desktop dev chord). Call `e.preventDefault()` when back means in-app
|
|
302
436
|
* navigation right now (close a modal, previous screen); unprevented, the
|
|
303
437
|
* event passes to the handler registered before this one, and if none of them
|
|
304
|
-
* prevents it either, to the default action:
|
|
305
|
-
*
|
|
438
|
+
* prevents it either, to the default action: what the platform does at the
|
|
439
|
+
* root of an app - `background()` on Android, `exit()` elsewhere. Apps
|
|
440
|
+
* without a handler get the platform's behavior, which is the correct
|
|
441
|
+
* zero-effort default.
|
|
306
442
|
*
|
|
307
443
|
* Handlers form a stack: the most recently registered runs first and the first
|
|
308
444
|
* to prevent ends the dispatch, so each screen or overlay owns one step of the
|
|
@@ -516,7 +652,7 @@ export function attachWindow(nodeId: number) {
|
|
|
516
652
|
// Top of the stack down, stopping as soon as one takes the event.
|
|
517
653
|
let stack = [...backHandlers]
|
|
518
654
|
for (let i = stack.length - 1; i >= 0 && !prevented; i--) stack[i]!(e)
|
|
519
|
-
if (!prevented)
|
|
655
|
+
if (!prevented) backDefault()
|
|
520
656
|
})
|
|
521
657
|
|
|
522
658
|
unsubTextInput = on("textInput", (e: any) => {
|