@promptctl/rich-js 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -16,16 +16,16 @@
16
16
  * sized and coloured a source-code excerpt this renderer does not produce.
17
17
  */
18
18
  import { Segment } from "../core/segment.js";
19
- import type { Renderable, RenderOptions } from "../core/protocol.js";
19
+ import { type Renderable, type RenderOptions } from "../core/protocol.js";
20
20
  export interface TracebackOptions {
21
- suppress?: string[];
21
+ suppress?: readonly string[];
22
22
  maxFrames?: number;
23
23
  }
24
24
  export declare class Traceback implements Renderable {
25
25
  /** What was caught, as it was handed in. */
26
26
  readonly error: unknown;
27
27
  readonly maxFrames: number;
28
- readonly suppress: string[];
28
+ readonly suppress: readonly string[];
29
29
  private readonly caught;
30
30
  constructor(error: unknown, options?: TracebackOptions);
31
31
  render(options: RenderOptions): Iterable<Segment>;
@@ -18,7 +18,9 @@
18
18
  import { Segment } from "../core/segment.js";
19
19
  import { RichText } from "../core/text.js";
20
20
  import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
21
- import { layoutPadding } from "./padding.js";
21
+ import { render } from "../core/protocol.js";
22
+ import { paddedContentWidth } from "./padding.js";
23
+ import { pattern } from "../core/pattern.js";
22
24
  /**
23
25
  * [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
24
26
  * JavaScript lets a program throw any value — and the report. Anything with a
@@ -140,13 +142,13 @@ function parseStack(stack, name, message) {
140
142
  * any name, so it is read and dropped; the lookahead keeps `at async (…)` — a
141
143
  * function named `async` — a name.
142
144
  */
143
- const V8_FRAME = /^at\s+(?:async\s+(?!\())?(.*)$/;
145
+ const V8_FRAME = pattern(/^at\s+(?:async\s+(?!\())?(.*)$/);
144
146
  /**
145
147
  * SpiderMonkey and JavaScriptCore write `[fn]@location`, Firefox prefixing an
146
148
  * awaited call's name with a cause up to a `*` (`async*`, `promise callback*`),
147
149
  * which is dropped as V8's `async` is.
148
150
  */
149
- const AT_SIGN_FRAME = /^(?:[^@]*\*)?([^@]*)@(.*)$/;
151
+ const AT_SIGN_FRAME = pattern(/^(?:[^@]*\*)?([^@]*)@(.*)$/);
150
152
  function parseFrame(text) {
151
153
  const v8 = V8_FRAME.exec(text);
152
154
  if (v8) {
@@ -187,9 +189,9 @@ function openingParen(body) {
187
189
  * the code to it (`file line 8 > eval:1:1`).
188
190
  */
189
191
  const LOCATIONS = [
190
- /^eval at [^]*?\(([^()]+):(\d+):\d+\)/,
191
- /^(.*?) line (\d+) > (?:eval|Function)/,
192
- /^(.*?):(\d+)(?::\d+)?$/,
192
+ pattern(/^eval at [^]*?\(([^()]+):(\d+):\d+\)/),
193
+ pattern(/^(.*?) line (\d+) > (?:eval|Function)/),
194
+ pattern(/^(.*?):(\d+)(?::\d+)?$/),
193
195
  ];
194
196
  function locate(fn, location) {
195
197
  for (const shape of LOCATIONS) {
@@ -225,7 +227,7 @@ export class Traceback {
225
227
  // width that is left on that line rather than overrunning it.
226
228
  header.append(message({ ...options, maxWidth: Math.max(1, options.maxWidth - header.cellLength) }));
227
229
  header.append("\n\n");
228
- yield* header.render(options);
230
+ yield* render(header, options);
229
231
  // A suppressed frame keeps its place and shows its file and line only: it
230
232
  // is the same frame with no function name to show.
231
233
  const frames = this.caught.frames.map((frame) => this.suppress.some((s) => frame.location.includes(s)) ? { ...frame, function: undefined } : frame);
@@ -253,10 +255,10 @@ export class Traceback {
253
255
  // frame of its own. The width is divided as `Padding` divides it; the rows
254
256
  // are not padded out on the right, as `Padding`'s are, because trailing
255
257
  // blanks on every line of a crash report are bytes nobody asked for.
256
- const { left, contentWidth } = layoutPadding(options.maxWidth, 2, 0);
257
- const indent = new Segment(" ".repeat(left));
258
+ const frameOptions = { ...options, maxWidth: paddedContentWidth(options.maxWidth, 2, 0) };
259
+ const indent = new Segment(" ");
258
260
  for (const entry of entries) {
259
- for (const line of Segment.splitLines(entry.render({ ...options, maxWidth: contentWidth }))) {
261
+ for (const line of Segment.splitLines(render(entry, frameOptions))) {
260
262
  yield indent;
261
263
  yield* line;
262
264
  yield Segment.line();
@@ -13,13 +13,16 @@ export interface TreeOptions {
13
13
  }
14
14
  export declare class Tree implements Renderable, Measurable {
15
15
  readonly label: Renderable;
16
- readonly children: Tree[];
17
- expanded: boolean;
16
+ private readonly _children;
17
+ private readonly _expanded;
18
18
  readonly hideRoot: boolean;
19
19
  readonly guideStyle: string | Style;
20
20
  readonly style: string | Style;
21
21
  constructor(label: string | RichText | Renderable, options?: TreeOptions);
22
22
  add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
23
+ get children(): readonly Tree[];
24
+ get expanded(): boolean;
25
+ set expanded(value: boolean);
23
26
  render(rawOptions: RenderOptions): Iterable<Segment>;
24
27
  /**
25
28
  * The rows `render` and `measure` both read, entered in exactly one place so
@@ -29,7 +32,8 @@ export declare class Tree implements Renderable, Measurable {
29
32
  */
30
33
  private _walk;
31
34
  /**
32
- * The tree walked into the rows it emits, one row per label, guides first.
35
+ * This node's subtree walked into the rows it emits, one row per label,
36
+ * guides first, each guide column counted from this node.
33
37
  *
34
38
  * [LAW:one-source-of-truth] The walk that decides which rows exist and what
35
39
  * leads them is here and only here. `render` turns each row into segments and
@@ -38,35 +42,34 @@ export declare class Tree implements Renderable, Measurable {
38
42
  * them run — which they would the moment a second walk existed.
39
43
  *
40
44
  * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
41
- * this one recursion: its children get the columns above it, settled, plus a
42
- * branch of their own, so no depth is drawn by a rule of its own. A node's
43
- * style and its guide style are handed down the same way, as one more entry
44
- * on the stack its ancestors built.
45
+ * this one recursion: each child's rows get a branch column of their own,
46
+ * settled below the first of them drawn, so no depth is drawn by a rule of
47
+ * its own. A node's style and its guide style are handed down the same way,
48
+ * as one more entry on the stack its ancestors built.
49
+ *
50
+ * The branch is chosen once every child is drawn: a subtree that draws no row
51
+ * takes no place among its siblings, so the last one that draws a row ends
52
+ * the branch, and the branch stands on a subtree's first drawn row — a child
53
+ * that drew nothing above children that did leaves them on its branch, not
54
+ * the sibling's before it. Rich chooses it from the children alone, and
55
+ * draws a fork over a last child that drew nothing — an element not yet
56
+ * arrived, or gone.
45
57
  */
46
58
  private _rows;
59
+ /** Each of this node's children walked into its rows, `columns` of guide before each child's own. */
60
+ private _subtrees;
47
61
  /**
48
- * One row of the tree: its label in whatever width the guides left, each of
49
- * the label's lines led by the guides.
50
- *
51
- * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
52
- * Before this, each call site emitted its guides and then handed the label
53
- * `options` unchanged — the label was told it had the whole outer width while
54
- * the guides had already spent four cells of it, so a tree at maxWidth 5
55
- * emitted rows of 9. That is the same defect Panel and Table were fixed for,
56
- * and the same fix: divide the requested width once and let every part read
57
- * its share off the division. `options` arrives parsed from `render`.
62
+ * One row of the tree: each of the label's lines led by the guides.
58
63
  *
59
- * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
64
+ * [LAW:single-enforcer] Tree owns its row boundaries here rather than
60
65
  * depending on label renderables to invent trailing newlines: the label's
61
66
  * output is cut into lines and every line ends where Tree ends it. A label
62
- * that emits nothing still holds its row, and one that ends in a newline of
63
- * its own gets the blank row that line is, as in Rich — padded to the label's
64
- * width when the label is justified.
67
+ * that emits nothing — one left no column, say — has no row, and one
68
+ * that ends in a newline of its own gets the blank row that line is, as in
69
+ * Rich — padded to the label's width when the label is justified.
65
70
  *
66
- * The node's style sits under the label's own, as Rich's `Styled` puts it, so
67
- * markup in the label still wins; its background alone reaches under the
68
- * guides, so a row with a background is filled from its first guide to the
69
- * end of its label.
71
+ * The node's background alone reaches under the guides, so a row with a
72
+ * background is filled from its first guide to the end of its label.
70
73
  */
71
74
  private _renderRow;
72
75
  measure(options: RenderOptions): {
@@ -5,8 +5,9 @@ import { Segment } from "../core/segment.js";
5
5
  import { cellFit, cellLen, asCellCol } from "../core/cells.js";
6
6
  import { Style, NULL_STYLE } from "../core/style.js";
7
7
  import { embed } from "./embed.js";
8
- import { drawable, getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
8
+ import { drawable, getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
9
  import { Measurement } from "../core/measure.js";
10
+ import { ListState, State } from "../core/state.js";
10
11
  /** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
11
12
  const GUIDE_CELLS = 4;
12
13
  const LIGHT_GUIDES = { fork: "├── ", end: "└── ", continue: "│ ", space: " " };
@@ -28,21 +29,38 @@ function glyphsFor(options, style) {
28
29
  const weight = style.bold === true ? HEAVY_GUIDES : style.underline2 === true ? DOUBLE_GUIDES : LIGHT_GUIDES;
29
30
  return drawable(options, weight, ASCII_GUIDES, guideGlyphs);
30
31
  }
32
+ /** A row a label is known to draw, unread. */
33
+ const A_ROW = [[]];
34
+ /** The cells a row with `columns` of guide leaves its label. */
35
+ const labelRoom = (options, columns) => Math.max(0, options.maxWidth - columns * GUIDE_CELLS);
36
+ /**
37
+ * `label` drawn in `width` cells, as both `render` and `measure` draw it, so
38
+ * whether it draws a row is answered once. Cropped, so a label that ignores the
39
+ * width it is handed cannot push the row past the offer the guides were fitted
40
+ * into. No highlighter: a label is drawn plain, as Rich's `Tree` hands its
41
+ * labels `highlight=False`.
42
+ */
43
+ function drawLabel(options, label, width) {
44
+ const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
45
+ return Segment.cropLines(render(label, labelOptions), width);
46
+ }
31
47
  /** How wide a label wants to be. */
32
48
  function labelWidth(options, label) {
33
49
  return Measurement.get(options, label).maximum;
34
50
  }
51
+ /** Whether a row is drawn: a subtree with one takes a place among its siblings. */
52
+ const isDrawn = (row) => row.lines.length > 0;
35
53
  export class Tree {
36
54
  label;
37
- children;
38
- expanded;
55
+ // Rich's Tree grows by `add` and is expanded or collapsed after it is built.
56
+ _children = new ListState();
57
+ _expanded;
39
58
  hideRoot;
40
59
  guideStyle;
41
60
  style;
42
61
  constructor(label, options) {
43
62
  this.label = embed(label);
44
- this.children = [];
45
- this.expanded = options?.expanded !== false;
63
+ this._expanded = new State(options?.expanded !== false);
46
64
  this.hideRoot = options?.hideRoot ?? false;
47
65
  this.guideStyle = options?.guide_style ?? NULL_STYLE;
48
66
  this.style = options?.style ?? NULL_STYLE;
@@ -53,13 +71,35 @@ export class Tree {
53
71
  style: options?.style,
54
72
  expanded: options?.expanded,
55
73
  });
56
- this.children.push(child);
74
+ this._children.push(child);
57
75
  return child;
58
76
  }
77
+ get children() {
78
+ return this._children.get();
79
+ }
80
+ get expanded() {
81
+ return this._expanded.get();
82
+ }
83
+ set expanded(value) {
84
+ this._expanded.set(value);
85
+ }
59
86
  *render(rawOptions) {
60
87
  const options = withBoundedWidth(rawOptions, this);
61
- for (const row of this._walk()) {
62
- yield* this._renderRow(options, row);
88
+ const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
89
+ // [LAW:single-enforcer] The row's width is divided in exactly one place,
90
+ // here. Before this, each call site emitted its guides and then handed the
91
+ // label `options` unchanged — the label was told it had the whole outer
92
+ // width while the guides had already spent four cells of it, so a tree at
93
+ // maxWidth 5 emitted rows of 9. That is the same defect Panel and Table
94
+ // were fixed for, and the same fix: divide the requested width once and
95
+ // let every part read its share off the division.
96
+ //
97
+ // The guides are cropped by `cellFit`, the label by `drawLabel`. The
98
+ // node's style sits under the label's own, as Rich's `Styled` puts it, so
99
+ // markup in the label still wins.
100
+ const draw = (label, styles, columns) => Segment.splitLines(Segment.applyStyle(drawLabel(options, label, labelRoom(options, columns)), resolve(styles)));
101
+ for (const row of this._walk(draw)) {
102
+ yield* this._renderRow(options, resolve, row);
63
103
  }
64
104
  }
65
105
  /**
@@ -68,12 +108,14 @@ export class Tree {
68
108
  * and the column its children hang from with it, as Rich does — its children
69
109
  * stand at the left edge, and its guide style still reaches their guides.
70
110
  */
71
- _walk() {
72
- const rows = [...this._rows([], [], [])];
73
- return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
111
+ _walk(draw) {
112
+ return this.hideRoot
113
+ ? this._subtrees(0, draw, [this.guideStyle], [this.style]).flat()
114
+ : this._rows(0, draw, [], []);
74
115
  }
75
116
  /**
76
- * The tree walked into the rows it emits, one row per label, guides first.
117
+ * This node's subtree walked into the rows it emits, one row per label,
118
+ * guides first, each guide column counted from this node.
77
119
  *
78
120
  * [LAW:one-source-of-truth] The walk that decides which rows exist and what
79
121
  * leads them is here and only here. `render` turns each row into segments and
@@ -82,50 +124,54 @@ export class Tree {
82
124
  * them run — which they would the moment a second walk existed.
83
125
  *
84
126
  * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
85
- * this one recursion: its children get the columns above it, settled, plus a
86
- * branch of their own, so no depth is drawn by a rule of its own. A node's
87
- * style and its guide style are handed down the same way, as one more entry
88
- * on the stack its ancestors built.
127
+ * this one recursion: each child's rows get a branch column of their own,
128
+ * settled below the first of them drawn, so no depth is drawn by a rule of
129
+ * its own. A node's style and its guide style are handed down the same way,
130
+ * as one more entry on the stack its ancestors built.
131
+ *
132
+ * The branch is chosen once every child is drawn: a subtree that draws no row
133
+ * takes no place among its siblings, so the last one that draws a row ends
134
+ * the branch, and the branch stands on a subtree's first drawn row — a child
135
+ * that drew nothing above children that did leaves them on its branch, not
136
+ * the sibling's before it. Rich chooses it from the children alone, and
137
+ * draws a fork over a last child that drew nothing — an element not yet
138
+ * arrived, or gone.
89
139
  */
90
- *_rows(guides, inheritedGuideStyles, inheritedStyles) {
140
+ _rows(columns, draw, inheritedGuideStyles, inheritedStyles) {
91
141
  const styles = [...inheritedStyles, this.style];
92
- yield { guides, label: this.label, styles };
93
- const children = this.expanded ? this.children : [];
94
142
  const guideStyles = [...inheritedGuideStyles, this.guideStyle];
95
- const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
96
- for (let i = 0; i < children.length; i++) {
97
- const branch = i === children.length - 1
98
- ? { first: "end", rest: "space", styles: guideStyles }
99
- : { first: "fork", rest: "continue", styles: guideStyles };
100
- yield* children[i]._rows([...above, branch], guideStyles, styles);
101
- }
143
+ const own = { guides: [], label: this.label, styles, lines: draw(this.label, styles, columns) };
144
+ const subtrees = this._subtrees(columns + 1, draw, guideStyles, styles);
145
+ const firsts = subtrees.map((rows) => rows.findIndex(isDrawn));
146
+ const last = firsts.reduce((found, first, i) => (first >= 0 ? i : found), -1);
147
+ const branched = subtrees.flatMap((rows, i) => {
148
+ const branch = i < last
149
+ ? { first: "fork", rest: "continue", styles: guideStyles }
150
+ : { first: "end", rest: "space", styles: guideStyles };
151
+ const settled = { ...branch, first: branch.rest };
152
+ return rows.map((row, r) => ({ ...row, guides: [r === firsts[i] ? branch : settled, ...row.guides] }));
153
+ });
154
+ return [own, ...branched];
155
+ }
156
+ /** Each of this node's children walked into its rows, `columns` of guide before each child's own. */
157
+ _subtrees(columns, draw, guideStyles, styles) {
158
+ const children = this.expanded ? this.children : [];
159
+ return children.map((child) => child._rows(columns, draw, guideStyles, styles));
102
160
  }
103
161
  /**
104
- * One row of the tree: its label in whatever width the guides left, each of
105
- * the label's lines led by the guides.
106
- *
107
- * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
108
- * Before this, each call site emitted its guides and then handed the label
109
- * `options` unchanged — the label was told it had the whole outer width while
110
- * the guides had already spent four cells of it, so a tree at maxWidth 5
111
- * emitted rows of 9. That is the same defect Panel and Table were fixed for,
112
- * and the same fix: divide the requested width once and let every part read
113
- * its share off the division. `options` arrives parsed from `render`.
162
+ * One row of the tree: each of the label's lines led by the guides.
114
163
  *
115
- * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
164
+ * [LAW:single-enforcer] Tree owns its row boundaries here rather than
116
165
  * depending on label renderables to invent trailing newlines: the label's
117
166
  * output is cut into lines and every line ends where Tree ends it. A label
118
- * that emits nothing still holds its row, and one that ends in a newline of
119
- * its own gets the blank row that line is, as in Rich — padded to the label's
120
- * width when the label is justified.
167
+ * that emits nothing — one left no column, say — has no row, and one
168
+ * that ends in a newline of its own gets the blank row that line is, as in
169
+ * Rich — padded to the label's width when the label is justified.
121
170
  *
122
- * The node's style sits under the label's own, as Rich's `Styled` puts it, so
123
- * markup in the label still wins; its background alone reaches under the
124
- * guides, so a row with a background is filled from its first guide to the
125
- * end of its label.
171
+ * The node's background alone reaches under the guides, so a row with a
172
+ * background is filled from its first guide to the end of its label.
126
173
  */
127
- *_renderRow(options, row) {
128
- const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
174
+ *_renderRow(options, resolve, row) {
129
175
  const style = resolve(row.styles);
130
176
  const drawn = row.guides.map((guide) => {
131
177
  const guideStyle = resolve(guide.styles);
@@ -134,15 +180,7 @@ export class Tree {
134
180
  style: style.backgroundStyle.add(guideStyle).add(REMOVE_GUIDE_STYLES),
135
181
  };
136
182
  });
137
- const width = Math.max(0, options.maxWidth - row.guides.length * GUIDE_CELLS);
138
- // The guides are cropped by `cellFit`; the label is cropped too, so a label
139
- // that ignores the width it is handed cannot push the row past the offer the
140
- // guides were fitted into. No highlighter: a label is drawn plain, as
141
- // Rich's `Tree` hands its labels `highlight=False`.
142
- const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
143
- const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(row.label.render(labelOptions), width), style));
144
- if (lines.length === 0)
145
- lines.push([]);
183
+ const lines = row.lines;
146
184
  for (let i = 0; i < lines.length; i++) {
147
185
  let left = options.maxWidth;
148
186
  for (let g = 0; g < row.guides.length; g++) {
@@ -164,10 +202,22 @@ export class Tree {
164
202
  // The widest row the walk emits, not the offer. Reported as the offer, a
165
203
  // tree told any parent that asked "I want all of it" — so `Panel` in fit
166
204
  // mode drew a 40-cell frame around nine cells of tree, and an unbounded
167
- // offer came back unbounded.
205
+ // offer came back unbounded. A row the walk does not draw wants nothing.
206
+ // A label that wants cells draws them, so only one that wants none can
207
+ // draw no row, and only that one is drawn to ask — in the one column it
208
+ // wants, as its row does, because under one column nothing draws. A label
209
+ // of no width that does draw is a blank row, as wide as its guides and that
210
+ // column: the width the tree, drawn at what this reports, hands it.
211
+ const widths = new Map();
212
+ const widthOf = (label) => {
213
+ const width = widths.get(label) ?? labelWidth(parsed, label);
214
+ widths.set(label, width);
215
+ return width;
216
+ };
217
+ const draw = (label, _styles, columns) => widthOf(label) > 0 ? A_ROW : Segment.splitLines(drawLabel(parsed, label, Math.min(labelRoom(parsed, columns), 1)));
168
218
  let natural = 0;
169
- for (const row of this._walk()) {
170
- natural = Math.max(natural, row.guides.length * GUIDE_CELLS + labelWidth(parsed, row.label));
219
+ for (const row of this._walk(draw).filter(isDrawn)) {
220
+ natural = Math.max(natural, row.guides.length * GUIDE_CELLS + Math.max(1, widthOf(row.label)));
171
221
  }
172
222
  const maximum = Math.min(natural, ceiling);
173
223
  // A row is its guides plus at least one cell of label — but never more than
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Viewport — a fixed number of rows onto a taller renderable, scrolled by an
3
- * offset that cannot leave the content.
2
+ * Viewport — a scroll position: a fixed number of rows onto taller content,
3
+ * scrolled by an offset that cannot leave it. It holds the position, not the
4
+ * content: content is drawn through it with `showing` (or `showingLines`), so
5
+ * a view rebuilt every frame keeps one viewport under content it builds anew.
6
+ * [LAW:one-source-of-truth] That is the one way to draw through it, so no
7
+ * viewport is built around content it never draws.
4
8
  *
5
9
  * Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
6
10
  * when it is given one; otherwise its own configured rows, or the content's
@@ -19,11 +23,18 @@
19
23
  * against exists before a render: the rows come from the budget the render is
20
24
  * handed, and the content's length from rendering it at the width it is handed.
21
25
  * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
- * are called. Each queues a move, and the next render resolves the queue in
23
- * call order, clamping after every move, against the rows and length that
24
- * render found. Calling an operation before the first render, or three of them
26
+ * are called. Each queues a move, and the next render that draws the content
27
+ * resolves the queue in call order, clamping after every move, against the
28
+ * rows and length that render found. Calling an operation before the first render, or three of them
25
29
  * between two renders, is the same code path as calling one.
26
30
  *
31
+ * A render changes nothing (`State`, `../core/state.ts`), so the render draws
32
+ * the offset it resolved without keeping it, and hands the keeping to its pass
33
+ * (`afterPass`): once the frame has been drawn whole, the offset, the rows and
34
+ * length it was clamped against, and the followed range become the viewport's,
35
+ * and the moves it resolved leave the queue. A measurement, which draws
36
+ * nothing anyone sees, keeps nothing.
37
+ *
27
38
  * Every cell of its rows, gutter included, is stamped as its own
28
39
  * (`Segment.anchorLines`), around whatever anchors its content drew. So the
29
40
  * composed frame names the viewport under any of its cells however deep it is
@@ -34,6 +45,24 @@
34
45
  import { Segment } from "../core/segment.js";
35
46
  import type { Style } from "../core/style.js";
36
47
  import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
48
+ /** Lines `start` up to but not including `end` of the content. */
49
+ interface Range {
50
+ readonly start: number;
51
+ readonly end: number;
52
+ }
53
+ /**
54
+ * Content a viewport shows by the line: laid out at a width, it says how many
55
+ * lines it is and draws only the ones asked for. What a view too long to draw
56
+ * whole every frame — a textarea's rows — is shown as (`showingLines`).
57
+ */
58
+ export interface Lines extends Measurable {
59
+ layout(options: RenderOptions): LaidLines;
60
+ }
61
+ /** Content laid out at one width: its length, and its lines `start` up to but not including `end`. */
62
+ export interface LaidLines {
63
+ readonly length: number;
64
+ lines(start: number, end: number): Segment[][];
65
+ }
37
66
  /** How one part of a scrollbar draws each row it covers. */
38
67
  export interface ScrollbarPart {
39
68
  readonly glyph: string;
@@ -60,22 +89,15 @@ export interface ViewportOptions {
60
89
  /** A scrollbar down the right edge. Absent, the content has the full width. */
61
90
  scrollbar?: Scrollbar;
62
91
  }
63
- export declare class Viewport implements Renderable, Measurable, Scrollable {
64
- /**
65
- * What the viewport shows. Replace it to show new content from the same
66
- * scroll position — a view rebuilt every frame keeps one `Viewport`.
67
- */
68
- content: Renderable;
92
+ export declare class Viewport implements Scrollable {
69
93
  readonly rows: number | undefined;
70
94
  readonly scrollbar: Scrollbar;
71
- private _offset;
72
- private _extent;
73
- private _moves;
74
- private _followed;
75
- constructor(content: Renderable, options?: ViewportOptions);
95
+ private readonly _scroll;
96
+ private readonly _moves;
97
+ constructor(options?: ViewportOptions);
76
98
  /**
77
- * The first content line the last render showed. A move requested since is
78
- * not reflected until the next render resolves it.
99
+ * The first content line the last frame showed. A move requested since is
100
+ * not reflected until the next frame resolves it.
79
101
  */
80
102
  get offset(): number;
81
103
  /** Scroll so `line` is the first line shown. */
@@ -84,7 +106,7 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
84
106
  scrollBy(lines: number): void;
85
107
  /**
86
108
  * Whether `scrollBy(lines)` would move the viewport from where the moves
87
- * queued so far leave it, against the rows and length the last render
109
+ * queued so far leave it, against the rows and length the last frame
88
110
  * found.
89
111
  */
90
112
  canScrollBy(lines: number): boolean;
@@ -96,16 +118,30 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
96
118
  */
97
119
  ensureVisible(start: number, end: number): void;
98
120
  /**
99
- * `ensureVisible(start, end)`, when the range differs from the one the last
100
- * `follow` named. A view rebuilt every frame calls this in every frame with
101
- * its selection's lines, and the selection is brought into view when it
102
- * moves — not on every frame, which would undo on its very render each
103
- * scroll the mouse wheel queues. A followed range the last render showed
104
- * whole stays shown when the row count changes: the same lines can fall off
105
- * the bottom of a viewport that got shorter. One the wheel had scrolled
106
- * away from stays away.
121
+ * `content` shown through this viewport from its scroll position. What it
122
+ * draws is anchored to this viewport, so the wheel finds it.
123
+ *
124
+ * `follow` is the lines of `content` to keep in view — a selection's — and
125
+ * is brought into view with `ensureVisible` when it differs from the range
126
+ * the last frame followed: when the selection moves, not on every frame,
127
+ * which would undo on its very render each scroll the mouse wheel queues. A
128
+ * followed range the last frame showed whole stays shown when the row count
129
+ * changes: the same lines can fall off the bottom of a viewport that got
130
+ * shorter. One the wheel had scrolled away from stays away. Absent, the
131
+ * range last followed stands.
132
+ *
133
+ * [LAW:effects-at-boundaries] What a frame drawing it resolves becomes the
134
+ * viewport's once that frame has been drawn whole, so it may be built and
135
+ * drawn inside a render — `ListView` draws its items to learn their lines
136
+ * and shows them this way.
137
+ */
138
+ showing(content: Renderable, follow?: Range): Renderable & Measurable;
139
+ /**
140
+ * `content` shown through this viewport as `showing` shows a renderable,
141
+ * drawing only the lines the frame shows: a frame costs the rows in view,
142
+ * not the content's whole length.
107
143
  */
108
- follow(start: number, end: number): void;
144
+ showingLines(content: Lines, follow?: Range): Renderable & Measurable;
109
145
  /**
110
146
  * The width its content renders at, out of `width`: what the scrollbar's
111
147
  * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
@@ -113,18 +149,20 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
113
149
  contentWidth(width: number): number;
114
150
  /** The cells the scrollbar's gutter takes beside the content. */
115
151
  get gutter(): number;
116
- render(rawOptions: RenderOptions): Iterable<Segment>;
152
+ /** The scroll, and the moves queued against it, as they stand. */
153
+ private asked;
117
154
  /**
118
- * [LAW:one-source-of-truth] Where the queued moves leave the offset against
119
- * `extent`: what a render commits, and what `canScrollBy` asks from.
120
- * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
121
- * queued moves: content that shrank since the last render pulls it back.
155
+ * The offset `settled` resolves, with the scroll it leaves kept once the frame
156
+ * is drawn and the `resolved` moves gone from the queue.
157
+ * [LAW:effects-at-boundaries] Kept once the frame is drawn, never while it is.
158
+ * The resolved moves leave by identity, not by count: another frame drawing
159
+ * this viewport in the same pass may already have taken them, and a move
160
+ * queued since stays.
122
161
  */
123
- private resolve;
124
- measure(rawOptions: RenderOptions): {
125
- minimum: number;
126
- maximum: number;
127
- };
162
+ private keep;
163
+ /** `content`, laid out by `layout`, drawn from the scroll and the moves queued against it, with `follow` followed. */
164
+ private draw;
165
+ private measureOf;
128
166
  }
129
167
  /**
130
168
  * One viewport per thing a pane shows, named by a key. The same viewport comes
@@ -142,3 +180,4 @@ export declare class ViewportPerKey {
142
180
  of(key: string): Viewport;
143
181
  private fresh;
144
182
  }
183
+ export {};