@textui/core 0.3.0 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/app/app.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/app/app.ts"],"names":[],"mappings":"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"}
package/dist/app/app.js CHANGED
@@ -727,7 +727,21 @@ export class App {
727
727
  if (position)
728
728
  return;
729
729
  const cursor = instance.props.cursor;
730
- if (!cursor || !instance.box)
730
+ /*
731
+ * Absent, not falsy.
732
+ *
733
+ * `cursor` is a column, and column 0 is a real one - it is where the
734
+ * caret sits in an empty field, and where it sits at the start of any
735
+ * field. `!cursor` treated that as "no caret here", so a text input
736
+ * published no cursor position at all until its first character was
737
+ * typed: the terminal cursor stayed hidden, and the only thing marking
738
+ * the focused control was its border colour.
739
+ *
740
+ * `true` still means "here, at offset 0", which is what the `typeof`
741
+ * below is for - so this rejects the three ways of saying nothing and
742
+ * nothing else.
743
+ */
744
+ if (cursor === undefined || cursor === null || cursor === false || !instance.box)
731
745
  return;
732
746
  const id = typeof instance.props.id === 'string' ? instance.props.id : instance.id;
733
747
  if (id !== focused && `${instance.id}:focus` !== focused)
@@ -846,6 +860,32 @@ export class App {
846
860
  catch (err) {
847
861
  this.handleError(err, `input:${event.type}`);
848
862
  }
863
+ /*
864
+ * Pointer motion and the wheel are streams, and only where they got to
865
+ * means anything.
866
+ *
867
+ * A terminal reports every cell the pointer crosses and every notch of the
868
+ * wheel, and one read can carry twenty of them - so rendering per event
869
+ * lays out and paints the whole tree twenty times to draw one gesture. The
870
+ * cost is the size of the tree, which is why it is invisible on a short
871
+ * screen and, on a long transcript, is the thing being dragged trailing
872
+ * seconds behind the pointer and a wheel that goes on scrolling after the
873
+ * hand has stopped. That second one is the tell: it is not momentum, it is
874
+ * a backlog of events still being drawn one frame each.
875
+ *
876
+ * Only the drawing is coalesced. Every event is still dispatched, so the
877
+ * notches still add up and the pointer still ends where it ended; what
878
+ * they share is the one frame `handleMouse` asked for, at the animation
879
+ * ceiling rather than at whatever rate the terminal felt like reporting.
880
+ *
881
+ * Keys are the opposite and stay synchronous: every one of them is a
882
+ * separate thing a person did, and a handler closes over the props from
883
+ * its last render, so typing "ab" without a frame in between makes the
884
+ * handler for "b" see the state from before "a".
885
+ */
886
+ if (event.type === 'mouse'
887
+ && (event.action === 'move' || event.action === 'drag' || event.action === 'wheel'))
888
+ return;
849
889
  if (this.running_ && this.isDirty())
850
890
  this.renderFrame();
851
891
  }
@@ -69,7 +69,22 @@ export declare class Buffer implements CellBuffer {
69
69
  cellChanged(i: number): boolean;
70
70
  dirtyRows(): number[];
71
71
  commit(): void;
72
- /** Force the next diff to repaint everything. After a resize or a redraw. */
72
+ /**
73
+ * Force the next diff to repaint everything. After a resize or a redraw.
74
+ *
75
+ * Both halves, and the second one is the one that was missing. Clearing
76
+ * `committed` makes `dirtyRows` return every row, but `diffFrame` still
77
+ * skips each cell that matches the previous frame - and a blank cell in the
78
+ * new frame matched the reset previous frame exactly, so it was never
79
+ * written. Under a theme with an opaque canvas nothing showed, because every
80
+ * cell carries a background colour and therefore differs anyway. Under a
81
+ * theme with a transparent one the blanks are default-on-default, and the
82
+ * terminal kept whatever the old layout had left in them: a resize scattered
83
+ * the previous frame across the new one and it never cleared.
84
+ *
85
+ * So the previous frame is filled with a character no cell can hold, which
86
+ * is a comparison that cannot come out equal.
87
+ */
73
88
  invalidate(): void;
74
89
  toText(rect?: Rect): string;
75
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/render/buffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAExE,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,WAAW,CAAC;IAChB,EAAE,EAAE,WAAW,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAGD,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;;GAMG;AACH,qBAAa,MAAO,YAAW,UAAU;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IAEf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,EAAE,EAAE,UAAU,CAAC;IACf,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAE9B,OAAO,CAAC,SAAS,CAAW;IAC5B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,SAAS,CAAS;gBAEd,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAkBzC,8EAA8E;IAC9E,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAa3C;;;;;;;OAOG;IACH,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAYtD,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI;IAO3C,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAInC,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;OAIG;IACH,GAAG,CACD,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,WAA2B,EAC/B,EAAE,GAAE,WAA2B,EAC/B,KAAK,SAAI,EACT,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI;IAqDP;;;;;;;;OAQG;IACH,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,GAAE,WAA2B,GAAG,IAAI;IAmBzD,OAAO,CAAC,QAAQ;IAShB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAoC3C,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;IAU/B,SAAS,IAAI,MAAM,EAAE;IAiBrB,MAAM,IAAI,IAAI;IASd,6EAA6E;IAC7E,UAAU,IAAI,IAAI;IAIlB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM;CAgB5B;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE"}
1
+ {"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/render/buffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAYxE,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,WAAW,CAAC;IAChB,EAAE,EAAE,WAAW,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAGD,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC;;;;;;GAMG;AACH,qBAAa,MAAO,YAAW,UAAU;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IAEf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,EAAE,EAAE,UAAU,CAAC;IACf,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAE9B,OAAO,CAAC,SAAS,CAAW;IAC5B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,SAAS,CAAS;gBAEd,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAkBzC,8EAA8E;IAC9E,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS;IAa3C;;;;;;;OAOG;IACH,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAYtD,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI;IAO3C,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAInC,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;OAIG;IACH,GAAG,CACD,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,EAAE,GAAE,WAA2B,EAC/B,EAAE,GAAE,WAA2B,EAC/B,KAAK,SAAI,EACT,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI;IAqDP;;;;;;;;OAQG;IACH,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,GAAE,WAA2B,GAAG,IAAI;IAmBzD,OAAO,CAAC,QAAQ;IAShB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAoC3C,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO;IAU/B,SAAS,IAAI,MAAM,EAAE;IAiBrB,MAAM,IAAI,IAAI;IASd;;;;;;;;;;;;;;;OAeG;IACH,UAAU,IAAI,IAAI;IAKlB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM;CAgB5B;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE"}
@@ -1,4 +1,13 @@
1
1
  import { COLOR_DEFAULT, packColor } from './color.js';
2
+ /**
3
+ * A character no cell can hold, for the previous frame when it must not match.
4
+ *
5
+ * `put` refuses the empty string and every real glyph is one the terminal can
6
+ * print, so NUL is a value that never arrives from a render and never leaves
7
+ * one: `diffFrame` reads `chars`, never `prevChars`, so this is only ever a
8
+ * comparison and is never written out.
9
+ */
10
+ const NEVER = '\u0000';
2
11
  import { graphemeWidth } from '../util/text.js';
3
12
  /** Internal cell flags, kept out of the public attrs bitfield. */
4
13
  export const FLAG_CONTINUATION = 1;
@@ -200,7 +209,7 @@ export class Buffer {
200
209
  this.links = next.links;
201
210
  // A resize invalidates the previous frame entirely.
202
211
  const n = width * height;
203
- this.prevChars = new Array(n).fill(' ');
212
+ this.prevChars = new Array(n).fill(NEVER);
204
213
  this.prevFg = new Int32Array(n).fill(COLOR_DEFAULT);
205
214
  this.prevBg = new Int32Array(n).fill(COLOR_DEFAULT);
206
215
  this.prevAttrs = new Uint16Array(n);
@@ -238,9 +247,25 @@ export class Buffer {
238
247
  this.prevLinks = this.links.slice();
239
248
  this.committed = true;
240
249
  }
241
- /** Force the next diff to repaint everything. After a resize or a redraw. */
250
+ /**
251
+ * Force the next diff to repaint everything. After a resize or a redraw.
252
+ *
253
+ * Both halves, and the second one is the one that was missing. Clearing
254
+ * `committed` makes `dirtyRows` return every row, but `diffFrame` still
255
+ * skips each cell that matches the previous frame - and a blank cell in the
256
+ * new frame matched the reset previous frame exactly, so it was never
257
+ * written. Under a theme with an opaque canvas nothing showed, because every
258
+ * cell carries a background colour and therefore differs anyway. Under a
259
+ * theme with a transparent one the blanks are default-on-default, and the
260
+ * terminal kept whatever the old layout had left in them: a resize scattered
261
+ * the previous frame across the new one and it never cleared.
262
+ *
263
+ * So the previous frame is filled with a character no cell can hold, which
264
+ * is a comparison that cannot come out equal.
265
+ */
242
266
  invalidate() {
243
267
  this.committed = false;
268
+ this.prevChars.fill(NEVER);
244
269
  }
245
270
  toText(rect) {
246
271
  const r = rect ?? { x: 0, y: 0, width: this.width, height: this.height };
@@ -34,6 +34,24 @@ export interface LayoutBox {
34
34
  content: Rect;
35
35
  /** Content larger than `content`, when overflow is 'scroll'. */
36
36
  scrollSize?: Size;
37
+ /**
38
+ * Frame and gaps, worked out once for this box.
39
+ *
40
+ * Neither depends on the space being offered - they are the box's own
41
+ * margin, border, padding and gap - and the box is measured about four
42
+ * times a frame at different bounds, each of which was rebuilding both and
43
+ * allocating three objects to do it. Same lifetime as `measured` below, and
44
+ * for the same reason: a layout box is rebuilt from the instance tree every
45
+ * frame, so nothing here can go stale.
46
+ */
47
+ frame?: {
48
+ margin: Edges;
49
+ inset: Edges;
50
+ };
51
+ gaps?: {
52
+ main: number;
53
+ cross: number;
54
+ };
37
55
  /**
38
56
  * The last answer `measureBox` gave for this box, and what it was asked.
39
57
  *
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,KAAK,EAA6B,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGpF;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,yEAAyE;IACzE,WAAW,EAAE,KAAK,CAAC;IACnB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,yEAAyE;IACzE,OAAO,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,IAAI,CAAC;IACX,8DAA8D;IAC9D,OAAO,EAAE,IAAI,CAAC;IACd,gEAAgE;IAChE,UAAU,CAAC,EAAE,IAAI,CAAC;IAElB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;CACjD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAuB9D;AA0CD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,QAAQ,CAElE;AAkED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAsJ/E;AA0FD,6EAA6E;AAC7E,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,CAE5D"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,KAAK,EAA6B,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAGpF;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,yEAAyE;IACzE,WAAW,EAAE,KAAK,CAAC;IACnB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,yEAAyE;IACzE,OAAO,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,IAAI,CAAC;IACX,8DAA8D;IAC9D,OAAO,EAAE,IAAI,CAAC;IACd,gEAAgE;IAChE,UAAU,CAAC,EAAE,IAAI,CAAC;IAElB;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC;IACxC,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAEvC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC;CACjD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAuB9D;AA0CD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,QAAQ,CAElE;AA0ED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAsJ/E;AA0FD,6EAA6E;AAC7E,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,CAE5D"}
@@ -82,11 +82,16 @@ function mainOverflow(box) {
82
82
  * that sets both reads the same either way round.
83
83
  */
84
84
  function gapsOf(box) {
85
+ const seen = box.gaps;
86
+ if (seen !== undefined)
87
+ return seen;
85
88
  const vertical = box.style.rowGap ?? box.style.gap ?? 0;
86
89
  const horizontal = box.style.columnGap ?? box.style.gap ?? 0;
87
- return isColumn(box)
90
+ const gaps = isColumn(box)
88
91
  ? { main: vertical, cross: horizontal }
89
92
  : { main: horizontal, cross: vertical };
93
+ box.gaps = gaps;
94
+ return gaps;
90
95
  }
91
96
  /**
92
97
  * Cut a run of main-axis sizes into lines that each fit `limit`.
@@ -115,10 +120,13 @@ function splitLines(mains, gap, limit) {
115
120
  }
116
121
  /** Space this box consumes outside its content: margin, border, padding. */
117
122
  function frameOf(box) {
123
+ const seen = box.frame;
124
+ if (seen !== undefined)
125
+ return seen;
118
126
  const margin = resolveEdges(box.style.margin);
119
127
  const padding = resolveEdges(box.style.padding);
120
128
  const b = box.borderEdges;
121
- return {
129
+ const frame = {
122
130
  margin,
123
131
  inset: {
124
132
  top: b.top + padding.top,
@@ -127,6 +135,8 @@ function frameOf(box) {
127
135
  left: b.left + padding.left,
128
136
  },
129
137
  };
138
+ box.frame = frame;
139
+ return frame;
130
140
  }
131
141
  /**
132
142
  * Intrinsic size, ignoring grow. Containers ask their children; leaves are
@@ -178,7 +188,7 @@ export function measureBox(box, availW, availH) {
178
188
  continue;
179
189
  count++;
180
190
  const m = measureBox(child, innerAvailW, innerAvailH);
181
- const cm = resolveEdges(child.style.margin);
191
+ const cm = frameOf(child).margin;
182
192
  mains.push(column ? m.height + edgeV(cm) : m.width + edgeH(cm));
183
193
  crosses.push(column ? m.width + edgeH(cm) : m.height + edgeV(cm));
184
194
  }
@@ -222,7 +232,7 @@ export function measureBox(box, availW, availH) {
222
232
  continue;
223
233
  const flex = Math.max(0, child.style.flex ?? 0);
224
234
  if (flex > 0) {
225
- const cm = resolveEdges(child.style.margin);
235
+ const cm = frameOf(child).margin;
226
236
  const share = Math.max(0, Math.floor((room * flex) / totalFlex) - edgeH(cm));
227
237
  const m = measureBox(child, share, innerAvailH);
228
238
  crosses[index] = m.height + edgeV(cm);
@@ -439,7 +449,7 @@ function layoutWrapped(box, flow) {
439
449
  const mains = [];
440
450
  const crosses = [];
441
451
  for (const child of flow) {
442
- const margin = resolveEdges(child.style.margin);
452
+ const margin = frameOf(child).margin;
443
453
  const basis = child.style.basis ?? (column ? child.style.height : child.style.width);
444
454
  const fixed = resolveDimension(basis, mainAvail);
445
455
  const m = measureBox(child, column ? crossAvail : mainAvail, column ? mainAvail : crossAvail);
@@ -480,7 +490,7 @@ function layoutLine(box, flow, content, scrollOffset) {
480
490
  const crossAvail = column ? content.width : content.height;
481
491
  const gapTotal = flow.length > 1 ? gap * (flow.length - 1) : 0;
482
492
  // 1. base sizes
483
- const margins = flow.map((c) => resolveEdges(c.style.margin));
493
+ const margins = flow.map((c) => frameOf(c).margin);
484
494
  const bases = [];
485
495
  for (let i = 0; i < flow.length; i++) {
486
496
  const child = flow[i];
@@ -1 +1 @@
1
- {"version":3,"file":"paint.d.ts","sourceRoot":"","sources":["../../src/runtime/paint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGrE,OAAO,KAAK,EAA2B,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMrF,OAAO,EAES,KAAK,gBAAgB,EACpC,MAAM,YAAY,CAAC;AAwHpB,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,aAAa,EACpB,YAAY,EAAE,oBAAoB,EAClC,KAAK,GAAE,gBAAiC,GACvC,aAAa,CAiBf;AAID,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,EAAE,oBAAoB,CAAC;IACnC,kEAAkE;IAClE,OAAO,CAAC,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,CAAC;CAC/C;AAeD,6DAA6D;AAC7D,UAAU,SAAS;IACjB,EAAE,EAAE,WAAW,CAAC;IAChB,EAAE,EAAE,WAAW,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AA4DD,6EAA6E;AAC7E,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,QAAQ,EACb,SAAS,GAAE,SAA0B,GACpC,SAAS,EAAE,CAwDb;AA4DD,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,IAAI,GACT,IAAI,CA4FN"}
1
+ {"version":3,"file":"paint.d.ts","sourceRoot":"","sources":["../../src/runtime/paint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGrE,OAAO,KAAK,EAA2B,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMrF,OAAO,EAES,KAAK,gBAAgB,EACpC,MAAM,YAAY,CAAC;AAuIpB,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,aAAa,EACpB,YAAY,EAAE,oBAAoB,EAClC,KAAK,GAAE,gBAAiC,GACvC,aAAa,CAiBf;AAID,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,EAAE,oBAAoB,CAAC;IACnC,kEAAkE;IAClE,OAAO,CAAC,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,CAAC;CAC/C;AAeD,6DAA6D;AAC7D,UAAU,SAAS;IACjB,EAAE,EAAE,WAAW,CAAC;IAChB,EAAE,EAAE,WAAW,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AA4DD,6EAA6E;AAC7E,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,QAAQ,EACb,SAAS,GAAE,SAA0B,GACpC,SAAS,EAAE,CAwDb;AAsFD,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,IAAI,GACT,IAAI,CA4FN"}
@@ -2,7 +2,7 @@ import { ATTR_DIM } from '../types/cells.js';
2
2
  import { overflowOn } from '../render/layout.js';
3
3
  import { COLOR_DEFAULT, mix, packColor } from '../render/color.js';
4
4
  import { rectIntersect } from '../types/geometry.js';
5
- import { graphemes, graphemeWidth, sanitize, stringWidth, truncate, truncateSideOf, wrapModeOf, wrapText, } from '../util/text.js';
5
+ import { graphemes, graphemeWidth, isAscii, sanitize, stringWidth, truncate, truncateSideOf, wrapModeOf, wrapText, } from '../util/text.js';
6
6
  import { attrsFromStyle, NO_INTERACTION, packStyleColor, resolveBorder, resolveStyle, } from './style.js';
7
7
  /**
8
8
  * Painting.
@@ -62,8 +62,22 @@ class Surface {
62
62
  const attrs = style?.attrs ?? 0;
63
63
  const link = style?.link;
64
64
  const oy = this.rect.y + y;
65
+ const clean = sanitize(text);
66
+ // Plain ASCII is one cell per character and needs none of the clustering
67
+ // machinery - and it is nearly every string a terminal ever draws. The
68
+ // general path allocates an array of one-character strings for the whole
69
+ // run before writing any of it.
65
70
  let cx = x;
66
- for (const g of graphemes(sanitize(text))) {
71
+ if (isAscii(clean)) {
72
+ for (let i = 0; i < clean.length; i++) {
73
+ const ax = this.rect.x + cx;
74
+ if (this.visible(ax, oy))
75
+ this.buffer.put(ax, oy, clean[i], fg, bg, attrs, link);
76
+ cx += 1;
77
+ }
78
+ return cx - x;
79
+ }
80
+ for (const g of graphemes(clean)) {
67
81
  const w = graphemeWidth(g);
68
82
  if (w === 0)
69
83
  continue;
@@ -246,31 +260,56 @@ function textContent(instance) {
246
260
  function oneLine(text) {
247
261
  return text.includes('\n') ? text.split('\n').join(' ') : text;
248
262
  }
263
+ /**
264
+ * Text measurements already taken.
265
+ *
266
+ * A measurement is a pure function of the string, how it wraps and the width
267
+ * it is offered - and a layout asks for the same three several times in a
268
+ * frame, then the next frame asks for all of them again. The text in a
269
+ * transcript does not change; only the one line somebody is typing does.
270
+ */
271
+ const measurements = new Map();
272
+ const MEASURE_CACHE = 4096;
249
273
  function textMeasure(instance, style) {
250
274
  return (maxWidth) => {
251
275
  const text = sanitize(textContent(instance));
252
276
  if (text === '')
253
277
  return { width: 0, height: text.includes('\n') ? 1 : 1 };
254
278
  const wrap = style.wrap ?? 'none';
279
+ const key = `${wrap}\u0000${String(maxWidth)}\u0000${text}`;
280
+ const seen = measurements.get(key);
281
+ if (seen !== undefined)
282
+ return seen;
255
283
  // A truncating text is one row, always. It still asks for the width it
256
284
  // would like - a box sizing itself around it gets the whole string when
257
285
  // there is room, and only cuts when there is not.
258
- if (truncateSideOf(wrap) !== undefined) {
259
- return { width: stringWidth(oneLine(text)), height: 1 };
260
- }
261
- if (wrap === 'none') {
262
- const lines = text.split('\n');
263
- return {
264
- width: Math.max(...lines.map(stringWidth)),
265
- height: lines.length,
266
- };
267
- }
268
- const limit = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : stringWidth(text);
269
- const lines = wrapText(text, limit, wrapModeOf(wrap));
286
+ const size = measure(text, wrap, maxWidth);
287
+ if (measurements.size >= MEASURE_CACHE)
288
+ measurements.clear();
289
+ measurements.set(key, size);
290
+ return size;
291
+ };
292
+ }
293
+ /** The measurement itself, with nothing remembered. */
294
+ function measure(text, wrap, maxWidth) {
295
+ // A truncating text is one row, always. It still asks for the width it
296
+ // would like - a box sizing itself around it gets the whole string when
297
+ // there is room, and only cuts when there is not.
298
+ if (truncateSideOf(wrap) !== undefined) {
299
+ return { width: stringWidth(oneLine(text)), height: 1 };
300
+ }
301
+ if (wrap === 'none') {
302
+ const lines = text.split('\n');
270
303
  return {
271
- width: Math.min(limit, Math.max(0, ...lines.map(stringWidth))),
272
- height: Math.max(1, lines.length),
304
+ width: Math.max(...lines.map(stringWidth)),
305
+ height: lines.length,
273
306
  };
307
+ }
308
+ const limit = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : stringWidth(text);
309
+ const lines = wrapText(text, limit, wrapModeOf(wrap));
310
+ return {
311
+ width: Math.min(limit, Math.max(0, ...lines.map(stringWidth))),
312
+ height: Math.max(1, lines.length),
274
313
  };
275
314
  }
276
315
  // ------------------------------------------------------------------- paint
@@ -1,4 +1,12 @@
1
1
  import type { TextWrap } from '../types/style.js';
2
+ /**
3
+ * True for a string that is entirely printable ASCII.
4
+ *
5
+ * Worth asking, because the answer is yes for very nearly all of it: source
6
+ * code, file names, labels, key hints. Every character is then one grapheme
7
+ * one cell wide, and none of the Unicode machinery below has anything to do.
8
+ */
9
+ export declare function isAscii(text: string): boolean;
2
10
  /**
3
11
  * Split into grapheme clusters. Falls back to code points without Intl.
4
12
  *
@@ -58,7 +66,16 @@ export declare function wrapModeOf(wrap: TextWrap | undefined): WrapMode;
58
66
  /** Break into lines of at most `width` cells. Honours existing newlines. */
59
67
  export declare function wrapText(text: string, width: number, mode?: WrapMode): string[];
60
68
  export declare function stripAnsi(text: string): string;
61
- /** Remove control characters that would corrupt the frame. Keeps newlines. */
69
+ /**
70
+ * Remove control characters that would corrupt the frame. Keeps newlines.
71
+ *
72
+ * Two regular expressions over every string on screen, every frame, and for
73
+ * almost all of them the answer is the string it was given. `test` before
74
+ * `replace` is one scan that stops at the first match instead of two that
75
+ * always run to the end and allocate a copy each - and it returns the original
76
+ * string, so the callers that follow it get a cache hit on the width they
77
+ * already worked out rather than an equal-but-different string.
78
+ */
62
79
  export declare function sanitize(text: string): string;
63
80
  /** Repeat a grapheme to exactly `width` cells (wide chars land short). */
64
81
  export declare function repeatToWidth(char: string, width: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/util/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AA8BlD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAShD;AA8ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAED,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgC/E;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAuB7D;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtD,+DAA+D;AAC/D,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,SAAM,EACd,IAAI,GAAE,YAAoB,GACzB,MAAM,CAkCR;AAED,2EAA2E;AAC3E,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,IAAI,SAAM,GACT,MAAM,CAQR;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,QAAQ,SAAM,GACb,MAAM,CAER;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAOnF;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAE/D;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,QAAiB,GAAG,MAAM,EAAE,CAuDvF;AAOD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjE"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/util/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAelD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAShD;AA8ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAsBD,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBhD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgC/E;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAuB7D;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtD,+DAA+D;AAC/D,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,SAAM,EACd,IAAI,GAAE,YAAoB,GACzB,MAAM,CAkCR;AAED,2EAA2E;AAC3E,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,IAAI,SAAM,GACT,MAAM,CAQR;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,QAAQ,SAAM,GACb,MAAM,CAER;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAOnF;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAE/D;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,QAAiB,GAAG,MAAM,EAAE,CAuDvF;AAOD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjE"}
package/dist/util/text.js CHANGED
@@ -15,7 +15,7 @@ const segmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
15
15
  * code, file names, labels, key hints. Every character is then one grapheme
16
16
  * one cell wide, and none of the Unicode machinery below has anything to do.
17
17
  */
18
- function isAscii(text) {
18
+ export function isAscii(text) {
19
19
  for (let i = 0; i < text.length; i++) {
20
20
  const c = text.charCodeAt(i);
21
21
  if (c < 0x20 || c > 0x7e)
@@ -148,6 +148,25 @@ export function graphemeWidth(cluster) {
148
148
  return 2;
149
149
  return 1;
150
150
  }
151
+ /**
152
+ * Widths already worked out, for strings that are not plain ASCII.
153
+ *
154
+ * The same strings are measured over and over. A frame measures the tree, lays
155
+ * it out and paints it - which is three walks over the same text - and then
156
+ * the next frame does all three again for content that has not changed. Nearly
157
+ * every row in a terminal application carries one glyph among its ASCII, a box
158
+ * rule or a bullet or an icon, and a single non-ASCII character sends the whole
159
+ * string down the slow path: an array of one-character strings, and a width
160
+ * classification for each of them. That was a quarter of the frame in a long
161
+ * transcript, spent proving the same sentences were the same width they were a
162
+ * thirtieth of a second ago.
163
+ *
164
+ * Only the slow path is remembered. ASCII is answered by `.length` before the
165
+ * map is even consulted, which is cheaper than the lookup would be, so the
166
+ * cache holds only what it saves anything on.
167
+ */
168
+ const widths = new Map();
169
+ const WIDTH_CACHE = 8192;
151
170
  /** Width of a string in terminal cells. */
152
171
  export function stringWidth(text) {
153
172
  if (text === '')
@@ -155,9 +174,18 @@ export function stringWidth(text) {
155
174
  // Fast path: pure ASCII printable.
156
175
  if (isAscii(text))
157
176
  return text.length;
177
+ const seen = widths.get(text);
178
+ if (seen !== undefined)
179
+ return seen;
158
180
  let w = 0;
159
181
  for (const g of graphemes(text))
160
182
  w += graphemeWidth(g);
183
+ // Emptied rather than evicted one at a time: a terminal's vocabulary of
184
+ // strings is small and slow-moving, so the cap is only ever reached by
185
+ // something streaming new text, and for that the whole map is stale.
186
+ if (widths.size >= WIDTH_CACHE)
187
+ widths.clear();
188
+ widths.set(text, w);
161
189
  return w;
162
190
  }
163
191
  /** Take at most `width` cells from the front. Never splits a wide grapheme. */
@@ -405,10 +433,33 @@ const ANSI_RE =
405
433
  export function stripAnsi(text) {
406
434
  return text.replace(ANSI_RE, '');
407
435
  }
408
- /** Remove control characters that would corrupt the frame. Keeps newlines. */
436
+ /*
437
+ * Test copies, without the `g` flag.
438
+ *
439
+ * `test` on a global regular expression advances its `lastIndex` and starts
440
+ * the next call from there, so the same string tested twice answers yes then
441
+ * no. Asking is a different question from replacing and gets its own pattern.
442
+ */
443
+ // eslint-disable-next-line no-control-regex
444
+ const ANSI_TEST = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1B]*(?:\x07|\x1B\\))/;
445
+ // eslint-disable-next-line no-control-regex
446
+ const CONTROL_TEST = /[\x00-\x08\x0B-\x1F\x7F]/;
447
+ // eslint-disable-next-line no-control-regex
448
+ const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
449
+ /**
450
+ * Remove control characters that would corrupt the frame. Keeps newlines.
451
+ *
452
+ * Two regular expressions over every string on screen, every frame, and for
453
+ * almost all of them the answer is the string it was given. `test` before
454
+ * `replace` is one scan that stops at the first match instead of two that
455
+ * always run to the end and allocate a copy each - and it returns the original
456
+ * string, so the callers that follow it get a cache hit on the width they
457
+ * already worked out rather than an equal-but-different string.
458
+ */
409
459
  export function sanitize(text) {
410
- // eslint-disable-next-line no-control-regex
411
- return stripAnsi(text).replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '');
460
+ if (ANSI_TEST.test(text))
461
+ return stripAnsi(text).replace(CONTROL_RE, '');
462
+ return CONTROL_TEST.test(text) ? text.replace(CONTROL_RE, '') : text;
412
463
  }
413
464
  /** Repeat a grapheme to exactly `width` cells (wide chars land short). */
414
465
  export function repeatToWidth(char, width) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textui/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Graph-mounted terminal UI runtime - store, registries, renderer, host primitives",
5
5
  "keywords": [
6
6
  "terminal",
package/src/app/app.ts CHANGED
@@ -816,7 +816,21 @@ export class App implements TextUIApp {
816
816
  walkInstances(this.root, (instance) => {
817
817
  if (position) return;
818
818
  const cursor = instance.props.cursor;
819
- if (!cursor || !instance.box) return;
819
+ /*
820
+ * Absent, not falsy.
821
+ *
822
+ * `cursor` is a column, and column 0 is a real one - it is where the
823
+ * caret sits in an empty field, and where it sits at the start of any
824
+ * field. `!cursor` treated that as "no caret here", so a text input
825
+ * published no cursor position at all until its first character was
826
+ * typed: the terminal cursor stayed hidden, and the only thing marking
827
+ * the focused control was its border colour.
828
+ *
829
+ * `true` still means "here, at offset 0", which is what the `typeof`
830
+ * below is for - so this rejects the three ways of saying nothing and
831
+ * nothing else.
832
+ */
833
+ if (cursor === undefined || cursor === null || cursor === false || !instance.box) return;
820
834
  const id = typeof instance.props.id === 'string' ? instance.props.id : instance.id;
821
835
  if (id !== focused && `${instance.id}:focus` !== focused) return;
822
836
 
@@ -941,6 +955,33 @@ export class App implements TextUIApp {
941
955
  } catch (err) {
942
956
  this.handleError(err, `input:${event.type}`);
943
957
  }
958
+
959
+ /*
960
+ * Pointer motion and the wheel are streams, and only where they got to
961
+ * means anything.
962
+ *
963
+ * A terminal reports every cell the pointer crosses and every notch of the
964
+ * wheel, and one read can carry twenty of them - so rendering per event
965
+ * lays out and paints the whole tree twenty times to draw one gesture. The
966
+ * cost is the size of the tree, which is why it is invisible on a short
967
+ * screen and, on a long transcript, is the thing being dragged trailing
968
+ * seconds behind the pointer and a wheel that goes on scrolling after the
969
+ * hand has stopped. That second one is the tell: it is not momentum, it is
970
+ * a backlog of events still being drawn one frame each.
971
+ *
972
+ * Only the drawing is coalesced. Every event is still dispatched, so the
973
+ * notches still add up and the pointer still ends where it ended; what
974
+ * they share is the one frame `handleMouse` asked for, at the animation
975
+ * ceiling rather than at whatever rate the terminal felt like reporting.
976
+ *
977
+ * Keys are the opposite and stay synchronous: every one of them is a
978
+ * separate thing a person did, and a handler closes over the props from
979
+ * its last render, so typing "ab" without a frame in between makes the
980
+ * handler for "b" see the state from before "a".
981
+ */
982
+ if (event.type === 'mouse'
983
+ && (event.action === 'move' || event.action === 'drag' || event.action === 'wheel')) return;
984
+
944
985
  if (this.running_ && this.isDirty()) this.renderFrame();
945
986
  }
946
987
 
@@ -2,6 +2,16 @@ import type { Rect } from '../types/geometry.js';
2
2
  import type { Cell, CellBuffer } from '../types/cells.js';
3
3
  import { COLOR_DEFAULT, packColor, type PackedColor } from './color.js';
4
4
 
5
+ /**
6
+ * A character no cell can hold, for the previous frame when it must not match.
7
+ *
8
+ * `put` refuses the empty string and every real glyph is one the terminal can
9
+ * print, so NUL is a value that never arrives from a render and never leaves
10
+ * one: `diffFrame` reads `chars`, never `prevChars`, so this is only ever a
11
+ * comparison and is never written out.
12
+ */
13
+ const NEVER = '\u0000';
14
+
5
15
  /** One cell as it is stored: colours packed, nothing allocated to read it. */
6
16
  export interface PackedCell {
7
17
  char: string;
@@ -238,7 +248,7 @@ export class Buffer implements CellBuffer {
238
248
 
239
249
  // A resize invalidates the previous frame entirely.
240
250
  const n = width * height;
241
- this.prevChars = new Array<string>(n).fill(' ');
251
+ this.prevChars = new Array<string>(n).fill(NEVER);
242
252
  this.prevFg = new Int32Array(n).fill(COLOR_DEFAULT);
243
253
  this.prevBg = new Int32Array(n).fill(COLOR_DEFAULT);
244
254
  this.prevAttrs = new Uint16Array(n);
@@ -282,9 +292,25 @@ export class Buffer implements CellBuffer {
282
292
  this.committed = true;
283
293
  }
284
294
 
285
- /** Force the next diff to repaint everything. After a resize or a redraw. */
295
+ /**
296
+ * Force the next diff to repaint everything. After a resize or a redraw.
297
+ *
298
+ * Both halves, and the second one is the one that was missing. Clearing
299
+ * `committed` makes `dirtyRows` return every row, but `diffFrame` still
300
+ * skips each cell that matches the previous frame - and a blank cell in the
301
+ * new frame matched the reset previous frame exactly, so it was never
302
+ * written. Under a theme with an opaque canvas nothing showed, because every
303
+ * cell carries a background colour and therefore differs anyway. Under a
304
+ * theme with a transparent one the blanks are default-on-default, and the
305
+ * terminal kept whatever the old layout had left in them: a resize scattered
306
+ * the previous frame across the new one and it never cleared.
307
+ *
308
+ * So the previous frame is filled with a character no cell can hold, which
309
+ * is a comparison that cannot come out equal.
310
+ */
286
311
  invalidate(): void {
287
312
  this.committed = false;
313
+ this.prevChars.fill(NEVER);
288
314
  }
289
315
 
290
316
  toText(rect?: Rect): string {
@@ -40,6 +40,19 @@ export interface LayoutBox {
40
40
  /** Content larger than `content`, when overflow is 'scroll'. */
41
41
  scrollSize?: Size;
42
42
 
43
+ /**
44
+ * Frame and gaps, worked out once for this box.
45
+ *
46
+ * Neither depends on the space being offered - they are the box's own
47
+ * margin, border, padding and gap - and the box is measured about four
48
+ * times a frame at different bounds, each of which was rebuilding both and
49
+ * allocating three objects to do it. Same lifetime as `measured` below, and
50
+ * for the same reason: a layout box is rebuilt from the instance tree every
51
+ * frame, so nothing here can go stale.
52
+ */
53
+ frame?: { margin: Edges; inset: Edges };
54
+ gaps?: { main: number; cross: number };
55
+
43
56
  /**
44
57
  * The last answer `measureBox` gave for this box, and what it was asked.
45
58
  *
@@ -145,11 +158,15 @@ function mainOverflow(box: LayoutBox): Overflow {
145
158
  * that sets both reads the same either way round.
146
159
  */
147
160
  function gapsOf(box: LayoutBox): { main: number; cross: number } {
161
+ const seen = box.gaps;
162
+ if (seen !== undefined) return seen;
148
163
  const vertical = box.style.rowGap ?? box.style.gap ?? 0;
149
164
  const horizontal = box.style.columnGap ?? box.style.gap ?? 0;
150
- return isColumn(box)
165
+ const gaps = isColumn(box)
151
166
  ? { main: vertical, cross: horizontal }
152
167
  : { main: horizontal, cross: vertical };
168
+ box.gaps = gaps;
169
+ return gaps;
153
170
  }
154
171
 
155
172
  /**
@@ -181,10 +198,12 @@ function splitLines(mains: number[], gap: number, limit: number): [number, numbe
181
198
 
182
199
  /** Space this box consumes outside its content: margin, border, padding. */
183
200
  function frameOf(box: LayoutBox): { margin: Edges; inset: Edges } {
201
+ const seen = box.frame;
202
+ if (seen !== undefined) return seen;
184
203
  const margin = resolveEdges(box.style.margin);
185
204
  const padding = resolveEdges(box.style.padding);
186
205
  const b = box.borderEdges;
187
- return {
206
+ const frame = {
188
207
  margin,
189
208
  inset: {
190
209
  top: b.top + padding.top,
@@ -193,6 +212,8 @@ function frameOf(box: LayoutBox): { margin: Edges; inset: Edges } {
193
212
  left: b.left + padding.left,
194
213
  },
195
214
  };
215
+ box.frame = frame;
216
+ return frame;
196
217
  }
197
218
 
198
219
  /**
@@ -247,7 +268,7 @@ export function measureBox(box: LayoutBox, availW: number, availH: number): Size
247
268
  if (isAbsolute(child) || isHidden(child)) continue;
248
269
  count++;
249
270
  const m = measureBox(child, innerAvailW, innerAvailH);
250
- const cm = resolveEdges(child.style.margin);
271
+ const cm = frameOf(child).margin;
251
272
  mains.push(column ? m.height + edgeV(cm) : m.width + edgeH(cm));
252
273
  crosses.push(column ? m.width + edgeH(cm) : m.height + edgeV(cm));
253
274
  }
@@ -289,7 +310,7 @@ export function measureBox(box: LayoutBox, availW: number, availH: number): Size
289
310
  if (isAbsolute(child) || isHidden(child)) continue;
290
311
  const flex = Math.max(0, child.style.flex ?? 0);
291
312
  if (flex > 0) {
292
- const cm = resolveEdges(child.style.margin);
313
+ const cm = frameOf(child).margin;
293
314
  const share = Math.max(0, Math.floor((room * flex) / totalFlex) - edgeH(cm));
294
315
  const m = measureBox(child, share, innerAvailH);
295
316
  crosses[index] = m.height + edgeV(cm);
@@ -519,7 +540,7 @@ function layoutWrapped(box: LayoutBox, flow: LayoutBox[]): void {
519
540
  const mains: number[] = [];
520
541
  const crosses: number[] = [];
521
542
  for (const child of flow) {
522
- const margin = resolveEdges(child.style.margin);
543
+ const margin = frameOf(child).margin;
523
544
  const basis = child.style.basis ?? (column ? child.style.height : child.style.width);
524
545
  const fixed = resolveDimension(basis, mainAvail);
525
546
  const m = measureBox(
@@ -580,7 +601,7 @@ function layoutLine(
580
601
  const gapTotal = flow.length > 1 ? gap * (flow.length - 1) : 0;
581
602
 
582
603
  // 1. base sizes
583
- const margins = flow.map((c) => resolveEdges(c.style.margin));
604
+ const margins = flow.map((c) => frameOf(c).margin);
584
605
  const bases: number[] = [];
585
606
  for (let i = 0; i < flow.length; i++) {
586
607
  const child = flow[i] as LayoutBox;
@@ -1,5 +1,5 @@
1
1
  import type { Rect } from '../types/geometry.js';
2
- import type { Style } from '../types/style.js';
2
+ import type { Style, TextWrap } from '../types/style.js';
3
3
  import type { ResolvedTheme } from '../types/theme.js';
4
4
  import type { TerminalCapabilities } from '../types/capabilities.js';
5
5
  import type { Cell, Color } from '../types/cells.js';
@@ -12,7 +12,7 @@ import type { Buffer } from '../render/buffer.js';
12
12
  import { COLOR_DEFAULT, mix, packColor, type PackedColor } from '../render/color.js';
13
13
  import { rectIntersect } from '../types/geometry.js';
14
14
  import {
15
- graphemes, graphemeWidth, sanitize, stringWidth, truncate,
15
+ graphemes, graphemeWidth, isAscii, sanitize, stringWidth, truncate,
16
16
  truncateSideOf, wrapModeOf, wrapText,
17
17
  } from '../util/text.js';
18
18
  import {
@@ -87,8 +87,23 @@ class Surface implements PaintSurface {
87
87
  const link = style?.link;
88
88
  const oy = this.rect.y + y;
89
89
 
90
+ const clean = sanitize(text);
91
+
92
+ // Plain ASCII is one cell per character and needs none of the clustering
93
+ // machinery - and it is nearly every string a terminal ever draws. The
94
+ // general path allocates an array of one-character strings for the whole
95
+ // run before writing any of it.
90
96
  let cx = x;
91
- for (const g of graphemes(sanitize(text))) {
97
+ if (isAscii(clean)) {
98
+ for (let i = 0; i < clean.length; i++) {
99
+ const ax = this.rect.x + cx;
100
+ if (this.visible(ax, oy)) this.buffer.put(ax, oy, clean[i] as string, fg, bg, attrs, link);
101
+ cx += 1;
102
+ }
103
+ return cx - x;
104
+ }
105
+
106
+ for (const g of graphemes(clean)) {
92
107
  const w = graphemeWidth(g);
93
108
  if (w === 0) continue;
94
109
  const ax = this.rect.x + cx;
@@ -337,6 +352,17 @@ function oneLine(text: string): string {
337
352
  return text.includes('\n') ? text.split('\n').join(' ') : text;
338
353
  }
339
354
 
355
+ /**
356
+ * Text measurements already taken.
357
+ *
358
+ * A measurement is a pure function of the string, how it wraps and the width
359
+ * it is offered - and a layout asks for the same three several times in a
360
+ * frame, then the next frame asks for all of them again. The text in a
361
+ * transcript does not change; only the one line somebody is typing does.
362
+ */
363
+ const measurements = new Map<string, { width: number; height: number }>();
364
+ const MEASURE_CACHE = 4096;
365
+
340
366
  function textMeasure(instance: Instance, style: Style): (w: number, h: number) => { width: number; height: number } {
341
367
  return (maxWidth: number) => {
342
368
  const text = sanitize(textContent(instance));
@@ -344,26 +370,41 @@ function textMeasure(instance: Instance, style: Style): (w: number, h: number) =
344
370
 
345
371
  const wrap = style.wrap ?? 'none';
346
372
 
373
+ const key = `${wrap}\u0000${String(maxWidth)}\u0000${text}`;
374
+ const seen = measurements.get(key);
375
+ if (seen !== undefined) return seen;
376
+
347
377
  // A truncating text is one row, always. It still asks for the width it
348
378
  // would like - a box sizing itself around it gets the whole string when
349
379
  // there is room, and only cuts when there is not.
350
- if (truncateSideOf(wrap) !== undefined) {
351
- return { width: stringWidth(oneLine(text)), height: 1 };
352
- }
380
+ const size = measure(text, wrap, maxWidth);
381
+ if (measurements.size >= MEASURE_CACHE) measurements.clear();
382
+ measurements.set(key, size);
383
+ return size;
384
+ };
385
+ }
353
386
 
354
- if (wrap === 'none') {
355
- const lines = text.split('\n');
356
- return {
357
- width: Math.max(...lines.map(stringWidth)),
358
- height: lines.length,
359
- };
360
- }
361
- const limit = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : stringWidth(text);
362
- const lines = wrapText(text, limit, wrapModeOf(wrap));
387
+ /** The measurement itself, with nothing remembered. */
388
+ function measure(text: string, wrap: TextWrap, maxWidth: number): { width: number; height: number } {
389
+ // A truncating text is one row, always. It still asks for the width it
390
+ // would like - a box sizing itself around it gets the whole string when
391
+ // there is room, and only cuts when there is not.
392
+ if (truncateSideOf(wrap) !== undefined) {
393
+ return { width: stringWidth(oneLine(text)), height: 1 };
394
+ }
395
+
396
+ if (wrap === 'none') {
397
+ const lines = text.split('\n');
363
398
  return {
364
- width: Math.min(limit, Math.max(0, ...lines.map(stringWidth))),
365
- height: Math.max(1, lines.length),
399
+ width: Math.max(...lines.map(stringWidth)),
400
+ height: lines.length,
366
401
  };
402
+ }
403
+ const limit = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : stringWidth(text);
404
+ const lines = wrapText(text, limit, wrapModeOf(wrap));
405
+ return {
406
+ width: Math.min(limit, Math.max(0, ...lines.map(stringWidth))),
407
+ height: Math.max(1, lines.length),
367
408
  };
368
409
  }
369
410
 
package/src/util/text.ts CHANGED
@@ -20,7 +20,7 @@ const segmenter =
20
20
  * code, file names, labels, key hints. Every character is then one grapheme
21
21
  * one cell wide, and none of the Unicode machinery below has anything to do.
22
22
  */
23
- function isAscii(text: string): boolean {
23
+ export function isAscii(text: string): boolean {
24
24
  for (let i = 0; i < text.length; i++) {
25
25
  const c = text.charCodeAt(i);
26
26
  if (c < 0x20 || c > 0x7e) return false;
@@ -152,14 +152,42 @@ export function graphemeWidth(cluster: string): number {
152
152
  return 1;
153
153
  }
154
154
 
155
+ /**
156
+ * Widths already worked out, for strings that are not plain ASCII.
157
+ *
158
+ * The same strings are measured over and over. A frame measures the tree, lays
159
+ * it out and paints it - which is three walks over the same text - and then
160
+ * the next frame does all three again for content that has not changed. Nearly
161
+ * every row in a terminal application carries one glyph among its ASCII, a box
162
+ * rule or a bullet or an icon, and a single non-ASCII character sends the whole
163
+ * string down the slow path: an array of one-character strings, and a width
164
+ * classification for each of them. That was a quarter of the frame in a long
165
+ * transcript, spent proving the same sentences were the same width they were a
166
+ * thirtieth of a second ago.
167
+ *
168
+ * Only the slow path is remembered. ASCII is answered by `.length` before the
169
+ * map is even consulted, which is cheaper than the lookup would be, so the
170
+ * cache holds only what it saves anything on.
171
+ */
172
+ const widths = new Map<string, number>();
173
+ const WIDTH_CACHE = 8192;
174
+
155
175
  /** Width of a string in terminal cells. */
156
176
  export function stringWidth(text: string): number {
157
177
  if (text === '') return 0;
158
178
  // Fast path: pure ASCII printable.
159
179
  if (isAscii(text)) return text.length;
160
180
 
181
+ const seen = widths.get(text);
182
+ if (seen !== undefined) return seen;
183
+
161
184
  let w = 0;
162
185
  for (const g of graphemes(text)) w += graphemeWidth(g);
186
+ // Emptied rather than evicted one at a time: a terminal's vocabulary of
187
+ // strings is small and slow-moving, so the cap is only ever reached by
188
+ // something streaming new text, and for that the whole map is stale.
189
+ if (widths.size >= WIDTH_CACHE) widths.clear();
190
+ widths.set(text, w);
163
191
  return w;
164
192
  }
165
193
 
@@ -414,10 +442,33 @@ export function stripAnsi(text: string): string {
414
442
  return text.replace(ANSI_RE, '');
415
443
  }
416
444
 
417
- /** Remove control characters that would corrupt the frame. Keeps newlines. */
445
+ /*
446
+ * Test copies, without the `g` flag.
447
+ *
448
+ * `test` on a global regular expression advances its `lastIndex` and starts
449
+ * the next call from there, so the same string tested twice answers yes then
450
+ * no. Asking is a different question from replacing and gets its own pattern.
451
+ */
452
+ // eslint-disable-next-line no-control-regex
453
+ const ANSI_TEST = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1B]*(?:\x07|\x1B\\))/;
454
+ // eslint-disable-next-line no-control-regex
455
+ const CONTROL_TEST = /[\x00-\x08\x0B-\x1F\x7F]/;
456
+ // eslint-disable-next-line no-control-regex
457
+ const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
458
+
459
+ /**
460
+ * Remove control characters that would corrupt the frame. Keeps newlines.
461
+ *
462
+ * Two regular expressions over every string on screen, every frame, and for
463
+ * almost all of them the answer is the string it was given. `test` before
464
+ * `replace` is one scan that stops at the first match instead of two that
465
+ * always run to the end and allocate a copy each - and it returns the original
466
+ * string, so the callers that follow it get a cache hit on the width they
467
+ * already worked out rather than an equal-but-different string.
468
+ */
418
469
  export function sanitize(text: string): string {
419
- // eslint-disable-next-line no-control-regex
420
- return stripAnsi(text).replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '');
470
+ if (ANSI_TEST.test(text)) return stripAnsi(text).replace(CONTROL_RE, '');
471
+ return CONTROL_TEST.test(text) ? text.replace(CONTROL_RE, '') : text;
421
472
  }
422
473
 
423
474
  /** Repeat a grapheme to exactly `width` cells (wide chars land short). */