@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.
Files changed (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +21 -29
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +13 -16
  47. package/dist/core/strip.js +119 -110
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +132 -54
  73. package/dist/renderables/effect.js +188 -115
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  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 +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. 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 { observable, action, observableRef, runInAction } from "mobx";
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, cellStepFrom, asCellCol, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
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 `cellStepFrom`.
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
- let pos = asCodePoint(0);
121
- let isFirst = true;
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
- const end = cellStepFrom(line, pos, cap);
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 = [observable];
215
- __cursor_decorators = [observable];
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, __cursor_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
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] Top visual row of the scroll viewport.
340
- // Persisted across renders so the cursor can move *within* the viewport
341
- // without dragging it along — the viewport scrolls only when the cursor
342
- // would leave it. Without this, recomputing scroll from cursor position
343
- // alone pins the cursor to the viewport's bottom edge whenever
344
- // `cursorRow >= maxRows`, making every subsequent Up/Down scroll in
345
- // lockstep with the cursor.
346
- _scrollStart = 0;
347
- // [LAW:one-source-of-truth] Viewport scroll offset in cells for the
348
- // single-line renderer. Captured by _renderSingleLine and read by
349
- // handleMouse so the hit-test uses the same visual origin the user sees.
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
- this._wrap = options.wrap;
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
- 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") {
556
663
  // Multiline has no brackets; rows may have a leading continuation marker.
557
- // Use event.y to pick the visual row, then project event.x into its content.
558
- const rows = this._visualRows;
559
- if (!rows || rows.length === 0)
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 = null;
670
+ this._preferredColumn.set(null);
566
671
  return;
567
672
  }
568
- const rowIdx = Math.max(0, rawRowIdx);
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. Add _singleLineViewportStart (the scroll offset captured
578
- // by _renderSingleLine) so a click on a scrolled viewport maps to the
579
- // correct code-unit in the underlying value.
580
- const relX = asCellCol(Math.max(0, event.x - 1) + this._singleLineViewportStart);
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 = null;
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 = null;
693
+ this._preferredColumn.set(null);
591
694
  }
592
695
  moveCharRight() {
593
696
  this.cursorPosition = nextGrapheme(this.value, this.cursorPosition);
594
- this._preferredColumn = null;
697
+ this._preferredColumn.set(null);
595
698
  }
596
699
  moveLineUp() {
597
- // Prefer the on-screen visual decomposition when it exists — wraps,
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
- if (this._visualRows !== null && this._visualRows.length > 1) {
626
- const rowIdx = this._cursorVisualRow();
627
- if (rowIdx === this._visualRows.length - 1)
628
- return;
629
- const col = this._preferredColumn ?? this._cursorVisualCol();
630
- this.cursorPosition = asCodeUnit(this._visualRows[rowIdx + 1].valueStart + this._clampColForRow(col, rowIdx + 1));
631
- this._preferredColumn = col;
632
- return;
633
- }
634
- const lineEnd = this._lineEnd();
635
- 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)
636
714
  return;
637
- const lineStart = this._lineStart();
638
- // Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
639
- const col = this._preferredColumn ?? this._shownCells(this.value.slice(lineStart, this.cursorPosition));
640
- const nextLineStart = lineEnd + 1;
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 `_cursorVisualRow` to resolve to the *later* row, making
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 = null;
743
+ this._preferredColumn.set(null);
693
744
  }
694
745
  moveLineEnd() {
695
746
  this.cursorPosition = this._lineEnd();
696
- this._preferredColumn = null;
747
+ this._preferredColumn.set(null);
697
748
  }
698
749
  moveDocStart() {
699
750
  this.cursorPosition = asCodeUnit(0);
700
- this._preferredColumn = null;
751
+ this._preferredColumn.set(null);
701
752
  }
702
753
  moveDocEnd() {
703
754
  this.cursorPosition = asCodeUnit(this.value.length);
704
- this._preferredColumn = null;
755
+ this._preferredColumn.set(null);
705
756
  }
706
757
  moveWordLeft() {
707
- let p = this.cursorPosition;
708
- while (p > 0) {
709
- const prev = prevGrapheme(this.value, p);
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
- let p = this.cursorPosition;
725
- while (p < this.value.length) {
726
- const next = nextGrapheme(this.value, p);
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 = null;
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 = null;
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
- let p = this.cursorPosition;
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 = this.value.slice(p, this.cursorPosition);
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 = null;
795
+ this._preferredColumn.set(null);
781
796
  this.emitChange();
782
797
  }
783
798
  deleteWordForward() {
784
- let p = this.cursorPosition;
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 = this.value.slice(this.cursorPosition, p);
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 = null;
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 = this.value.slice(start, this.cursorPosition);
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 = null;
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 = this.value.slice(this.cursorPosition, end);
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 = null;
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 = "\n";
828
+ this._killBuffer.set("\n");
826
829
  this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
827
- this._preferredColumn = null;
830
+ this._preferredColumn.set(null);
828
831
  this.emitChange();
829
832
  }
830
833
  }
831
834
  yank() {
832
- if (this._killBuffer.length === 0)
835
+ const killed = this._killBuffer.get();
836
+ if (killed.length === 0)
833
837
  return;
834
- this._insertText(this._killBuffer);
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 = null;
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 = null;
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 = null;
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
- this._drawn = { asciiOnly: options.asciiOnly ?? false };
914
- if (this._multiline)
915
- return this._renderMultiline(options);
916
- 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()]);
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 in the last render lie wholly
939
- // left of `col`, so a column inside a wide glyph lands before it.
940
- _offsetAtShownCell(text, col) {
941
- const shownAs = this._shownAs(this._drawn);
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
- _renderSingleLine(options) {
953
- const showPlaceholder = this.focused && this.value.length === 0 && this.placeholder.length > 0;
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, options);
957
- 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);
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, options.maxWidth - 2));
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 viewport (in cells) starts where the most of the text before 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
- this._singleLineViewportStart = startCell;
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 = showPlaceholder && !this.disabled
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(new Segment(at, cursorStyle));
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
- // exactly once per render, cache the result, then drive both display
1002
- // *and* cursor projection from the same array. Vertical motion reads
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._cursorVisualRow();
1015
- let scrollStart = 0;
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
- for (let i = 0; i < visibleCount; i++) {
1053
- 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) => {
1054
1027
  const row = visualRows[rowIdx];
1055
- if (i > 0)
1056
- 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 = [];
1057
1032
  if (row.isContinuation) {
1058
1033
  // An ASCII stand-in as wide as the marker, so the rows keep their geometry.
1059
- segments.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
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
- else if (i === visibleCount - 1 && canScrollDown) {
1066
- indicator = { ch: drawable(options, "▼", "v"), style: indicatorStyle };
1067
- }
1068
- // When a continuation marker was emitted, the row's printable width is
1069
- // reduced by the marker width — the indicator column is still at
1070
- // `options.maxWidth - 1` in the panel's view, which is
1071
- // `options.maxWidth - markerWidth - 1` columns past the marker.
1072
- const rowPrintWidth = row.isContinuation
1073
- ? asCellCol(Math.max(0, options.maxWidth - this._markerWidth))
1074
- : asCellCol(options.maxWidth);
1075
- this._emitRowContent(segments, options, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle, indicator, rowPrintWidth);
1076
- }
1077
- // Trailing empty rows for minRows padding (no cursor, no marker).
1078
- // Emit a whitespace segment of the full content width so Segment.splitLines
1079
- // does not drop the row — an empty segment before a newline would produce
1080
- // an invisible row that the renderer discards.
1081
- for (let i = 0; i < padRows; i++) {
1082
- segments.push(new Segment("\n"));
1083
- segments.push(new Segment(setCellSize("", asCellCol(options.maxWidth)), contentStyle));
1084
- }
1085
- 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 } };
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, indicator, rowPrintWidth) {
1092
- const shown = this._shown(row.content, options);
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
- const cursorCell = cursorOnRow
1106
- ? cellLen(this._shown(row.content.slice(0, this.cursorPosition - row.valueStart), options))
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(new Segment(at, cursorStyle));
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
- _computeVisualRows(options) {
1126
- const firstWidth = asCellCol(Math.max(1, options.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));
1127
1139
  const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
1128
- const shownAs = this._shownAs(options);
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 (this._wrap !== undefined) {
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 = this._wrap(shownLine, { firstWidth, continuationWidth });
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. Reads cached state populated by the most
1194
- * recent `render()` — call from a `Panel.bottomRightAccessory` thunk so
1195
- * it evaluates after content has been rendered for the current frame;
1196
- * `BorderAccessory` names the widths where it has not.
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
- * - no render has happened yet (cache is empty).
1217
+ * - `frame` shows none of the field's rows (a width too narrow to draw it).
1203
1218
  */
1204
- get scrollIndicatorText() {
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 rows = this._visualRows;
1210
- if (rows === null || rows.length <= this._maxRows)
1224
+ const layout = layoutIn(frame, this);
1225
+ if (layout?.kind !== "rows" || layout.rows.length <= this._maxRows)
1211
1226
  return undefined;
1212
- const cursorRow1 = this._cursorVisualRow() + 1;
1213
- return `[${cursorRow1}/${rows.length}]`;
1227
+ return `[${cursorRowIn(layout.rows, this.cursorPosition) + 1}/${layout.rows.length}]`;
1214
1228
  }
1215
1229
  };
1216
1230
  })();