@solidrt/core 0.0.50 → 0.0.51

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/window.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createSignal, onCleanup, onSettled, flush } from "@solidjs/signals"
2
- import { requestFrame } from "flux:rendertree"
2
+ import { requestFrame, setPointerLock } from "flux:rendertree"
3
3
  import { renderFrame } from "srt:render"
4
4
  import { on, once } from "srt:events"
5
5
  import { exit } from "srt:app"
@@ -38,9 +38,12 @@ let refreshRate = 60
38
38
  * the present count, and `rate` is the current refresh rate in Hz. `tick` is paced
39
39
  * by the runtime (one refresh period per present, slow-corrected toward the wall
40
40
  * clock) so animations driven off it stay smooth even when swap-return times
41
- * jitter. performance.now() and timers report/march on this same paced timeline
42
- * (so the whole time surface freezes together under the dev tools' clock
43
- * control); for real wall-clock time use Date.now().
41
+ * jitter, and it is continuous across hot reloads: every tick an instance
42
+ * sees is on one timebase, so dt is well-defined from the second call on.
43
+ * Timers freeze together with frame callbacks under the dev tools' clock
44
+ * control, but measure their delays against the wall clock, not this paced
45
+ * timeline. performance.now() is not on it either: it is real elapsed time
46
+ * for measuring work; Date.now() is calendar time.
44
47
  * Returns a cleanup function; also auto-cleans within a reactive scope.
45
48
  */
46
49
  export function onFrame(fn: (tick: number, frame: number, rate: number) => void) {
@@ -161,6 +164,33 @@ export function windowFocused(): boolean {
161
164
  return focusedAccessor()
162
165
  }
163
166
 
167
+ /**
168
+ * Lock the pointer to the window (relative mouse mode) or release it. While
169
+ * locked the cursor is hidden and confined, clientX/clientY freeze at the
170
+ * lock point, and mouse motion keeps reporting through the pointer events'
171
+ * movementX/movementY - the mouse-look primitive. Window-level, no
172
+ * permission dance: one app, one window. Observe the applied state through
173
+ * pointerLocked(); the platform can refuse (no relative mode) and the OS
174
+ * may drop the lock, e.g. on focus loss.
175
+ */
176
+ export function lockPointer(locked: boolean) {
177
+ if (typeof locked !== "boolean") throw new Error(`lockPointer: expected a boolean, got ${typeof locked}`)
178
+ setPointerLock(locked)
179
+ }
180
+
181
+ let pointerLockedAccessor: (() => boolean) | undefined
182
+
183
+ /** Whether the pointer is currently locked (relative mouse mode), as a reactive accessor. */
184
+ export function pointerLocked(): boolean {
185
+ if (!pointerLockedAccessor) {
186
+ let [locked, setLocked] = createSignal(false)
187
+ // Sticky event: a subscriber after the lock still observes the state.
188
+ on("pointerLock", ({ locked }: { locked: boolean }) => setLocked(locked))
189
+ pointerLockedAccessor = locked
190
+ }
191
+ return pointerLockedAccessor()
192
+ }
193
+
164
194
  let keyboardHeightAccessor: (() => number) | undefined
165
195
 
166
196
  /**
@@ -255,6 +285,7 @@ export function attachWindow(nodeId: number) {
255
285
  let unsubEnter: () => void = null!
256
286
  let unsubLeave: () => void = null!
257
287
  let unsubWheel: () => void = null!
288
+ let unsubTransitionEnd: () => void = null!
258
289
  let unsubKeyDown: () => void = null!
259
290
  let unsubKeyUp: () => void = null!
260
291
  let unsubBack: () => void = null!
@@ -263,8 +294,16 @@ export function attachWindow(nodeId: number) {
263
294
  let unsubRefreshRate: () => void = null!
264
295
  let unsubFirstResize: (() => void) | null = null
265
296
 
266
- function runFrame(t: number, frame: number) {
267
- if (animationFrames.size > 0) {
297
+ // `bootstrap` marks the synthetic first frame (see the first-resize
298
+ // subscription below): it flushes and paints the freshly initialized graph
299
+ // but does not invoke onFrame callbacks, because its timestamp is not a
300
+ // reading of the frame timeline - handing apps a zero tick gave every
301
+ // reloaded instance one enormous dt on the next real frame
302
+ // (okf/done/onframe-tick-reset-on-reload.md). The callbacks stay
303
+ // registered and run on the first real render event, before the first
304
+ // paint with their writes applied.
305
+ function runFrame(t: number, frame: number, bootstrap = false) {
306
+ if (!bootstrap && animationFrames.size > 0) {
268
307
  let frames = animationFrames
269
308
  animationFrames = new Map()
270
309
  for (let fn of frames.values()) fn(t, frame, refreshRate)
@@ -368,6 +407,17 @@ export function attachWindow(nodeId: number) {
368
407
  bubble(raw, "onWheel")
369
408
  })
370
409
 
410
+ // A native transition finished (runtime-side animation; see the
411
+ // `transition` prop). Target-only delivery, no bubbling: the element
412
+ // that declared the transition is the one interested in its end.
413
+ unsubTransitionEnd = on("transitionEnd", (raw: { target: number; property: string }) => {
414
+ try {
415
+ getEventHandler(raw.target, "onTransitionEnd")?.({ property: raw.property })
416
+ } catch (err) {
417
+ console.error("Error in onTransitionEnd handler:", err)
418
+ }
419
+ })
420
+
371
421
  // Key events dispatch along the focused node's ancestor chain, leaf->root
372
422
  // (the pointer bubbling contract), so a container hears keys from focused
373
423
  // descendants and the window root hears everything: <window onKeyDown> is
@@ -429,7 +479,7 @@ export function attachWindow(nodeId: number) {
429
479
  // where flush() is illegal (not reentrant). Defer runFrame to a microtask
430
480
  // so the first frame always runs after this callback returns.
431
481
  unsubFirstResize = once("resize", () => {
432
- queueMicrotask(() => runFrame(0, 0))
482
+ queueMicrotask(() => runFrame(0, 0, true))
433
483
  })
434
484
  })
435
485
 
@@ -442,6 +492,7 @@ export function attachWindow(nodeId: number) {
442
492
  if (unsubEnter) unsubEnter()
443
493
  if (unsubLeave) unsubLeave()
444
494
  if (unsubWheel) unsubWheel()
495
+ if (unsubTransitionEnd) unsubTransitionEnd()
445
496
  if (unsubKeyDown) unsubKeyDown()
446
497
  if (unsubKeyUp) unsubKeyUp()
447
498
  if (unsubBack) unsubBack()