@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/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. Per-property entries only (not under `all`); a later
483
- * move or reorder re-runs nothing.
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 unmounted element stays visible, animates
488
- * the property to this value (honoring `delay`), and is freed when its
489
- * exit animations settle. Same value forms as `from`, per-property only.
490
- * A move never plays it, the exiting element is hit-test invisible, its
491
- * whole subtree stays painted with it, and no onTransitionEnd fires (the
492
- * component is already disposed). An attached element keeps its layout
493
- * slot until the exit finishes.
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. Per-property entries only (not under `all`); a later
515
- * move or reorder re-runs nothing.
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 unmounted element stays visible, animates
520
- * the property to this value (honoring `delay`), and is freed when its
521
- * exit animations settle. Same value forms as `from`, per-property only.
522
- * A move never plays it, the exiting element is hit-test invisible, its
523
- * whole subtree stays painted with it, and no onTransitionEnd fires (the
524
- * component is already disposed). An attached element keeps its layout
525
- * slot until the exit finishes.
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
- /** Payload of onTransitionEnd: which animated property finished. */
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, H.264 + AAC) and exposes it as reactive signals:
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, unconditionally: back to the player in a dev
11
- * client, quitting when standalone or at the player itself (on Android the
12
- * client backgrounds instead of dying). The default action of an unprevented
13
- * `back` event; call it directly to exit programmatically, e.g. after
14
- * intercepting back for an unsaved-changes dialog.
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 { exit }
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: exit(). Apps without a handler
305
- * exit on back everywhere, which is the correct zero-effort default.
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) exit()
655
+ if (!prevented) backDefault()
520
656
  })
521
657
 
522
658
  unsubTextInput = on("textInput", (e: any) => {