@promptctl/rich-js 0.24.1 → 0.25.0
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 +7 -0
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +175 -10
- package/dist/core/animation.js +283 -15
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cells.d.ts +23 -13
- package/dist/core/cells.js +58 -48
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +44 -39
- package/dist/core/easing.d.ts +30 -0
- package/dist/core/easing.js +65 -20
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +2 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +2 -2
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/pretty.js +9 -10
- package/dist/core/protocol.d.ts +100 -39
- package/dist/core/protocol.js +167 -21
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +25 -5
- package/dist/core/segment.d.ts +13 -5
- package/dist/core/segment.js +23 -9
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +13 -16
- package/dist/core/strip.js +119 -110
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +57 -28
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +116 -124
- package/dist/core/timeline.d.ts +30 -5
- package/dist/core/timeline.js +43 -7
- package/dist/core/tween.d.ts +1 -1
- package/dist/core/tween.js +5 -3
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +71 -37
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +24 -12
- package/dist/index.js +9 -4
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/align.d.ts +2 -3
- package/dist/renderables/align.js +0 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +3 -4
- package/dist/renderables/columns.js +2 -7
- package/dist/renderables/constrain.d.ts +2 -3
- package/dist/renderables/constrain.js +0 -3
- package/dist/renderables/effect.d.ts +132 -54
- package/dist/renderables/effect.js +188 -115
- package/dist/renderables/effects.d.ts +122 -11
- package/dist/renderables/effects.js +238 -94
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +16 -8
- package/dist/renderables/flexStrip.d.ts +2 -3
- package/dist/renderables/flexStrip.js +0 -4
- package/dist/renderables/frames.d.ts +35 -54
- package/dist/renderables/frames.js +43 -58
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +3 -4
- package/dist/renderables/group.js +0 -3
- package/dist/renderables/layout.d.ts +12 -12
- package/dist/renderables/layout.js +40 -46
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +4 -5
- package/dist/renderables/list-view.js +9 -13
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +88 -41
- package/dist/renderables/markdown-parse.d.ts +4 -4
- package/dist/renderables/markdown-parse.js +52 -46
- package/dist/renderables/markdown-url.js +11 -11
- package/dist/renderables/padding.d.ts +4 -5
- package/dist/renderables/padding.js +0 -3
- package/dist/renderables/panel.d.ts +14 -16
- package/dist/renderables/panel.js +6 -14
- package/dist/renderables/progress.d.ts +83 -34
- package/dist/renderables/progress.js +182 -95
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +32 -9
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.js +2 -1
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +35 -14
- package/dist/renderables/spinner.js +65 -25
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +3 -2
- package/dist/renderables/table.d.ts +34 -40
- package/dist/renderables/table.js +41 -63
- package/dist/renderables/traceback.d.ts +2 -2
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +27 -25
- package/dist/renderables/tree.js +104 -58
- package/dist/renderables/viewport.d.ts +75 -44
- package/dist/renderables/viewport.js +145 -82
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +5 -5
- package/dist/themes/ramp.d.ts +46 -3
- package/dist/themes/ramp.js +61 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +2 -0
- package/dist/widgets/focus-manager.js +6 -4
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +2 -3
- package/dist/widgets/static-item.js +0 -3
- package/dist/widgets/text-input.d.ts +28 -32
- package/dist/widgets/text-input.js +360 -346
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +14 -4
- package/dist/widgets/widget-app.js +36 -7
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -18
- package/package.json +8 -1
|
@@ -96,13 +96,72 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
96
96
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
97
97
|
done = true;
|
|
98
98
|
};
|
|
99
|
-
import {
|
|
99
|
+
import { action, runInAction } from "mobx";
|
|
100
|
+
import { observableRef } from "./observable.js";
|
|
100
101
|
import { Segment } from "../core/segment.js";
|
|
101
102
|
import { Style } from "../core/style.js";
|
|
102
|
-
import { cellLen, setCellSize, splitText, cellFitEnd,
|
|
103
|
-
import { drawable } from "../core/protocol.js";
|
|
103
|
+
import { cellLen, setCellSize, splitText, cellFitEnd, cellRows, clustersBefore, clustersFrom, asCellCol, cellCount, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
|
|
104
|
+
import { afterPass, drawable, render } from "../core/protocol.js";
|
|
105
|
+
import { State } from "../core/state.js";
|
|
106
|
+
import { Viewport } from "../renderables/viewport.js";
|
|
107
|
+
import { Animation } from "../core/animation.js";
|
|
108
|
+
import { beat } from "../core/easing.js";
|
|
109
|
+
import { Timeline } from "../core/timeline.js";
|
|
110
|
+
import { holdOf, shownAt, stampFrame } from "../core/animation.js";
|
|
111
|
+
import { levelOf } from "../core/anchor.js";
|
|
104
112
|
import { ThemedWidget } from "./themed-widget.js";
|
|
105
113
|
import { ink } from "./ink.js";
|
|
114
|
+
import { pattern } from "../core/pattern.js";
|
|
115
|
+
/** The row of `rows` the cursor at `cursor` sits on: the last that starts at or before it. */
|
|
116
|
+
function cursorRowIn(rows, cursor) {
|
|
117
|
+
let row = rows.length - 1;
|
|
118
|
+
while (row > 0 && rows[row].valueStart > cursor)
|
|
119
|
+
row--;
|
|
120
|
+
return Math.max(0, row);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The content row `shown`'s first row draws: what a viewport showing content
|
|
124
|
+
* stamped by `owner` scrolled it to. [LAW:no-silent-failure] Every content row
|
|
125
|
+
* is padded and stamped, so a first row without the stamp is a broken frame,
|
|
126
|
+
* not an offset of 0.
|
|
127
|
+
*/
|
|
128
|
+
function shownRow(shown, owner) {
|
|
129
|
+
if (shown.length === 0)
|
|
130
|
+
return 0;
|
|
131
|
+
const anchor = levelOf(Segment.anchorAt(shown, 0, 0), owner);
|
|
132
|
+
if (anchor === undefined)
|
|
133
|
+
throw new Error("TextInput: the viewport's first row carries no row of the field's content");
|
|
134
|
+
return anchor.row;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* [LAW:one-source-of-truth] A frame's layout, as the owner of every cell the
|
|
138
|
+
* field drew in it: a click and the scroll indicator read the layout of the
|
|
139
|
+
* frame they land on off its cells, never one a later frame kept.
|
|
140
|
+
*/
|
|
141
|
+
class Laid {
|
|
142
|
+
layout;
|
|
143
|
+
constructor(layout) {
|
|
144
|
+
this.layout = layout;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** The layout stamped on the cell `anchor` names, at whatever depth. */
|
|
148
|
+
function layoutOn(anchor) {
|
|
149
|
+
let a = anchor;
|
|
150
|
+
while (a !== undefined && !(a.owner instanceof Laid))
|
|
151
|
+
a = a.inner;
|
|
152
|
+
return a === undefined ? undefined : a.owner.layout;
|
|
153
|
+
}
|
|
154
|
+
/** The layout `field` drew in the frame `lines` hold: off the first of its cells. */
|
|
155
|
+
function layoutIn(lines, field) {
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
for (const segment of line) {
|
|
158
|
+
const layout = layoutOn(levelOf(segment.style?.anchor, field));
|
|
159
|
+
if (layout !== undefined)
|
|
160
|
+
return layout;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
106
165
|
/**
|
|
107
166
|
* Character-greedy soft wrap. Breaks at any character once `firstWidth`
|
|
108
167
|
* (or `continuationWidth` for continuation rows) cells are exhausted. The
|
|
@@ -111,22 +170,17 @@ import { ink } from "./ink.js";
|
|
|
111
170
|
*
|
|
112
171
|
* Wide characters (CJK, emoji) are treated as atomic: a char that would
|
|
113
172
|
* straddle the budget is moved to the next row rather than split, and one too
|
|
114
|
-
* wide for the whole budget is force-taken — see `
|
|
173
|
+
* wide for the whole budget is force-taken — see `cellRows`.
|
|
115
174
|
*/
|
|
116
175
|
export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
|
|
117
176
|
if (line.length === 0)
|
|
118
177
|
return [{ start: asCodePoint(0), end: asCodePoint(0) }];
|
|
178
|
+
const capOf = (row) => (row === 0 ? firstWidth : continuationWidth);
|
|
119
179
|
const rows = [];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
while (pos < line.length) {
|
|
123
|
-
const cap = isFirst ? firstWidth : continuationWidth;
|
|
124
|
-
if (cap <= 0)
|
|
180
|
+
for (const row of cellRows(line, capOf)) {
|
|
181
|
+
if (capOf(rows.length) <= 0)
|
|
125
182
|
break;
|
|
126
|
-
|
|
127
|
-
rows.push({ start: pos, end });
|
|
128
|
-
pos = end;
|
|
129
|
-
isFirst = false;
|
|
183
|
+
rows.push(row);
|
|
130
184
|
}
|
|
131
185
|
return rows;
|
|
132
186
|
};
|
|
@@ -149,12 +203,12 @@ const ZERO_CELL_GLYPH = "◌";
|
|
|
149
203
|
const ASCII_ZERO_CELL_GLYPH = "?";
|
|
150
204
|
// Text a key may insert: anything but the C0 controls, DEL and the C1
|
|
151
205
|
// controls, which a terminal reads as escape sequences (U+009B is CSI).
|
|
152
|
-
const PRINTABLE = /^[^\x00-\x1f\x7f-\x9f]+$/u;
|
|
206
|
+
const PRINTABLE = pattern(/^[^\x00-\x1f\x7f-\x9f]+$/u);
|
|
153
207
|
// Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
|
|
154
208
|
// readline default (alphanumeric + underscore), which is what users typing
|
|
155
209
|
// shell commands or code identifiers expect.
|
|
156
|
-
const WORD_CHAR_RE = /[A-Za-z0-9_]
|
|
157
|
-
const WHITESPACE_RE = /\s
|
|
210
|
+
const WORD_CHAR_RE = pattern(/[A-Za-z0-9_]/);
|
|
211
|
+
const WHITESPACE_RE = pattern(/\s/);
|
|
158
212
|
/** CRLF and lone CR as LF, the line break `value` holds. */
|
|
159
213
|
function toLf(text) {
|
|
160
214
|
return text.replace(/\r\n?/g, "\n");
|
|
@@ -165,6 +219,33 @@ function isWordChar(c) {
|
|
|
165
219
|
function isWhitespace(c) {
|
|
166
220
|
return c !== undefined && WHITESPACE_RE.test(c);
|
|
167
221
|
}
|
|
222
|
+
const isNotWordChar = (c) => !isWordChar(c);
|
|
223
|
+
const isNotWhitespace = (c) => !isWhitespace(c);
|
|
224
|
+
/**
|
|
225
|
+
* How many code units a word motion crosses: every leading cluster `skipped`
|
|
226
|
+
* accepts, then every one after them `taken` accepts. `clusters` is one walk
|
|
227
|
+
* from the cursor (`clustersFrom` or `clustersBefore`), so a motion across a
|
|
228
|
+
* long word segments the value once, not once a cluster.
|
|
229
|
+
*/
|
|
230
|
+
function wordSpan(clusters, skipped, taken) {
|
|
231
|
+
let span = 0;
|
|
232
|
+
let skipping = true;
|
|
233
|
+
for (const cluster of clusters) {
|
|
234
|
+
skipping &&= skipped(cluster);
|
|
235
|
+
if (!skipping && !taken(cluster))
|
|
236
|
+
break;
|
|
237
|
+
span += cluster.length;
|
|
238
|
+
}
|
|
239
|
+
return span;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The caret's blink, from the moment it restarts: lit for the first half
|
|
243
|
+
* second, so the caret is solid the moment the field is touched, then dark
|
|
244
|
+
* for the next, on a `beat`. A loop, so with its motion taken out it holds lit.
|
|
245
|
+
*/
|
|
246
|
+
const CARET_BLINK = Animation.map(beat([0.5, 0.5]), (hold) => hold === 0);
|
|
247
|
+
/** The caret of a frame drawn for no moment — a `print` — which nothing moves: lit, and never changing. */
|
|
248
|
+
const CARET_UNTIMED = { lit: true, hold: { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY } };
|
|
168
249
|
let TextInput = (() => {
|
|
169
250
|
let _classSuper = ThemedWidget;
|
|
170
251
|
let _instanceExtraInitializers = [];
|
|
@@ -174,6 +255,9 @@ let TextInput = (() => {
|
|
|
174
255
|
let __cursor_decorators;
|
|
175
256
|
let __cursor_initializers = [];
|
|
176
257
|
let __cursor_extraInitializers = [];
|
|
258
|
+
let __blinkFrom_decorators;
|
|
259
|
+
let __blinkFrom_initializers = [];
|
|
260
|
+
let __blinkFrom_extraInitializers = [];
|
|
177
261
|
let _placeholder_decorators;
|
|
178
262
|
let _placeholder_initializers = [];
|
|
179
263
|
let _placeholder_extraInitializers = [];
|
|
@@ -187,6 +271,7 @@ let TextInput = (() => {
|
|
|
187
271
|
let _contentStyleOverride_initializers = [];
|
|
188
272
|
let _contentStyleOverride_extraInitializers = [];
|
|
189
273
|
let _handleKey_decorators;
|
|
274
|
+
let _handleFocus_decorators;
|
|
190
275
|
let _handleMouse_decorators;
|
|
191
276
|
let _setHovered_decorators;
|
|
192
277
|
let _moveCharLeft_decorators;
|
|
@@ -211,13 +296,15 @@ let TextInput = (() => {
|
|
|
211
296
|
return class TextInput extends _classSuper {
|
|
212
297
|
static {
|
|
213
298
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
214
|
-
__value_decorators = [
|
|
215
|
-
__cursor_decorators = [
|
|
299
|
+
__value_decorators = [observableRef];
|
|
300
|
+
__cursor_decorators = [observableRef];
|
|
301
|
+
__blinkFrom_decorators = [observableRef];
|
|
216
302
|
_placeholder_decorators = [observableRef];
|
|
217
303
|
_indicatorStyleOverride_decorators = [observableRef];
|
|
218
304
|
_cursorStyleOverride_decorators = [observableRef];
|
|
219
305
|
_contentStyleOverride_decorators = [observableRef];
|
|
220
306
|
_handleKey_decorators = [action];
|
|
307
|
+
_handleFocus_decorators = [action];
|
|
221
308
|
_handleMouse_decorators = [action];
|
|
222
309
|
_setHovered_decorators = [action];
|
|
223
310
|
_moveCharLeft_decorators = [action];
|
|
@@ -241,11 +328,13 @@ let TextInput = (() => {
|
|
|
241
328
|
__insertText_decorators = [action];
|
|
242
329
|
__esDecorate(this, null, __value_decorators, { kind: "accessor", name: "_value", static: false, private: false, access: { has: obj => "_value" in obj, get: obj => obj._value, set: (obj, value) => { obj._value = value; } }, metadata: _metadata }, __value_initializers, __value_extraInitializers);
|
|
243
330
|
__esDecorate(this, null, __cursor_decorators, { kind: "accessor", name: "_cursor", static: false, private: false, access: { has: obj => "_cursor" in obj, get: obj => obj._cursor, set: (obj, value) => { obj._cursor = value; } }, metadata: _metadata }, __cursor_initializers, __cursor_extraInitializers);
|
|
331
|
+
__esDecorate(this, null, __blinkFrom_decorators, { kind: "accessor", name: "_blinkFrom", static: false, private: false, access: { has: obj => "_blinkFrom" in obj, get: obj => obj._blinkFrom, set: (obj, value) => { obj._blinkFrom = value; } }, metadata: _metadata }, __blinkFrom_initializers, __blinkFrom_extraInitializers);
|
|
244
332
|
__esDecorate(this, null, _placeholder_decorators, { kind: "accessor", name: "placeholder", static: false, private: false, access: { has: obj => "placeholder" in obj, get: obj => obj.placeholder, set: (obj, value) => { obj.placeholder = value; } }, metadata: _metadata }, _placeholder_initializers, _placeholder_extraInitializers);
|
|
245
333
|
__esDecorate(this, null, _indicatorStyleOverride_decorators, { kind: "accessor", name: "indicatorStyleOverride", static: false, private: false, access: { has: obj => "indicatorStyleOverride" in obj, get: obj => obj.indicatorStyleOverride, set: (obj, value) => { obj.indicatorStyleOverride = value; } }, metadata: _metadata }, _indicatorStyleOverride_initializers, _indicatorStyleOverride_extraInitializers);
|
|
246
334
|
__esDecorate(this, null, _cursorStyleOverride_decorators, { kind: "accessor", name: "cursorStyleOverride", static: false, private: false, access: { has: obj => "cursorStyleOverride" in obj, get: obj => obj.cursorStyleOverride, set: (obj, value) => { obj.cursorStyleOverride = value; } }, metadata: _metadata }, _cursorStyleOverride_initializers, _cursorStyleOverride_extraInitializers);
|
|
247
335
|
__esDecorate(this, null, _contentStyleOverride_decorators, { kind: "accessor", name: "contentStyleOverride", static: false, private: false, access: { has: obj => "contentStyleOverride" in obj, get: obj => obj.contentStyleOverride, set: (obj, value) => { obj.contentStyleOverride = value; } }, metadata: _metadata }, _contentStyleOverride_initializers, _contentStyleOverride_extraInitializers);
|
|
248
336
|
__esDecorate(this, null, _handleKey_decorators, { kind: "method", name: "handleKey", static: false, private: false, access: { has: obj => "handleKey" in obj, get: obj => obj.handleKey }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
337
|
+
__esDecorate(this, null, _handleFocus_decorators, { kind: "method", name: "handleFocus", static: false, private: false, access: { has: obj => "handleFocus" in obj, get: obj => obj.handleFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
249
338
|
__esDecorate(this, null, _handleMouse_decorators, { kind: "method", name: "handleMouse", static: false, private: false, access: { has: obj => "handleMouse" in obj, get: obj => obj.handleMouse }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
250
339
|
__esDecorate(this, null, _setHovered_decorators, { kind: "method", name: "setHovered", static: false, private: false, access: { has: obj => "setHovered" in obj, get: obj => obj.setHovered }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
251
340
|
__esDecorate(this, null, _moveCharLeft_decorators, { kind: "method", name: "moveCharLeft", static: false, private: false, access: { has: obj => "moveCharLeft" in obj, get: obj => obj.moveCharLeft }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -277,6 +366,13 @@ let TextInput = (() => {
|
|
|
277
366
|
#_cursor_accessor_storage = (__runInitializers(this, __value_extraInitializers), __runInitializers(this, __cursor_initializers, void 0));
|
|
278
367
|
get _cursor() { return this.#_cursor_accessor_storage; }
|
|
279
368
|
set _cursor(value) { this.#_cursor_accessor_storage = value; }
|
|
369
|
+
#_blinkFrom_accessor_storage = (__runInitializers(this, __cursor_extraInitializers), __runInitializers(this, __blinkFrom_initializers, 0));
|
|
370
|
+
// When the field last took focus, a click, or a key it handled, on the
|
|
371
|
+
// frame's clock (each event's `time`): where the caret's blink restarts.
|
|
372
|
+
// The blink itself is a function of this and the time a frame is drawn
|
|
373
|
+
// for, so nothing about it is kept.
|
|
374
|
+
get _blinkFrom() { return this.#_blinkFrom_accessor_storage; }
|
|
375
|
+
set _blinkFrom(value) { this.#_blinkFrom_accessor_storage = value; }
|
|
280
376
|
// [LAW:single-enforcer] Every write to either half of the pair passes
|
|
281
377
|
// `graphemeBoundary`, so the cursor is a cluster boundary inside `value`
|
|
282
378
|
// whoever set it last — a caller replacing `value`, a click, an insert that
|
|
@@ -292,7 +388,7 @@ let TextInput = (() => {
|
|
|
292
388
|
}
|
|
293
389
|
get cursorPosition() { return this._cursor; }
|
|
294
390
|
set cursorPosition(p) { this._cursor = graphemeBoundary(this._value, p); }
|
|
295
|
-
#placeholder_accessor_storage = (__runInitializers(this,
|
|
391
|
+
#placeholder_accessor_storage = (__runInitializers(this, __blinkFrom_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
|
|
296
392
|
get placeholder() { return this.#placeholder_accessor_storage; }
|
|
297
393
|
set placeholder(value) { this.#placeholder_accessor_storage = value; }
|
|
298
394
|
_maxLength = __runInitializers(this, _placeholder_extraInitializers);
|
|
@@ -313,44 +409,28 @@ let TextInput = (() => {
|
|
|
313
409
|
#contentStyleOverride_accessor_storage = (__runInitializers(this, _cursorStyleOverride_extraInitializers), __runInitializers(this, _contentStyleOverride_initializers, void 0));
|
|
314
410
|
get contentStyleOverride() { return this.#contentStyleOverride_accessor_storage; }
|
|
315
411
|
set contentStyleOverride(value) { this.#contentStyleOverride_accessor_storage = value; }
|
|
316
|
-
/**
|
|
317
|
-
* Last computed visual-row decomposition. Cached at the end of `render()`
|
|
318
|
-
* so vertical motion (Up/Down) can step row-by-row without re-running the
|
|
319
|
-
* wrap strategy. Null before the first render — vertical motion falls
|
|
320
|
-
* back to logical-line motion in that case.
|
|
321
|
-
*
|
|
322
|
-
* [LAW:dataflow-not-control-flow] One source of truth for "what does Up
|
|
323
|
-
* mean right now": the row table the renderer just produced. No parallel
|
|
324
|
-
* "where would the cursor go" math; both the renderer and the keymap
|
|
325
|
-
* read from the same array.
|
|
326
|
-
*/
|
|
327
|
-
_visualRows = (__runInitializers(this, _contentStyleOverride_extraInitializers), null);
|
|
328
412
|
// [LAW:types-are-the-program] `_preferredColumn` is a cell column (not a
|
|
329
413
|
// code-unit index) — vertical motion navigates the visual surface, not the
|
|
330
414
|
// string's byte layout. Reset to null on every non-vertical motion or edit
|
|
331
415
|
// so the next Up/Down captures a fresh intent. Without this, Down through a
|
|
332
416
|
// short line then back Up would land at the short-line column instead of the
|
|
333
417
|
// original.
|
|
334
|
-
_preferredColumn = null;
|
|
418
|
+
_preferredColumn = (__runInitializers(this, _contentStyleOverride_extraInitializers), new State(null));
|
|
335
419
|
// Single-slot kill buffer used by Ctrl+U / Ctrl+K → Ctrl+Y. Not a full
|
|
336
420
|
// kill ring (no Alt+Y rotation) — sufficient for the common cut/paste
|
|
337
421
|
// pattern that's the readline 90% case.
|
|
338
|
-
_killBuffer = "";
|
|
339
|
-
// [LAW:one-source-of-truth]
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
_singleLineViewportStart = asCellCol(0);
|
|
351
|
-
// How the last render could draw, so the hit-test and vertical motion
|
|
352
|
-
// measure `value` as it was shown.
|
|
353
|
-
_drawn = { asciiOnly: false };
|
|
422
|
+
_killBuffer = new State("");
|
|
423
|
+
// [LAW:one-source-of-truth] What the last frame drew under — the one layout
|
|
424
|
+
// fact only a render learns. The visual rows, the single-line window and
|
|
425
|
+
// the cursor's row are functions of it and of `value`, derived where they
|
|
426
|
+
// are read: a click and Up/Down see the rows of the value as it is now at
|
|
427
|
+
// the width the user last saw. Kept once the frame is drawn (`afterPass`).
|
|
428
|
+
// Before any frame the width bounds nothing, and nothing wraps.
|
|
429
|
+
_drawn = new State({ maxWidth: Infinity, asciiOnly: false });
|
|
430
|
+
// [LAW:single-enforcer] The multiline scroll: the viewport keeps the cursor
|
|
431
|
+
// row in view, clamps when the value shrinks, and keeps its offset once the
|
|
432
|
+
// frame is drawn, as it does for a `ListView`.
|
|
433
|
+
_viewport;
|
|
354
434
|
multiline;
|
|
355
435
|
constructor(options = {}) {
|
|
356
436
|
super(options.theme);
|
|
@@ -368,7 +448,8 @@ let TextInput = (() => {
|
|
|
368
448
|
this._maxLength = options.maxLength;
|
|
369
449
|
this._password = options.password ?? false;
|
|
370
450
|
this._multiline = options.multiline ?? false;
|
|
371
|
-
|
|
451
|
+
// Soft wrap is multiline's: one line wraps to nothing.
|
|
452
|
+
this._wrap = this._multiline ? options.wrap : undefined;
|
|
372
453
|
this._continuationMarker = options.continuationMarker ?? "↳ ";
|
|
373
454
|
this._markerWidth = asCellCol(cellLen(this._continuationMarker));
|
|
374
455
|
this._maxRows = options.maxRows;
|
|
@@ -378,9 +459,29 @@ let TextInput = (() => {
|
|
|
378
459
|
this.cursorStyleOverride = options.cursorStyle;
|
|
379
460
|
this.contentStyleOverride = options.contentStyle;
|
|
380
461
|
this.multiline = this._multiline;
|
|
462
|
+
this._viewport = new Viewport();
|
|
381
463
|
}
|
|
382
464
|
// --- Event handlers ---
|
|
465
|
+
// A key the field handles is one it stops; only that restarts the blink,
|
|
466
|
+
// so a key passing through to the app — Tab, Escape — moves nothing here.
|
|
383
467
|
handleKey(event) {
|
|
468
|
+
this._edit(event);
|
|
469
|
+
if (!event.stopped)
|
|
470
|
+
return;
|
|
471
|
+
this._blinkFrom = event.time;
|
|
472
|
+
// An edit on the cursor's row moves no followed range, so the row is
|
|
473
|
+
// brought into view here too: typing after the wheel scrolled away scrolls
|
|
474
|
+
// back. The rows are the edited value's, at the width last drawn.
|
|
475
|
+
if (this._multiline) {
|
|
476
|
+
const row = cursorRowIn(this._rows(this._drawn.get()), this.cursorPosition);
|
|
477
|
+
this._viewport.ensureVisible(row, row + 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
handleFocus(event) {
|
|
481
|
+
super.handleFocus(event);
|
|
482
|
+
this._blinkFrom = event.time;
|
|
483
|
+
}
|
|
484
|
+
_edit(event) {
|
|
384
485
|
if (this.disabled)
|
|
385
486
|
return;
|
|
386
487
|
// ─── editing keys (modifier-conscious) ───
|
|
@@ -552,98 +653,69 @@ let TextInput = (() => {
|
|
|
552
653
|
return;
|
|
553
654
|
if (event.type !== "mouse_down")
|
|
554
655
|
return;
|
|
555
|
-
|
|
656
|
+
this._blinkFrom = event.time;
|
|
657
|
+
// [LAW:no-silent-failure] Every cell the field draws carries its frame's
|
|
658
|
+
// layout, so a press without one did not come off a frame it drew.
|
|
659
|
+
const layout = layoutOn(event.cell);
|
|
660
|
+
if (layout === undefined)
|
|
661
|
+
throw new Error("TextInput: a press carries no layout of the frame it landed on");
|
|
662
|
+
if (layout.kind === "rows") {
|
|
556
663
|
// Multiline has no brackets; rows may have a leading continuation marker.
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return;
|
|
561
|
-
const rawRowIdx = this._scrollStart + event.y;
|
|
664
|
+
// event.y is a row of the window the clicked frame showed; event.x is
|
|
665
|
+
// projected into the row's content.
|
|
666
|
+
const rawRowIdx = layout.offset + event.y;
|
|
562
667
|
// Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
|
|
563
|
-
if (rawRowIdx >= rows.length) {
|
|
668
|
+
if (rawRowIdx >= layout.rows.length) {
|
|
564
669
|
this.cursorPosition = asCodeUnit(this.value.length);
|
|
565
|
-
this._preferredColumn
|
|
670
|
+
this._preferredColumn.set(null);
|
|
566
671
|
return;
|
|
567
672
|
}
|
|
568
|
-
const
|
|
569
|
-
const row = rows[rowIdx];
|
|
673
|
+
const row = layout.rows[rawRowIdx];
|
|
570
674
|
// Continuation rows start after the marker; non-continuation rows start at column 0.
|
|
571
675
|
const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
|
|
572
676
|
const relX = asCellCol(Math.max(0, event.x - contentXOff));
|
|
573
|
-
this.cursorPosition = asCodeUnit(row.valueStart + this._offsetAtShownCell(row.content, relX));
|
|
677
|
+
this.cursorPosition = asCodeUnit(row.valueStart + this._offsetAtShownCell(row.content, relX, layout.drawn));
|
|
574
678
|
}
|
|
575
679
|
else {
|
|
576
680
|
// [LAW:types-are-the-program] relX is a cell-column offset into the full
|
|
577
|
-
// display string
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
this.cursorPosition = this._offsetAtShownCell(this.value, relX);
|
|
681
|
+
// display string: the click's column past the bracket, from where the
|
|
682
|
+
// window the clicked frame showed begins.
|
|
683
|
+
const relX = asCellCol(Math.max(0, event.x - 1) + layout.startCell);
|
|
684
|
+
this.cursorPosition = this._offsetAtShownCell(layout.value, relX, layout.drawn);
|
|
582
685
|
}
|
|
583
|
-
this._preferredColumn
|
|
686
|
+
this._preferredColumn.set(null);
|
|
584
687
|
}
|
|
585
688
|
// --- Hover mutator (router fast-path) ---
|
|
586
689
|
setHovered(value) { this.hovered = value; }
|
|
587
690
|
// ─── Public motion primitives ───────────────────────────────────────────
|
|
588
691
|
moveCharLeft() {
|
|
589
692
|
this.cursorPosition = prevGrapheme(this.value, this.cursorPosition);
|
|
590
|
-
this._preferredColumn
|
|
693
|
+
this._preferredColumn.set(null);
|
|
591
694
|
}
|
|
592
695
|
moveCharRight() {
|
|
593
696
|
this.cursorPosition = nextGrapheme(this.value, this.cursorPosition);
|
|
594
|
-
this._preferredColumn
|
|
697
|
+
this._preferredColumn.set(null);
|
|
595
698
|
}
|
|
596
699
|
moveLineUp() {
|
|
597
|
-
|
|
598
|
-
// marker offsets, and scroll alignment are all already accounted for.
|
|
599
|
-
// Fall back to logical-line motion only when no render has occurred yet
|
|
600
|
-
// (e.g. cursor motion before mount).
|
|
601
|
-
if (this._visualRows !== null && this._visualRows.length > 1) {
|
|
602
|
-
const rowIdx = this._cursorVisualRow();
|
|
603
|
-
if (rowIdx === 0)
|
|
604
|
-
return;
|
|
605
|
-
const col = this._preferredColumn ?? this._cursorVisualCol();
|
|
606
|
-
const target = this._visualRows[rowIdx - 1];
|
|
607
|
-
this.cursorPosition = asCodeUnit(target.valueStart + this._clampColForRow(col, rowIdx - 1));
|
|
608
|
-
this._preferredColumn = col;
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
const lineStart = this._lineStart();
|
|
612
|
-
if (lineStart === 0)
|
|
613
|
-
return;
|
|
614
|
-
// Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
|
|
615
|
-
const col = this._preferredColumn ?? this._shownCells(this.value.slice(lineStart, this.cursorPosition));
|
|
616
|
-
const prevLineEnd = lineStart - 1;
|
|
617
|
-
let prevLineStart = prevLineEnd;
|
|
618
|
-
while (prevLineStart > 0 && this.value[prevLineStart - 1] !== "\n")
|
|
619
|
-
prevLineStart--;
|
|
620
|
-
const prevLineContent = this.value.slice(prevLineStart, prevLineEnd);
|
|
621
|
-
this.cursorPosition = asCodeUnit(prevLineStart + this._offsetAtShownCell(prevLineContent, col));
|
|
622
|
-
this._preferredColumn = col;
|
|
700
|
+
this._moveLine(-1);
|
|
623
701
|
}
|
|
624
702
|
moveLineDown() {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
703
|
+
this._moveLine(1);
|
|
704
|
+
}
|
|
705
|
+
// [LAW:dataflow-not-control-flow] One motion over the rows as last drawn —
|
|
706
|
+
// wraps and marker offsets accounted for. Before any frame they are the
|
|
707
|
+
// logical lines, so motion before mount is this same path, not a fallback.
|
|
708
|
+
_moveLine(step) {
|
|
709
|
+
const rows = this._rows(this._drawn.get());
|
|
710
|
+
const rowIdx = cursorRowIn(rows, this.cursorPosition);
|
|
711
|
+
const targetIdx = rowIdx + step;
|
|
712
|
+
const target = rows[targetIdx];
|
|
713
|
+
if (target === undefined)
|
|
636
714
|
return;
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
let nextLineEnd = nextLineStart;
|
|
642
|
-
while (nextLineEnd < this.value.length && this.value[nextLineEnd] !== "\n")
|
|
643
|
-
nextLineEnd++;
|
|
644
|
-
const nextLineContent = this.value.slice(nextLineStart, nextLineEnd);
|
|
645
|
-
this.cursorPosition = asCodeUnit(nextLineStart + this._offsetAtShownCell(nextLineContent, col));
|
|
646
|
-
this._preferredColumn = col;
|
|
715
|
+
const row = rows[rowIdx];
|
|
716
|
+
const col = this._preferredColumn.get() ?? this._shownCells(row.content.slice(0, this.cursorPosition - row.valueStart));
|
|
717
|
+
this.cursorPosition = asCodeUnit(target.valueStart + this._clampColForRow(rows, col, targetIdx));
|
|
718
|
+
this._preferredColumn.set(col);
|
|
647
719
|
}
|
|
648
720
|
// Clamp `col` (a cell column) to the target visual row's content, returning
|
|
649
721
|
// the code-unit offset within that row. When the row IS followed by a
|
|
@@ -651,91 +723,46 @@ let TextInput = (() => {
|
|
|
651
723
|
// cluster boundary strictly inside the row (which may be more than
|
|
652
724
|
// one cell before the visual end when the last glyph is wide) so the
|
|
653
725
|
// cursor stays strictly inside the target row — landing at the boundary
|
|
654
|
-
// would cause `
|
|
726
|
+
// would cause `cursorRowIn` to resolve to the *later* row, making
|
|
655
727
|
// every subsequent Up/Down stick at the boundary.
|
|
656
728
|
//
|
|
657
729
|
// For rows NOT followed by a continuation (last wrap row, or any unwrapped
|
|
658
730
|
// logical line), the end-of-line position is valid — there is no adjacent
|
|
659
731
|
// continuation row to collide with.
|
|
660
|
-
_clampColForRow(col, targetIdx) {
|
|
661
|
-
const rows = this._visualRows;
|
|
732
|
+
_clampColForRow(rows, col, targetIdx) {
|
|
662
733
|
const target = rows[targetIdx];
|
|
663
734
|
const nextIsContinuation = targetIdx + 1 < rows.length && rows[targetIdx + 1].isContinuation;
|
|
664
735
|
const contentCellWidth = this._shownCells(target.content);
|
|
665
736
|
const capCells = nextIsContinuation
|
|
666
737
|
? asCellCol(Math.max(0, contentCellWidth - 1))
|
|
667
738
|
: contentCellWidth;
|
|
668
|
-
return this._offsetAtShownCell(target.content, asCellCol(Math.min(col, capCells)));
|
|
669
|
-
}
|
|
670
|
-
// Locate which cached visual row the cursor sits on. Returns the row index
|
|
671
|
-
// for use in `moveLineUp`/`moveLineDown`; caller must guard `_visualRows`.
|
|
672
|
-
_cursorVisualRow() {
|
|
673
|
-
const rows = this._visualRows;
|
|
674
|
-
let idx = 0;
|
|
675
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
676
|
-
if (rows[i].valueStart <= this.cursorPosition) {
|
|
677
|
-
idx = i;
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
return idx;
|
|
682
|
-
}
|
|
683
|
-
// Returns the cursor's cell column within its current visual row.
|
|
684
|
-
_cursorVisualCol() {
|
|
685
|
-
const rows = this._visualRows;
|
|
686
|
-
const idx = this._cursorVisualRow();
|
|
687
|
-
const row = rows[idx];
|
|
688
|
-
return this._shownCells(row.content.slice(0, this.cursorPosition - row.valueStart));
|
|
739
|
+
return this._offsetAtShownCell(target.content, asCellCol(Math.min(col, capCells)), this._drawn.get());
|
|
689
740
|
}
|
|
690
741
|
moveLineStart() {
|
|
691
742
|
this.cursorPosition = this._lineStart();
|
|
692
|
-
this._preferredColumn
|
|
743
|
+
this._preferredColumn.set(null);
|
|
693
744
|
}
|
|
694
745
|
moveLineEnd() {
|
|
695
746
|
this.cursorPosition = this._lineEnd();
|
|
696
|
-
this._preferredColumn
|
|
747
|
+
this._preferredColumn.set(null);
|
|
697
748
|
}
|
|
698
749
|
moveDocStart() {
|
|
699
750
|
this.cursorPosition = asCodeUnit(0);
|
|
700
|
-
this._preferredColumn
|
|
751
|
+
this._preferredColumn.set(null);
|
|
701
752
|
}
|
|
702
753
|
moveDocEnd() {
|
|
703
754
|
this.cursorPosition = asCodeUnit(this.value.length);
|
|
704
|
-
this._preferredColumn
|
|
755
|
+
this._preferredColumn.set(null);
|
|
705
756
|
}
|
|
706
757
|
moveWordLeft() {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (isWordChar(this.value.slice(prev, p)))
|
|
711
|
-
break;
|
|
712
|
-
p = prev;
|
|
713
|
-
}
|
|
714
|
-
while (p > 0) {
|
|
715
|
-
const prev = prevGrapheme(this.value, p);
|
|
716
|
-
if (!isWordChar(this.value.slice(prev, p)))
|
|
717
|
-
break;
|
|
718
|
-
p = prev;
|
|
719
|
-
}
|
|
720
|
-
this.cursorPosition = p;
|
|
721
|
-
this._preferredColumn = null;
|
|
758
|
+
const at = this.cursorPosition;
|
|
759
|
+
this.cursorPosition = asCodePoint(at - wordSpan(clustersBefore(this.value, at), isNotWordChar, isWordChar));
|
|
760
|
+
this._preferredColumn.set(null);
|
|
722
761
|
}
|
|
723
762
|
moveWordRight() {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (isWordChar(this.value.slice(p, next)))
|
|
728
|
-
break;
|
|
729
|
-
p = next;
|
|
730
|
-
}
|
|
731
|
-
while (p < this.value.length) {
|
|
732
|
-
const next = nextGrapheme(this.value, p);
|
|
733
|
-
if (!isWordChar(this.value.slice(p, next)))
|
|
734
|
-
break;
|
|
735
|
-
p = next;
|
|
736
|
-
}
|
|
737
|
-
this.cursorPosition = p;
|
|
738
|
-
this._preferredColumn = null;
|
|
763
|
+
const at = this.cursorPosition;
|
|
764
|
+
this.cursorPosition = asCodePoint(at + wordSpan(clustersFrom(this.value, at), isNotWordChar, isWordChar));
|
|
765
|
+
this._preferredColumn.set(null);
|
|
739
766
|
}
|
|
740
767
|
// ─── Public editing primitives ──────────────────────────────────────────
|
|
741
768
|
deleteCharBack() {
|
|
@@ -744,7 +771,7 @@ let TextInput = (() => {
|
|
|
744
771
|
const newPos = prevGrapheme(this.value, this.cursorPosition);
|
|
745
772
|
this.value = this.value.slice(0, newPos) + this.value.slice(this.cursorPosition);
|
|
746
773
|
this.cursorPosition = newPos;
|
|
747
|
-
this._preferredColumn
|
|
774
|
+
this._preferredColumn.set(null);
|
|
748
775
|
this.emitChange();
|
|
749
776
|
}
|
|
750
777
|
deleteCharForward() {
|
|
@@ -752,86 +779,63 @@ let TextInput = (() => {
|
|
|
752
779
|
return;
|
|
753
780
|
const nextPos = nextGrapheme(this.value, this.cursorPosition);
|
|
754
781
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(nextPos);
|
|
755
|
-
this._preferredColumn
|
|
782
|
+
this._preferredColumn.set(null);
|
|
756
783
|
this.emitChange();
|
|
757
784
|
}
|
|
758
785
|
deleteWordBack() {
|
|
759
786
|
// Readline `unix-word-rubout` semantics: delete back to nearest whitespace,
|
|
760
787
|
// skipping trailing whitespace first so successive Ctrl+W at "foo bar |"
|
|
761
788
|
// → "foo |" → "|" rather than getting stuck on the trailing space.
|
|
762
|
-
|
|
763
|
-
while (p > 0) {
|
|
764
|
-
const prev = prevGrapheme(this.value, p);
|
|
765
|
-
if (!isWhitespace(this.value.slice(prev, p)))
|
|
766
|
-
break;
|
|
767
|
-
p = prev;
|
|
768
|
-
}
|
|
769
|
-
while (p > 0) {
|
|
770
|
-
const prev = prevGrapheme(this.value, p);
|
|
771
|
-
if (isWhitespace(this.value.slice(prev, p)))
|
|
772
|
-
break;
|
|
773
|
-
p = prev;
|
|
774
|
-
}
|
|
789
|
+
const p = asCodePoint(this.cursorPosition - wordSpan(clustersBefore(this.value, this.cursorPosition), isWhitespace, isNotWhitespace));
|
|
775
790
|
if (p === this.cursorPosition)
|
|
776
791
|
return;
|
|
777
|
-
this._killBuffer
|
|
792
|
+
this._killBuffer.set(this.value.slice(p, this.cursorPosition));
|
|
778
793
|
this.value = this.value.slice(0, p) + this.value.slice(this.cursorPosition);
|
|
779
794
|
this.cursorPosition = p;
|
|
780
|
-
this._preferredColumn
|
|
795
|
+
this._preferredColumn.set(null);
|
|
781
796
|
this.emitChange();
|
|
782
797
|
}
|
|
783
798
|
deleteWordForward() {
|
|
784
|
-
|
|
785
|
-
while (p < this.value.length) {
|
|
786
|
-
const next = nextGrapheme(this.value, p);
|
|
787
|
-
if (isWordChar(this.value.slice(p, next)))
|
|
788
|
-
break;
|
|
789
|
-
p = next;
|
|
790
|
-
}
|
|
791
|
-
while (p < this.value.length) {
|
|
792
|
-
const next = nextGrapheme(this.value, p);
|
|
793
|
-
if (!isWordChar(this.value.slice(p, next)))
|
|
794
|
-
break;
|
|
795
|
-
p = next;
|
|
796
|
-
}
|
|
799
|
+
const p = this.cursorPosition + wordSpan(clustersFrom(this.value, this.cursorPosition), isNotWordChar, isWordChar);
|
|
797
800
|
if (p === this.cursorPosition)
|
|
798
801
|
return;
|
|
799
|
-
this._killBuffer
|
|
802
|
+
this._killBuffer.set(this.value.slice(this.cursorPosition, p));
|
|
800
803
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(p);
|
|
801
|
-
this._preferredColumn
|
|
804
|
+
this._preferredColumn.set(null);
|
|
802
805
|
this.emitChange();
|
|
803
806
|
}
|
|
804
807
|
killLineBack() {
|
|
805
808
|
const start = this._lineStart();
|
|
806
809
|
if (start === this.cursorPosition)
|
|
807
810
|
return;
|
|
808
|
-
this._killBuffer
|
|
811
|
+
this._killBuffer.set(this.value.slice(start, this.cursorPosition));
|
|
809
812
|
this.value = this.value.slice(0, start) + this.value.slice(this.cursorPosition);
|
|
810
813
|
this.cursorPosition = start;
|
|
811
|
-
this._preferredColumn
|
|
814
|
+
this._preferredColumn.set(null);
|
|
812
815
|
this.emitChange();
|
|
813
816
|
}
|
|
814
817
|
killLineForward() {
|
|
815
818
|
const end = this._lineEnd();
|
|
816
819
|
if (end > this.cursorPosition) {
|
|
817
|
-
this._killBuffer
|
|
820
|
+
this._killBuffer.set(this.value.slice(this.cursorPosition, end));
|
|
818
821
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(end);
|
|
819
|
-
this._preferredColumn
|
|
822
|
+
this._preferredColumn.set(null);
|
|
820
823
|
this.emitChange();
|
|
821
824
|
return;
|
|
822
825
|
}
|
|
823
826
|
// Already at line end → kill the trailing `\n` (join next line).
|
|
824
827
|
if (this.cursorPosition < this.value.length) {
|
|
825
|
-
this._killBuffer
|
|
828
|
+
this._killBuffer.set("\n");
|
|
826
829
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
|
|
827
|
-
this._preferredColumn
|
|
830
|
+
this._preferredColumn.set(null);
|
|
828
831
|
this.emitChange();
|
|
829
832
|
}
|
|
830
833
|
}
|
|
831
834
|
yank() {
|
|
832
|
-
|
|
835
|
+
const killed = this._killBuffer.get();
|
|
836
|
+
if (killed.length === 0)
|
|
833
837
|
return;
|
|
834
|
-
this._insertText(
|
|
838
|
+
this._insertText(killed);
|
|
835
839
|
}
|
|
836
840
|
transposeChars() {
|
|
837
841
|
// Readline `transpose-chars`: swap the char before the cursor with the
|
|
@@ -848,7 +852,7 @@ let TextInput = (() => {
|
|
|
848
852
|
return;
|
|
849
853
|
const cp1 = prevGrapheme(this.value, cp2);
|
|
850
854
|
this.value = this.value.slice(0, cp1) + this.value.slice(cp2) + this.value.slice(cp1, cp2);
|
|
851
|
-
this._preferredColumn
|
|
855
|
+
this._preferredColumn.set(null);
|
|
852
856
|
this.emitChange();
|
|
853
857
|
return;
|
|
854
858
|
}
|
|
@@ -859,7 +863,7 @@ let TextInput = (() => {
|
|
|
859
863
|
const charAt = this.value.slice(p, cpAfter);
|
|
860
864
|
this.value = this.value.slice(0, cpBefore) + charAt + charBefore + this.value.slice(cpAfter);
|
|
861
865
|
this.cursorPosition = cpAfter;
|
|
862
|
-
this._preferredColumn
|
|
866
|
+
this._preferredColumn.set(null);
|
|
863
867
|
this.emitChange();
|
|
864
868
|
}
|
|
865
869
|
// ─── Internal helpers ───────────────────────────────────────────────────
|
|
@@ -898,7 +902,7 @@ let TextInput = (() => {
|
|
|
898
902
|
toInsert +
|
|
899
903
|
this.value.slice(this.cursorPosition);
|
|
900
904
|
this.cursorPosition = end;
|
|
901
|
-
this._preferredColumn
|
|
905
|
+
this._preferredColumn.set(null);
|
|
902
906
|
this.emitChange();
|
|
903
907
|
}
|
|
904
908
|
// --- Rendering ---
|
|
@@ -910,10 +914,11 @@ let TextInput = (() => {
|
|
|
910
914
|
: this.contentStyleOverride ?? super.ground;
|
|
911
915
|
}
|
|
912
916
|
draw(options) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
+
const drawn = { maxWidth: options.maxWidth, asciiOnly: options.asciiOnly ?? false };
|
|
918
|
+
// [LAW:effects-at-boundaries] Kept once the frame is drawn, never while it is.
|
|
919
|
+
afterPass(() => this._drawn.set(drawn));
|
|
920
|
+
const { lines, layout } = this._multiline ? this._renderMultiline(options, drawn) : this._renderSingleLine(options, drawn);
|
|
921
|
+
return Segment.anchorLines(lines, new Laid(layout)).flatMap((line) => [...line, Segment.line()]);
|
|
917
922
|
}
|
|
918
923
|
// [LAW:dataflow-not-control-flow] How a stretch of `value` is shown in one
|
|
919
924
|
// line derives glyph by glyph from mode: a mask cell in password mode, else
|
|
@@ -933,12 +938,13 @@ let TextInput = (() => {
|
|
|
933
938
|
}
|
|
934
939
|
// The cells a stretch of `value` took in the last render.
|
|
935
940
|
_shownCells(text) {
|
|
936
|
-
return asCellCol(cellLen(this._shown(text, this._drawn)));
|
|
941
|
+
return asCellCol(cellLen(this._shown(text, this._drawn.get())));
|
|
937
942
|
}
|
|
938
|
-
// The end of the glyphs of `text` whose cells
|
|
939
|
-
// left of `col`, so a column inside a wide glyph
|
|
940
|
-
|
|
941
|
-
|
|
943
|
+
// The end of the glyphs of `text` whose cells, as a frame drawn under `drawn`
|
|
944
|
+
// shows them, lie wholly left of `col`, so a column inside a wide glyph
|
|
945
|
+
// lands before it.
|
|
946
|
+
_offsetAtShownCell(text, col, drawn) {
|
|
947
|
+
const shownAs = this._shownAs(drawn);
|
|
942
948
|
let at = 0;
|
|
943
949
|
let cells = 0;
|
|
944
950
|
for (const g of graphemes(text)) {
|
|
@@ -949,24 +955,32 @@ let TextInput = (() => {
|
|
|
949
955
|
}
|
|
950
956
|
return asCodePoint(at);
|
|
951
957
|
}
|
|
952
|
-
|
|
953
|
-
|
|
958
|
+
// The window single-line content shows at `drawn`: what it displays, how
|
|
959
|
+
// many cells wide, from which cell, and the cursor's cell in it. The render
|
|
960
|
+
// draws it and a click maps through it.
|
|
961
|
+
_singleLine(drawn) {
|
|
954
962
|
// The cursor's column is the width of the shown text before it, whatever
|
|
955
963
|
// the mode — a placeholder is shown only over an empty value.
|
|
956
|
-
const shown = (text) => this._shown(text,
|
|
957
|
-
const rawDisplay =
|
|
964
|
+
const shown = (text) => this._shown(text, drawn);
|
|
965
|
+
const rawDisplay = this._showsPlaceholder() ? this.placeholder : shown(this.value);
|
|
958
966
|
const rawCellWidth = asCellCol(cellLen(rawDisplay));
|
|
959
967
|
const beforeCursor = shown(this.value.slice(0, this.cursorPosition));
|
|
960
968
|
const cursorCellCol = asCellCol(cellLen(beforeCursor));
|
|
961
969
|
// The cursor's glyph, or the one cell past the text's end.
|
|
962
970
|
const cursorCells = Math.max(1, cellLen(shown(this.value.slice(this.cursorPosition, nextGrapheme(this.value, this.cursorPosition)))));
|
|
963
|
-
const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH,
|
|
971
|
+
const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, drawn.maxWidth - 2));
|
|
964
972
|
const contentWidth = asCellCol(Math.min(maxAvailable, singleLineExtent(rawCellWidth, cursorCellCol)));
|
|
965
|
-
// The
|
|
973
|
+
// The window (in cells) starts where the most of the text before the
|
|
966
974
|
// cursor that fits beside the cursor's glyph begins: a glyph boundary, so
|
|
967
975
|
// a wide glyph is never cut, and the cursor's glyph always in view.
|
|
968
976
|
const startCell = asCellCol(cursorCellCol - cellLen(cellFitEnd(beforeCursor, asCellCol(contentWidth - cursorCells))));
|
|
969
|
-
|
|
977
|
+
return { rawDisplay, contentWidth, startCell, cursorCellCol };
|
|
978
|
+
}
|
|
979
|
+
_showsPlaceholder() {
|
|
980
|
+
return this.focused && this.value.length === 0 && this.placeholder.length > 0;
|
|
981
|
+
}
|
|
982
|
+
_renderSingleLine(options, drawn) {
|
|
983
|
+
const { rawDisplay, contentWidth, startCell, cursorCellCol } = this._singleLine(drawn);
|
|
970
984
|
const [, afterStart] = splitText(rawDisplay, startCell);
|
|
971
985
|
const [visible] = splitText(afterStart, contentWidth);
|
|
972
986
|
const display = setCellSize(visible, contentWidth);
|
|
@@ -974,7 +988,7 @@ let TextInput = (() => {
|
|
|
974
988
|
const bracketStyle = this.disabled
|
|
975
989
|
? new Style(this.disabledInk)
|
|
976
990
|
: new Style(ink(this.theme, "foreground", "background"));
|
|
977
|
-
const contentStyle =
|
|
991
|
+
const contentStyle = this._showsPlaceholder() && !this.disabled
|
|
978
992
|
? new Style({ ...ink(this.theme, "foreground", "background"), dim: true })
|
|
979
993
|
: this.ground;
|
|
980
994
|
const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
|
|
@@ -986,7 +1000,7 @@ let TextInput = (() => {
|
|
|
986
1000
|
const after = rest.slice(firstCh.length);
|
|
987
1001
|
if (before.length > 0)
|
|
988
1002
|
segments.push(new Segment(before, contentStyle));
|
|
989
|
-
segments.push(
|
|
1003
|
+
segments.push(...this._caret(at, cursorStyle, contentStyle, options));
|
|
990
1004
|
if (after.length > 0)
|
|
991
1005
|
segments.push(new Segment(after, contentStyle));
|
|
992
1006
|
}
|
|
@@ -994,144 +1008,142 @@ let TextInput = (() => {
|
|
|
994
1008
|
segments.push(new Segment(display, contentStyle));
|
|
995
1009
|
}
|
|
996
1010
|
segments.push(new Segment("]", bracketStyle));
|
|
997
|
-
return segments;
|
|
1011
|
+
return { lines: [segments], layout: { drawn, kind: "line", value: this.value, startCell } };
|
|
998
1012
|
}
|
|
999
|
-
_renderMultiline(options) {
|
|
1000
|
-
// [LAW:dataflow-not-control-flow] Decompose `value` into visual rows
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
// from the cache so Up/Down step through whatever the user actually
|
|
1004
|
-
// sees, including soft-wrap continuations.
|
|
1005
|
-
const visualRows = this._computeVisualRows(options);
|
|
1006
|
-
this._visualRows = visualRows;
|
|
1007
|
-
// Scroll window: keep `_scrollStart` (the viewport's top row) stable
|
|
1008
|
-
// across renders, adjusting only when the cursor would leave the
|
|
1009
|
-
// viewport. This is the textarea-conventional "viewport follows cursor
|
|
1010
|
-
// when it has to" behavior — cursor moves freely *within* the visible
|
|
1011
|
-
// rows; the window scrolls only at the edges. When `maxRows` is unset,
|
|
1012
|
-
// render every row.
|
|
1013
|
+
_renderMultiline(options, drawn) {
|
|
1014
|
+
// [LAW:dataflow-not-control-flow] Decompose `value` into visual rows once,
|
|
1015
|
+
// then drive both display and cursor projection from the same array.
|
|
1016
|
+
const visualRows = this._rows(drawn);
|
|
1013
1017
|
const total = visualRows.length;
|
|
1014
|
-
const cursorRow = this.
|
|
1015
|
-
|
|
1016
|
-
let visibleCount = total;
|
|
1017
|
-
if (this._maxRows !== undefined && total > this._maxRows) {
|
|
1018
|
-
const maxStart = total - this._maxRows;
|
|
1019
|
-
if (cursorRow < this._scrollStart) {
|
|
1020
|
-
this._scrollStart = cursorRow;
|
|
1021
|
-
}
|
|
1022
|
-
else if (cursorRow >= this._scrollStart + this._maxRows) {
|
|
1023
|
-
this._scrollStart = cursorRow - this._maxRows + 1;
|
|
1024
|
-
}
|
|
1025
|
-
this._scrollStart = Math.max(0, Math.min(maxStart, this._scrollStart));
|
|
1026
|
-
scrollStart = this._scrollStart;
|
|
1027
|
-
visibleCount = this._maxRows;
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
this._scrollStart = 0;
|
|
1031
|
-
}
|
|
1032
|
-
// Pad with empty rows when the value is shorter than `minRows`.
|
|
1033
|
-
let padRows = 0;
|
|
1034
|
-
if (this._minRows !== undefined && total < this._minRows && this._maxRows === undefined) {
|
|
1035
|
-
padRows = this._minRows - total;
|
|
1036
|
-
}
|
|
1018
|
+
const cursorRow = cursorRowIn(visualRows, this.cursorPosition);
|
|
1019
|
+
const arrows = this._drawsArrows(total);
|
|
1037
1020
|
const contentStyle = this.ground;
|
|
1038
1021
|
const markerStyle = new Style({ ...ink(this.theme, "foreground", "background"), dim: true });
|
|
1039
1022
|
const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
|
|
1040
|
-
// Scroll-direction arrows appear in the rightmost cell of the first/last
|
|
1041
|
-
// visible row, *only* when scroll is actually possible in that direction.
|
|
1042
|
-
// _emitRowContent reserves the indicator's cells so content never
|
|
1043
|
-
// collides with the arrow. Only emitted in `"arrows"` mode; other modes
|
|
1044
|
-
// leave the content area untouched and rely on the host to surface scroll
|
|
1045
|
-
// state externally (e.g. Panel border accessory).
|
|
1046
|
-
const arrows = this._drawsArrows(total);
|
|
1047
|
-
const canScrollUp = arrows && this._scrollStart > 0;
|
|
1048
|
-
const canScrollDown = arrows && this._scrollStart + this._maxRows < total;
|
|
1049
|
-
const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
|
|
1050
|
-
const segments = [];
|
|
1051
1023
|
const showCursor = this.focused && !this.disabled;
|
|
1052
|
-
|
|
1053
|
-
|
|
1024
|
+
// One visual row as drawn: its continuation marker, then its content, the
|
|
1025
|
+
// caret on the cursor's row.
|
|
1026
|
+
const rowAt = (rowIdx) => {
|
|
1054
1027
|
const row = visualRows[rowIdx];
|
|
1055
|
-
|
|
1056
|
-
|
|
1028
|
+
// A row past the value's last is `minRows` padding: blank.
|
|
1029
|
+
if (row === undefined)
|
|
1030
|
+
return [];
|
|
1031
|
+
const line = [];
|
|
1057
1032
|
if (row.isContinuation) {
|
|
1058
1033
|
// An ASCII stand-in as wide as the marker, so the rows keep their geometry.
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
let indicator;
|
|
1062
|
-
if (i === 0 && canScrollUp) {
|
|
1063
|
-
indicator = { ch: drawable(options, "▲", "^"), style: indicatorStyle };
|
|
1034
|
+
line.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
|
|
1064
1035
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1036
|
+
this._emitRowContent(line, options, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle);
|
|
1037
|
+
return line;
|
|
1038
|
+
};
|
|
1039
|
+
// The window is drawn at the width offered, or wider where the cursor's
|
|
1040
|
+
// row draws past it: a caret past the end of a full row.
|
|
1041
|
+
const offered = cellCount(options.maxWidth);
|
|
1042
|
+
const width = Math.max(offered, Segment.getLineLength(rowAt(cursorRow)));
|
|
1043
|
+
// Pad with empty rows when the value is shorter than `minRows`.
|
|
1044
|
+
const padRows = this._minRows !== undefined && this._maxRows === undefined ? Math.max(0, this._minRows - total) : 0;
|
|
1045
|
+
// [LAW:dataflow-not-control-flow] Only the rows a frame shows are drawn,
|
|
1046
|
+
// so a frame costs the rows in view and not the value's length. Every row
|
|
1047
|
+
// is padded to the width, so each has cells to stamp — a blank row
|
|
1048
|
+
// included — and the stamp names its row of the content.
|
|
1049
|
+
const owner = {};
|
|
1050
|
+
const rowLines = {
|
|
1051
|
+
layout: () => ({
|
|
1052
|
+
length: total + padRows,
|
|
1053
|
+
lines: (start, end) => Segment.anchorLines(Array.from({ length: end - start }, (_, i) => Segment.adjustLineLength(rowAt(start + i), width, contentStyle)), owner, start),
|
|
1054
|
+
}),
|
|
1055
|
+
measure: () => ({ minimum: width, maximum: width }),
|
|
1056
|
+
};
|
|
1057
|
+
// The viewport picks the window, at most `maxRows` tall whatever region the
|
|
1058
|
+
// field is placed in, with the cursor's row followed.
|
|
1059
|
+
const shown = Segment.splitLines(render(this._viewport.showingLines(rowLines, { start: cursorRow, end: cursorRow + 1 }), { ...options, maxWidth: width, height: this._maxRows === undefined ? undefined : { rows: this._maxRows, exact: false } }));
|
|
1060
|
+
// The content row the window's first row shows: the offset this frame
|
|
1061
|
+
// drew, read off what the viewport drew.
|
|
1062
|
+
const offset = shownRow(shown, owner);
|
|
1063
|
+
// The field is as wide as the widest row it shows, and never narrower than
|
|
1064
|
+
// its least measure the offer allows, so a blank field has cells to click;
|
|
1065
|
+
// a field with arrows is the whole offer, the arrows in its last cell. Read
|
|
1066
|
+
// off the rows in view, so a frame never measures the whole value.
|
|
1067
|
+
const widest = shown.reduce((w, _, i) => Math.max(w, Segment.getLineLength(rowAt(offset + i))), 0);
|
|
1068
|
+
const fieldWidth = arrows ? width : Math.min(width, Math.max(widest, Math.min(offered, MIN_CONTENT_WIDTH + 2)));
|
|
1069
|
+
// Scroll-direction arrows appear in the rightmost cell of the first/last
|
|
1070
|
+
// visible row, *only* when scroll is actually possible in that direction.
|
|
1071
|
+
// Only emitted in `"arrows"` mode; other modes leave the content area
|
|
1072
|
+
// untouched and rely on the host to surface scroll state externally (e.g.
|
|
1073
|
+
// Panel border accessory).
|
|
1074
|
+
const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
|
|
1075
|
+
const arrowOn = (i) => !arrows ? undefined
|
|
1076
|
+
: i === 0 && offset > 0 ? drawable(options, "▲", "^")
|
|
1077
|
+
: i === shown.length - 1 && offset + shown.length < total ? drawable(options, "▼", "v")
|
|
1078
|
+
: undefined;
|
|
1079
|
+
// The arrow's cells are reserved at the end of the offer, so a wide glyph
|
|
1080
|
+
// never collides with it: the row is cut to the cells before it. The arrow
|
|
1081
|
+
// keeps the place of the cell it covers, the viewport's included, so the
|
|
1082
|
+
// wheel over it scrolls the field.
|
|
1083
|
+
const withArrows = shown.map((line, i) => {
|
|
1084
|
+
const arrow = arrowOn(i);
|
|
1085
|
+
if (arrow === undefined)
|
|
1086
|
+
return Segment.adjustLineLength(line, fieldWidth, contentStyle);
|
|
1087
|
+
const at = offered - cellLen(arrow);
|
|
1088
|
+
const cell = indicatorStyle.withAnchor(Segment.anchorAt([line], at, 0));
|
|
1089
|
+
return Segment.adjustLineLength([...Segment.adjustLineLength(line, at, contentStyle), new Segment(arrow, cell)], fieldWidth, contentStyle);
|
|
1090
|
+
});
|
|
1091
|
+
return { lines: withArrows, layout: { drawn, kind: "rows", rows: visualRows, offset } };
|
|
1092
|
+
}
|
|
1093
|
+
// The caret's cell for the frame `options` draws: the blink restarted at the
|
|
1094
|
+
// last touch, sampled at the frame's time (`shownAt`, so under reduced
|
|
1095
|
+
// motion it holds lit). It runs away from the restart in either direction
|
|
1096
|
+
// of time, so a timeline played backwards also finds it lit just after a
|
|
1097
|
+
// key. Lit, the cell is the cursor's; dark, it is drawn as the
|
|
1098
|
+
// text around it, since the cursor is a colour pair and no attribute hides
|
|
1099
|
+
// it. The cell says when it next flips, which is when a host draws again.
|
|
1100
|
+
_caret(at, cursorStyle, contentStyle, options) {
|
|
1101
|
+
const { frame } = options;
|
|
1102
|
+
const blink = (time) => {
|
|
1103
|
+
const shown = shownAt(Timeline.run(Timeline.play(this._blinkFrom, time < this._blinkFrom ? -1 : 1), CARET_BLINK), time, options);
|
|
1104
|
+
return { lit: shown.value, hold: holdOf(shown, time) };
|
|
1105
|
+
};
|
|
1106
|
+
const { lit, hold } = frame === undefined ? CARET_UNTIMED : blink(frame.time);
|
|
1107
|
+
return stampFrame([new Segment(at, lit ? cursorStyle : contentStyle)], hold);
|
|
1086
1108
|
}
|
|
1087
1109
|
// Whether `rows` visual rows draw a scroll arrow in the last cell of a row.
|
|
1088
1110
|
_drawsArrows(rows) {
|
|
1089
1111
|
return this._scrollIndicator === "arrows" && this._maxRows !== undefined && rows > this._maxRows;
|
|
1090
1112
|
}
|
|
1091
|
-
_emitRowContent(out, options, row, cursorOnRow, contentStyle, cursorStyle
|
|
1092
|
-
const
|
|
1093
|
-
// An indicator's cells are reserved at the end of the row, so wide glyphs
|
|
1094
|
-
// never collide with it: the content is cut to the cells before it and
|
|
1095
|
-
// padded out to them. With no indicator the wrapper pads the row.
|
|
1096
|
-
const contentCellWidth = indicator === undefined
|
|
1097
|
-
? Infinity
|
|
1098
|
-
: Math.max(0, rowPrintWidth - cellLen(indicator.ch));
|
|
1099
|
-
const display = indicator === undefined
|
|
1100
|
-
? shown
|
|
1101
|
-
: setCellSize(splitText(shown, asCellCol(contentCellWidth))[0], asCellCol(contentCellWidth));
|
|
1113
|
+
_emitRowContent(out, options, row, cursorOnRow, contentStyle, cursorStyle) {
|
|
1114
|
+
const display = this._shown(row.content, options);
|
|
1102
1115
|
// cursorPosition and valueStart are both cluster boundaries, so the
|
|
1103
1116
|
// cursor's offset into the row is one too, and the shown cells before it
|
|
1104
1117
|
// are its column.
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
: -1;
|
|
1108
|
-
if (cursorCell >= 0 && cursorCell < contentCellWidth) {
|
|
1118
|
+
if (cursorOnRow) {
|
|
1119
|
+
const cursorCell = cellLen(this._shown(row.content.slice(0, this.cursorPosition - row.valueStart), options));
|
|
1109
1120
|
const [before, rest] = splitText(display, asCellCol(cursorCell));
|
|
1110
1121
|
const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
|
|
1111
1122
|
const at = firstCh || " ";
|
|
1112
1123
|
const after = rest.slice(firstCh.length);
|
|
1113
1124
|
if (before.length > 0)
|
|
1114
1125
|
out.push(new Segment(before, contentStyle));
|
|
1115
|
-
out.push(
|
|
1126
|
+
out.push(...this._caret(at, cursorStyle, contentStyle, options));
|
|
1116
1127
|
if (after.length > 0)
|
|
1117
1128
|
out.push(new Segment(after, contentStyle));
|
|
1118
1129
|
}
|
|
1119
1130
|
else if (display.length > 0) {
|
|
1120
1131
|
out.push(new Segment(display, contentStyle));
|
|
1121
1132
|
}
|
|
1122
|
-
if (indicator !== undefined)
|
|
1123
|
-
out.push(new Segment(indicator.ch, indicator.style));
|
|
1124
1133
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1134
|
+
// The visual rows `value` lays out into at `drawn`. A width that bounds
|
|
1135
|
+
// nothing wraps nothing, so before any frame they are the logical lines.
|
|
1136
|
+
_rows(drawn) {
|
|
1137
|
+
const wrap = Number.isFinite(drawn.maxWidth) ? this._wrap : undefined;
|
|
1138
|
+
const firstWidth = asCellCol(Math.max(1, drawn.maxWidth));
|
|
1127
1139
|
const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
|
|
1128
|
-
const shownAs = this._shownAs(
|
|
1140
|
+
const shownAs = this._shownAs(drawn);
|
|
1129
1141
|
const rows = [];
|
|
1130
1142
|
const lines = this.value.split("\n");
|
|
1131
1143
|
let pos = 0;
|
|
1132
1144
|
for (let li = 0; li < lines.length; li++) {
|
|
1133
1145
|
const line = lines[li];
|
|
1134
|
-
if (
|
|
1146
|
+
if (wrap !== undefined) {
|
|
1135
1147
|
// `lineOffset[s]` is where offset `s` of the line as shown falls in
|
|
1136
1148
|
// `line`: a glyph shown as itself maps code unit for code unit, and
|
|
1137
1149
|
// every offset inside a stand-in falls at the start of its glyph, so
|
|
@@ -1147,7 +1159,7 @@ let TextInput = (() => {
|
|
|
1147
1159
|
at += g.length;
|
|
1148
1160
|
}
|
|
1149
1161
|
lineOffset.push(at);
|
|
1150
|
-
const wrapRows =
|
|
1162
|
+
const wrapRows = wrap(shownLine, { firstWidth, continuationWidth });
|
|
1151
1163
|
if (wrapRows.length === 0) {
|
|
1152
1164
|
rows.push({ content: "", valueStart: asCodePoint(pos), isContinuation: false });
|
|
1153
1165
|
}
|
|
@@ -1190,27 +1202,29 @@ let TextInput = (() => {
|
|
|
1190
1202
|
/**
|
|
1191
1203
|
* `[X/Y]` scroll-position string when there's actually something to
|
|
1192
1204
|
* scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
|
|
1193
|
-
* the total visual row count.
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1205
|
+
* the total visual row count. Hand it a frame this field is drawn in — a
|
|
1206
|
+
* `Panel`'s `bottomRightAccessory` function is handed its body's:
|
|
1207
|
+
*
|
|
1208
|
+
* bottomRightAccessory: (body) => input.scrollIndicatorText(body)
|
|
1209
|
+
*
|
|
1210
|
+
* It counts the rows that frame laid out, read off the field's own cells, so
|
|
1211
|
+
* it is right on the frame it labels however deep the field sits in it.
|
|
1197
1212
|
*
|
|
1198
1213
|
* Returns `undefined` when:
|
|
1199
1214
|
* - `scrollIndicator` is not `"indices"`, or
|
|
1200
1215
|
* - `maxRows` is unset, or total visual rows ≤ maxRows (nothing to
|
|
1201
1216
|
* scroll), or
|
|
1202
|
-
* -
|
|
1217
|
+
* - `frame` shows none of the field's rows (a width too narrow to draw it).
|
|
1203
1218
|
*/
|
|
1204
|
-
|
|
1219
|
+
scrollIndicatorText(frame) {
|
|
1205
1220
|
if (this._scrollIndicator !== "indices")
|
|
1206
1221
|
return undefined;
|
|
1207
1222
|
if (this._maxRows === undefined)
|
|
1208
1223
|
return undefined;
|
|
1209
|
-
const
|
|
1210
|
-
if (
|
|
1224
|
+
const layout = layoutIn(frame, this);
|
|
1225
|
+
if (layout?.kind !== "rows" || layout.rows.length <= this._maxRows)
|
|
1211
1226
|
return undefined;
|
|
1212
|
-
|
|
1213
|
-
return `[${cursorRow1}/${rows.length}]`;
|
|
1227
|
+
return `[${cursorRowIn(layout.rows, this.cursorPosition) + 1}/${layout.rows.length}]`;
|
|
1214
1228
|
}
|
|
1215
1229
|
};
|
|
1216
1230
|
})();
|