@promptctl/rich-js 0.24.0 → 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.
Files changed (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * Two modes:
5
5
  * - single-line (default): Enter submits; newlines in value are rendered
6
- * as a literal `↵` glyph if they ever appear.
6
+ * as a literal `↵` glyph if they ever appear, and
7
+ * any other glyph of zero cells as `◌`.
7
8
  * - multi-line (`multiline: true`): Enter inserts a `\n`; `value` may
8
9
  * contain logical line breaks; Up/Down navigate
9
10
  * between logical lines preserving column intent.
11
+ * A glyph of zero cells is drawn as `◌` here too.
10
12
  *
11
13
  * [LAW:dataflow-not-control-flow] cursor and value are observable data; the
12
14
  * keymap dispatches to small `@action` mutators that all flow through the
@@ -94,13 +96,72 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
94
96
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
95
97
  done = true;
96
98
  };
97
- import { observable, action, observableRef, runInAction } from "mobx";
99
+ import { action, runInAction } from "mobx";
100
+ import { observableRef } from "./observable.js";
98
101
  import { Segment } from "../core/segment.js";
99
102
  import { Style } from "../core/style.js";
100
- import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
101
- 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";
102
112
  import { ThemedWidget } from "./themed-widget.js";
103
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
+ }
104
165
  /**
105
166
  * Character-greedy soft wrap. Breaks at any character once `firstWidth`
106
167
  * (or `continuationWidth` for continuation rows) cells are exhausted. The
@@ -109,40 +170,45 @@ import { ink } from "./ink.js";
109
170
  *
110
171
  * Wide characters (CJK, emoji) are treated as atomic: a char that would
111
172
  * straddle the budget is moved to the next row rather than split, and one too
112
- * wide for the whole budget is force-taken — see `cellStepFrom`.
173
+ * wide for the whole budget is force-taken — see `cellRows`.
113
174
  */
114
175
  export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
115
176
  if (line.length === 0)
116
- return [{ content: "", start: asCodePoint(0) }];
177
+ return [{ start: asCodePoint(0), end: asCodePoint(0) }];
178
+ const capOf = (row) => (row === 0 ? firstWidth : continuationWidth);
117
179
  const rows = [];
118
- let pos = asCodePoint(0);
119
- let isFirst = true;
120
- while (pos < line.length) {
121
- const cap = isFirst ? firstWidth : continuationWidth;
122
- if (cap <= 0)
180
+ for (const row of cellRows(line, capOf)) {
181
+ if (capOf(rows.length) <= 0)
123
182
  break;
124
- const end = cellStepFrom(line, pos, cap);
125
- rows.push({ content: line.slice(pos, end), start: pos });
126
- pos = end;
127
- isFirst = false;
183
+ rows.push(row);
128
184
  }
129
185
  return rows;
130
186
  };
131
187
  const MIN_CONTENT_WIDTH = 8;
132
- // Each glyph and its ASCII stand-in are one code unit and one cell, so the
133
- // display string maps 1:1 onto `value` and hit-testing needs no render options.
188
+ // [LAW:one-source-of-truth] The cells single-line content takes: its text and,
189
+ // past the text's end, the cell the cursor stands on there — never fewer than
190
+ // the floor. The render sizes and scrolls the field by it; `measure` asks it
191
+ // with the cursor at the end, the widest a value draws.
192
+ function singleLineExtent(textCells, cursorCell) {
193
+ return asCellCol(Math.max(MIN_CONTENT_WIDTH, textCells, cursorCell + 1));
194
+ }
195
+ // What one line shows in place of a glyph of `value`; each is one cell.
134
196
  const NEWLINE_GLYPH = "↵";
135
197
  const ASCII_NEWLINE_GLYPH = "~";
136
198
  const MASK_GLYPH = "•";
137
199
  const ASCII_MASK_GLYPH = "*";
200
+ // The dotted circle Unicode draws for a mark with no base to sit on; `?` is the
201
+ // ASCII replacement character.
202
+ const ZERO_CELL_GLYPH = "◌";
203
+ const ASCII_ZERO_CELL_GLYPH = "?";
138
204
  // Text a key may insert: anything but the C0 controls, DEL and the C1
139
205
  // controls, which a terminal reads as escape sequences (U+009B is CSI).
140
- const PRINTABLE = /^[^\x00-\x1f\x7f-\x9f]+$/u;
206
+ const PRINTABLE = pattern(/^[^\x00-\x1f\x7f-\x9f]+$/u);
141
207
  // Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
142
208
  // readline default (alphanumeric + underscore), which is what users typing
143
209
  // shell commands or code identifiers expect.
144
- const WORD_CHAR_RE = /[A-Za-z0-9_]/;
145
- const WHITESPACE_RE = /\s/;
210
+ const WORD_CHAR_RE = pattern(/[A-Za-z0-9_]/);
211
+ const WHITESPACE_RE = pattern(/\s/);
146
212
  /** CRLF and lone CR as LF, the line break `value` holds. */
147
213
  function toLf(text) {
148
214
  return text.replace(/\r\n?/g, "\n");
@@ -153,6 +219,33 @@ function isWordChar(c) {
153
219
  function isWhitespace(c) {
154
220
  return c !== undefined && WHITESPACE_RE.test(c);
155
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 } };
156
249
  let TextInput = (() => {
157
250
  let _classSuper = ThemedWidget;
158
251
  let _instanceExtraInitializers = [];
@@ -162,6 +255,9 @@ let TextInput = (() => {
162
255
  let __cursor_decorators;
163
256
  let __cursor_initializers = [];
164
257
  let __cursor_extraInitializers = [];
258
+ let __blinkFrom_decorators;
259
+ let __blinkFrom_initializers = [];
260
+ let __blinkFrom_extraInitializers = [];
165
261
  let _placeholder_decorators;
166
262
  let _placeholder_initializers = [];
167
263
  let _placeholder_extraInitializers = [];
@@ -175,6 +271,7 @@ let TextInput = (() => {
175
271
  let _contentStyleOverride_initializers = [];
176
272
  let _contentStyleOverride_extraInitializers = [];
177
273
  let _handleKey_decorators;
274
+ let _handleFocus_decorators;
178
275
  let _handleMouse_decorators;
179
276
  let _setHovered_decorators;
180
277
  let _moveCharLeft_decorators;
@@ -199,13 +296,15 @@ let TextInput = (() => {
199
296
  return class TextInput extends _classSuper {
200
297
  static {
201
298
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
202
- __value_decorators = [observable];
203
- __cursor_decorators = [observable];
299
+ __value_decorators = [observableRef];
300
+ __cursor_decorators = [observableRef];
301
+ __blinkFrom_decorators = [observableRef];
204
302
  _placeholder_decorators = [observableRef];
205
303
  _indicatorStyleOverride_decorators = [observableRef];
206
304
  _cursorStyleOverride_decorators = [observableRef];
207
305
  _contentStyleOverride_decorators = [observableRef];
208
306
  _handleKey_decorators = [action];
307
+ _handleFocus_decorators = [action];
209
308
  _handleMouse_decorators = [action];
210
309
  _setHovered_decorators = [action];
211
310
  _moveCharLeft_decorators = [action];
@@ -229,11 +328,13 @@ let TextInput = (() => {
229
328
  __insertText_decorators = [action];
230
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);
231
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);
232
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);
233
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);
234
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);
235
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);
236
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);
237
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);
238
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);
239
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);
@@ -265,6 +366,13 @@ let TextInput = (() => {
265
366
  #_cursor_accessor_storage = (__runInitializers(this, __value_extraInitializers), __runInitializers(this, __cursor_initializers, void 0));
266
367
  get _cursor() { return this.#_cursor_accessor_storage; }
267
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; }
268
376
  // [LAW:single-enforcer] Every write to either half of the pair passes
269
377
  // `graphemeBoundary`, so the cursor is a cluster boundary inside `value`
270
378
  // whoever set it last — a caller replacing `value`, a click, an insert that
@@ -280,7 +388,7 @@ let TextInput = (() => {
280
388
  }
281
389
  get cursorPosition() { return this._cursor; }
282
390
  set cursorPosition(p) { this._cursor = graphemeBoundary(this._value, p); }
283
- #placeholder_accessor_storage = (__runInitializers(this, __cursor_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
391
+ #placeholder_accessor_storage = (__runInitializers(this, __blinkFrom_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
284
392
  get placeholder() { return this.#placeholder_accessor_storage; }
285
393
  set placeholder(value) { this.#placeholder_accessor_storage = value; }
286
394
  _maxLength = __runInitializers(this, _placeholder_extraInitializers);
@@ -301,42 +409,28 @@ let TextInput = (() => {
301
409
  #contentStyleOverride_accessor_storage = (__runInitializers(this, _cursorStyleOverride_extraInitializers), __runInitializers(this, _contentStyleOverride_initializers, void 0));
302
410
  get contentStyleOverride() { return this.#contentStyleOverride_accessor_storage; }
303
411
  set contentStyleOverride(value) { this.#contentStyleOverride_accessor_storage = value; }
304
- /**
305
- * Last computed visual-row decomposition. Cached at the end of `render()`
306
- * so vertical motion (Up/Down) can step row-by-row without re-running the
307
- * wrap strategy. Null before the first render — vertical motion falls
308
- * back to logical-line motion in that case.
309
- *
310
- * [LAW:dataflow-not-control-flow] One source of truth for "what does Up
311
- * mean right now": the row table the renderer just produced. No parallel
312
- * "where would the cursor go" math; both the renderer and the keymap
313
- * read from the same array.
314
- */
315
- _visualRows = (__runInitializers(this, _contentStyleOverride_extraInitializers), null);
316
412
  // [LAW:types-are-the-program] `_preferredColumn` is a cell column (not a
317
413
  // code-unit index) — vertical motion navigates the visual surface, not the
318
414
  // string's byte layout. Reset to null on every non-vertical motion or edit
319
415
  // so the next Up/Down captures a fresh intent. Without this, Down through a
320
416
  // short line then back Up would land at the short-line column instead of the
321
417
  // original.
322
- _preferredColumn = null;
418
+ _preferredColumn = (__runInitializers(this, _contentStyleOverride_extraInitializers), new State(null));
323
419
  // Single-slot kill buffer used by Ctrl+U / Ctrl+K → Ctrl+Y. Not a full
324
420
  // kill ring (no Alt+Y rotation) — sufficient for the common cut/paste
325
421
  // pattern that's the readline 90% case.
326
- _killBuffer = "";
327
- // [LAW:one-source-of-truth] Top visual row of the scroll viewport.
328
- // Persisted across renders so the cursor can move *within* the viewport
329
- // without dragging it along — the viewport scrolls only when the cursor
330
- // would leave it. Without this, recomputing scroll from cursor position
331
- // alone pins the cursor to the viewport's bottom edge whenever
332
- // `cursorRow >= maxRows`, making every subsequent Up/Down scroll in
333
- // lockstep with the cursor.
334
- _scrollStart = 0;
335
- // [LAW:one-source-of-truth] Viewport scroll offset in cells for the
336
- // single-line renderer. Captured by _renderSingleLine and read by
337
- // handleMouse so the hit-test uses the same visual origin the user sees.
338
- _singleLineViewportStart = asCellCol(0);
339
- _singleLineAsciiOnly = 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;
340
434
  multiline;
341
435
  constructor(options = {}) {
342
436
  super(options.theme);
@@ -354,7 +448,8 @@ let TextInput = (() => {
354
448
  this._maxLength = options.maxLength;
355
449
  this._password = options.password ?? false;
356
450
  this._multiline = options.multiline ?? false;
357
- this._wrap = options.wrap;
451
+ // Soft wrap is multiline's: one line wraps to nothing.
452
+ this._wrap = this._multiline ? options.wrap : undefined;
358
453
  this._continuationMarker = options.continuationMarker ?? "↳ ";
359
454
  this._markerWidth = asCellCol(cellLen(this._continuationMarker));
360
455
  this._maxRows = options.maxRows;
@@ -364,9 +459,29 @@ let TextInput = (() => {
364
459
  this.cursorStyleOverride = options.cursorStyle;
365
460
  this.contentStyleOverride = options.contentStyle;
366
461
  this.multiline = this._multiline;
462
+ this._viewport = new Viewport();
367
463
  }
368
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.
369
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) {
370
485
  if (this.disabled)
371
486
  return;
372
487
  // ─── editing keys (modifier-conscious) ───
@@ -538,110 +653,69 @@ let TextInput = (() => {
538
653
  return;
539
654
  if (event.type !== "mouse_down")
540
655
  return;
541
- if (this._multiline) {
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") {
542
663
  // Multiline has no brackets; rows may have a leading continuation marker.
543
- // Use event.y to pick the visual row, then project event.x into its content.
544
- const rows = this._visualRows;
545
- if (!rows || rows.length === 0)
546
- return;
547
- 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;
548
667
  // Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
549
- if (rawRowIdx >= rows.length) {
668
+ if (rawRowIdx >= layout.rows.length) {
550
669
  this.cursorPosition = asCodeUnit(this.value.length);
551
- this._preferredColumn = null;
670
+ this._preferredColumn.set(null);
552
671
  return;
553
672
  }
554
- const rowIdx = Math.max(0, rawRowIdx);
555
- const row = rows[rowIdx];
673
+ const row = layout.rows[rawRowIdx];
556
674
  // Continuation rows start after the marker; non-continuation rows start at column 0.
557
675
  const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
558
676
  const relX = asCellCol(Math.max(0, event.x - contentXOff));
559
- this.cursorPosition = asCodeUnit(row.valueStart + cellColToCodeUnitOffset(row.content, relX));
677
+ this.cursorPosition = asCodeUnit(row.valueStart + this._offsetAtShownCell(row.content, relX, layout.drawn));
560
678
  }
561
679
  else {
562
680
  // [LAW:types-are-the-program] relX is a cell-column offset into the full
563
- // display string. Add _singleLineViewportStart (the scroll offset captured
564
- // by _renderSingleLine) so a click on a scrolled viewport maps to the
565
- // correct code-unit in the underlying value.
566
- const relX = asCellCol(Math.max(0, event.x - 1) + this._singleLineViewportStart);
567
- // The glyphs of `value` whose shown cells lie wholly left of the click,
568
- // shown as the last render showed them.
569
- const drawn = { asciiOnly: this._singleLineAsciiOnly };
570
- let at = 0;
571
- let col = 0;
572
- for (const g of graphemes(this.value)) {
573
- const w = cellLen(this._shown(g, drawn));
574
- if (col + w > relX)
575
- break;
576
- col += w;
577
- at += g.length;
578
- }
579
- this.cursorPosition = asCodeUnit(at);
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);
580
685
  }
581
- this._preferredColumn = null;
686
+ this._preferredColumn.set(null);
582
687
  }
583
688
  // --- Hover mutator (router fast-path) ---
584
689
  setHovered(value) { this.hovered = value; }
585
690
  // ─── Public motion primitives ───────────────────────────────────────────
586
691
  moveCharLeft() {
587
692
  this.cursorPosition = prevGrapheme(this.value, this.cursorPosition);
588
- this._preferredColumn = null;
693
+ this._preferredColumn.set(null);
589
694
  }
590
695
  moveCharRight() {
591
696
  this.cursorPosition = nextGrapheme(this.value, this.cursorPosition);
592
- this._preferredColumn = null;
697
+ this._preferredColumn.set(null);
593
698
  }
594
699
  moveLineUp() {
595
- // Prefer the on-screen visual decomposition when it exists — wraps,
596
- // marker offsets, and scroll alignment are all already accounted for.
597
- // Fall back to logical-line motion only when no render has occurred yet
598
- // (e.g. cursor motion before mount).
599
- if (this._visualRows !== null && this._visualRows.length > 1) {
600
- const rowIdx = this._cursorVisualRow();
601
- if (rowIdx === 0)
602
- return;
603
- const col = this._preferredColumn ?? this._cursorVisualCol();
604
- const target = this._visualRows[rowIdx - 1];
605
- this.cursorPosition = asCodeUnit(target.valueStart + this._clampColForRow(col, rowIdx - 1));
606
- this._preferredColumn = col;
607
- return;
608
- }
609
- const lineStart = this._lineStart();
610
- if (lineStart === 0)
611
- return;
612
- // Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
613
- const col = this._preferredColumn ?? asCellCol(cellLen(this.value.slice(lineStart, this.cursorPosition)));
614
- const prevLineEnd = lineStart - 1;
615
- let prevLineStart = prevLineEnd;
616
- while (prevLineStart > 0 && this.value[prevLineStart - 1] !== "\n")
617
- prevLineStart--;
618
- const prevLineContent = this.value.slice(prevLineStart, prevLineEnd);
619
- this.cursorPosition = asCodeUnit(prevLineStart + cellColToCodeUnitOffset(prevLineContent, asCellCol(Math.min(col, cellLen(prevLineContent)))));
620
- this._preferredColumn = col;
700
+ this._moveLine(-1);
621
701
  }
622
702
  moveLineDown() {
623
- if (this._visualRows !== null && this._visualRows.length > 1) {
624
- const rowIdx = this._cursorVisualRow();
625
- if (rowIdx === this._visualRows.length - 1)
626
- return;
627
- const col = this._preferredColumn ?? this._cursorVisualCol();
628
- this.cursorPosition = asCodeUnit(this._visualRows[rowIdx + 1].valueStart + this._clampColForRow(col, rowIdx + 1));
629
- this._preferredColumn = col;
630
- return;
631
- }
632
- const lineEnd = this._lineEnd();
633
- if (lineEnd === this.value.length)
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)
634
714
  return;
635
- const lineStart = this._lineStart();
636
- // Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
637
- const col = this._preferredColumn ?? asCellCol(cellLen(this.value.slice(lineStart, this.cursorPosition)));
638
- const nextLineStart = lineEnd + 1;
639
- let nextLineEnd = nextLineStart;
640
- while (nextLineEnd < this.value.length && this.value[nextLineEnd] !== "\n")
641
- nextLineEnd++;
642
- const nextLineContent = this.value.slice(nextLineStart, nextLineEnd);
643
- this.cursorPosition = asCodeUnit(nextLineStart + cellColToCodeUnitOffset(nextLineContent, asCellCol(Math.min(col, cellLen(nextLineContent)))));
644
- 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);
645
719
  }
646
720
  // Clamp `col` (a cell column) to the target visual row's content, returning
647
721
  // the code-unit offset within that row. When the row IS followed by a
@@ -649,91 +723,46 @@ let TextInput = (() => {
649
723
  // cluster boundary strictly inside the row (which may be more than
650
724
  // one cell before the visual end when the last glyph is wide) so the
651
725
  // cursor stays strictly inside the target row — landing at the boundary
652
- // would cause `_cursorVisualRow` to resolve to the *later* row, making
726
+ // would cause `cursorRowIn` to resolve to the *later* row, making
653
727
  // every subsequent Up/Down stick at the boundary.
654
728
  //
655
729
  // For rows NOT followed by a continuation (last wrap row, or any unwrapped
656
730
  // logical line), the end-of-line position is valid — there is no adjacent
657
731
  // continuation row to collide with.
658
- _clampColForRow(col, targetIdx) {
659
- const rows = this._visualRows;
732
+ _clampColForRow(rows, col, targetIdx) {
660
733
  const target = rows[targetIdx];
661
734
  const nextIsContinuation = targetIdx + 1 < rows.length && rows[targetIdx + 1].isContinuation;
662
- const contentCellWidth = asCellCol(cellLen(target.content));
735
+ const contentCellWidth = this._shownCells(target.content);
663
736
  const capCells = nextIsContinuation
664
737
  ? asCellCol(Math.max(0, contentCellWidth - 1))
665
738
  : contentCellWidth;
666
- return cellColToCodeUnitOffset(target.content, asCellCol(Math.min(col, capCells)));
667
- }
668
- // Locate which cached visual row the cursor sits on. Returns the row index
669
- // for use in `moveLineUp`/`moveLineDown`; caller must guard `_visualRows`.
670
- _cursorVisualRow() {
671
- const rows = this._visualRows;
672
- let idx = 0;
673
- for (let i = rows.length - 1; i >= 0; i--) {
674
- if (rows[i].valueStart <= this.cursorPosition) {
675
- idx = i;
676
- break;
677
- }
678
- }
679
- return idx;
680
- }
681
- // Returns the cursor's cell column within its current visual row.
682
- _cursorVisualCol() {
683
- const rows = this._visualRows;
684
- const idx = this._cursorVisualRow();
685
- const row = rows[idx];
686
- return asCellCol(cellLen(row.content.slice(0, this.cursorPosition - row.valueStart)));
739
+ return this._offsetAtShownCell(target.content, asCellCol(Math.min(col, capCells)), this._drawn.get());
687
740
  }
688
741
  moveLineStart() {
689
742
  this.cursorPosition = this._lineStart();
690
- this._preferredColumn = null;
743
+ this._preferredColumn.set(null);
691
744
  }
692
745
  moveLineEnd() {
693
746
  this.cursorPosition = this._lineEnd();
694
- this._preferredColumn = null;
747
+ this._preferredColumn.set(null);
695
748
  }
696
749
  moveDocStart() {
697
750
  this.cursorPosition = asCodeUnit(0);
698
- this._preferredColumn = null;
751
+ this._preferredColumn.set(null);
699
752
  }
700
753
  moveDocEnd() {
701
754
  this.cursorPosition = asCodeUnit(this.value.length);
702
- this._preferredColumn = null;
755
+ this._preferredColumn.set(null);
703
756
  }
704
757
  moveWordLeft() {
705
- let p = this.cursorPosition;
706
- while (p > 0) {
707
- const prev = prevGrapheme(this.value, p);
708
- if (isWordChar(this.value.slice(prev, p)))
709
- break;
710
- p = prev;
711
- }
712
- while (p > 0) {
713
- const prev = prevGrapheme(this.value, p);
714
- if (!isWordChar(this.value.slice(prev, p)))
715
- break;
716
- p = prev;
717
- }
718
- this.cursorPosition = p;
719
- 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);
720
761
  }
721
762
  moveWordRight() {
722
- let p = this.cursorPosition;
723
- while (p < this.value.length) {
724
- const next = nextGrapheme(this.value, p);
725
- if (isWordChar(this.value.slice(p, next)))
726
- break;
727
- p = next;
728
- }
729
- while (p < this.value.length) {
730
- const next = nextGrapheme(this.value, p);
731
- if (!isWordChar(this.value.slice(p, next)))
732
- break;
733
- p = next;
734
- }
735
- this.cursorPosition = p;
736
- 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);
737
766
  }
738
767
  // ─── Public editing primitives ──────────────────────────────────────────
739
768
  deleteCharBack() {
@@ -742,7 +771,7 @@ let TextInput = (() => {
742
771
  const newPos = prevGrapheme(this.value, this.cursorPosition);
743
772
  this.value = this.value.slice(0, newPos) + this.value.slice(this.cursorPosition);
744
773
  this.cursorPosition = newPos;
745
- this._preferredColumn = null;
774
+ this._preferredColumn.set(null);
746
775
  this.emitChange();
747
776
  }
748
777
  deleteCharForward() {
@@ -750,86 +779,63 @@ let TextInput = (() => {
750
779
  return;
751
780
  const nextPos = nextGrapheme(this.value, this.cursorPosition);
752
781
  this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(nextPos);
753
- this._preferredColumn = null;
782
+ this._preferredColumn.set(null);
754
783
  this.emitChange();
755
784
  }
756
785
  deleteWordBack() {
757
786
  // Readline `unix-word-rubout` semantics: delete back to nearest whitespace,
758
787
  // skipping trailing whitespace first so successive Ctrl+W at "foo bar |"
759
788
  // → "foo |" → "|" rather than getting stuck on the trailing space.
760
- let p = this.cursorPosition;
761
- while (p > 0) {
762
- const prev = prevGrapheme(this.value, p);
763
- if (!isWhitespace(this.value.slice(prev, p)))
764
- break;
765
- p = prev;
766
- }
767
- while (p > 0) {
768
- const prev = prevGrapheme(this.value, p);
769
- if (isWhitespace(this.value.slice(prev, p)))
770
- break;
771
- p = prev;
772
- }
789
+ const p = asCodePoint(this.cursorPosition - wordSpan(clustersBefore(this.value, this.cursorPosition), isWhitespace, isNotWhitespace));
773
790
  if (p === this.cursorPosition)
774
791
  return;
775
- this._killBuffer = this.value.slice(p, this.cursorPosition);
792
+ this._killBuffer.set(this.value.slice(p, this.cursorPosition));
776
793
  this.value = this.value.slice(0, p) + this.value.slice(this.cursorPosition);
777
794
  this.cursorPosition = p;
778
- this._preferredColumn = null;
795
+ this._preferredColumn.set(null);
779
796
  this.emitChange();
780
797
  }
781
798
  deleteWordForward() {
782
- let p = this.cursorPosition;
783
- while (p < this.value.length) {
784
- const next = nextGrapheme(this.value, p);
785
- if (isWordChar(this.value.slice(p, next)))
786
- break;
787
- p = next;
788
- }
789
- while (p < this.value.length) {
790
- const next = nextGrapheme(this.value, p);
791
- if (!isWordChar(this.value.slice(p, next)))
792
- break;
793
- p = next;
794
- }
799
+ const p = this.cursorPosition + wordSpan(clustersFrom(this.value, this.cursorPosition), isNotWordChar, isWordChar);
795
800
  if (p === this.cursorPosition)
796
801
  return;
797
- this._killBuffer = this.value.slice(this.cursorPosition, p);
802
+ this._killBuffer.set(this.value.slice(this.cursorPosition, p));
798
803
  this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(p);
799
- this._preferredColumn = null;
804
+ this._preferredColumn.set(null);
800
805
  this.emitChange();
801
806
  }
802
807
  killLineBack() {
803
808
  const start = this._lineStart();
804
809
  if (start === this.cursorPosition)
805
810
  return;
806
- this._killBuffer = this.value.slice(start, this.cursorPosition);
811
+ this._killBuffer.set(this.value.slice(start, this.cursorPosition));
807
812
  this.value = this.value.slice(0, start) + this.value.slice(this.cursorPosition);
808
813
  this.cursorPosition = start;
809
- this._preferredColumn = null;
814
+ this._preferredColumn.set(null);
810
815
  this.emitChange();
811
816
  }
812
817
  killLineForward() {
813
818
  const end = this._lineEnd();
814
819
  if (end > this.cursorPosition) {
815
- this._killBuffer = this.value.slice(this.cursorPosition, end);
820
+ this._killBuffer.set(this.value.slice(this.cursorPosition, end));
816
821
  this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(end);
817
- this._preferredColumn = null;
822
+ this._preferredColumn.set(null);
818
823
  this.emitChange();
819
824
  return;
820
825
  }
821
826
  // Already at line end → kill the trailing `\n` (join next line).
822
827
  if (this.cursorPosition < this.value.length) {
823
- this._killBuffer = "\n";
828
+ this._killBuffer.set("\n");
824
829
  this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
825
- this._preferredColumn = null;
830
+ this._preferredColumn.set(null);
826
831
  this.emitChange();
827
832
  }
828
833
  }
829
834
  yank() {
830
- if (this._killBuffer.length === 0)
835
+ const killed = this._killBuffer.get();
836
+ if (killed.length === 0)
831
837
  return;
832
- this._insertText(this._killBuffer);
838
+ this._insertText(killed);
833
839
  }
834
840
  transposeChars() {
835
841
  // Readline `transpose-chars`: swap the char before the cursor with the
@@ -846,7 +852,7 @@ let TextInput = (() => {
846
852
  return;
847
853
  const cp1 = prevGrapheme(this.value, cp2);
848
854
  this.value = this.value.slice(0, cp1) + this.value.slice(cp2) + this.value.slice(cp1, cp2);
849
- this._preferredColumn = null;
855
+ this._preferredColumn.set(null);
850
856
  this.emitChange();
851
857
  return;
852
858
  }
@@ -857,7 +863,7 @@ let TextInput = (() => {
857
863
  const charAt = this.value.slice(p, cpAfter);
858
864
  this.value = this.value.slice(0, cpBefore) + charAt + charBefore + this.value.slice(cpAfter);
859
865
  this.cursorPosition = cpAfter;
860
- this._preferredColumn = null;
866
+ this._preferredColumn.set(null);
861
867
  this.emitChange();
862
868
  }
863
869
  // ─── Internal helpers ───────────────────────────────────────────────────
@@ -896,7 +902,7 @@ let TextInput = (() => {
896
902
  toInsert +
897
903
  this.value.slice(this.cursorPosition);
898
904
  this.cursorPosition = end;
899
- this._preferredColumn = null;
905
+ this._preferredColumn.set(null);
900
906
  this.emitChange();
901
907
  }
902
908
  // --- Rendering ---
@@ -908,56 +914,93 @@ let TextInput = (() => {
908
914
  : this.contentStyleOverride ?? super.ground;
909
915
  }
910
916
  draw(options) {
911
- if (this._multiline)
912
- return this._renderMultiline(options);
913
- return this._renderSingleLine(options);
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()]);
914
922
  }
915
923
  // [LAW:dataflow-not-control-flow] How a stretch of `value` is shown in one
916
- // line derives from mode: one mask cell per glyph, or `\n` as a visible
917
- // glyph. The render and the click hit-test both read it.
924
+ // line derives glyph by glyph from mode: a mask cell in password mode, else
925
+ // the glyph itself — or, for a glyph of zero cells, a stand-in of one cell.
926
+ // Every glyph then has a column of its own, so each cursor stop draws the
927
+ // cursor somewhere new. The render and the click hit-test both read it.
928
+ _shownAs(options) {
929
+ const mask = drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH);
930
+ const newline = drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH);
931
+ const zeroCell = drawable(options, ZERO_CELL_GLYPH, ASCII_ZERO_CELL_GLYPH);
932
+ return (g) => this._password ? mask
933
+ : cellLen(g) > 0 ? g
934
+ : g === "\n" ? newline : zeroCell;
935
+ }
918
936
  _shown(text, options) {
919
- return this._password
920
- ? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(graphemes(text).length)
921
- : text.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH));
937
+ return graphemes(text).map(this._shownAs(options)).join("");
922
938
  }
923
- _renderSingleLine(options) {
924
- const showPlaceholder = this.focused && this.value.length === 0 && this.placeholder.length > 0;
939
+ // The cells a stretch of `value` took in the last render.
940
+ _shownCells(text) {
941
+ return asCellCol(cellLen(this._shown(text, this._drawn.get())));
942
+ }
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);
948
+ let at = 0;
949
+ let cells = 0;
950
+ for (const g of graphemes(text)) {
951
+ cells += cellLen(shownAs(g));
952
+ if (cells > col)
953
+ break;
954
+ at += g.length;
955
+ }
956
+ return asCodePoint(at);
957
+ }
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) {
925
962
  // The cursor's column is the width of the shown text before it, whatever
926
963
  // the mode — a placeholder is shown only over an empty value.
927
- this._singleLineAsciiOnly = options.asciiOnly ?? false;
928
- const shown = (text) => this._shown(text, options);
929
- const rawDisplay = showPlaceholder ? this.placeholder : shown(this.value);
964
+ const shown = (text) => this._shown(text, drawn);
965
+ const rawDisplay = this._showsPlaceholder() ? this.placeholder : shown(this.value);
930
966
  const rawCellWidth = asCellCol(cellLen(rawDisplay));
931
- const cursorCellCol = asCellCol(cellLen(shown(this.value.slice(0, this.cursorPosition))));
932
- const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, options.maxWidth - 2));
933
- const desiredWidth = asCellCol(Math.max(MIN_CONTENT_WIDTH, rawCellWidth, cursorCellCol + 1));
934
- const contentWidth = asCellCol(Math.min(maxAvailable, desiredWidth));
935
- // Slide viewport (in cells) to keep cursor visible when content overflows.
936
- const startCell = asCellCol(Math.max(0, Math.min(rawCellWidth - contentWidth, cursorCellCol - contentWidth + 1)));
967
+ const beforeCursor = shown(this.value.slice(0, this.cursorPosition));
968
+ const cursorCellCol = asCellCol(cellLen(beforeCursor));
969
+ // The cursor's glyph, or the one cell past the text's end.
970
+ const cursorCells = Math.max(1, cellLen(shown(this.value.slice(this.cursorPosition, nextGrapheme(this.value, this.cursorPosition)))));
971
+ const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, drawn.maxWidth - 2));
972
+ const contentWidth = asCellCol(Math.min(maxAvailable, singleLineExtent(rawCellWidth, cursorCellCol)));
973
+ // The window (in cells) starts where the most of the text before the
974
+ // cursor that fits beside the cursor's glyph begins: a glyph boundary, so
975
+ // a wide glyph is never cut, and the cursor's glyph always in view.
976
+ const startCell = asCellCol(cursorCellCol - cellLen(cellFitEnd(beforeCursor, asCellCol(contentWidth - cursorCells))));
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);
937
984
  const [, afterStart] = splitText(rawDisplay, startCell);
938
- // splitText snaps backward when startCell falls mid-wide-char, so afterStart
939
- // may start 1 cell earlier than requested. actualStartCell is the real offset.
940
- const actualStartCell = asCellCol(rawCellWidth - cellLen(afterStart));
941
- this._singleLineViewportStart = actualStartCell;
942
985
  const [visible] = splitText(afterStart, contentWidth);
943
986
  const display = setCellSize(visible, contentWidth);
944
- const cursorDisplayCellCol = asCellCol(cursorCellCol - actualStartCell);
987
+ const cursorDisplayCellCol = asCellCol(cursorCellCol - startCell);
945
988
  const bracketStyle = this.disabled
946
989
  ? new Style(this.disabledInk)
947
990
  : new Style(ink(this.theme, "foreground", "background"));
948
- const contentStyle = showPlaceholder && !this.disabled
991
+ const contentStyle = this._showsPlaceholder() && !this.disabled
949
992
  ? new Style({ ...ink(this.theme, "foreground", "background"), dim: true })
950
993
  : this.ground;
951
994
  const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
952
995
  const segments = [new Segment("[", bracketStyle)];
953
- if (this.focused && !this.disabled && cursorDisplayCellCol >= 0 && cursorDisplayCellCol < contentWidth) {
996
+ if (this.focused && !this.disabled) {
954
997
  const [before, rest] = splitText(display, cursorDisplayCellCol);
955
998
  const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
956
999
  const at = firstCh || " ";
957
1000
  const after = rest.slice(firstCh.length);
958
1001
  if (before.length > 0)
959
1002
  segments.push(new Segment(before, contentStyle));
960
- segments.push(new Segment(at, cursorStyle));
1003
+ segments.push(...this._caret(at, cursorStyle, contentStyle, options));
961
1004
  if (after.length > 0)
962
1005
  segments.push(new Segment(after, contentStyle));
963
1006
  }
@@ -965,169 +1008,168 @@ let TextInput = (() => {
965
1008
  segments.push(new Segment(display, contentStyle));
966
1009
  }
967
1010
  segments.push(new Segment("]", bracketStyle));
968
- return segments;
1011
+ return { lines: [segments], layout: { drawn, kind: "line", value: this.value, startCell } };
969
1012
  }
970
- _renderMultiline(options) {
971
- // [LAW:dataflow-not-control-flow] Decompose `value` into visual rows
972
- // exactly once per render, cache the result, then drive both display
973
- // *and* cursor projection from the same array. Vertical motion reads
974
- // from the cache so Up/Down step through whatever the user actually
975
- // sees, including soft-wrap continuations.
976
- const visualRows = this._computeVisualRows(options.maxWidth);
977
- this._visualRows = visualRows;
978
- // Scroll window: keep `_scrollStart` (the viewport's top row) stable
979
- // across renders, adjusting only when the cursor would leave the
980
- // viewport. This is the textarea-conventional "viewport follows cursor
981
- // when it has to" behavior — cursor moves freely *within* the visible
982
- // rows; the window scrolls only at the edges. When `maxRows` is unset,
983
- // 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);
984
1017
  const total = visualRows.length;
985
- const cursorRow = this._cursorVisualRow();
986
- let scrollStart = 0;
987
- let visibleCount = total;
988
- if (this._maxRows !== undefined && total > this._maxRows) {
989
- const maxStart = total - this._maxRows;
990
- if (cursorRow < this._scrollStart) {
991
- this._scrollStart = cursorRow;
992
- }
993
- else if (cursorRow >= this._scrollStart + this._maxRows) {
994
- this._scrollStart = cursorRow - this._maxRows + 1;
995
- }
996
- this._scrollStart = Math.max(0, Math.min(maxStart, this._scrollStart));
997
- scrollStart = this._scrollStart;
998
- visibleCount = this._maxRows;
999
- }
1000
- else {
1001
- this._scrollStart = 0;
1002
- }
1003
- // Pad with empty rows when the value is shorter than `minRows`.
1004
- let padRows = 0;
1005
- if (this._minRows !== undefined && total < this._minRows && this._maxRows === undefined) {
1006
- padRows = this._minRows - total;
1007
- }
1018
+ const cursorRow = cursorRowIn(visualRows, this.cursorPosition);
1019
+ const arrows = this._drawsArrows(total);
1008
1020
  const contentStyle = this.ground;
1009
1021
  const markerStyle = new Style({ ...ink(this.theme, "foreground", "background"), dim: true });
1010
1022
  const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
1011
- // Scroll-direction arrows appear in the rightmost cell of the first/last
1012
- // visible row, *only* when scroll is actually possible in that direction.
1013
- // _emitRowContent reserves the indicator's cells so content never
1014
- // collides with the arrow. Only emitted in `"arrows"` mode; other modes
1015
- // leave the content area untouched and rely on the host to surface scroll
1016
- // state externally (e.g. Panel border accessory).
1017
- const scrollable = this._maxRows !== undefined && total > this._maxRows;
1018
- const arrowsMode = this._scrollIndicator === "arrows";
1019
- const canScrollUp = arrowsMode && scrollable && this._scrollStart > 0;
1020
- const canScrollDown = arrowsMode && scrollable && this._scrollStart + this._maxRows < total;
1021
- const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
1022
- const segments = [];
1023
1023
  const showCursor = this.focused && !this.disabled;
1024
- for (let i = 0; i < visibleCount; i++) {
1025
- const rowIdx = scrollStart + i;
1024
+ // One visual row as drawn: its continuation marker, then its content, the
1025
+ // caret on the cursor's row.
1026
+ const rowAt = (rowIdx) => {
1026
1027
  const row = visualRows[rowIdx];
1027
- if (i > 0)
1028
- segments.push(new Segment("\n"));
1028
+ // A row past the value's last is `minRows` padding: blank.
1029
+ if (row === undefined)
1030
+ return [];
1031
+ const line = [];
1029
1032
  if (row.isContinuation) {
1030
1033
  // An ASCII stand-in as wide as the marker, so the rows keep their geometry.
1031
- segments.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
1032
- }
1033
- let indicator;
1034
- if (i === 0 && canScrollUp) {
1035
- indicator = { ch: drawable(options, "▲", "^"), style: indicatorStyle };
1036
- }
1037
- else if (i === visibleCount - 1 && canScrollDown) {
1038
- indicator = { ch: drawable(options, "▼", "v"), style: indicatorStyle };
1034
+ line.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
1039
1035
  }
1040
- // When a continuation marker was emitted, the row's printable width is
1041
- // reduced by the marker width — the indicator column is still at
1042
- // `options.maxWidth - 1` in the panel's view, which is
1043
- // `options.maxWidth - markerWidth - 1` columns past the marker.
1044
- const rowPrintWidth = row.isContinuation
1045
- ? asCellCol(Math.max(0, options.maxWidth - this._markerWidth))
1046
- : asCellCol(options.maxWidth);
1047
- this._emitRowContent(segments, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle, indicator, rowPrintWidth);
1048
- }
1049
- // Trailing empty rows for minRows padding (no cursor, no marker).
1050
- // Emit a whitespace segment of the full content width so Segment.splitLines
1051
- // does not drop the row — an empty segment before a newline would produce
1052
- // an invisible row that the renderer discards.
1053
- for (let i = 0; i < padRows; i++) {
1054
- segments.push(new Segment("\n"));
1055
- segments.push(new Segment(setCellSize("", asCellCol(options.maxWidth)), contentStyle));
1056
- }
1057
- return segments;
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 } };
1058
1092
  }
1059
- _emitRowContent(out, row, cursorOnRow, contentStyle, cursorStyle, indicator, rowPrintWidth) {
1060
- const content = row.content;
1061
- // cursorCol is a code-unit offset into row.content; both cursorPosition
1062
- // and valueStart are CodePoint, so their difference lands on a code-point
1063
- // boundary within content and is safe for string slicing.
1064
- const cursorCol = cursorOnRow ? this.cursorPosition - row.valueStart : -1;
1065
- // Fast path: no indicator on this row. Let the wrapper pad to width.
1066
- if (indicator === undefined) {
1067
- if (!cursorOnRow) {
1068
- if (content.length > 0)
1069
- out.push(new Segment(content, contentStyle));
1070
- return;
1071
- }
1072
- const before = content.slice(0, cursorCol);
1073
- const nextCp = nextGrapheme(content, asCodePoint(cursorCol));
1074
- const at = content.slice(cursorCol, nextCp) || " ";
1075
- const after = content.slice(nextCp);
1076
- if (before.length > 0)
1077
- out.push(new Segment(before, contentStyle));
1078
- out.push(new Segment(at, cursorStyle));
1079
- if (after.length > 0)
1080
- out.push(new Segment(after, contentStyle));
1081
- return;
1082
- }
1083
- // Indicator path: reserve the indicator's cells at the end of the row so
1084
- // wide characters never collide with it. Content fills [0, contentCellWidth)
1085
- // cells; the indicator occupies the remaining cells.
1086
- const indicatorWidth = asCellCol(cellLen(indicator.ch));
1087
- const contentCellWidth = asCellCol(Math.max(0, rowPrintWidth - indicatorWidth));
1088
- // Clip row content to the non-indicator region and pad to exact cell width.
1089
- const [visibleContent] = splitText(content, contentCellWidth);
1090
- const paddedContent = setCellSize(visibleContent, contentCellWidth);
1091
- // Map the code-unit cursor offset to a cell column within this row.
1092
- const cursorCellColInRow = cursorOnRow && cursorCol >= 0
1093
- ? cellLen(content.slice(0, cursorCol))
1094
- : -1;
1095
- if (cursorCellColInRow >= 0 && cursorCellColInRow < contentCellWidth) {
1096
- const [before, rest] = splitText(paddedContent, asCellCol(cursorCellColInRow));
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);
1108
+ }
1109
+ // Whether `rows` visual rows draw a scroll arrow in the last cell of a row.
1110
+ _drawsArrows(rows) {
1111
+ return this._scrollIndicator === "arrows" && this._maxRows !== undefined && rows > this._maxRows;
1112
+ }
1113
+ _emitRowContent(out, options, row, cursorOnRow, contentStyle, cursorStyle) {
1114
+ const display = this._shown(row.content, options);
1115
+ // cursorPosition and valueStart are both cluster boundaries, so the
1116
+ // cursor's offset into the row is one too, and the shown cells before it
1117
+ // are its column.
1118
+ if (cursorOnRow) {
1119
+ const cursorCell = cellLen(this._shown(row.content.slice(0, this.cursorPosition - row.valueStart), options));
1120
+ const [before, rest] = splitText(display, asCellCol(cursorCell));
1097
1121
  const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
1098
1122
  const at = firstCh || " ";
1099
1123
  const after = rest.slice(firstCh.length);
1100
1124
  if (before.length > 0)
1101
1125
  out.push(new Segment(before, contentStyle));
1102
- out.push(new Segment(at, cursorStyle));
1126
+ out.push(...this._caret(at, cursorStyle, contentStyle, options));
1103
1127
  if (after.length > 0)
1104
1128
  out.push(new Segment(after, contentStyle));
1105
1129
  }
1106
- else {
1107
- if (paddedContent.length > 0)
1108
- out.push(new Segment(paddedContent, contentStyle));
1130
+ else if (display.length > 0) {
1131
+ out.push(new Segment(display, contentStyle));
1109
1132
  }
1110
- out.push(new Segment(indicator.ch, indicator.style));
1111
1133
  }
1112
- _computeVisualRows(maxWidth) {
1113
- const firstWidth = asCellCol(Math.max(1, maxWidth));
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));
1114
1139
  const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
1140
+ const shownAs = this._shownAs(drawn);
1115
1141
  const rows = [];
1116
1142
  const lines = this.value.split("\n");
1117
1143
  let pos = 0;
1118
1144
  for (let li = 0; li < lines.length; li++) {
1119
1145
  const line = lines[li];
1120
- if (this._wrap !== undefined) {
1121
- const wrapRows = this._wrap(line, { firstWidth, continuationWidth });
1146
+ if (wrap !== undefined) {
1147
+ // `lineOffset[s]` is where offset `s` of the line as shown falls in
1148
+ // `line`: a glyph shown as itself maps code unit for code unit, and
1149
+ // every offset inside a stand-in falls at the start of its glyph, so
1150
+ // a row the strategy ends there draws the glyph on the next row.
1151
+ let shownLine = "";
1152
+ const lineOffset = [];
1153
+ let at = 0;
1154
+ for (const g of graphemes(line)) {
1155
+ const s = shownAs(g);
1156
+ for (let i = 0; i < s.length; i++)
1157
+ lineOffset.push(s === g ? at + i : at);
1158
+ shownLine += s;
1159
+ at += g.length;
1160
+ }
1161
+ lineOffset.push(at);
1162
+ const wrapRows = wrap(shownLine, { firstWidth, continuationWidth });
1122
1163
  if (wrapRows.length === 0) {
1123
1164
  rows.push({ content: "", valueStart: asCodePoint(pos), isContinuation: false });
1124
1165
  }
1125
1166
  else {
1126
1167
  for (let ri = 0; ri < wrapRows.length; ri++) {
1127
1168
  const wr = wrapRows[ri];
1169
+ const start = lineOffset[wr.start];
1128
1170
  rows.push({
1129
- content: wr.content,
1130
- valueStart: asCodePoint(pos + wr.start),
1171
+ content: line.slice(start, lineOffset[wr.end]),
1172
+ valueStart: asCodePoint(pos + start),
1131
1173
  isContinuation: ri > 0,
1132
1174
  });
1133
1175
  }
@@ -1140,34 +1182,49 @@ let TextInput = (() => {
1140
1182
  }
1141
1183
  return rows;
1142
1184
  }
1143
- measure(_options) {
1185
+ measure(options) {
1144
1186
  const minimum = MIN_CONTENT_WIDTH + 2;
1145
- const maximum = Math.max(minimum, Math.max(cellLen(this.value), cellLen(this.placeholder)) + 2);
1146
- return { minimum, maximum };
1187
+ const shownCells = (line) => cellLen(this._shown(line, options));
1188
+ // The widest the value draws, its cursor at the end of a line included. A
1189
+ // multi-line value draws each logical line on rows of its own, with no
1190
+ // brackets and no placeholder, and a scroll arrow's cell past each line
1191
+ // when its lines outnumber the rows.
1192
+ if (this._multiline) {
1193
+ const lines = this.value.split("\n");
1194
+ const arrowCells = this._drawsArrows(lines.length) ? 1 : 0;
1195
+ return { minimum, maximum: lines.reduce((w, line) => Math.max(w, shownCells(line) + 1 + arrowCells), minimum) };
1196
+ }
1197
+ // A single-line value draws its breaks as glyphs between brackets, and its
1198
+ // placeholder over an empty value.
1199
+ const valueCells = shownCells(this.value);
1200
+ return { minimum, maximum: singleLineExtent(this.value === "" ? cellLen(this.placeholder) : valueCells, valueCells) + 2 };
1147
1201
  }
1148
1202
  /**
1149
1203
  * `[X/Y]` scroll-position string when there's actually something to
1150
1204
  * scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
1151
- * the total visual row count. Reads cached state populated by the most
1152
- * recent `render()` — call from a `Panel.bottomRightAccessory` thunk so
1153
- * it evaluates after content has been rendered for the current frame.
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.
1154
1212
  *
1155
1213
  * Returns `undefined` when:
1156
1214
  * - `scrollIndicator` is not `"indices"`, or
1157
1215
  * - `maxRows` is unset, or total visual rows ≤ maxRows (nothing to
1158
1216
  * scroll), or
1159
- * - no render has happened yet (cache is empty).
1217
+ * - `frame` shows none of the field's rows (a width too narrow to draw it).
1160
1218
  */
1161
- get scrollIndicatorText() {
1219
+ scrollIndicatorText(frame) {
1162
1220
  if (this._scrollIndicator !== "indices")
1163
1221
  return undefined;
1164
1222
  if (this._maxRows === undefined)
1165
1223
  return undefined;
1166
- const rows = this._visualRows;
1167
- if (rows === null || rows.length <= this._maxRows)
1224
+ const layout = layoutIn(frame, this);
1225
+ if (layout?.kind !== "rows" || layout.rows.length <= this._maxRows)
1168
1226
  return undefined;
1169
- const cursorRow1 = this._cursorVisualRow() + 1;
1170
- return `[${cursorRow1}/${rows.length}]`;
1227
+ return `[${cursorRowIn(layout.rows, this.cursorPosition) + 1}/${layout.rows.length}]`;
1171
1228
  }
1172
1229
  };
1173
1230
  })();