@effect-tui/react 0.16.0 → 2.0.1
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/README.md +9 -0
- package/dist/src/codeblock.d.ts +1 -1
- package/dist/src/codeblock.d.ts.map +1 -1
- package/dist/src/codeblock.js +2 -2
- package/dist/src/codeblock.js.map +1 -1
- package/dist/src/components/Markdown.js +3 -3
- package/dist/src/components/Markdown.js.map +1 -1
- package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
- package/dist/src/components/MultilineTextInput.js +133 -305
- package/dist/src/components/MultilineTextInput.js.map +1 -1
- package/dist/src/components/TextInput.d.ts.map +1 -1
- package/dist/src/components/TextInput.js +51 -98
- package/dist/src/components/TextInput.js.map +1 -1
- package/dist/src/components/text-editing.d.ts +61 -0
- package/dist/src/components/text-editing.d.ts.map +1 -1
- package/dist/src/components/text-editing.js +131 -0
- package/dist/src/components/text-editing.js.map +1 -1
- package/dist/src/hosts/base.d.ts +13 -2
- package/dist/src/hosts/base.d.ts.map +1 -1
- package/dist/src/hosts/base.js +74 -2
- package/dist/src/hosts/base.js.map +1 -1
- package/dist/src/hosts/box.d.ts +2 -2
- package/dist/src/hosts/box.d.ts.map +1 -1
- package/dist/src/hosts/box.js +29 -2
- package/dist/src/hosts/box.js.map +1 -1
- package/dist/src/hosts/canvas.d.ts +22 -2
- package/dist/src/hosts/canvas.d.ts.map +1 -1
- package/dist/src/hosts/canvas.js +99 -31
- package/dist/src/hosts/canvas.js.map +1 -1
- package/dist/src/hosts/codeblock.d.ts +8 -10
- package/dist/src/hosts/codeblock.d.ts.map +1 -1
- package/dist/src/hosts/codeblock.js +36 -33
- package/dist/src/hosts/codeblock.js.map +1 -1
- package/dist/src/hosts/flex-container.d.ts +2 -2
- package/dist/src/hosts/flex-container.d.ts.map +1 -1
- package/dist/src/hosts/flex-container.js +17 -2
- package/dist/src/hosts/flex-container.js.map +1 -1
- package/dist/src/hosts/index.d.ts +1 -1
- package/dist/src/hosts/index.d.ts.map +1 -1
- package/dist/src/hosts/index.js.map +1 -1
- package/dist/src/hosts/overlay-item.d.ts +2 -2
- package/dist/src/hosts/overlay-item.d.ts.map +1 -1
- package/dist/src/hosts/overlay-item.js +7 -2
- package/dist/src/hosts/overlay-item.js.map +1 -1
- package/dist/src/hosts/overlay.d.ts +2 -2
- package/dist/src/hosts/overlay.d.ts.map +1 -1
- package/dist/src/hosts/overlay.js +2 -2
- package/dist/src/hosts/overlay.js.map +1 -1
- package/dist/src/hosts/scroll.d.ts +7 -2
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +126 -45
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/hosts/single-child.d.ts.map +1 -1
- package/dist/src/hosts/single-child.js +2 -0
- package/dist/src/hosts/single-child.js.map +1 -1
- package/dist/src/hosts/spacer.d.ts +1 -1
- package/dist/src/hosts/spacer.d.ts.map +1 -1
- package/dist/src/hosts/spacer.js +6 -1
- package/dist/src/hosts/spacer.js.map +1 -1
- package/dist/src/hosts/text.d.ts +20 -15
- package/dist/src/hosts/text.d.ts.map +1 -1
- package/dist/src/hosts/text.js +104 -71
- package/dist/src/hosts/text.js.map +1 -1
- package/dist/src/hosts/zstack.d.ts +2 -2
- package/dist/src/hosts/zstack.d.ts.map +1 -1
- package/dist/src/hosts/zstack.js +7 -2
- package/dist/src/hosts/zstack.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/internal/renderer/index.d.ts.map +1 -1
- package/dist/src/internal/renderer/index.js +41 -16
- package/dist/src/internal/renderer/index.js.map +1 -1
- package/dist/src/internal/renderer/types.d.ts +4 -0
- package/dist/src/internal/renderer/types.d.ts.map +1 -1
- package/dist/src/motion/hooks.d.ts +1 -1
- package/dist/src/motion/hooks.js +1 -1
- package/dist/src/reconciler/host-config.js +2 -2
- package/dist/src/reconciler/host-config.js.map +1 -1
- package/dist/src/reconciler/types.d.ts +5 -1
- package/dist/src/reconciler/types.d.ts.map +1 -1
- package/dist/src/utils/border.d.ts +1 -1
- package/dist/src/utils/border.d.ts.map +1 -1
- package/dist/src/utils/border.js +2 -0
- package/dist/src/utils/border.js.map +1 -1
- package/dist/src/utils/index.d.ts +2 -1
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +2 -1
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/text-layout.d.ts +22 -0
- package/dist/src/utils/text-layout.d.ts.map +1 -0
- package/dist/src/utils/text-layout.js +37 -0
- package/dist/src/utils/text-layout.js.map +1 -0
- package/dist/src/utils/text-wrap.d.ts +26 -1
- package/dist/src/utils/text-wrap.d.ts.map +1 -1
- package/dist/src/utils/text-wrap.js +106 -11
- package/dist/src/utils/text-wrap.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/codeblock.tsx +2 -2
- package/src/components/Markdown.tsx +3 -3
- package/src/components/MultilineTextInput.tsx +138 -344
- package/src/components/TextInput.tsx +54 -99
- package/src/components/text-editing.ts +180 -0
- package/src/hosts/base.ts +86 -3
- package/src/hosts/box.ts +37 -2
- package/src/hosts/canvas.ts +120 -31
- package/src/hosts/codeblock.ts +46 -33
- package/src/hosts/flex-container.ts +21 -2
- package/src/hosts/index.ts +1 -1
- package/src/hosts/overlay-item.ts +8 -2
- package/src/hosts/overlay.ts +2 -2
- package/src/hosts/scroll.ts +142 -45
- package/src/hosts/single-child.ts +2 -0
- package/src/hosts/spacer.ts +6 -1
- package/src/hosts/text.ts +122 -75
- package/src/hosts/zstack.ts +7 -2
- package/src/index.ts +1 -1
- package/src/internal/renderer/index.ts +53 -20
- package/src/internal/renderer/types.ts +4 -0
- package/src/motion/hooks.ts +1 -1
- package/src/reconciler/host-config.ts +2 -2
- package/src/reconciler/types.ts +7 -1
- package/src/utils/border.ts +11 -1
- package/src/utils/index.ts +15 -1
- package/src/utils/text-layout.ts +65 -0
- package/src/utils/text-wrap.ts +135 -13
|
@@ -7,12 +7,15 @@ import {
|
|
|
7
7
|
deleteCharBackward,
|
|
8
8
|
deleteCharForward,
|
|
9
9
|
deleteWordBackward,
|
|
10
|
+
deleteWordForward,
|
|
10
11
|
insertText,
|
|
11
12
|
killToEnd,
|
|
12
13
|
killToStart,
|
|
13
14
|
matchNextWord,
|
|
14
15
|
matchPrevWord,
|
|
16
|
+
resolveTextInputAction,
|
|
15
17
|
type TextState,
|
|
18
|
+
type TextKeyEvent,
|
|
16
19
|
transposeChars,
|
|
17
20
|
} from "./text-editing.js"
|
|
18
21
|
|
|
@@ -97,17 +100,20 @@ export function TextInput({
|
|
|
97
100
|
}, [value, cursorPos])
|
|
98
101
|
|
|
99
102
|
const handleKey = useCallback(
|
|
100
|
-
(key:
|
|
103
|
+
(key: TextKeyEvent) => {
|
|
101
104
|
if (!focused) return
|
|
102
105
|
|
|
106
|
+
const action = resolveTextInputAction(key)
|
|
107
|
+
if (!action) return
|
|
108
|
+
|
|
103
109
|
const state: TextState = { text: value, cursor: cursorPos, killRing }
|
|
104
110
|
|
|
105
111
|
// Helper to apply an edit result
|
|
106
|
-
const applyEdit = (result: { state: TextState; changed: boolean }) => {
|
|
112
|
+
const applyEdit = (result: { state: TextState; changed: boolean }, options?: { keepKillRing?: boolean }) => {
|
|
107
113
|
if (result.changed) {
|
|
108
114
|
onChange(result.state.text)
|
|
109
115
|
setCursorPos(result.state.cursor)
|
|
110
|
-
if (result.state.killRing !== killRing) {
|
|
116
|
+
if (!options?.keepKillRing && result.state.killRing !== killRing) {
|
|
111
117
|
setKillRing(result.state.killRing)
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -126,114 +132,63 @@ export function TextInput({
|
|
|
126
132
|
setCursorPos(match ? cursorPos + match.length : value.length)
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
switch (
|
|
130
|
-
case "left":
|
|
131
|
-
|
|
132
|
-
moveToPrevWord()
|
|
133
|
-
} else {
|
|
134
|
-
setCursorPos(Math.max(0, cursorPos - 1))
|
|
135
|
-
}
|
|
135
|
+
switch (action.type) {
|
|
136
|
+
case "move-left":
|
|
137
|
+
setCursorPos(Math.max(0, cursorPos - 1))
|
|
136
138
|
break
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (key.meta) {
|
|
140
|
-
moveToNextWord()
|
|
141
|
-
} else {
|
|
142
|
-
setCursorPos(Math.min(value.length, cursorPos + 1))
|
|
143
|
-
}
|
|
139
|
+
case "move-right":
|
|
140
|
+
setCursorPos(Math.min(value.length, cursorPos + 1))
|
|
144
141
|
break
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
case "move-word-left":
|
|
143
|
+
moveToPrevWord()
|
|
144
|
+
break
|
|
145
|
+
case "move-word-right":
|
|
146
|
+
moveToNextWord()
|
|
147
|
+
break
|
|
148
|
+
case "move-start":
|
|
149
|
+
case "move-doc-start":
|
|
147
150
|
setCursorPos(0)
|
|
148
151
|
break
|
|
149
|
-
|
|
150
|
-
case "end":
|
|
152
|
+
case "move-end":
|
|
153
|
+
case "move-doc-end":
|
|
151
154
|
setCursorPos(value.length)
|
|
152
155
|
break
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (key.meta) {
|
|
156
|
-
// Option+Backspace: Delete to previous word boundary
|
|
157
|
-
const beforeCursor = value.slice(0, cursorPos)
|
|
158
|
-
const match = matchPrevWord(beforeCursor)
|
|
159
|
-
if (match) {
|
|
160
|
-
const newPos = cursorPos - match.length
|
|
161
|
-
onChange(value.slice(0, newPos) + value.slice(cursorPos))
|
|
162
|
-
setCursorPos(newPos)
|
|
163
|
-
} else if (cursorPos > 0) {
|
|
164
|
-
onChange(value.slice(cursorPos))
|
|
165
|
-
setCursorPos(0)
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
applyEdit(deleteCharBackward(state))
|
|
169
|
-
}
|
|
156
|
+
case "delete-backward":
|
|
157
|
+
applyEdit(deleteCharBackward(state))
|
|
170
158
|
break
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
159
|
+
case "delete-forward":
|
|
160
|
+
applyEdit(deleteCharForward(state))
|
|
161
|
+
break
|
|
162
|
+
case "delete-word-backward":
|
|
163
|
+
applyEdit(deleteWordBackward(state), { keepKillRing: action.scope === "line" })
|
|
164
|
+
break
|
|
165
|
+
case "delete-word-forward":
|
|
166
|
+
applyEdit(deleteWordForward(state), { keepKillRing: action.scope === "line" })
|
|
167
|
+
break
|
|
168
|
+
case "kill-to-end":
|
|
169
|
+
applyEdit(killToEnd(state))
|
|
170
|
+
break
|
|
171
|
+
case "kill-to-start":
|
|
172
|
+
applyEdit(killToStart(state))
|
|
173
|
+
break
|
|
174
|
+
case "transpose":
|
|
175
|
+
applyEdit(transposeChars(state))
|
|
176
|
+
break
|
|
177
|
+
case "yank":
|
|
178
|
+
applyEdit(insertText(state, killRing))
|
|
179
|
+
break
|
|
180
|
+
case "insert":
|
|
181
|
+
applyEdit(insertText(state, action.text))
|
|
185
182
|
break
|
|
186
|
-
|
|
187
183
|
case "enter":
|
|
184
|
+
case "submit":
|
|
188
185
|
onSubmit?.(value)
|
|
189
186
|
break
|
|
190
|
-
|
|
191
|
-
case "escape":
|
|
187
|
+
case "cancel":
|
|
192
188
|
onCancel?.()
|
|
193
189
|
break
|
|
194
|
-
|
|
195
|
-
case "
|
|
196
|
-
case "space":
|
|
197
|
-
if (key.ctrl && key.text) {
|
|
198
|
-
// Emacs-style keybindings
|
|
199
|
-
switch (key.text) {
|
|
200
|
-
case "a":
|
|
201
|
-
setCursorPos(0)
|
|
202
|
-
break
|
|
203
|
-
case "e":
|
|
204
|
-
setCursorPos(value.length)
|
|
205
|
-
break
|
|
206
|
-
case "b":
|
|
207
|
-
setCursorPos(Math.max(0, cursorPos - 1))
|
|
208
|
-
break
|
|
209
|
-
case "f":
|
|
210
|
-
setCursorPos(Math.min(value.length, cursorPos + 1))
|
|
211
|
-
break
|
|
212
|
-
case "d":
|
|
213
|
-
applyEdit(deleteCharForward(state))
|
|
214
|
-
break
|
|
215
|
-
case "h":
|
|
216
|
-
applyEdit(deleteCharBackward(state))
|
|
217
|
-
break
|
|
218
|
-
case "k":
|
|
219
|
-
applyEdit(killToEnd(state))
|
|
220
|
-
break
|
|
221
|
-
case "u":
|
|
222
|
-
applyEdit(killToStart(state))
|
|
223
|
-
break
|
|
224
|
-
case "w":
|
|
225
|
-
applyEdit(deleteWordBackward(state))
|
|
226
|
-
break
|
|
227
|
-
case "t":
|
|
228
|
-
applyEdit(transposeChars(state))
|
|
229
|
-
break
|
|
230
|
-
case "y":
|
|
231
|
-
applyEdit(insertText(state, killRing))
|
|
232
|
-
break
|
|
233
|
-
}
|
|
234
|
-
} else if (key.text && !key.meta) {
|
|
235
|
-
applyEdit(insertText(state, key.text))
|
|
236
|
-
}
|
|
190
|
+
case "move-up":
|
|
191
|
+
case "move-down":
|
|
237
192
|
break
|
|
238
193
|
}
|
|
239
194
|
},
|
|
@@ -268,7 +223,7 @@ export function TextInput({
|
|
|
268
223
|
|
|
269
224
|
// Clear the line with background color
|
|
270
225
|
if (bg !== undefined) {
|
|
271
|
-
ctx.
|
|
226
|
+
ctx.fillRect(0, 0, ctx.width, 1, " ", { bg })
|
|
272
227
|
}
|
|
273
228
|
|
|
274
229
|
// Calculate scroll offset to keep cursor visible
|
|
@@ -25,6 +25,40 @@ export interface MultilineState {
|
|
|
25
25
|
killRing: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface TextKeyEvent {
|
|
29
|
+
name: string
|
|
30
|
+
text?: string
|
|
31
|
+
ctrl?: boolean
|
|
32
|
+
meta?: boolean
|
|
33
|
+
shift?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type WordScope = "line" | "document"
|
|
37
|
+
|
|
38
|
+
export type TextInputAction =
|
|
39
|
+
| { type: "move-left" }
|
|
40
|
+
| { type: "move-right" }
|
|
41
|
+
| { type: "move-up" }
|
|
42
|
+
| { type: "move-down" }
|
|
43
|
+
| { type: "move-start" }
|
|
44
|
+
| { type: "move-end" }
|
|
45
|
+
| { type: "move-doc-start" }
|
|
46
|
+
| { type: "move-doc-end" }
|
|
47
|
+
| { type: "move-word-left" }
|
|
48
|
+
| { type: "move-word-right" }
|
|
49
|
+
| { type: "delete-backward" }
|
|
50
|
+
| { type: "delete-forward" }
|
|
51
|
+
| { type: "delete-word-backward"; scope: WordScope }
|
|
52
|
+
| { type: "delete-word-forward"; scope: WordScope }
|
|
53
|
+
| { type: "kill-to-end" }
|
|
54
|
+
| { type: "kill-to-start" }
|
|
55
|
+
| { type: "transpose" }
|
|
56
|
+
| { type: "yank" }
|
|
57
|
+
| { type: "insert"; text: string }
|
|
58
|
+
| { type: "enter" }
|
|
59
|
+
| { type: "submit" }
|
|
60
|
+
| { type: "cancel" }
|
|
61
|
+
|
|
28
62
|
/** Result of an edit operation */
|
|
29
63
|
export interface EditResult<T> {
|
|
30
64
|
state: T
|
|
@@ -37,6 +71,79 @@ export interface EditResult<T> {
|
|
|
37
71
|
|
|
38
72
|
export { matchNextWord, matchPrevWord }
|
|
39
73
|
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Keybinding resolution (shared by TextInput and MultilineTextInput)
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
export function resolveTextInputAction(key: TextKeyEvent): TextInputAction | null {
|
|
79
|
+
switch (key.name) {
|
|
80
|
+
case "left":
|
|
81
|
+
return key.meta ? { type: "move-word-left" } : { type: "move-left" }
|
|
82
|
+
case "right":
|
|
83
|
+
return key.meta ? { type: "move-word-right" } : { type: "move-right" }
|
|
84
|
+
case "up":
|
|
85
|
+
return key.meta ? { type: "move-doc-start" } : { type: "move-up" }
|
|
86
|
+
case "down":
|
|
87
|
+
return key.meta ? { type: "move-doc-end" } : { type: "move-down" }
|
|
88
|
+
case "home":
|
|
89
|
+
return { type: "move-start" }
|
|
90
|
+
case "end":
|
|
91
|
+
return { type: "move-end" }
|
|
92
|
+
case "backspace":
|
|
93
|
+
return key.meta
|
|
94
|
+
? { type: "delete-word-backward", scope: "line" }
|
|
95
|
+
: { type: "delete-backward" }
|
|
96
|
+
case "delete":
|
|
97
|
+
return key.meta
|
|
98
|
+
? { type: "delete-word-forward", scope: "line" }
|
|
99
|
+
: { type: "delete-forward" }
|
|
100
|
+
case "enter":
|
|
101
|
+
return key.ctrl || key.meta ? { type: "submit" } : { type: "enter" }
|
|
102
|
+
case "escape":
|
|
103
|
+
return { type: "cancel" }
|
|
104
|
+
case "char":
|
|
105
|
+
case "space": {
|
|
106
|
+
if (key.ctrl && key.text) {
|
|
107
|
+
switch (key.text) {
|
|
108
|
+
case "a":
|
|
109
|
+
return { type: "move-start" }
|
|
110
|
+
case "e":
|
|
111
|
+
return { type: "move-end" }
|
|
112
|
+
case "b":
|
|
113
|
+
return { type: "move-left" }
|
|
114
|
+
case "f":
|
|
115
|
+
return { type: "move-right" }
|
|
116
|
+
case "n":
|
|
117
|
+
return { type: "move-down" }
|
|
118
|
+
case "p":
|
|
119
|
+
return { type: "move-up" }
|
|
120
|
+
case "d":
|
|
121
|
+
return { type: "delete-forward" }
|
|
122
|
+
case "h":
|
|
123
|
+
return { type: "delete-backward" }
|
|
124
|
+
case "k":
|
|
125
|
+
return { type: "kill-to-end" }
|
|
126
|
+
case "u":
|
|
127
|
+
return { type: "kill-to-start" }
|
|
128
|
+
case "w":
|
|
129
|
+
return { type: "delete-word-backward", scope: "document" }
|
|
130
|
+
case "t":
|
|
131
|
+
return { type: "transpose" }
|
|
132
|
+
case "y":
|
|
133
|
+
return { type: "yank" }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (key.text && !key.meta) {
|
|
138
|
+
return { type: "insert", text: key.text }
|
|
139
|
+
}
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
40
147
|
// ============================================================================
|
|
41
148
|
// Single-line operations (for TextInput)
|
|
42
149
|
// ============================================================================
|
|
@@ -64,6 +171,35 @@ export function deleteWordBackward(state: TextState): EditResult<TextState> {
|
|
|
64
171
|
}
|
|
65
172
|
}
|
|
66
173
|
|
|
174
|
+
/** Delete word forward */
|
|
175
|
+
export function deleteWordForward(state: TextState): EditResult<TextState> {
|
|
176
|
+
const { text, cursor } = state
|
|
177
|
+
if (cursor >= text.length) {
|
|
178
|
+
return { state, changed: false }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const afterCursor = text.slice(cursor)
|
|
182
|
+
const match = matchNextWord(afterCursor)
|
|
183
|
+
|
|
184
|
+
if (match) {
|
|
185
|
+
const newText = text.slice(0, cursor) + text.slice(cursor + match.length)
|
|
186
|
+
return {
|
|
187
|
+
state: { text: newText, cursor, killRing: match },
|
|
188
|
+
changed: true,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const killed = text.slice(cursor)
|
|
193
|
+
if (!killed) {
|
|
194
|
+
return { state, changed: false }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
state: { text: text.slice(0, cursor), cursor, killRing: killed },
|
|
199
|
+
changed: true,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
67
203
|
/** Kill to end of line */
|
|
68
204
|
export function killToEnd(state: TextState): EditResult<TextState> {
|
|
69
205
|
const { text, cursor } = state
|
|
@@ -247,6 +383,50 @@ export function deleteWordBackwardMultiline(state: MultilineState): EditResult<M
|
|
|
247
383
|
return { state, changed: false }
|
|
248
384
|
}
|
|
249
385
|
|
|
386
|
+
/** Delete word forward in multiline (line-scoped) */
|
|
387
|
+
export function deleteWordForwardMultiline(state: MultilineState): EditResult<MultilineState> {
|
|
388
|
+
const { lines, cursor } = state
|
|
389
|
+
const line = lines[cursor.row]
|
|
390
|
+
const lineLen = graphemes(line).length
|
|
391
|
+
|
|
392
|
+
if (cursor.col < lineLen) {
|
|
393
|
+
const charIdx = graphemeColToCharIdx(line, cursor.col)
|
|
394
|
+
const afterCursor = line.slice(charIdx)
|
|
395
|
+
const match = matchNextWord(afterCursor)
|
|
396
|
+
const newLines = [...lines]
|
|
397
|
+
|
|
398
|
+
if (match) {
|
|
399
|
+
const newLine = line.slice(0, charIdx) + line.slice(charIdx + match.length)
|
|
400
|
+
newLines[cursor.row] = newLine
|
|
401
|
+
return {
|
|
402
|
+
state: {
|
|
403
|
+
lines: newLines,
|
|
404
|
+
cursor,
|
|
405
|
+
killRing: match,
|
|
406
|
+
},
|
|
407
|
+
changed: true,
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const killed = line.slice(charIdx)
|
|
412
|
+
if (!killed) {
|
|
413
|
+
return { state, changed: false }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
newLines[cursor.row] = line.slice(0, charIdx)
|
|
417
|
+
return {
|
|
418
|
+
state: {
|
|
419
|
+
lines: newLines,
|
|
420
|
+
cursor,
|
|
421
|
+
killRing: killed,
|
|
422
|
+
},
|
|
423
|
+
changed: true,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { state, changed: false }
|
|
428
|
+
}
|
|
429
|
+
|
|
250
430
|
/** Kill to end of line in multiline */
|
|
251
431
|
export function killToEndMultiline(state: MultilineState): EditResult<MultilineState> {
|
|
252
432
|
const { lines, cursor } = state
|
package/src/hosts/base.ts
CHANGED
|
@@ -67,6 +67,12 @@ export abstract class BaseHost implements HostInstance {
|
|
|
67
67
|
parent: HostInstance | null = null
|
|
68
68
|
children: HostInstance[] = []
|
|
69
69
|
rect: Rect | null = null
|
|
70
|
+
private _layoutDirty = true
|
|
71
|
+
private _renderDirty = true
|
|
72
|
+
private _lastMeasureW = -1
|
|
73
|
+
private _lastMeasureH = -1
|
|
74
|
+
private _lastMeasuredSize: Size | null = null
|
|
75
|
+
private _lastLayoutRect: Rect | null = null
|
|
70
76
|
|
|
71
77
|
// Greedy layout - expands to fill remaining space
|
|
72
78
|
// undefined = not greedy (hug content)
|
|
@@ -117,17 +123,38 @@ export abstract class BaseHost implements HostInstance {
|
|
|
117
123
|
* Optional pre-frame hook. BaseHost will call prepareSelf() and then recurse into children.
|
|
118
124
|
*/
|
|
119
125
|
prepareFrame(): void {
|
|
120
|
-
this.
|
|
126
|
+
this.ensurePrepared()
|
|
121
127
|
for (const child of this.children) {
|
|
122
128
|
child.prepareFrame?.()
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
/** Override in subclasses to precompute caches once per frame. */
|
|
127
|
-
protected prepareSelf(): void {
|
|
133
|
+
protected prepareSelf(_layoutDirty: boolean, _renderDirty: boolean): void {
|
|
128
134
|
// Default no-op
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
protected ensurePrepared(): void {
|
|
138
|
+
if (!this._layoutDirty && !this._renderDirty) return
|
|
139
|
+
const layoutDirty = this._layoutDirty
|
|
140
|
+
const renderDirty = this._renderDirty
|
|
141
|
+
this._renderDirty = false
|
|
142
|
+
this.prepareSelf(layoutDirty, renderDirty)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
invalidateLayout(): void {
|
|
146
|
+
this._layoutDirty = true
|
|
147
|
+
this._renderDirty = true
|
|
148
|
+
this._lastMeasuredSize = null
|
|
149
|
+
this.ctx.requestRender()
|
|
150
|
+
this.parent?.invalidateLayout?.()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
invalidateRender(): void {
|
|
154
|
+
this._renderDirty = true
|
|
155
|
+
this.ctx.requestRender()
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
/**
|
|
132
159
|
* Resolve a prop that may be a MotionValue/ColorMotionValue.
|
|
133
160
|
* If it's a spring, subscribes to changes and returns current value.
|
|
@@ -204,10 +231,39 @@ export abstract class BaseHost implements HostInstance {
|
|
|
204
231
|
this._springSubscriptions.clear()
|
|
205
232
|
}
|
|
206
233
|
|
|
207
|
-
|
|
234
|
+
measure(maxW: number, maxH: number): Size {
|
|
235
|
+
const constraintsUnchanged = this._lastMeasureW === maxW && this._lastMeasureH === maxH
|
|
236
|
+
if (!this._layoutDirty && constraintsUnchanged && this._lastMeasuredSize) {
|
|
237
|
+
return this._lastMeasuredSize
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!constraintsUnchanged) {
|
|
241
|
+
this._layoutDirty = true
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const size = this.measureSelf(maxW, maxH)
|
|
245
|
+
this._lastMeasureW = maxW
|
|
246
|
+
this._lastMeasureH = maxH
|
|
247
|
+
this._lastMeasuredSize = size
|
|
248
|
+
return size
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected abstract measureSelf(maxW: number, maxH: number): Size
|
|
208
252
|
abstract render(buffer: CellBuffer, palette: Palette): void
|
|
209
253
|
|
|
210
254
|
layout(rect: Rect): void {
|
|
255
|
+
const prev = this._lastLayoutRect
|
|
256
|
+
const rectUnchanged =
|
|
257
|
+
prev?.x === rect.x && prev?.y === rect.y && prev?.w === rect.w && prev?.h === rect.h
|
|
258
|
+
|
|
259
|
+
if (!this._layoutDirty && rectUnchanged) return
|
|
260
|
+
|
|
261
|
+
this.layoutSelf(rect)
|
|
262
|
+
this._layoutDirty = false
|
|
263
|
+
this._lastLayoutRect = this.rect ?? rect
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
protected layoutSelf(rect: Rect): void {
|
|
211
267
|
this.layoutWithConstraints(rect)
|
|
212
268
|
}
|
|
213
269
|
|
|
@@ -319,6 +375,7 @@ export abstract class BaseHost implements HostInstance {
|
|
|
319
375
|
updateProps(props: Record<string, unknown>): void {
|
|
320
376
|
// Greedy layout - reset to undefined unless explicitly set
|
|
321
377
|
// Subclasses like Spacer/Scroll set their own default before calling super
|
|
378
|
+
const prevGreedy = this.greedy
|
|
322
379
|
if ("greedy" in props) {
|
|
323
380
|
const greedy = props.greedy
|
|
324
381
|
if (greedy === true) {
|
|
@@ -336,6 +393,12 @@ export abstract class BaseHost implements HostInstance {
|
|
|
336
393
|
}
|
|
337
394
|
|
|
338
395
|
// Frame constraints - only accept valid numbers (ignore strings like "100%")
|
|
396
|
+
const prevFrameWidth = this.frameWidth
|
|
397
|
+
const prevFrameHeight = this.frameHeight
|
|
398
|
+
const prevFrameMinWidth = this.frameMinWidth
|
|
399
|
+
const prevFrameMaxWidth = this.frameMaxWidth
|
|
400
|
+
const prevFrameMinHeight = this.frameMinHeight
|
|
401
|
+
const prevFrameMaxHeight = this.frameMaxHeight
|
|
339
402
|
this.frameWidth = typeof props.width === "number" ? props.width : undefined
|
|
340
403
|
this.frameHeight = typeof props.height === "number" ? props.height : undefined
|
|
341
404
|
this.frameMinWidth = typeof props.minWidth === "number" ? props.minWidth : undefined
|
|
@@ -345,6 +408,19 @@ export abstract class BaseHost implements HostInstance {
|
|
|
345
408
|
|
|
346
409
|
// onLayout callback
|
|
347
410
|
this.onLayout = typeof props.onLayout === "function" ? (props.onLayout as typeof this.onLayout) : undefined
|
|
411
|
+
|
|
412
|
+
const layoutChanged =
|
|
413
|
+
prevGreedy !== this.greedy ||
|
|
414
|
+
prevFrameWidth !== this.frameWidth ||
|
|
415
|
+
prevFrameHeight !== this.frameHeight ||
|
|
416
|
+
prevFrameMinWidth !== this.frameMinWidth ||
|
|
417
|
+
prevFrameMaxWidth !== this.frameMaxWidth ||
|
|
418
|
+
prevFrameMinHeight !== this.frameMinHeight ||
|
|
419
|
+
prevFrameMaxHeight !== this.frameMaxHeight
|
|
420
|
+
|
|
421
|
+
if (layoutChanged) {
|
|
422
|
+
this.invalidateLayout()
|
|
423
|
+
}
|
|
348
424
|
}
|
|
349
425
|
|
|
350
426
|
/**
|
|
@@ -353,7 +429,11 @@ export abstract class BaseHost implements HostInstance {
|
|
|
353
429
|
*/
|
|
354
430
|
protected applyGreedyDefault(props: Record<string, unknown>, fallback: number): void {
|
|
355
431
|
if (!("greedy" in props)) {
|
|
432
|
+
const prevGreedy = this.greedy
|
|
356
433
|
this.greedy = fallback
|
|
434
|
+
if (prevGreedy !== this.greedy) {
|
|
435
|
+
this.invalidateLayout()
|
|
436
|
+
}
|
|
357
437
|
}
|
|
358
438
|
}
|
|
359
439
|
|
|
@@ -366,6 +446,7 @@ export abstract class BaseHost implements HostInstance {
|
|
|
366
446
|
appendChild(child: HostInstance): void {
|
|
367
447
|
this.children.push(child)
|
|
368
448
|
child.parent = this
|
|
449
|
+
this.invalidateLayout()
|
|
369
450
|
}
|
|
370
451
|
|
|
371
452
|
removeChild(child: HostInstance): void {
|
|
@@ -374,6 +455,7 @@ export abstract class BaseHost implements HostInstance {
|
|
|
374
455
|
this.children.splice(idx, 1)
|
|
375
456
|
child.parent = null
|
|
376
457
|
}
|
|
458
|
+
this.invalidateLayout()
|
|
377
459
|
}
|
|
378
460
|
|
|
379
461
|
insertBefore(child: HostInstance, before: HostInstance): void {
|
|
@@ -388,6 +470,7 @@ export abstract class BaseHost implements HostInstance {
|
|
|
388
470
|
this.children.push(child)
|
|
389
471
|
}
|
|
390
472
|
child.parent = this
|
|
473
|
+
this.invalidateLayout()
|
|
391
474
|
}
|
|
392
475
|
|
|
393
476
|
/**
|
package/src/hosts/box.ts
CHANGED
|
@@ -65,7 +65,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
65
65
|
return this.borderThickness + titleHeight + this.padding.top + this.padding.bottom + this.borderThickness
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
protected measureSelf(maxW: number, maxH: number): Size {
|
|
69
69
|
// Apply frame constraints first
|
|
70
70
|
const constrained = this.constrainProposal(maxW, maxH)
|
|
71
71
|
|
|
@@ -91,7 +91,7 @@ export class BoxHost extends SingleChildHost {
|
|
|
91
91
|
return this.constrainResult(naturalSize)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
override
|
|
94
|
+
protected override layoutSelf(rect: Rect): void {
|
|
95
95
|
const layoutRect = this.layoutWithConstraints(rect)
|
|
96
96
|
|
|
97
97
|
const t = this.borderThickness
|
|
@@ -173,6 +173,15 @@ export class BoxHost extends SingleChildHost {
|
|
|
173
173
|
|
|
174
174
|
override updateProps(props: Record<string, unknown>): void {
|
|
175
175
|
super.updateProps(props)
|
|
176
|
+
const prevPadding = this.padding
|
|
177
|
+
const prevBorder = this.border
|
|
178
|
+
const prevBorderColor = this.borderColor
|
|
179
|
+
const prevBg = this.bg
|
|
180
|
+
const prevTitle = this.title
|
|
181
|
+
const prevTitleColor = this.titleColor
|
|
182
|
+
const prevTitleBold = this.titleBold
|
|
183
|
+
const prevTitleDivider = this.titleDivider
|
|
184
|
+
|
|
176
185
|
this.padding = resolvePadding(props.padding as BoxProps["padding"])
|
|
177
186
|
this.border = (props.border as BorderKind | undefined) ?? "none"
|
|
178
187
|
// Color props support MotionValue/ColorMotionValue - auto-subscribe and animate
|
|
@@ -188,5 +197,31 @@ export class BoxHost extends SingleChildHost {
|
|
|
188
197
|
}) as Color | undefined
|
|
189
198
|
this.titleBold = Boolean(props.titleBold)
|
|
190
199
|
this.titleDivider = Boolean(props.titleDivider)
|
|
200
|
+
|
|
201
|
+
const paddingChanged =
|
|
202
|
+
prevPadding.top !== this.padding.top ||
|
|
203
|
+
prevPadding.right !== this.padding.right ||
|
|
204
|
+
prevPadding.bottom !== this.padding.bottom ||
|
|
205
|
+
prevPadding.left !== this.padding.left
|
|
206
|
+
const layoutChanged =
|
|
207
|
+
paddingChanged ||
|
|
208
|
+
prevBorder !== this.border ||
|
|
209
|
+
prevTitle !== this.title ||
|
|
210
|
+
prevTitleDivider !== this.titleDivider
|
|
211
|
+
|
|
212
|
+
if (layoutChanged) {
|
|
213
|
+
this.invalidateLayout()
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const renderChanged =
|
|
218
|
+
prevBorderColor !== this.borderColor ||
|
|
219
|
+
prevBg !== this.bg ||
|
|
220
|
+
prevTitleColor !== this.titleColor ||
|
|
221
|
+
prevTitleBold !== this.titleBold
|
|
222
|
+
|
|
223
|
+
if (renderChanged) {
|
|
224
|
+
this.invalidateRender()
|
|
225
|
+
}
|
|
191
226
|
}
|
|
192
227
|
}
|