@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.
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/animation.d.ts +78 -0
- package/dist/core/animation.js +107 -0
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +17 -11
- package/dist/core/cells.js +100 -25
- package/dist/core/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/console.js +5 -5
- package/dist/core/easing.d.ts +138 -7
- package/dist/core/easing.js +355 -17
- package/dist/core/json.js +2 -1
- package/dist/core/measure.js +2 -2
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +2 -2
- package/dist/core/protocol.d.ts +73 -19
- package/dist/core/protocol.js +56 -15
- package/dist/core/render.js +2 -1
- package/dist/core/segment.d.ts +28 -1
- package/dist/core/segment.js +60 -44
- package/dist/core/strip.d.ts +107 -29
- package/dist/core/strip.js +242 -131
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +49 -9
- package/dist/core/text.js +26 -15
- package/dist/core/timeline.d.ts +84 -0
- package/dist/core/timeline.js +144 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +69 -0
- package/dist/host/app.js +4 -6
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +15 -5
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/align.d.ts +3 -2
- package/dist/renderables/align.js +3 -0
- package/dist/renderables/columns.d.ts +3 -2
- package/dist/renderables/columns.js +7 -2
- package/dist/renderables/constrain.d.ts +3 -2
- package/dist/renderables/constrain.js +5 -1
- package/dist/renderables/effect.d.ts +83 -30
- package/dist/renderables/effect.js +155 -57
- package/dist/renderables/effects.d.ts +88 -39
- package/dist/renderables/effects.js +444 -105
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.d.ts +3 -2
- package/dist/renderables/flexStrip.js +6 -1
- package/dist/renderables/frames.d.ts +116 -0
- package/dist/renderables/frames.js +133 -0
- package/dist/renderables/group.d.ts +3 -2
- package/dist/renderables/group.js +5 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +3 -2
- package/dist/renderables/layout.js +15 -2
- package/dist/renderables/list-view.d.ts +3 -2
- package/dist/renderables/list-view.js +12 -5
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +69 -35
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1421 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +7 -6
- package/dist/renderables/markdown.js +363 -499
- package/dist/renderables/padding.d.ts +8 -20
- package/dist/renderables/padding.js +32 -50
- package/dist/renderables/panel.d.ts +26 -26
- package/dist/renderables/panel.js +67 -103
- package/dist/renderables/progress.d.ts +5 -3
- package/dist/renderables/progress.js +14 -3
- package/dist/renderables/progressBar.js +4 -4
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +24 -0
- package/dist/renderables/spinner.d.ts +73 -27
- package/dist/renderables/spinner.js +110 -51
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +19 -11
- package/dist/renderables/table.js +140 -156
- package/dist/renderables/traceback.d.ts +1 -1
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +6 -5
- package/dist/renderables/tree.js +11 -7
- package/dist/renderables/viewport.d.ts +13 -5
- package/dist/renderables/viewport.js +41 -20
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +8 -3
- package/dist/themes/ramp.d.ts +18 -3
- package/dist/themes/ramp.js +23 -2
- package/dist/widgets/focus-manager.d.ts +21 -14
- package/dist/widgets/focus-manager.js +28 -29
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/static-item.d.ts +4 -3
- package/dist/widgets/static-item.js +9 -7
- package/dist/widgets/text-input.d.ts +28 -18
- package/dist/widgets/text-input.js +144 -101
- package/dist/widgets/widget-app.d.ts +19 -3
- package/dist/widgets/widget-app.js +39 -21
- package/dist/widgets/widget-base.js +0 -3
- 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,
|
|
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 [{
|
|
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({
|
|
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
|
-
//
|
|
133
|
-
//
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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 ??
|
|
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 +
|
|
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 ??
|
|
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 +
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
917
|
-
// glyph
|
|
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.
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
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
|
|
934
|
-
|
|
935
|
-
//
|
|
936
|
-
|
|
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 -
|
|
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
|
|
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
|
|
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
|
|
1018
|
-
const
|
|
1019
|
-
const
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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 (
|
|
1096
|
-
const [before, rest] = splitText(
|
|
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
|
-
|
|
1108
|
-
out.push(new Segment(paddedContent, contentStyle));
|
|
1119
|
+
else if (display.length > 0) {
|
|
1120
|
+
out.push(new Segment(display, contentStyle));
|
|
1109
1121
|
}
|
|
1110
|
-
|
|
1122
|
+
if (indicator !== undefined)
|
|
1123
|
+
out.push(new Segment(indicator.ch, indicator.style));
|
|
1111
1124
|
}
|
|
1112
|
-
_computeVisualRows(
|
|
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
|
-
|
|
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.
|
|
1130
|
-
valueStart: asCodePoint(pos +
|
|
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(
|
|
1173
|
+
measure(options) {
|
|
1144
1174
|
const minimum = MIN_CONTENT_WIDTH + 2;
|
|
1145
|
-
const
|
|
1146
|
-
|
|
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
|
|
11
|
-
* (`RenderOptions.
|
|
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
|
|
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
|
|
11
|
-
* (`RenderOptions.
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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(
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
145
|
-
// and visiting `
|
|
146
|
-
|
|
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()
|
|
149
|
-
for (const widget of
|
|
150
|
-
paintOverlay(base, widget,
|
|
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.
|
|
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);
|