@tuidom/core 0.9.0 → 0.10.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,5 +1,5 @@
|
|
|
1
|
+
import { segmentGraphemes } from "./segmentGraphemes.js";
|
|
1
2
|
import { getCharDisplayWidth, getGraphemeDisplayWidth } from "./unicodeWidth.js";
|
|
2
|
-
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
3
3
|
const DEFAULT_TAB_SIZE = 4;
|
|
4
4
|
/**
|
|
5
5
|
* Maps a raw document string to an array of grapheme slots with correct
|
|
@@ -34,7 +34,7 @@ export class DisplayLine {
|
|
|
34
34
|
// отброшенной графемы (сегменты непрерывны, поэтому это ровно граница).
|
|
35
35
|
let scannedLength = raw.length;
|
|
36
36
|
let truncated = false;
|
|
37
|
-
for (const { segment, index } of
|
|
37
|
+
for (const { segment, index } of segmentGraphemes(raw)) {
|
|
38
38
|
if (index >= stopAfter) {
|
|
39
39
|
truncated = true;
|
|
40
40
|
scannedLength = index;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { segmentGraphemes } from "./segmentGraphemes.js";
|
|
1
2
|
import { getCharDisplayWidth, getGraphemeDisplayWidth } from "./unicodeWidth.js";
|
|
2
|
-
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
3
3
|
const DEFAULT_TAB_SIZE = 4;
|
|
4
4
|
/**
|
|
5
5
|
* Display width of `raw` in terminal columns.
|
|
@@ -23,7 +23,7 @@ const DEFAULT_TAB_SIZE = 4;
|
|
|
23
23
|
*/
|
|
24
24
|
export function measureTextWidth(raw, tabSize = DEFAULT_TAB_SIZE, stopAfter = Infinity) {
|
|
25
25
|
let column = 0;
|
|
26
|
-
for (const { segment, index } of
|
|
26
|
+
for (const { segment, index } of segmentGraphemes(raw)) {
|
|
27
27
|
if (index >= stopAfter)
|
|
28
28
|
break;
|
|
29
29
|
if (segment === "\t") {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** One grapheme cluster of a string — the shape of `Intl.SegmentData` callers use. */
|
|
2
|
+
export interface GraphemeSegment {
|
|
3
|
+
/** The grapheme cluster. */
|
|
4
|
+
readonly segment: string;
|
|
5
|
+
/** Its start offset in the source string (code units). */
|
|
6
|
+
readonly index: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Grapheme segmentation of `raw`, lazy as `Intl.Segmenter.segment`: a caller
|
|
10
|
+
* that stops early never touches the tail.
|
|
11
|
+
*
|
|
12
|
+
* Text where every code unit is a grapheme of its own (ASCII, Latin, Cyrillic,
|
|
13
|
+
* Greek, punctuation, box drawing, symbols, icon PUA) is split by code units
|
|
14
|
+
* without ICU — the same answer, but without loading ICU's break iterator,
|
|
15
|
+
* which costs a first frame ~15 ms. From the first code unit outside that set
|
|
16
|
+
* the rest of the string goes to `Intl.Segmenter`. VS Code does the same with
|
|
17
|
+
* its `isBasicASCII` fast paths and its own grapheme break table.
|
|
18
|
+
*
|
|
19
|
+
* The segmenter is created on first need, not at module load: a native `Intl`
|
|
20
|
+
* object held by a module-level binding cannot be serialized into a V8 startup
|
|
21
|
+
* snapshot, and hosts build one from the whole engine graph
|
|
22
|
+
* (`npm run check:snapshot`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function segmentGraphemes(raw: string): Iterable<GraphemeSegment>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
let segmenter = null;
|
|
2
|
+
/**
|
|
3
|
+
* BMP code points that are always a grapheme of their own next to each other:
|
|
4
|
+
* inclusive `[from, to]`, ascending. Beyond the Latin-1 + Latin Extended
|
|
5
|
+
* prefix checked inline in {@link isStandalone}.
|
|
6
|
+
*
|
|
7
|
+
* Every code point here has the UAX #29 Grapheme_Cluster_Break value `Other`
|
|
8
|
+
* or `Control` (CR and LF excluded). Between two such code points there is
|
|
9
|
+
* always a break: GB4/GB5 for `Control`, GB999 for `Other`, and the rules that
|
|
10
|
+
* could keep two of them together (GB9c, GB11, GB12/13) need an `Extend`,
|
|
11
|
+
* `ZWJ` or regional indicator right before the break. The exhaustive test in
|
|
12
|
+
* `segmentGraphemes.test.ts` checks every code point here against ICU.
|
|
13
|
+
*/
|
|
14
|
+
const STANDALONE_RANGES = [
|
|
15
|
+
[0x0370, 0x0482], // Greek, Cyrillic up to the combining titlo
|
|
16
|
+
[0x048a, 0x052f], // Cyrillic, Cyrillic Supplement
|
|
17
|
+
[0x2010, 0x2027], // dashes, quotes, bullets, ellipsis
|
|
18
|
+
[0x2030, 0x205e], // per mille, primes, punctuation
|
|
19
|
+
[0x2070, 0x20cf], // super/subscripts, currency
|
|
20
|
+
[0x2100, 0x23ff], // letterlike, number forms, arrows, math, technical
|
|
21
|
+
[0x2400, 0x27bf], // control pictures, box drawing, blocks, shapes, symbols, dingbats
|
|
22
|
+
[0x27c0, 0x2bff], // math, arrows, braille, symbols
|
|
23
|
+
[0xe000, 0xf8ff], // Private Use Area — icon fonts
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* `true` when code unit `c` always forms a grapheme of its own next to another
|
|
27
|
+
* such code unit: everything below U+0300 except CR and LF, plus
|
|
28
|
+
* {@link STANDALONE_RANGES}.
|
|
29
|
+
*/
|
|
30
|
+
function isStandalone(c) {
|
|
31
|
+
if (c < 0x0300)
|
|
32
|
+
return c !== 0x0a && c !== 0x0d;
|
|
33
|
+
for (const [from, to] of STANDALONE_RANGES) {
|
|
34
|
+
if (c < from)
|
|
35
|
+
return false;
|
|
36
|
+
if (c <= to)
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function segmentWithIcu(raw) {
|
|
42
|
+
segmenter ??= new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
43
|
+
return segmenter.segment(raw);
|
|
44
|
+
}
|
|
45
|
+
function* segmentFrom(raw) {
|
|
46
|
+
let i = 0;
|
|
47
|
+
let standalone = raw.length > 0 && isStandalone(raw.charCodeAt(0));
|
|
48
|
+
while (i < raw.length) {
|
|
49
|
+
const nextStandalone = i + 1 >= raw.length || isStandalone(raw.charCodeAt(i + 1));
|
|
50
|
+
// Break before `i` is certain (start of string, or two standalone code
|
|
51
|
+
// units); the cluster at `i` is one code unit only if its right
|
|
52
|
+
// neighbour is standalone too.
|
|
53
|
+
if (!standalone || !nextStandalone)
|
|
54
|
+
break;
|
|
55
|
+
yield { segment: raw[i], index: i };
|
|
56
|
+
i++;
|
|
57
|
+
standalone = nextStandalone;
|
|
58
|
+
}
|
|
59
|
+
if (i >= raw.length)
|
|
60
|
+
return;
|
|
61
|
+
// The rest goes to ICU from a certain break: no rule looks back across a
|
|
62
|
+
// break between two standalone code units, so the tail segments as it
|
|
63
|
+
// would inside the whole string.
|
|
64
|
+
const tail = i === 0 ? raw : raw.slice(i);
|
|
65
|
+
for (const { segment, index } of segmentWithIcu(tail)) {
|
|
66
|
+
yield { segment, index: index + i };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Grapheme segmentation of `raw`, lazy as `Intl.Segmenter.segment`: a caller
|
|
71
|
+
* that stops early never touches the tail.
|
|
72
|
+
*
|
|
73
|
+
* Text where every code unit is a grapheme of its own (ASCII, Latin, Cyrillic,
|
|
74
|
+
* Greek, punctuation, box drawing, symbols, icon PUA) is split by code units
|
|
75
|
+
* without ICU — the same answer, but without loading ICU's break iterator,
|
|
76
|
+
* which costs a first frame ~15 ms. From the first code unit outside that set
|
|
77
|
+
* the rest of the string goes to `Intl.Segmenter`. VS Code does the same with
|
|
78
|
+
* its `isBasicASCII` fast paths and its own grapheme break table.
|
|
79
|
+
*
|
|
80
|
+
* The segmenter is created on first need, not at module load: a native `Intl`
|
|
81
|
+
* object held by a module-level binding cannot be serialized into a V8 startup
|
|
82
|
+
* snapshot, and hosts build one from the whole engine graph
|
|
83
|
+
* (`npm run check:snapshot`).
|
|
84
|
+
*/
|
|
85
|
+
export function segmentGraphemes(raw) {
|
|
86
|
+
return segmentFrom(raw);
|
|
87
|
+
}
|
|
@@ -3,6 +3,23 @@ import { TerminalScreen } from "../rendering/terminalScreen.js";
|
|
|
3
3
|
import { FocusManager } from "./events/focusManager.js";
|
|
4
4
|
import { MouseEventDispatcher } from "./events/mouseEventDispatcher.js";
|
|
5
5
|
import { type TUIElement } from "./tuiElement.js";
|
|
6
|
+
/**
|
|
7
|
+
* Сколько занял кадр по фазам {@link TuiApplication.renderFrame}, мс. Для
|
|
8
|
+
* профилирования хостом (трасса старта, бенчи): кадр — layout → стили →
|
|
9
|
+
* damage-обход → отрисовка повреждённых областей → flush в бэкенд (diff +
|
|
10
|
+
* запись в терминал).
|
|
11
|
+
*/
|
|
12
|
+
export interface IFrameTimings {
|
|
13
|
+
/** Номер кадра — {@link TuiApplication.frameCount} после этого кадра. */
|
|
14
|
+
readonly frame: number;
|
|
15
|
+
/** Начало кадра, `performance.now()`. */
|
|
16
|
+
readonly start: number;
|
|
17
|
+
readonly layout: number;
|
|
18
|
+
readonly style: number;
|
|
19
|
+
readonly damage: number;
|
|
20
|
+
readonly paint: number;
|
|
21
|
+
readonly flush: number;
|
|
22
|
+
}
|
|
6
23
|
export declare class TuiApplication {
|
|
7
24
|
backend: ITerminalBackend;
|
|
8
25
|
root: TUIElement | null;
|
|
@@ -17,6 +34,11 @@ export declare class TuiApplication {
|
|
|
17
34
|
* кадре, а не молчаливым «не показывается / не кликается».
|
|
18
35
|
*/
|
|
19
36
|
validateTreeAfterRender: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Наблюдатель фаз кадра: зовётся после каждого кадра с таймингами
|
|
39
|
+
* {@link IFrameTimings}. Пока он null, кадр не читает часы вовсе.
|
|
40
|
+
*/
|
|
41
|
+
onFrameTimings: ((timings: IFrameTimings) => void) | null;
|
|
20
42
|
private frameCounter;
|
|
21
43
|
private pinnedKeypressTarget;
|
|
22
44
|
private needsFullRepaint;
|
|
@@ -23,6 +23,11 @@ export class TuiApplication {
|
|
|
23
23
|
* кадре, а не молчаливым «не показывается / не кликается».
|
|
24
24
|
*/
|
|
25
25
|
validateTreeAfterRender = false;
|
|
26
|
+
/**
|
|
27
|
+
* Наблюдатель фаз кадра: зовётся после каждого кадра с таймингами
|
|
28
|
+
* {@link IFrameTimings}. Пока он null, кадр не читает часы вовсе.
|
|
29
|
+
*/
|
|
30
|
+
onFrameTimings = null;
|
|
26
31
|
// Монотонный счётчик отрисованных кадров. Инкремент на каждый renderFrame —
|
|
27
32
|
// инспектор ждёт «рендер устоялся» (frameCount не менялся + нет
|
|
28
33
|
// запланированного рендера), не завися от sleep. См. inspector/idleWaiter.
|
|
@@ -47,6 +52,8 @@ export class TuiApplication {
|
|
|
47
52
|
renderFrame() {
|
|
48
53
|
if (this.root) {
|
|
49
54
|
this.frameCounter++;
|
|
55
|
+
const observer = this.onFrameTimings;
|
|
56
|
+
const start = observer === null ? 0 : performance.now();
|
|
50
57
|
// Root at (0, 0) — top-left of screen. globalPosition производный
|
|
51
58
|
// (localPosition корня без родителя), выставлять его не нужно.
|
|
52
59
|
this.root.localPosition = new Offset(0, 0);
|
|
@@ -54,7 +61,9 @@ export class TuiApplication {
|
|
|
54
61
|
// validateTree не зависят от damage); стили гейтятся внутри.
|
|
55
62
|
const constraints = BoxConstraints.tight(this.screen.size);
|
|
56
63
|
this.root.layout(constraints);
|
|
64
|
+
const layoutEnd = observer === null ? 0 : performance.now();
|
|
57
65
|
this.root.performStyleResolution(ROOT_STYLE_CONTEXT);
|
|
66
|
+
const styleEnd = observer === null ? 0 : performance.now();
|
|
58
67
|
// Damage-обход — всегда: и собирает области, и актуализирует
|
|
59
68
|
// lastPaintedRect/флаги (даже когда кадр будет полным).
|
|
60
69
|
const damage = new DamageList();
|
|
@@ -62,6 +71,7 @@ export class TuiApplication {
|
|
|
62
71
|
damage.add(detached);
|
|
63
72
|
}
|
|
64
73
|
this.root.collectDamage(damage, new Point(0, 0));
|
|
74
|
+
const damageEnd = observer === null ? 0 : performance.now();
|
|
65
75
|
const screenBounds = new Rect(new Point(0, 0), this.screen.size);
|
|
66
76
|
let rects;
|
|
67
77
|
if (this.needsFullRepaint) {
|
|
@@ -91,8 +101,21 @@ export class TuiApplication {
|
|
|
91
101
|
}
|
|
92
102
|
this.root.render(new RenderContext(this.screen, new Offset(0, 0), rect));
|
|
93
103
|
}
|
|
104
|
+
const paintEnd = observer === null ? 0 : performance.now();
|
|
94
105
|
// Всегда: пустой damage — пустой diff, ноль байт в терминал.
|
|
95
106
|
this.screen.flush(this.backend);
|
|
107
|
+
if (observer !== null) {
|
|
108
|
+
const flushEnd = performance.now();
|
|
109
|
+
observer({
|
|
110
|
+
frame: this.frameCounter,
|
|
111
|
+
start,
|
|
112
|
+
layout: layoutEnd - start,
|
|
113
|
+
style: styleEnd - layoutEnd,
|
|
114
|
+
damage: damageEnd - styleEnd,
|
|
115
|
+
paint: paintEnd - damageEnd,
|
|
116
|
+
flush: flushEnd - paintEnd,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
96
119
|
if (this.validateTreeAfterRender) {
|
|
97
120
|
assertValidTree(this.root);
|
|
98
121
|
}
|
package/package.json
CHANGED