@solidrt/core 0.0.48 → 0.0.49
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/package.json +5 -5
- package/src/text-input.ts +18 -4
- package/src/video.ts +100 -0
package/AGENTS.md
CHANGED
|
@@ -94,7 +94,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
97
|
-
2.0.0-
|
|
97
|
+
2.0.0-rc.0); bun resolves them from peerDependencies.
|
|
98
98
|
|
|
99
99
|
## Element model (the parts that are easy to get wrong)
|
|
100
100
|
|
|
@@ -200,8 +200,8 @@ Peer deps @solidjs/signals and @solidjs/universal must match (currently
|
|
|
200
200
|
Also onResize, onLayout, onWindowFocus, onWindowBlur.
|
|
201
201
|
|
|
202
202
|
- Device/GPU access via subpath imports: @solidrt/core/camera, /microphone,
|
|
203
|
-
/speech, /gpu. Image flow: `decodeImage(bytes)` ->
|
|
204
|
-
-> `<texture src={id} />`.
|
|
203
|
+
/speech, /gpu. Image flow: `decodeImage(bytes)` ->
|
|
204
|
+
`createTexture(data,w,h)` -> `<texture src={id} />`.
|
|
205
205
|
|
|
206
206
|
## Minimal app, core primitives only (verified to render)
|
|
207
207
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"colord": "^2.9.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@solidrt/flux-types": "0.0.
|
|
30
|
+
"@solidrt/flux-types": "0.0.49"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@solidjs/signals": "2.0.0-
|
|
34
|
-
"@solidjs/universal": "2.0.0-
|
|
35
|
-
"solid-js": "2.0.0-
|
|
33
|
+
"@solidjs/signals": "2.0.0-rc.0",
|
|
34
|
+
"@solidjs/universal": "2.0.0-rc.0",
|
|
35
|
+
"solid-js": "2.0.0-rc.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/text-input.ts
CHANGED
|
@@ -63,14 +63,19 @@ export type TextBuffer = {
|
|
|
63
63
|
* source. Without one it holds the text itself. The selection is always
|
|
64
64
|
* buffer-owned state and is clamped to the current text length on read, so an
|
|
65
65
|
* external truncation of a controlled value cannot leave the caret dangling.
|
|
66
|
-
* Every edit is clamped to `maxLength`.
|
|
66
|
+
* Every edit is clamped to `maxLength`. Mutations commit synchronously
|
|
67
|
+
* (flush), so consecutive edits in one task observe each other - required for
|
|
68
|
+
* burst input, where several handlers run with no microtask between them.
|
|
67
69
|
*/
|
|
68
70
|
export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
|
|
69
71
|
let initial = options.defaultValue ?? ""
|
|
70
72
|
let [internalValue, setInternalValue] = createSignal(initial)
|
|
73
|
+
// The caret starts at the end of the current text: for a controlled buffer
|
|
74
|
+
// that is the owner's value, which defaultValue does not reflect.
|
|
75
|
+
let initialCaret = (options.value?.() ?? initial).length
|
|
71
76
|
let [selectionState, setSelectionState] = createSignal<Selection>({
|
|
72
|
-
anchor:
|
|
73
|
-
focus:
|
|
77
|
+
anchor: initialCaret,
|
|
78
|
+
focus: initialCaret,
|
|
74
79
|
})
|
|
75
80
|
|
|
76
81
|
let value = () => options.value?.() ?? internalValue()
|
|
@@ -89,7 +94,12 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
|
|
|
89
94
|
|
|
90
95
|
let setCaret = (offset: number) => setSelectionState({ anchor: offset, focus: offset })
|
|
91
96
|
|
|
92
|
-
// Apply a text edit and place the caret, clamping to maxLength.
|
|
97
|
+
// Apply a text edit and place the caret, clamping to maxLength. The flush
|
|
98
|
+
// commits the writes (including a controlled owner's from onInput) before
|
|
99
|
+
// returning: edits must observe each other within one task, because event
|
|
100
|
+
// bursts can dispatch several handlers with no microtask between them
|
|
101
|
+
// (Android IME input arrives as backspace+commit bursts; see
|
|
102
|
+
// okf/backlog/event-burst-stale-signal-reads.md).
|
|
93
103
|
let apply = (next: string, caret: number) => {
|
|
94
104
|
let max = options.maxLength?.()
|
|
95
105
|
if (max != null && next.length > max) next = next.slice(0, max)
|
|
@@ -97,6 +107,7 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
|
|
|
97
107
|
if (options.value?.() == null) setInternalValue(next)
|
|
98
108
|
setCaret(caret)
|
|
99
109
|
options.onInput?.(next)
|
|
110
|
+
flush()
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
return {
|
|
@@ -131,6 +142,7 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
|
|
|
131
142
|
// A non-extending left/right on a range collapses to the near edge.
|
|
132
143
|
if (!extend && anchor !== focus && (direction === "left" || direction === "right")) {
|
|
133
144
|
setCaret(direction === "left" ? Math.min(anchor, focus) : Math.max(anchor, focus))
|
|
145
|
+
flush()
|
|
134
146
|
return
|
|
135
147
|
}
|
|
136
148
|
let next = focus
|
|
@@ -139,11 +151,13 @@ export function createTextBuffer(options: TextBufferOptions = {}): TextBuffer {
|
|
|
139
151
|
else if (direction === "start") next = 0
|
|
140
152
|
else if (direction === "end") next = len
|
|
141
153
|
setSelectionState({ anchor: extend ? anchor : next, focus: next })
|
|
154
|
+
flush()
|
|
142
155
|
},
|
|
143
156
|
|
|
144
157
|
setSelection: (anchor, focus) => {
|
|
145
158
|
let len = value().length
|
|
146
159
|
setSelectionState({ anchor: Math.min(anchor, len), focus: Math.min(focus, len) })
|
|
160
|
+
flush()
|
|
147
161
|
},
|
|
148
162
|
|
|
149
163
|
setValue: (next) => apply(next, next.length),
|
package/src/video.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Video playback, reactive (SolidJS) layer. There is no <video> element by
|
|
2
|
+
// design: a player streams into a texture id, so displaying it is just
|
|
3
|
+
// <texture src={video.texture()} /> (or <d-texture> for detached-layout
|
|
4
|
+
// placement), and every texture capability - shaders, offscreen targets,
|
|
5
|
+
// subtree effects - applies to video for free. A richer Video component
|
|
6
|
+
// composes on top of this in a higher layer, not in core.
|
|
7
|
+
//
|
|
8
|
+
// The imperative primitive lives in the `flux:video` module; import { open }
|
|
9
|
+
// from "flux:video" for non-reactive use.
|
|
10
|
+
|
|
11
|
+
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
12
|
+
import type { TextureId } from "flux:gpu"
|
|
13
|
+
import { open } from "flux:video"
|
|
14
|
+
|
|
15
|
+
export type VideoOptions = {
|
|
16
|
+
/** Start playback as soon as the file is open. */
|
|
17
|
+
autoplay?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** An opened video as reactive accessors plus playback controls. */
|
|
21
|
+
export type VideoStream = {
|
|
22
|
+
/** Texture id once the file is open, undefined while opening; render with <texture src={...}>. */
|
|
23
|
+
texture(): TextureId | undefined
|
|
24
|
+
/** Frame size, undefined while opening. */
|
|
25
|
+
width(): number | undefined
|
|
26
|
+
height(): number | undefined
|
|
27
|
+
/** Duration in seconds, undefined while opening. */
|
|
28
|
+
duration(): number | undefined
|
|
29
|
+
/** Set if opening failed (unreadable file, unsupported codec). */
|
|
30
|
+
error(): Error | undefined
|
|
31
|
+
/** Start or resume playback (before open resolves, playback starts on resolve). */
|
|
32
|
+
play(): void
|
|
33
|
+
/** Pause playback; the current frame stays displayed. */
|
|
34
|
+
pause(): void
|
|
35
|
+
/** Whether playback is running (plain read, not a signal). */
|
|
36
|
+
playing(): boolean
|
|
37
|
+
/** Presentation time of the displayed frame in seconds (plain read, not a signal). */
|
|
38
|
+
currentTime(): number
|
|
39
|
+
/** Whether the last frame has been displayed (plain read, not a signal). */
|
|
40
|
+
finished(): boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Opens a video file (MP4, H.264 + AAC) and exposes it as reactive signals:
|
|
45
|
+
* read texture() in JSX and the frames appear once playback starts. Closes
|
|
46
|
+
* automatically when the reactive owner is disposed (e.g. the component
|
|
47
|
+
* unmounts). For imperative use, call open() from "flux:video" directly.
|
|
48
|
+
*/
|
|
49
|
+
export function createVideo(path: string, options: VideoOptions = {}): VideoStream {
|
|
50
|
+
let [texture, setTexture] = createSignal<TextureId | undefined>(undefined)
|
|
51
|
+
let [width, setWidth] = createSignal<number | undefined>(undefined)
|
|
52
|
+
let [height, setHeight] = createSignal<number | undefined>(undefined)
|
|
53
|
+
let [duration, setDuration] = createSignal<number | undefined>(undefined)
|
|
54
|
+
let [error, setError] = createSignal<Error | undefined>(undefined)
|
|
55
|
+
let player: Awaited<ReturnType<typeof open>> | undefined
|
|
56
|
+
let disposed = false
|
|
57
|
+
let wantPlay = options.autoplay ?? false
|
|
58
|
+
|
|
59
|
+
open(path)
|
|
60
|
+
.then((video) => {
|
|
61
|
+
if (disposed) {
|
|
62
|
+
video.close()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
player = video
|
|
66
|
+
setTexture(video.texture)
|
|
67
|
+
setWidth(video.width)
|
|
68
|
+
setHeight(video.height)
|
|
69
|
+
setDuration(video.duration)
|
|
70
|
+
if (wantPlay) video.play()
|
|
71
|
+
})
|
|
72
|
+
.catch((e) => setError(e instanceof Error ? e : new Error(String(e))))
|
|
73
|
+
|
|
74
|
+
onCleanup(() => {
|
|
75
|
+
disposed = true
|
|
76
|
+
if (player) {
|
|
77
|
+
player.close()
|
|
78
|
+
player = undefined
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
texture,
|
|
84
|
+
width,
|
|
85
|
+
height,
|
|
86
|
+
duration,
|
|
87
|
+
error,
|
|
88
|
+
play() {
|
|
89
|
+
wantPlay = true
|
|
90
|
+
player?.play()
|
|
91
|
+
},
|
|
92
|
+
pause() {
|
|
93
|
+
wantPlay = false
|
|
94
|
+
player?.pause()
|
|
95
|
+
},
|
|
96
|
+
playing: () => player?.playing() ?? false,
|
|
97
|
+
currentTime: () => player?.currentTime() ?? 0,
|
|
98
|
+
finished: () => player?.finished() ?? false,
|
|
99
|
+
}
|
|
100
|
+
}
|