@promptctl/rich-js 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -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
@@ -97,7 +99,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
97
99
  import { observable, action, observableRef, runInAction } from "mobx";
98
100
  import { Segment } from "../core/segment.js";
99
101
  import { Style } from "../core/style.js";
100
- import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
102
+ import { cellLen, setCellSize, splitText, cellFitEnd, cellStepFrom, asCellCol, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
101
103
  import { drawable } from "../core/protocol.js";
102
104
  import { ThemedWidget } from "./themed-widget.js";
103
105
  import { ink } from "./ink.js";
@@ -113,7 +115,7 @@ import { ink } from "./ink.js";
113
115
  */
114
116
  export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
115
117
  if (line.length === 0)
116
- return [{ content: "", start: asCodePoint(0) }];
118
+ return [{ start: asCodePoint(0), end: asCodePoint(0) }];
117
119
  const rows = [];
118
120
  let pos = asCodePoint(0);
119
121
  let isFirst = true;
@@ -122,19 +124,29 @@ export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
122
124
  if (cap <= 0)
123
125
  break;
124
126
  const end = cellStepFrom(line, pos, cap);
125
- rows.push({ content: line.slice(pos, end), start: pos });
127
+ rows.push({ start: pos, end });
126
128
  pos = end;
127
129
  isFirst = false;
128
130
  }
129
131
  return rows;
130
132
  };
131
133
  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.
134
+ // [LAW:one-source-of-truth] The cells single-line content takes: its text and,
135
+ // past the text's end, the cell the cursor stands on there — never fewer than
136
+ // the floor. The render sizes and scrolls the field by it; `measure` asks it
137
+ // with the cursor at the end, the widest a value draws.
138
+ function singleLineExtent(textCells, cursorCell) {
139
+ return asCellCol(Math.max(MIN_CONTENT_WIDTH, textCells, cursorCell + 1));
140
+ }
141
+ // What one line shows in place of a glyph of `value`; each is one cell.
134
142
  const NEWLINE_GLYPH = "↵";
135
143
  const ASCII_NEWLINE_GLYPH = "~";
136
144
  const MASK_GLYPH = "•";
137
145
  const ASCII_MASK_GLYPH = "*";
146
+ // The dotted circle Unicode draws for a mark with no base to sit on; `?` is the
147
+ // ASCII replacement character.
148
+ const ZERO_CELL_GLYPH = "◌";
149
+ const ASCII_ZERO_CELL_GLYPH = "?";
138
150
  // Text a key may insert: anything but the C0 controls, DEL and the C1
139
151
  // controls, which a terminal reads as escape sequences (U+009B is CSI).
140
152
  const PRINTABLE = /^[^\x00-\x1f\x7f-\x9f]+$/u;
@@ -336,7 +348,9 @@ let TextInput = (() => {
336
348
  // single-line renderer. Captured by _renderSingleLine and read by
337
349
  // handleMouse so the hit-test uses the same visual origin the user sees.
338
350
  _singleLineViewportStart = asCellCol(0);
339
- _singleLineAsciiOnly = false;
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 };
340
354
  multiline;
341
355
  constructor(options = {}) {
342
356
  super(options.theme);
@@ -556,7 +570,7 @@ let TextInput = (() => {
556
570
  // Continuation rows start after the marker; non-continuation rows start at column 0.
557
571
  const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
558
572
  const relX = asCellCol(Math.max(0, event.x - contentXOff));
559
- this.cursorPosition = asCodeUnit(row.valueStart + cellColToCodeUnitOffset(row.content, relX));
573
+ this.cursorPosition = asCodeUnit(row.valueStart + this._offsetAtShownCell(row.content, relX));
560
574
  }
561
575
  else {
562
576
  // [LAW:types-are-the-program] relX is a cell-column offset into the full
@@ -564,19 +578,7 @@ let TextInput = (() => {
564
578
  // by _renderSingleLine) so a click on a scrolled viewport maps to the
565
579
  // correct code-unit in the underlying value.
566
580
  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);
581
+ this.cursorPosition = this._offsetAtShownCell(this.value, relX);
580
582
  }
581
583
  this._preferredColumn = null;
582
584
  }
@@ -610,13 +612,13 @@ let TextInput = (() => {
610
612
  if (lineStart === 0)
611
613
  return;
612
614
  // 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)));
615
+ const col = this._preferredColumn ?? this._shownCells(this.value.slice(lineStart, this.cursorPosition));
614
616
  const prevLineEnd = lineStart - 1;
615
617
  let prevLineStart = prevLineEnd;
616
618
  while (prevLineStart > 0 && this.value[prevLineStart - 1] !== "\n")
617
619
  prevLineStart--;
618
620
  const prevLineContent = this.value.slice(prevLineStart, prevLineEnd);
619
- this.cursorPosition = asCodeUnit(prevLineStart + cellColToCodeUnitOffset(prevLineContent, asCellCol(Math.min(col, cellLen(prevLineContent)))));
621
+ this.cursorPosition = asCodeUnit(prevLineStart + this._offsetAtShownCell(prevLineContent, col));
620
622
  this._preferredColumn = col;
621
623
  }
622
624
  moveLineDown() {
@@ -634,13 +636,13 @@ let TextInput = (() => {
634
636
  return;
635
637
  const lineStart = this._lineStart();
636
638
  // 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)));
639
+ const col = this._preferredColumn ?? this._shownCells(this.value.slice(lineStart, this.cursorPosition));
638
640
  const nextLineStart = lineEnd + 1;
639
641
  let nextLineEnd = nextLineStart;
640
642
  while (nextLineEnd < this.value.length && this.value[nextLineEnd] !== "\n")
641
643
  nextLineEnd++;
642
644
  const nextLineContent = this.value.slice(nextLineStart, nextLineEnd);
643
- this.cursorPosition = asCodeUnit(nextLineStart + cellColToCodeUnitOffset(nextLineContent, asCellCol(Math.min(col, cellLen(nextLineContent)))));
645
+ this.cursorPosition = asCodeUnit(nextLineStart + this._offsetAtShownCell(nextLineContent, col));
644
646
  this._preferredColumn = col;
645
647
  }
646
648
  // Clamp `col` (a cell column) to the target visual row's content, returning
@@ -659,11 +661,11 @@ let TextInput = (() => {
659
661
  const rows = this._visualRows;
660
662
  const target = rows[targetIdx];
661
663
  const nextIsContinuation = targetIdx + 1 < rows.length && rows[targetIdx + 1].isContinuation;
662
- const contentCellWidth = asCellCol(cellLen(target.content));
664
+ const contentCellWidth = this._shownCells(target.content);
663
665
  const capCells = nextIsContinuation
664
666
  ? asCellCol(Math.max(0, contentCellWidth - 1))
665
667
  : contentCellWidth;
666
- return cellColToCodeUnitOffset(target.content, asCellCol(Math.min(col, capCells)));
668
+ return this._offsetAtShownCell(target.content, asCellCol(Math.min(col, capCells)));
667
669
  }
668
670
  // Locate which cached visual row the cursor sits on. Returns the row index
669
671
  // for use in `moveLineUp`/`moveLineDown`; caller must guard `_visualRows`.
@@ -683,7 +685,7 @@ let TextInput = (() => {
683
685
  const rows = this._visualRows;
684
686
  const idx = this._cursorVisualRow();
685
687
  const row = rows[idx];
686
- return asCellCol(cellLen(row.content.slice(0, this.cursorPosition - row.valueStart)));
688
+ return this._shownCells(row.content.slice(0, this.cursorPosition - row.valueStart));
687
689
  }
688
690
  moveLineStart() {
689
691
  this.cursorPosition = this._lineStart();
@@ -908,40 +910,67 @@ let TextInput = (() => {
908
910
  : this.contentStyleOverride ?? super.ground;
909
911
  }
910
912
  draw(options) {
913
+ this._drawn = { asciiOnly: options.asciiOnly ?? false };
911
914
  if (this._multiline)
912
915
  return this._renderMultiline(options);
913
916
  return this._renderSingleLine(options);
914
917
  }
915
918
  // [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.
919
+ // line derives glyph by glyph from mode: a mask cell in password mode, else
920
+ // the glyph itself — or, for a glyph of zero cells, a stand-in of one cell.
921
+ // Every glyph then has a column of its own, so each cursor stop draws the
922
+ // cursor somewhere new. The render and the click hit-test both read it.
923
+ _shownAs(options) {
924
+ const mask = drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH);
925
+ const newline = drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH);
926
+ const zeroCell = drawable(options, ZERO_CELL_GLYPH, ASCII_ZERO_CELL_GLYPH);
927
+ return (g) => this._password ? mask
928
+ : cellLen(g) > 0 ? g
929
+ : g === "\n" ? newline : zeroCell;
930
+ }
918
931
  _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));
932
+ return graphemes(text).map(this._shownAs(options)).join("");
933
+ }
934
+ // The cells a stretch of `value` took in the last render.
935
+ _shownCells(text) {
936
+ return asCellCol(cellLen(this._shown(text, this._drawn)));
937
+ }
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);
942
+ let at = 0;
943
+ let cells = 0;
944
+ for (const g of graphemes(text)) {
945
+ cells += cellLen(shownAs(g));
946
+ if (cells > col)
947
+ break;
948
+ at += g.length;
949
+ }
950
+ return asCodePoint(at);
922
951
  }
923
952
  _renderSingleLine(options) {
924
953
  const showPlaceholder = this.focused && this.value.length === 0 && this.placeholder.length > 0;
925
954
  // The cursor's column is the width of the shown text before it, whatever
926
955
  // the mode — a placeholder is shown only over an empty value.
927
- this._singleLineAsciiOnly = options.asciiOnly ?? false;
928
956
  const shown = (text) => this._shown(text, options);
929
957
  const rawDisplay = showPlaceholder ? this.placeholder : shown(this.value);
930
958
  const rawCellWidth = asCellCol(cellLen(rawDisplay));
931
- const cursorCellCol = asCellCol(cellLen(shown(this.value.slice(0, this.cursorPosition))));
959
+ const beforeCursor = shown(this.value.slice(0, this.cursorPosition));
960
+ const cursorCellCol = asCellCol(cellLen(beforeCursor));
961
+ // The cursor's glyph, or the one cell past the text's end.
962
+ const cursorCells = Math.max(1, cellLen(shown(this.value.slice(this.cursorPosition, nextGrapheme(this.value, this.cursorPosition)))));
932
963
  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)));
964
+ const contentWidth = asCellCol(Math.min(maxAvailable, singleLineExtent(rawCellWidth, cursorCellCol)));
965
+ // The viewport (in cells) starts where the most of the text before the
966
+ // cursor that fits beside the cursor's glyph begins: a glyph boundary, so
967
+ // a wide glyph is never cut, and the cursor's glyph always in view.
968
+ const startCell = asCellCol(cursorCellCol - cellLen(cellFitEnd(beforeCursor, asCellCol(contentWidth - cursorCells))));
969
+ this._singleLineViewportStart = startCell;
937
970
  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
971
  const [visible] = splitText(afterStart, contentWidth);
943
972
  const display = setCellSize(visible, contentWidth);
944
- const cursorDisplayCellCol = asCellCol(cursorCellCol - actualStartCell);
973
+ const cursorDisplayCellCol = asCellCol(cursorCellCol - startCell);
945
974
  const bracketStyle = this.disabled
946
975
  ? new Style(this.disabledInk)
947
976
  : new Style(ink(this.theme, "foreground", "background"));
@@ -950,7 +979,7 @@ let TextInput = (() => {
950
979
  : this.ground;
951
980
  const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
952
981
  const segments = [new Segment("[", bracketStyle)];
953
- if (this.focused && !this.disabled && cursorDisplayCellCol >= 0 && cursorDisplayCellCol < contentWidth) {
982
+ if (this.focused && !this.disabled) {
954
983
  const [before, rest] = splitText(display, cursorDisplayCellCol);
955
984
  const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
956
985
  const at = firstCh || " ";
@@ -973,7 +1002,7 @@ let TextInput = (() => {
973
1002
  // *and* cursor projection from the same array. Vertical motion reads
974
1003
  // from the cache so Up/Down step through whatever the user actually
975
1004
  // sees, including soft-wrap continuations.
976
- const visualRows = this._computeVisualRows(options.maxWidth);
1005
+ const visualRows = this._computeVisualRows(options);
977
1006
  this._visualRows = visualRows;
978
1007
  // Scroll window: keep `_scrollStart` (the viewport's top row) stable
979
1008
  // across renders, adjusting only when the cursor would leave the
@@ -1014,10 +1043,9 @@ let TextInput = (() => {
1014
1043
  // collides with the arrow. Only emitted in `"arrows"` mode; other modes
1015
1044
  // leave the content area untouched and rely on the host to surface scroll
1016
1045
  // 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;
1046
+ const arrows = this._drawsArrows(total);
1047
+ const canScrollUp = arrows && this._scrollStart > 0;
1048
+ const canScrollDown = arrows && this._scrollStart + this._maxRows < total;
1021
1049
  const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
1022
1050
  const segments = [];
1023
1051
  const showCursor = this.focused && !this.disabled;
@@ -1044,7 +1072,7 @@ let TextInput = (() => {
1044
1072
  const rowPrintWidth = row.isContinuation
1045
1073
  ? asCellCol(Math.max(0, options.maxWidth - this._markerWidth))
1046
1074
  : asCellCol(options.maxWidth);
1047
- this._emitRowContent(segments, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle, indicator, rowPrintWidth);
1075
+ this._emitRowContent(segments, options, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle, indicator, rowPrintWidth);
1048
1076
  }
1049
1077
  // Trailing empty rows for minRows padding (no cursor, no marker).
1050
1078
  // Emit a whitespace segment of the full content width so Segment.splitLines
@@ -1056,44 +1084,29 @@ let TextInput = (() => {
1056
1084
  }
1057
1085
  return segments;
1058
1086
  }
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))
1087
+ // Whether `rows` visual rows draw a scroll arrow in the last cell of a row.
1088
+ _drawsArrows(rows) {
1089
+ return this._scrollIndicator === "arrows" && this._maxRows !== undefined && rows > this._maxRows;
1090
+ }
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));
1102
+ // cursorPosition and valueStart are both cluster boundaries, so the
1103
+ // cursor's offset into the row is one too, and the shown cells before it
1104
+ // are its column.
1105
+ const cursorCell = cursorOnRow
1106
+ ? cellLen(this._shown(row.content.slice(0, this.cursorPosition - row.valueStart), options))
1094
1107
  : -1;
1095
- if (cursorCellColInRow >= 0 && cursorCellColInRow < contentCellWidth) {
1096
- const [before, rest] = splitText(paddedContent, asCellCol(cursorCellColInRow));
1108
+ if (cursorCell >= 0 && cursorCell < contentCellWidth) {
1109
+ const [before, rest] = splitText(display, asCellCol(cursorCell));
1097
1110
  const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
1098
1111
  const at = firstCh || " ";
1099
1112
  const after = rest.slice(firstCh.length);
@@ -1103,31 +1116,48 @@ let TextInput = (() => {
1103
1116
  if (after.length > 0)
1104
1117
  out.push(new Segment(after, contentStyle));
1105
1118
  }
1106
- else {
1107
- if (paddedContent.length > 0)
1108
- out.push(new Segment(paddedContent, contentStyle));
1119
+ else if (display.length > 0) {
1120
+ out.push(new Segment(display, contentStyle));
1109
1121
  }
1110
- out.push(new Segment(indicator.ch, indicator.style));
1122
+ if (indicator !== undefined)
1123
+ out.push(new Segment(indicator.ch, indicator.style));
1111
1124
  }
1112
- _computeVisualRows(maxWidth) {
1113
- const firstWidth = asCellCol(Math.max(1, maxWidth));
1125
+ _computeVisualRows(options) {
1126
+ const firstWidth = asCellCol(Math.max(1, options.maxWidth));
1114
1127
  const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
1128
+ const shownAs = this._shownAs(options);
1115
1129
  const rows = [];
1116
1130
  const lines = this.value.split("\n");
1117
1131
  let pos = 0;
1118
1132
  for (let li = 0; li < lines.length; li++) {
1119
1133
  const line = lines[li];
1120
1134
  if (this._wrap !== undefined) {
1121
- const wrapRows = this._wrap(line, { firstWidth, continuationWidth });
1135
+ // `lineOffset[s]` is where offset `s` of the line as shown falls in
1136
+ // `line`: a glyph shown as itself maps code unit for code unit, and
1137
+ // every offset inside a stand-in falls at the start of its glyph, so
1138
+ // a row the strategy ends there draws the glyph on the next row.
1139
+ let shownLine = "";
1140
+ const lineOffset = [];
1141
+ let at = 0;
1142
+ for (const g of graphemes(line)) {
1143
+ const s = shownAs(g);
1144
+ for (let i = 0; i < s.length; i++)
1145
+ lineOffset.push(s === g ? at + i : at);
1146
+ shownLine += s;
1147
+ at += g.length;
1148
+ }
1149
+ lineOffset.push(at);
1150
+ const wrapRows = this._wrap(shownLine, { firstWidth, continuationWidth });
1122
1151
  if (wrapRows.length === 0) {
1123
1152
  rows.push({ content: "", valueStart: asCodePoint(pos), isContinuation: false });
1124
1153
  }
1125
1154
  else {
1126
1155
  for (let ri = 0; ri < wrapRows.length; ri++) {
1127
1156
  const wr = wrapRows[ri];
1157
+ const start = lineOffset[wr.start];
1128
1158
  rows.push({
1129
- content: wr.content,
1130
- valueStart: asCodePoint(pos + wr.start),
1159
+ content: line.slice(start, lineOffset[wr.end]),
1160
+ valueStart: asCodePoint(pos + start),
1131
1161
  isContinuation: ri > 0,
1132
1162
  });
1133
1163
  }
@@ -1140,17 +1170,30 @@ let TextInput = (() => {
1140
1170
  }
1141
1171
  return rows;
1142
1172
  }
1143
- measure(_options) {
1173
+ measure(options) {
1144
1174
  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 };
1175
+ const shownCells = (line) => cellLen(this._shown(line, options));
1176
+ // The widest the value draws, its cursor at the end of a line included. A
1177
+ // multi-line value draws each logical line on rows of its own, with no
1178
+ // brackets and no placeholder, and a scroll arrow's cell past each line
1179
+ // when its lines outnumber the rows.
1180
+ if (this._multiline) {
1181
+ const lines = this.value.split("\n");
1182
+ const arrowCells = this._drawsArrows(lines.length) ? 1 : 0;
1183
+ return { minimum, maximum: lines.reduce((w, line) => Math.max(w, shownCells(line) + 1 + arrowCells), minimum) };
1184
+ }
1185
+ // A single-line value draws its breaks as glyphs between brackets, and its
1186
+ // placeholder over an empty value.
1187
+ const valueCells = shownCells(this.value);
1188
+ return { minimum, maximum: singleLineExtent(this.value === "" ? cellLen(this.placeholder) : valueCells, valueCells) + 2 };
1147
1189
  }
1148
1190
  /**
1149
1191
  * `[X/Y]` scroll-position string when there's actually something to
1150
1192
  * scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
1151
1193
  * the total visual row count. Reads cached state populated by the most
1152
1194
  * recent `render()` — call from a `Panel.bottomRightAccessory` thunk so
1153
- * it evaluates after content has been rendered for the current frame.
1195
+ * it evaluates after content has been rendered for the current frame;
1196
+ * `BorderAccessory` names the widths where it has not.
1154
1197
  *
1155
1198
  * Returns `undefined` when:
1156
1199
  * - `scrollIndicator` is not `"indices"`, or
@@ -7,8 +7,10 @@
7
7
  * `Table` cell — and nothing lays it out a second time. Everything added here
8
8
  * reads the frame the view drew:
9
9
  *
10
- * - Focus moves in document order: the order widgets start to render in
11
- * (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
10
+ * - Focus moves in document order: the order the view reports its widgets in
11
+ * (`RenderOptions.onPresent`), among those with a cell on the frame on screen.
12
+ * A widget its container hands no column is reported held, not drawn: it is
13
+ * in the view, so it keeps focus, and it has no cell, so Tab passes it.
12
14
  * Not reading order — in a `Layout` split into columns, reading order
13
15
  * zigzags between the columns row by row.
14
16
  * - Overlays are painted over the view's frame, in document order, directly
@@ -24,10 +26,12 @@
24
26
  * hear the input.
25
27
  */
26
28
  import { Segment } from "../core/segment.js";
29
+ import type { Presence } from "../core/protocol.js";
27
30
  import type { Unsubscribe } from "../core/subscription.js";
28
31
  import { type AppOptions, type AppPhase, type Wheel } from "../host/app.js";
29
32
  import type { FocusManager, FrameSource, KeyEvent, KeyHandlerOptions } from "./types.js";
30
33
  import type { AppMouseEvent } from "../host/app.js";
34
+ import { WidgetBase } from "./widget-base.js";
31
35
  export interface WidgetAppOptions extends AppOptions {
32
36
  /**
33
37
  * Whether a left-button press focuses the widget under it, before it hears the
@@ -35,15 +39,27 @@ export interface WidgetAppOptions extends AppOptions {
35
39
  */
36
40
  readonly focusOnClick?: boolean;
37
41
  }
42
+ /** A widget the view reported for a frame, and whether it was drawn or held. */
43
+ export interface WidgetPresence {
44
+ readonly widget: WidgetBase;
45
+ readonly presence: Presence;
46
+ }
38
47
  export declare class WidgetApp implements FrameSource {
39
48
  readonly focusManager: FocusManager;
40
49
  private readonly focus;
41
50
  private readonly app;
42
51
  private readonly router;
43
52
  private readonly reaction;
44
- private drawn;
53
+ private _present;
45
54
  constructor(options: WidgetAppOptions);
46
55
  get phase(): AppPhase;
56
+ /**
57
+ * The widgets the view reported for the frame on screen, in document order,
58
+ * each drawn or held (`RenderOptions.onPresent`). A held widget is one its
59
+ * container handed no column: in the view, with no cell on the frame.
60
+ */
61
+ get present(): readonly WidgetPresence[];
62
+ private get widgets();
47
63
  /** The rows on screen now, as painted: what a pointer event lands on. */
48
64
  get frame(): readonly (readonly Segment[])[];
49
65
  /**
@@ -7,8 +7,10 @@
7
7
  * `Table` cell — and nothing lays it out a second time. Everything added here
8
8
  * reads the frame the view drew:
9
9
  *
10
- * - Focus moves in document order: the order widgets start to render in
11
- * (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
10
+ * - Focus moves in document order: the order the view reports its widgets in
11
+ * (`RenderOptions.onPresent`), among those with a cell on the frame on screen.
12
+ * A widget its container hands no column is reported held, not drawn: it is
13
+ * in the view, so it keeps focus, and it has no cell, so Tab passes it.
12
14
  * Not reading order — in a `Layout` split into columns, reading order
13
15
  * zigzags between the columns row by row.
14
16
  * - Overlays are painted over the view's frame, in document order, directly
@@ -25,6 +27,7 @@
25
27
  */
26
28
  import { Reaction } from "mobx";
27
29
  import { Segment } from "../core/segment.js";
30
+ import { render } from "../core/protocol.js";
28
31
  import { App } from "../host/app.js";
29
32
  import { DefaultFocusManager } from "./focus-manager.js";
30
33
  import { EventRouter } from "./event-router.js";
@@ -38,8 +41,7 @@ export class WidgetApp {
38
41
  router;
39
42
  // Paints the next frame when anything the last one read changes.
40
43
  reaction;
41
- // The widgets the view drew for the frame on screen, in document order.
42
- drawn = [];
44
+ _present = [];
43
45
  constructor(options) {
44
46
  const { view, focusOnClick = true } = options;
45
47
  this.reaction = new Reaction("WidgetApp", () => this.app.refresh());
@@ -47,9 +49,12 @@ export class WidgetApp {
47
49
  ...options,
48
50
  view: (t) => ({ render: (renderOptions) => this.compose(() => view(t), renderOptions) }),
49
51
  });
50
- this.focus = new DefaultFocusManager(() => {
51
- const owners = ownersOn(this.app.frame);
52
- return this.drawn.filter((widget) => owners.has(widget));
52
+ this.focus = new DefaultFocusManager({
53
+ present: () => this.widgets,
54
+ onScreen: () => {
55
+ const owners = ownersOn(this.app.frame);
56
+ return this.widgets.filter((widget) => owners.has(widget));
57
+ },
53
58
  });
54
59
  this.focusManager = this.focus;
55
60
  this.router = new EventRouter({ source: this, focusOnClick });
@@ -59,7 +64,7 @@ export class WidgetApp {
59
64
  this.app.onMouse((event) => this.router.mouse(event));
60
65
  this.app.onWheel((wheel) => this.router.wheel(wheel));
61
66
  this.app.onPaint(() => {
62
- this.focus.settle(this.drawn);
67
+ this.focus.settle();
63
68
  this.router.framePainted();
64
69
  });
65
70
  // [LAW:single-enforcer] `settle` judges every focus move, so every move
@@ -69,6 +74,17 @@ export class WidgetApp {
69
74
  get phase() {
70
75
  return this.app.phase;
71
76
  }
77
+ /**
78
+ * The widgets the view reported for the frame on screen, in document order,
79
+ * each drawn or held (`RenderOptions.onPresent`). A held widget is one its
80
+ * container handed no column: in the view, with no cell on the frame.
81
+ */
82
+ get present() {
83
+ return this._present;
84
+ }
85
+ get widgets() {
86
+ return this._present.map(({ widget }) => widget);
87
+ }
72
88
  /** The rows on screen now, as painted: what a pointer event lands on. */
73
89
  get frame() {
74
90
  return this.app.frame;
@@ -131,26 +147,28 @@ export class WidgetApp {
131
147
  return this.app.onWheel(handler);
132
148
  }
133
149
  compose(view, options) {
134
- const drawn = new Set();
135
- // [LAW:parse-dont-validate] An owner is any object; only a `WidgetBase`
136
- // reports itself as a widget, so this is where `object` becomes one.
137
- const onDraw = (owner) => {
138
- if (owner instanceof WidgetBase)
139
- drawn.add(owner);
150
+ // Each widget once, where it was first reported; drawn anywhere, it is drawn.
151
+ const present = new Map();
152
+ // [LAW:parse-dont-validate] What the view reports is any renderable; only
153
+ // a `WidgetBase` is a widget, so this is where a renderable becomes one.
154
+ const onPresent = (renderable, presence) => {
155
+ if (renderable instanceof WidgetBase)
156
+ present.set(renderable, present.get(renderable) === "drawn" ? "drawn" : presence);
140
157
  };
141
158
  // The view, its render and the overlays are all tracked, so an observable
142
159
  // any of them reads — a label, `focused`, the state that picks what the
143
160
  // view shows — paints the next frame when it changes.
144
- // An overlay reports what it draws too: a widget in it is on the frame,
145
- // and visiting `drawn` in insertion order paints its overlay in turn.
146
- const render = { ...options, onDraw };
161
+ // An overlay reports what it draws too: a widget in it is in the view,
162
+ // and visiting `present` in report order paints its overlay in turn. A
163
+ // held widget has no footprint on the frame, and so paints none.
164
+ const drawing = { ...options, onPresent };
147
165
  const lines = tracked(this.reaction, () => {
148
- const base = Segment.splitLines(view().render(render));
149
- for (const widget of drawn)
150
- paintOverlay(base, widget, render);
166
+ const base = Segment.splitLines(render(view(), drawing));
167
+ for (const widget of present.keys())
168
+ paintOverlay(base, widget, drawing);
151
169
  return base;
152
170
  });
153
- this.drawn = [...drawn];
171
+ this._present = [...present].map(([widget, presence]) => ({ widget, presence }));
154
172
  return lines.flatMap((line) => [...line, Segment.line()]);
155
173
  }
156
174
  }
@@ -153,10 +153,7 @@ let WidgetBase = (() => {
153
153
  // and a click past the end of a short row still reaches it; every row ends
154
154
  // in a newline, so a blank last row survives the next `splitLines`. The
155
155
  // padding stands on `ground`, whatever the widget draws its own cells on.
156
- // Reporting itself before it draws is what puts a widget ahead of the
157
- // widgets nested in it in document order — the order focus traverses.
158
156
  render(options) {
159
- options.onDraw?.(this);
160
157
  const drawn = Segment.splitLines(this.draw(options));
161
158
  const [width] = Segment.getShape(drawn);
162
159
  const lines = Segment.anchorLines(drawn.map((line) => Segment.adjustLineLength(line, width, this.ground)), this);