@solidrt/components 0.0.11 → 0.0.13
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/package.json +2 -2
- package/src/image.tsx +9 -4
- package/src/safe-area.tsx +3 -13
- package/src/text-input.tsx +101 -58
- package/src/window.tsx +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
],
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"@solidjs/signals": "2.0.0-beta.14",
|
|
17
|
-
"@solidrt/core": "0.0.
|
|
17
|
+
"@solidrt/core": "0.0.13"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/image.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSignal, createEffect, onCleanup } from "@solidjs/signals"
|
|
2
|
-
import {
|
|
2
|
+
import { createTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
3
|
+
import { decodeImage } from "@solidrt/core/image"
|
|
3
4
|
import type { LayoutProps, PointerProps } from "@solidrt/core"
|
|
4
5
|
import type { StyleProps } from "./types"
|
|
5
6
|
|
|
@@ -10,13 +11,14 @@ export interface ImageProps extends PointerProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
//TODO onLoad, onError
|
|
13
|
-
//TODO release texture in onCleanup
|
|
14
14
|
export function Image(props: ImageProps) {
|
|
15
15
|
let [res, setRes] = createSignal<{ id: number; width: number; height: number }>()
|
|
16
16
|
|
|
17
17
|
// Load (and decode) whenever src changes. A url is fetched; bytes are used
|
|
18
18
|
// directly. The async result is pushed into the signal so the texture shows
|
|
19
|
-
// once ready; a stale flag drops results from a superseded src.
|
|
19
|
+
// once ready; a stale flag drops results from a superseded src. The old
|
|
20
|
+
// texture is destroyed before installing the new one; on unmount the current
|
|
21
|
+
// texture is destroyed by onCleanup below.
|
|
20
22
|
createEffect(
|
|
21
23
|
() => props.src,
|
|
22
24
|
(source) => {
|
|
@@ -33,7 +35,9 @@ export function Image(props: ImageProps) {
|
|
|
33
35
|
if (stale) return
|
|
34
36
|
let { data, width, height } = decodeImage(bytes)
|
|
35
37
|
let id = createTexture(data, width, height)
|
|
38
|
+
let old = res()
|
|
36
39
|
setRes({ id, width, height })
|
|
40
|
+
if (old !== undefined) destroyTexture(old.id)
|
|
37
41
|
})()
|
|
38
42
|
|
|
39
43
|
return () => {
|
|
@@ -52,7 +56,8 @@ export function Image(props: ImageProps) {
|
|
|
52
56
|
let texH = () => (typeof props.layout?.height === "number" ? props.layout.height : undefined)
|
|
53
57
|
|
|
54
58
|
onCleanup(() => {
|
|
55
|
-
|
|
59
|
+
let r = res()
|
|
60
|
+
if (r !== undefined) destroyTexture(r.id)
|
|
56
61
|
})
|
|
57
62
|
|
|
58
63
|
return (
|
package/src/safe-area.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { onResize } from "@solidrt/core"
|
|
1
|
+
import { safeArea } from "@solidrt/core"
|
|
3
2
|
|
|
4
3
|
export function SafeArea(props: {
|
|
5
4
|
top?: boolean | number
|
|
@@ -9,21 +8,12 @@ export function SafeArea(props: {
|
|
|
9
8
|
relative?: boolean
|
|
10
9
|
children?: any
|
|
11
10
|
}) {
|
|
12
|
-
let [insets, setInsets] = createSignal({ top: 0, bottom: 0, left: 0, right: 0 })
|
|
13
|
-
onResize(({ width, height, safeArea }) => {
|
|
14
|
-
setInsets({
|
|
15
|
-
top: safeArea.top,
|
|
16
|
-
left: safeArea.left,
|
|
17
|
-
bottom: height - safeArea.bottom,
|
|
18
|
-
right: width - safeArea.right,
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
11
|
let pad = (edge: "top" | "bottom" | "left" | "right") => {
|
|
22
12
|
let defaultOn = edge === "top" || edge === "bottom"
|
|
23
13
|
let p = props[edge] ?? defaultOn
|
|
24
14
|
if (p === false) return 0
|
|
25
|
-
if (p === true) return
|
|
26
|
-
return Math.max(
|
|
15
|
+
if (p === true) return safeArea()[edge]
|
|
16
|
+
return Math.max(safeArea()[edge], p as number)
|
|
27
17
|
}
|
|
28
18
|
return (
|
|
29
19
|
<view
|
package/src/text-input.tsx
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createEffect, createSignal, onCleanup } from "@solidjs/signals"
|
|
2
|
+
import { setFocus } from "@solidrt/core"
|
|
3
|
+
import { createCaretScroll, createTextBuffer } from "@solidrt/core/text-input"
|
|
3
4
|
import type { LayoutProps } from "@solidrt/core"
|
|
4
5
|
import type { StyleProps } from "./types"
|
|
5
6
|
import { theme } from "./theme"
|
|
6
7
|
|
|
8
|
+
// Caret thickness. Shared so the drawn caret and the scroll offset's reserved
|
|
9
|
+
// edge column cannot drift apart.
|
|
10
|
+
const CARET_WIDTH = 1
|
|
11
|
+
|
|
7
12
|
export interface TextInputProps {
|
|
8
13
|
value?: string
|
|
9
14
|
defaultValue?: string
|
|
@@ -21,28 +26,38 @@ export interface TextInputProps {
|
|
|
21
26
|
style?: StyleProps
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
29
|
+
// Single-line. The caret moves through the text (Left/Right/Home/End), edits
|
|
30
|
+
// happen at the caret, and the inner box scrolls to keep it in view. Printable
|
|
31
|
+
// text arrives via onTextInput (post-IME commit). onKeyDown handles caret
|
|
32
|
+
// movement, Backspace/Delete, Enter, Escape. Range selection (shift-movement,
|
|
33
|
+
// highlight, click-to-position) is not wired yet. Outside-click-to-blur is the
|
|
34
|
+
// caller's job.
|
|
27
35
|
export function TextInput(props: TextInputProps) {
|
|
28
|
-
let [internalValue, setInternalValue] = createSignal(props.defaultValue ?? "")
|
|
29
36
|
let [focused, setFocused] = createSignal(false)
|
|
30
37
|
let [caretOn, setCaretOn] = createSignal(true)
|
|
31
|
-
let [viewportWidth, setViewportWidth] = createSignal(0)
|
|
32
38
|
|
|
33
39
|
let node: { id: number } | undefined
|
|
34
40
|
let viewport: { id: number } | undefined
|
|
35
41
|
let blinkId: any = null
|
|
36
42
|
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
let buffer = createTextBuffer({
|
|
44
|
+
value: () => props.value,
|
|
45
|
+
defaultValue: props.defaultValue,
|
|
46
|
+
onInput: (v) => props.onInput?.(v),
|
|
47
|
+
maxLength: () => props.maxLength,
|
|
48
|
+
})
|
|
49
|
+
let value = buffer.value
|
|
50
|
+
|
|
51
|
+
// autoFocus runs in an effect, not the ref: setFocus fires onFocus and reads
|
|
52
|
+
// the node's onTextInput handler to toggle the keyboard, and those handlers
|
|
53
|
+
// are only registered after the element's props are applied. The ref can fire
|
|
54
|
+
// before that, so focusing there would no-op.
|
|
55
|
+
createEffect(
|
|
56
|
+
() => props.autoFocus,
|
|
57
|
+
(autoFocus) => {
|
|
58
|
+
if (autoFocus && node) setFocus(node.id)
|
|
59
|
+
},
|
|
60
|
+
)
|
|
46
61
|
|
|
47
62
|
let handlePointerDown = () => {
|
|
48
63
|
if (props.disabled) return
|
|
@@ -70,11 +85,26 @@ export function TextInput(props: TextInputProps) {
|
|
|
70
85
|
let handleKeyDown = (e: any) => {
|
|
71
86
|
if (props.disabled) return
|
|
72
87
|
if (e.key === "Backspace") {
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
buffer.deleteBackward()
|
|
89
|
+
setCaretOn(true)
|
|
90
|
+
} else if (e.key === "Delete") {
|
|
91
|
+
buffer.deleteForward()
|
|
92
|
+
setCaretOn(true)
|
|
93
|
+
} else if (e.key === "Left") {
|
|
94
|
+
buffer.move("left")
|
|
95
|
+
setCaretOn(true)
|
|
96
|
+
} else if (e.key === "Right") {
|
|
97
|
+
buffer.move("right")
|
|
98
|
+
setCaretOn(true)
|
|
99
|
+
} else if (e.key === "Home") {
|
|
100
|
+
buffer.move("start")
|
|
101
|
+
setCaretOn(true)
|
|
102
|
+
} else if (e.key === "End") {
|
|
103
|
+
buffer.move("end")
|
|
75
104
|
setCaretOn(true)
|
|
76
105
|
} else if (e.key === "Return" || e.key === "Enter") {
|
|
77
106
|
props.onSubmit?.(value())
|
|
107
|
+
setFocus(null)
|
|
78
108
|
} else if (e.key === "Escape") {
|
|
79
109
|
if (node) setFocus(null)
|
|
80
110
|
}
|
|
@@ -82,7 +112,7 @@ export function TextInput(props: TextInputProps) {
|
|
|
82
112
|
|
|
83
113
|
let handleTextInput = (e: any) => {
|
|
84
114
|
if (props.disabled) return
|
|
85
|
-
|
|
115
|
+
buffer.insertText(e.text ?? "")
|
|
86
116
|
setCaretOn(true)
|
|
87
117
|
}
|
|
88
118
|
|
|
@@ -98,37 +128,44 @@ export function TextInput(props: TextInputProps) {
|
|
|
98
128
|
let borderRadius = () => props.style?.borderRadius ?? theme.radius.sm
|
|
99
129
|
|
|
100
130
|
let showPlaceholder = () => !focused() && value().length === 0 && (props.placeholder ?? "").length > 0
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// the
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
let showCaret = () => focused() && caretOn() && !showPlaceholder()
|
|
132
|
+
|
|
133
|
+
// The text is split at the caret into two nodes with a zero-size anchor view
|
|
134
|
+
// between them. Flow places the anchor at the caret x (the before-text width),
|
|
135
|
+
// and the caret is a detached d-rect inside it: detached nodes take no layout
|
|
136
|
+
// slot, so the anchor stays zero-width and the after-text is not shifted. The
|
|
137
|
+
// anchor sits at the row's vertical center (alignItems center, zero height),
|
|
138
|
+
// so the caret is offset up by half its height to straddle it. While the
|
|
139
|
+
// placeholder shows, value() is "" so the slices and the scroll offset are 0
|
|
140
|
+
// with no special case. The viewport node is the inner scroll container;
|
|
141
|
+
// createCaretScroll reads its laid-out width after layout, keeps the caret in
|
|
142
|
+
// view, and flushes the offset before paint.
|
|
143
|
+
let beforeCaret = () => value().slice(0, buffer.caret())
|
|
144
|
+
let afterCaret = () => value().slice(buffer.caret())
|
|
145
|
+
let scrollX = createCaretScroll(
|
|
146
|
+
() => viewport,
|
|
147
|
+
() => ({
|
|
148
|
+
text: value(),
|
|
149
|
+
fontSize: theme.text.body.size,
|
|
150
|
+
caret: buffer.caret(),
|
|
151
|
+
// Constant, not tied to caret visibility: the caret's footprint does not
|
|
152
|
+
// change as it blinks, so reserving the column only when shown would swing
|
|
153
|
+
// the scroll offset every blink and shift text that exactly fills the box.
|
|
154
|
+
caretWidth: CARET_WIDTH,
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
116
157
|
|
|
117
|
-
let
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
158
|
+
let textStyle = (color: string) => ({
|
|
159
|
+
fontSize: theme.text.body.size,
|
|
160
|
+
lineHeight: theme.text.body.lineHeight,
|
|
161
|
+
color,
|
|
162
|
+
maxLines: 1,
|
|
163
|
+
flexShrink: 0,
|
|
164
|
+
})
|
|
125
165
|
|
|
126
166
|
return (
|
|
127
167
|
<view
|
|
128
|
-
ref={(n: { id: number }) =>
|
|
129
|
-
node = n
|
|
130
|
-
if (props.autoFocus) setFocus(n.id)
|
|
131
|
-
}}
|
|
168
|
+
ref={(n: { id: number }) => (node = n)}
|
|
132
169
|
flexDirection="row"
|
|
133
170
|
alignItems="center"
|
|
134
171
|
paddingLeft={theme.spacing.md}
|
|
@@ -161,18 +198,24 @@ export function TextInput(props: TextInputProps) {
|
|
|
161
198
|
overflow="hidden"
|
|
162
199
|
scrollX={scrollX()}
|
|
163
200
|
>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
{showPlaceholder() ? (
|
|
202
|
+
<text {...textStyle(theme.color.textMuted)}>{props.placeholder ?? ""}</text>
|
|
203
|
+
) : (
|
|
204
|
+
<view flexDirection="row" alignItems="center" flexShrink={0}>
|
|
205
|
+
<text {...textStyle(textColor())}>{beforeCaret()}</text>
|
|
206
|
+
{showCaret() ? (
|
|
207
|
+
<view>
|
|
208
|
+
<d-rect
|
|
209
|
+
color={textColor()}
|
|
210
|
+
y={-theme.text.body.size / 2}
|
|
211
|
+
w={CARET_WIDTH}
|
|
212
|
+
h={theme.text.body.size}
|
|
213
|
+
/>
|
|
214
|
+
</view>
|
|
215
|
+
) : null}
|
|
216
|
+
<text {...textStyle(textColor())}>{afterCaret()}</text>
|
|
217
|
+
</view>
|
|
218
|
+
)}
|
|
176
219
|
</view>
|
|
177
220
|
</view>
|
|
178
221
|
)
|
package/src/window.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { LayoutProps } from "@solidrt/core"
|
|
1
|
+
import type { LayoutProps, PointerProps } from "@solidrt/core"
|
|
2
2
|
import type { StyleProps } from "./types"
|
|
3
3
|
|
|
4
|
-
export interface WindowProps {
|
|
4
|
+
export interface WindowProps extends PointerProps {
|
|
5
5
|
children?: any
|
|
6
6
|
title?: string
|
|
7
7
|
fullscreen?: boolean
|
|
@@ -21,6 +21,18 @@ export function Window(props: WindowProps) {
|
|
|
21
21
|
fullscreen={props.fullscreen}
|
|
22
22
|
vsync={props.vsync}
|
|
23
23
|
fps={props.fps}
|
|
24
|
+
onPointerEnter={props.onPointerEnter}
|
|
25
|
+
onPointerLeave={props.onPointerLeave}
|
|
26
|
+
onPointerDown={props.onPointerDown}
|
|
27
|
+
onPointerUp={props.onPointerUp}
|
|
28
|
+
onPointerMove={props.onPointerMove}
|
|
29
|
+
onWheel={props.onWheel}
|
|
30
|
+
onFocus={props.onFocus}
|
|
31
|
+
onBlur={props.onBlur}
|
|
32
|
+
onKeyDown={props.onKeyDown}
|
|
33
|
+
onKeyUp={props.onKeyUp}
|
|
34
|
+
onTextInput={props.onTextInput}
|
|
35
|
+
pointerEvents={props.pointerEvents}
|
|
24
36
|
>
|
|
25
37
|
{props.style?.backgroundColor != null ? (
|
|
26
38
|
<d-rect color={props.style.backgroundColor} />
|