@tuidom/elements 0.1.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.
- package/dist/body/bodyElement.d.ts +33 -0
- package/dist/body/bodyElement.js +87 -0
- package/dist/button/buttonElement.d.ts +18 -0
- package/dist/button/buttonElement.js +68 -0
- package/dist/completionlist/completionDetailsElement.d.ts +49 -0
- package/dist/completionlist/completionDetailsElement.js +165 -0
- package/dist/completionlist/completionItemKindIcon.d.ts +2 -0
- package/dist/completionlist/completionItemKindIcon.js +43 -0
- package/dist/completionlist/completionListElement.d.ts +102 -0
- package/dist/completionlist/completionListElement.js +309 -0
- package/dist/completionlist/completionWidgetElement.d.ts +41 -0
- package/dist/completionlist/completionWidgetElement.js +102 -0
- package/dist/contextview/contextMenuController.d.ts +23 -0
- package/dist/contextview/contextMenuController.js +75 -0
- package/dist/contextview/overlayHostElement.d.ts +23 -0
- package/dist/contextview/overlayHostElement.js +47 -0
- package/dist/editorgroup/editorTabItemElement.d.ts +41 -0
- package/dist/editorgroup/editorTabItemElement.js +186 -0
- package/dist/editorgroup/editorTabStripElement.d.ts +33 -0
- package/dist/editorgroup/editorTabStripElement.js +118 -0
- package/dist/editorpart/editorPartElement.d.ts +95 -0
- package/dist/editorpart/editorPartElement.js +285 -0
- package/dist/inputbox/inputElement.d.ts +34 -0
- package/dist/inputbox/inputElement.js +207 -0
- package/dist/inputbox/inputState.d.ts +69 -0
- package/dist/inputbox/inputState.js +326 -0
- package/dist/layout/boxContainerElement.d.ts +27 -0
- package/dist/layout/boxContainerElement.js +121 -0
- package/dist/layout/boxElement.d.ts +4 -0
- package/dist/layout/boxElement.js +30 -0
- package/dist/layout/fillerElement.d.ts +12 -0
- package/dist/layout/fillerElement.js +16 -0
- package/dist/layout/fitContentElement.d.ts +18 -0
- package/dist/layout/fitContentElement.js +46 -0
- package/dist/layout/hFlexElement.d.ts +26 -0
- package/dist/layout/hFlexElement.js +134 -0
- package/dist/layout/paddingContainerElement.d.ts +30 -0
- package/dist/layout/paddingContainerElement.js +88 -0
- package/dist/layout/sizedBoxElement.d.ts +30 -0
- package/dist/layout/sizedBoxElement.js +74 -0
- package/dist/layout/vFlexElement.d.ts +32 -0
- package/dist/layout/vFlexElement.js +136 -0
- package/dist/layout/vStackElement.d.ts +18 -0
- package/dist/layout/vStackElement.js +74 -0
- package/dist/list/listViewElement.d.ts +232 -0
- package/dist/list/listViewElement.js +944 -0
- package/dist/menu/menuBarElement.d.ts +47 -0
- package/dist/menu/menuBarElement.js +285 -0
- package/dist/menu/menuBarItemElement.d.ts +20 -0
- package/dist/menu/menuBarItemElement.js +68 -0
- package/dist/menu/popupMenuElement.d.ts +81 -0
- package/dist/menu/popupMenuElement.js +318 -0
- package/dist/menu/popupMenuItemElement.d.ts +24 -0
- package/dist/menu/popupMenuItemElement.js +89 -0
- package/dist/panel/panelContainerElement.d.ts +64 -0
- package/dist/panel/panelContainerElement.js +219 -0
- package/dist/quickpick/quickPickElement.d.ts +161 -0
- package/dist/quickpick/quickPickElement.js +561 -0
- package/dist/sash/sashElement.d.ts +30 -0
- package/dist/sash/sashElement.js +78 -0
- package/dist/scrollbar/iScrollable.d.ts +8 -0
- package/dist/scrollbar/iScrollable.js +1 -0
- package/dist/scrollbar/scrollBarRenderer.d.ts +36 -0
- package/dist/scrollbar/scrollBarRenderer.js +76 -0
- package/dist/scrollbar/scrollContainerElement.d.ts +39 -0
- package/dist/scrollbar/scrollContainerElement.js +91 -0
- package/dist/scrollbar/scrollViewport.d.ts +28 -0
- package/dist/scrollbar/scrollViewport.js +65 -0
- package/dist/scrollbar/scrollableElement.d.ts +22 -0
- package/dist/scrollbar/scrollableElement.js +42 -0
- package/dist/selectbox/selectBoxElement.d.ts +48 -0
- package/dist/selectbox/selectBoxElement.js +161 -0
- package/dist/terminal/encodeKeyForPty.d.ts +3 -0
- package/dist/terminal/encodeKeyForPty.js +56 -0
- package/dist/terminal/terminalViewElement.d.ts +17 -0
- package/dist/terminal/terminalViewElement.js +172 -0
- package/dist/text/textBlockElement.d.ts +13 -0
- package/dist/text/textBlockElement.js +35 -0
- package/dist/text/textLabelElement.d.ts +28 -0
- package/dist/text/textLabelElement.js +77 -0
- package/dist/titledpanel/titledPanelElement.d.ts +14 -0
- package/dist/titledpanel/titledPanelElement.js +40 -0
- package/dist/tree/iTreeDataProvider.d.ts +19 -0
- package/dist/tree/iTreeDataProvider.js +1 -0
- package/dist/tree/treeViewElement.d.ts +116 -0
- package/dist/tree/treeViewElement.js +748 -0
- package/dist/workbenchlayout/workbenchLayoutElement.d.ts +60 -0
- package/dist/workbenchlayout/workbenchLayoutElement.js +203 -0
- package/package.json +33 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Кодирование нажатия клавиши в байты, которые ждёт обычный PTY (xterm-совместимый).
|
|
2
|
+
//
|
|
3
|
+
// Пробрасывать `event.raw` напрямую нельзя: хост (NodeTerminalBackend) включает Kitty
|
|
4
|
+
// keyboard protocol, и raw приходит в виде CSI-u последовательностей, а шелл ждёт
|
|
5
|
+
// «легаси»-кодировку. Поэтому кодируем сами из логического `key` + модификаторов.
|
|
6
|
+
//
|
|
7
|
+
// Покрытие достаточно для спайка: печатные символы, ключевые управляющие клавиши,
|
|
8
|
+
// стрелки/навигация и Ctrl+буква (Ctrl+C/D/Z/L …). Application-cursor-keys режим не
|
|
9
|
+
// различаем — шлём стандартные CSI-последовательности.
|
|
10
|
+
const SPECIAL = {
|
|
11
|
+
Enter: "\r",
|
|
12
|
+
Backspace: "\x7f",
|
|
13
|
+
Tab: "\t",
|
|
14
|
+
Escape: "\x1b",
|
|
15
|
+
Delete: "\x1b[3~",
|
|
16
|
+
Insert: "\x1b[2~",
|
|
17
|
+
ArrowUp: "\x1b[A",
|
|
18
|
+
ArrowDown: "\x1b[B",
|
|
19
|
+
ArrowRight: "\x1b[C",
|
|
20
|
+
ArrowLeft: "\x1b[D",
|
|
21
|
+
Home: "\x1b[H",
|
|
22
|
+
End: "\x1b[F",
|
|
23
|
+
PageUp: "\x1b[5~",
|
|
24
|
+
PageDown: "\x1b[6~",
|
|
25
|
+
};
|
|
26
|
+
/** Вернуть байты для PTY, либо "" если клавишу не транслируем. */
|
|
27
|
+
export function encodeKeyForPty(event) {
|
|
28
|
+
const { key } = event;
|
|
29
|
+
// Ctrl+буква → управляющий байт 0x01..0x1a (Ctrl+A=1 … Ctrl+Z=26).
|
|
30
|
+
if (event.ctrlKey && key.length === 1) {
|
|
31
|
+
const lower = key.toLowerCase();
|
|
32
|
+
if (lower >= "a" && lower <= "z") {
|
|
33
|
+
return String.fromCharCode(lower.charCodeAt(0) - 96);
|
|
34
|
+
}
|
|
35
|
+
// Немного распространённых Ctrl-символов.
|
|
36
|
+
switch (key) {
|
|
37
|
+
case " ":
|
|
38
|
+
return "\x00"; // Ctrl+Space → NUL
|
|
39
|
+
case "[":
|
|
40
|
+
return "\x1b";
|
|
41
|
+
case "\\":
|
|
42
|
+
return "\x1c";
|
|
43
|
+
case "]":
|
|
44
|
+
return "\x1d";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const special = SPECIAL[key];
|
|
48
|
+
if (special !== undefined) {
|
|
49
|
+
return special;
|
|
50
|
+
}
|
|
51
|
+
// Печатный символ. Alt/Meta → ESC-префикс (meta-байт).
|
|
52
|
+
if (key.length === 1 && !event.ctrlKey) {
|
|
53
|
+
return event.altKey || event.metaKey ? "\x1b" + key : key;
|
|
54
|
+
}
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import type { ITerminalSurface } from "@tuidom/core/common/iTerminalSurface";
|
|
3
|
+
import type { TUIEventBase } from "@tuidom/core/dom/events/tuiEventBase";
|
|
4
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
5
|
+
export declare class TerminalViewElement extends TUIElement {
|
|
6
|
+
private readonly surface;
|
|
7
|
+
private readonly cell;
|
|
8
|
+
private readonly subscriptions;
|
|
9
|
+
constructor(surface: ITerminalSurface);
|
|
10
|
+
/** Рвёт подписки на поверхность. Вызывается владельцем виджета (контроллером). */
|
|
11
|
+
dispose(): void;
|
|
12
|
+
private forwardMouse;
|
|
13
|
+
private sendMouse;
|
|
14
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
15
|
+
render(context: RenderContext): void;
|
|
16
|
+
protected performDefaultAction(event: TUIEventBase): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Контрол отрисовки терминала — «клиент» встроенного терминала.
|
|
2
|
+
//
|
|
3
|
+
// TUIElement-лист: каждый кадр читает абстрактную сетку ячеек через `ITerminalSurface`
|
|
4
|
+
// и блитит её в наш grid через `RenderContext.setCell`. Виджет НЕ знает про PTY/эмулятор:
|
|
5
|
+
// вся связка (node-pty + @xterm/headless) живёт за `ITerminalSurface` на слое Workbench,
|
|
6
|
+
// поэтому TUIDom остаётся чистым (без импортов @xterm/headless и node-pty).
|
|
7
|
+
// Размером PTY управляет по реально выделенному месту (`performLayout` → `surface.resize`),
|
|
8
|
+
// поэтому ресайз окна автоматически рефлоует шелл. Ввод пробрасывает в surface через
|
|
9
|
+
// `encodeKeyForPty`. Колесо и Shift+PageUp/PageDown крутят вьюпорт по скролбэку
|
|
10
|
+
// (`surface.scrollLines`), пока программа в шелле не включила свой mouse-tracking —
|
|
11
|
+
// тогда колесо целиком её. См. docs/TODO/IntegratedTerminal.md.
|
|
12
|
+
import { DEFAULT_COLOR } from "@tuidom/core/common/colorUtils";
|
|
13
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
14
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
15
|
+
import { encodeKeyForPty } from "./encodeKeyForPty.js";
|
|
16
|
+
// wheelDirection ("up"|"down"|"left"|"right") → семантический action поверхности.
|
|
17
|
+
// Ключи типизированы точно (WheelDirection, а не string) — индексация тотальна,
|
|
18
|
+
// недостижимый фоллбэк на «неизвестное» направление не нужен.
|
|
19
|
+
const WHEEL_ACTION = {
|
|
20
|
+
up: "wheelUp",
|
|
21
|
+
down: "wheelDown",
|
|
22
|
+
left: "wheelLeft",
|
|
23
|
+
right: "wheelRight",
|
|
24
|
+
};
|
|
25
|
+
// Локальная прокрутка вьюпорта колесом: строк на «щелчок» и знак направления.
|
|
26
|
+
// Горизонтальные направления дают 0 — скролбэк одномерный, крутить вбок нечего.
|
|
27
|
+
const WHEEL_SCROLL_LINES = 3;
|
|
28
|
+
const WHEEL_SCROLL_SIGN = { up: -1, down: 1, left: 0, right: 0 };
|
|
29
|
+
export class TerminalViewElement extends TUIElement {
|
|
30
|
+
surface;
|
|
31
|
+
// Переиспользуемая ячейка — readCell(x, y, cell) не аллоцирует новый объект на каждую ячейку.
|
|
32
|
+
cell = { char: " ", fg: DEFAULT_COLOR, bg: DEFAULT_COLOR, style: 0, width: 1 };
|
|
33
|
+
// Подписки на поверхность — TUIElement не имеет lifecycle-хука, поэтому храним их сами
|
|
34
|
+
// и рвём в dispose() (владелец виджета — контроллер — обязан его вызвать).
|
|
35
|
+
subscriptions = [];
|
|
36
|
+
// Цвета «по умолчанию», которыми заменяем DEFAULT_COLOR ячеек при блите. Контроллер
|
|
37
|
+
// пушит сюда цвета темы (как EditorElement/PanelContainerElement получают цвета извне).
|
|
38
|
+
constructor(surface) {
|
|
39
|
+
super();
|
|
40
|
+
// Дефолтные цвета терминала — токены темы (фоллбэки panel/editor — в пуше палитры).
|
|
41
|
+
this.style = { fg: "terminal.foreground", bg: "terminal.background" };
|
|
42
|
+
this.surface = surface;
|
|
43
|
+
this.focusable = true; // фокусируемый — принимает клавиатуру
|
|
44
|
+
this.capturesPointer = true; // drag: move/up приходят сюда даже вне границ
|
|
45
|
+
// Новые данные из шелла → перерисовать контрол (TuiApplication батчит кадр).
|
|
46
|
+
this.subscriptions.push(this.surface.onUpdate(() => {
|
|
47
|
+
this.markDirty();
|
|
48
|
+
}));
|
|
49
|
+
// Выход шелла тоже перерисовывает — чтобы спрятать курсор (isExited в render).
|
|
50
|
+
this.subscriptions.push(this.surface.onExit(() => {
|
|
51
|
+
this.markDirty();
|
|
52
|
+
}));
|
|
53
|
+
// Мышь → surface (эмулятор сам решает по активному mouse-режиму, слать ли отчёт).
|
|
54
|
+
this.addEventListener("mousedown", (event) => {
|
|
55
|
+
this.forwardMouse(event, "down");
|
|
56
|
+
});
|
|
57
|
+
this.addEventListener("mouseup", (event) => {
|
|
58
|
+
this.forwardMouse(event, "up");
|
|
59
|
+
});
|
|
60
|
+
this.addEventListener("mousemove", (event) => {
|
|
61
|
+
this.forwardMouse(event, "move");
|
|
62
|
+
});
|
|
63
|
+
this.addEventListener("wheel", (event) => {
|
|
64
|
+
const mouse = event;
|
|
65
|
+
// Колесо без направления (бэкенд его не распознал) трактуем как прокрутку вверх.
|
|
66
|
+
const direction = mouse.wheelDirection ?? "up";
|
|
67
|
+
// Программа включила mouse-tracking (htop/vim/less --mouse) — колесо её,
|
|
68
|
+
// отдаём отчёт как есть. Обычный шелл mouse-режимов не включает, и тогда
|
|
69
|
+
// колесо наше: крутим вьюпорт по скролбэку (как xterm.js/VS Code).
|
|
70
|
+
if (this.surface.mouseEventsActive) {
|
|
71
|
+
this.sendMouse(mouse, "wheel", WHEEL_ACTION[direction]);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const sign = WHEEL_SCROLL_SIGN[direction];
|
|
75
|
+
if (sign === 0)
|
|
76
|
+
return;
|
|
77
|
+
mouse.preventDefault();
|
|
78
|
+
this.surface.scrollLines(sign * WHEEL_SCROLL_LINES);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Рвёт подписки на поверхность. Вызывается владельцем виджета (контроллером). */
|
|
82
|
+
dispose() {
|
|
83
|
+
for (const sub of this.subscriptions)
|
|
84
|
+
sub.dispose();
|
|
85
|
+
this.subscriptions.length = 0;
|
|
86
|
+
}
|
|
87
|
+
forwardMouse(event, action) {
|
|
88
|
+
this.sendMouse(event, event.button, action);
|
|
89
|
+
}
|
|
90
|
+
sendMouse(event, button, action) {
|
|
91
|
+
this.surface.sendMouse({
|
|
92
|
+
col: event.localX,
|
|
93
|
+
row: event.localY,
|
|
94
|
+
button,
|
|
95
|
+
action,
|
|
96
|
+
ctrl: event.ctrlKey,
|
|
97
|
+
alt: event.altKey,
|
|
98
|
+
shift: event.shiftKey,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
performLayout(constraints) {
|
|
102
|
+
const size = super.performLayout(constraints);
|
|
103
|
+
// Держим PTY и эмулятор ровно по выделенной области (TIOCSWINSZ+SIGWINCH).
|
|
104
|
+
this.surface.resize(size.width, size.height);
|
|
105
|
+
return size;
|
|
106
|
+
}
|
|
107
|
+
render(context) {
|
|
108
|
+
const width = this.layoutSize.width;
|
|
109
|
+
const height = this.layoutSize.height;
|
|
110
|
+
const cell = this.cell;
|
|
111
|
+
for (let y = 0; y < height; y++) {
|
|
112
|
+
// readCell отдаёт false и для continuation-ячейки wide-char, и для координаты
|
|
113
|
+
// вне диапазона. Различаем по предыдущей ячейке: после width=2 следующий false —
|
|
114
|
+
// это continuation (голова уже нарисована с шириной 2), пропускаем без покраски;
|
|
115
|
+
// иначе это пустая/внедиапазонная ячейка — красим пробелом, чтобы область виджета
|
|
116
|
+
// была закрашена полностью.
|
|
117
|
+
let prevWasWide = false;
|
|
118
|
+
for (let x = 0; x < width; x++) {
|
|
119
|
+
if (this.surface.readCell(x, y, cell)) {
|
|
120
|
+
context.setCell(x, y, {
|
|
121
|
+
char: cell.char,
|
|
122
|
+
fg: cell.fg === DEFAULT_COLOR ? this.resolvedStyle.fg : cell.fg,
|
|
123
|
+
bg: cell.bg === DEFAULT_COLOR ? this.resolvedStyle.bg : cell.bg,
|
|
124
|
+
style: cell.style,
|
|
125
|
+
width: cell.width,
|
|
126
|
+
});
|
|
127
|
+
prevWasWide = cell.width === 2;
|
|
128
|
+
}
|
|
129
|
+
else if (prevWasWide) {
|
|
130
|
+
prevWasWide = false; // continuation wide-char — пропускаем
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
context.setCell(x, y, { char: " " }); // пустая/вне диапазона
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Курсор PTY показываем только когда контрол в фокусе и шелл жив.
|
|
138
|
+
if (this.isFocused && !this.surface.isExited) {
|
|
139
|
+
const cursor = this.surface.getCursor();
|
|
140
|
+
if (cursor !== null && cursor.x >= 0 && cursor.x < width && cursor.y >= 0 && cursor.y < height) {
|
|
141
|
+
context.setCursorPosition(cursor.x, cursor.y);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
performDefaultAction(event) {
|
|
146
|
+
if (event.type === "paste") {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
this.surface.write(event.text);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (event.type === "keydown") {
|
|
152
|
+
const key = event;
|
|
153
|
+
// Shift+PageUp/PageDown — клавиатурный аналог колеса: листаем скролбэк
|
|
154
|
+
// страницами, в PTY эти сочетания не уходят.
|
|
155
|
+
if (key.shiftKey && (key.key === "PageUp" || key.key === "PageDown")) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
const page = Math.max(1, this.layoutSize.height - 1);
|
|
158
|
+
this.surface.scrollLines(key.key === "PageUp" ? -page : page);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const bytes = encodeKeyForPty(key);
|
|
162
|
+
if (bytes !== "") {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
this.surface.write(bytes);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Всё остальное (в т.ч. mousedown) → базовое поведение: клик по фокусируемому
|
|
169
|
+
// элементу переводит на него фокус (TUIElement.performDefaultAction).
|
|
170
|
+
super.performDefaultAction(event);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
2
|
+
import type { IContentSized } from "../scrollbar/iScrollable.js";
|
|
3
|
+
export declare class TextBlockElement extends TUIElement implements IContentSized {
|
|
4
|
+
contentHeight: number;
|
|
5
|
+
contentWidth: number;
|
|
6
|
+
private lines;
|
|
7
|
+
constructor(lineCount: number);
|
|
8
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
9
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
10
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
11
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
12
|
+
render(context: RenderContext): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DisplayLine } from "@tuidom/core/common/displayLine";
|
|
2
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
+
export class TextBlockElement extends TUIElement {
|
|
4
|
+
contentHeight;
|
|
5
|
+
contentWidth;
|
|
6
|
+
lines;
|
|
7
|
+
constructor(lineCount) {
|
|
8
|
+
super();
|
|
9
|
+
this.contentHeight = lineCount;
|
|
10
|
+
this.lines = [];
|
|
11
|
+
for (let i = 0; i < lineCount; i++) {
|
|
12
|
+
this.lines.push(`Line ${String(i + 1).padStart(3, "0")}`);
|
|
13
|
+
}
|
|
14
|
+
this.contentWidth = this.lines.reduce((max, l) => Math.max(max, new DisplayLine(l).displayWidth), 0);
|
|
15
|
+
}
|
|
16
|
+
getMinIntrinsicWidth(_height) {
|
|
17
|
+
return this.contentWidth;
|
|
18
|
+
}
|
|
19
|
+
getMaxIntrinsicWidth(_height) {
|
|
20
|
+
return this.contentWidth;
|
|
21
|
+
}
|
|
22
|
+
getMinIntrinsicHeight(_width) {
|
|
23
|
+
return this.contentHeight;
|
|
24
|
+
}
|
|
25
|
+
getMaxIntrinsicHeight(_width) {
|
|
26
|
+
return this.contentHeight;
|
|
27
|
+
}
|
|
28
|
+
render(context) {
|
|
29
|
+
const width = this.layoutSize.width;
|
|
30
|
+
for (let y = 0; y < this.contentHeight; y++) {
|
|
31
|
+
const lineContent = y < this.lines.length ? this.lines[y] : "";
|
|
32
|
+
context.drawText(0, y, lineContent, undefined, { maxWidth: width });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { StyleFlags } from "@tuidom/core/common/styleFlags";
|
|
3
|
+
import type { StyleColor } from "@tuidom/core/dom/styles/tuiStyle";
|
|
4
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
5
|
+
/** Посимвольный стиль; цвета — StyleColor: числа ИЛИ имена токенов темы. */
|
|
6
|
+
export interface StyledChar {
|
|
7
|
+
fg?: StyleColor;
|
|
8
|
+
bg?: StyleColor;
|
|
9
|
+
style?: StyleFlags;
|
|
10
|
+
}
|
|
11
|
+
export declare class TextLabelElement extends TUIElement {
|
|
12
|
+
private text;
|
|
13
|
+
private charStyles;
|
|
14
|
+
private cachedDisplayLine;
|
|
15
|
+
constructor(text: string);
|
|
16
|
+
private get displayLine();
|
|
17
|
+
getText(): string;
|
|
18
|
+
setText(text: string): void;
|
|
19
|
+
setColors(fg: StyleColor, bg: StyleColor): void;
|
|
20
|
+
setCharStyle(index: number, charStyle: StyledChar): void;
|
|
21
|
+
clearCharStyles(): void;
|
|
22
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
23
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
24
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
25
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
26
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
27
|
+
render(context: RenderContext): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { DisplayLine } from "@tuidom/core/common/displayLine";
|
|
2
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
3
|
+
import { StyleFlags } from "@tuidom/core/common/styleFlags";
|
|
4
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
5
|
+
export class TextLabelElement extends TUIElement {
|
|
6
|
+
text;
|
|
7
|
+
charStyles = new Map();
|
|
8
|
+
// Сегментация — O(длины текста) на каждое построение DisplayLine, а лейбл
|
|
9
|
+
// строил его дважды за кадр (layout + render). Кэш живёт до setText;
|
|
10
|
+
// charStyles/цвета на текст не влияют и кэш не трогают.
|
|
11
|
+
cachedDisplayLine = null;
|
|
12
|
+
constructor(text) {
|
|
13
|
+
super();
|
|
14
|
+
this.text = text;
|
|
15
|
+
}
|
|
16
|
+
get displayLine() {
|
|
17
|
+
return (this.cachedDisplayLine ??= new DisplayLine(this.text));
|
|
18
|
+
}
|
|
19
|
+
getText() {
|
|
20
|
+
return this.text;
|
|
21
|
+
}
|
|
22
|
+
setText(text) {
|
|
23
|
+
this.text = text;
|
|
24
|
+
this.cachedDisplayLine = null;
|
|
25
|
+
this.markDirty();
|
|
26
|
+
}
|
|
27
|
+
setColors(fg, bg) {
|
|
28
|
+
this.style = { ...this.style, fg, bg };
|
|
29
|
+
}
|
|
30
|
+
setCharStyle(index, charStyle) {
|
|
31
|
+
this.charStyles.set(index, charStyle);
|
|
32
|
+
this.markDirty();
|
|
33
|
+
}
|
|
34
|
+
clearCharStyles() {
|
|
35
|
+
this.charStyles.clear();
|
|
36
|
+
this.markDirty();
|
|
37
|
+
}
|
|
38
|
+
getMinIntrinsicWidth(_height) {
|
|
39
|
+
return this.displayLine.displayWidth;
|
|
40
|
+
}
|
|
41
|
+
getMaxIntrinsicWidth(_height) {
|
|
42
|
+
return this.displayLine.displayWidth;
|
|
43
|
+
}
|
|
44
|
+
getMinIntrinsicHeight(_width) {
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
getMaxIntrinsicHeight(_width) {
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
performLayout(constraints) {
|
|
51
|
+
const natural = new Size(this.getMaxIntrinsicWidth(1), this.getMaxIntrinsicHeight(0));
|
|
52
|
+
return super.performLayout(BoxConstraints.tight(constraints.constrain(natural)));
|
|
53
|
+
}
|
|
54
|
+
render(context) {
|
|
55
|
+
const width = this.layoutSize.width;
|
|
56
|
+
const resolved = this.resolvedStyle;
|
|
57
|
+
context.drawText(0, 0, this.text, { fg: resolved.fg, bg: resolved.bg, style: StyleFlags.None }, {
|
|
58
|
+
maxWidth: width,
|
|
59
|
+
displayLine: this.displayLine,
|
|
60
|
+
getStyle: (offset) => {
|
|
61
|
+
const charStyle = this.charStyles.get(offset);
|
|
62
|
+
if (charStyle === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
// Ключи — только для заданных полей: drawText мёржит
|
|
65
|
+
// спредом, и ключ с undefined затёр бы базовый цвет.
|
|
66
|
+
const override = {};
|
|
67
|
+
if (charStyle.fg !== undefined)
|
|
68
|
+
override.fg = this.resolveColor(charStyle.fg);
|
|
69
|
+
if (charStyle.bg !== undefined)
|
|
70
|
+
override.bg = this.resolveColor(charStyle.bg);
|
|
71
|
+
if (charStyle.style !== undefined)
|
|
72
|
+
override.style = charStyle.style;
|
|
73
|
+
return override;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
+
export declare class TitledPanelElement extends TUIElement {
|
|
4
|
+
private title;
|
|
5
|
+
private child;
|
|
6
|
+
private titlePaddingLeft;
|
|
7
|
+
constructor(title: string, child: TUIElement, options?: {
|
|
8
|
+
titlePaddingLeft?: number;
|
|
9
|
+
});
|
|
10
|
+
getTitle(): string;
|
|
11
|
+
setTitle(value: string): void;
|
|
12
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
13
|
+
render(context: RenderContext): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
+
export class TitledPanelElement extends TUIElement {
|
|
4
|
+
title;
|
|
5
|
+
child;
|
|
6
|
+
titlePaddingLeft;
|
|
7
|
+
constructor(title, child, options) {
|
|
8
|
+
super();
|
|
9
|
+
this.title = title;
|
|
10
|
+
this.child = child;
|
|
11
|
+
this.appendChild(this.child);
|
|
12
|
+
this.titlePaddingLeft = options?.titlePaddingLeft ?? 1;
|
|
13
|
+
}
|
|
14
|
+
getTitle() {
|
|
15
|
+
return this.title;
|
|
16
|
+
}
|
|
17
|
+
setTitle(value) {
|
|
18
|
+
this.title = value;
|
|
19
|
+
this.markDirty();
|
|
20
|
+
}
|
|
21
|
+
performLayout(constraints) {
|
|
22
|
+
const containerSize = super.performLayout(constraints);
|
|
23
|
+
const childHeight = Math.max(0, containerSize.height - 1);
|
|
24
|
+
this.layoutChild(this.child, 0, 1, BoxConstraints.tight(new Size(containerSize.width, childHeight)));
|
|
25
|
+
return containerSize;
|
|
26
|
+
}
|
|
27
|
+
render(context) {
|
|
28
|
+
this.paintOwnBackground(context);
|
|
29
|
+
const width = this.layoutSize.width;
|
|
30
|
+
const resolved = this.resolvedStyle;
|
|
31
|
+
const titleFg = this.styleVar("titledPanel.titleForeground");
|
|
32
|
+
const titleBg = resolved.bg;
|
|
33
|
+
for (let x = 0; x < width; x++) {
|
|
34
|
+
const textIndex = x - this.titlePaddingLeft;
|
|
35
|
+
const char = textIndex >= 0 && textIndex < this.title.length ? this.title[textIndex] : " ";
|
|
36
|
+
context.setCell(x, 0, { char, fg: titleFg, bg: titleBg });
|
|
37
|
+
}
|
|
38
|
+
this.renderChildren(context);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StyleColor } from "@tuidom/core/dom/styles/tuiStyle";
|
|
2
|
+
export interface ITreeItem {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly icon?: string;
|
|
5
|
+
readonly iconColor?: StyleColor;
|
|
6
|
+
readonly collapsible: boolean;
|
|
7
|
+
/** Помечает элемент как символическую ссылку: рисуется стрелка-badge у левого края. */
|
|
8
|
+
readonly symlink?: boolean;
|
|
9
|
+
/** Буква-бейдж статуса (1–2 символа), рисуется у правого края строки (напр. git-статус). */
|
|
10
|
+
readonly badge?: string;
|
|
11
|
+
/** Упакованный RGB-цвет имени: переопределяет fg спана метки (напр. git-статус). */
|
|
12
|
+
readonly labelColor?: StyleColor;
|
|
13
|
+
}
|
|
14
|
+
export interface ITreeDataProvider<T> {
|
|
15
|
+
getTreeItem(element: T): ITreeItem;
|
|
16
|
+
getChildren(element?: T): T[] | Promise<T[]>;
|
|
17
|
+
getKey(element: T): string;
|
|
18
|
+
onChange?: (element?: T) => void;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Point } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import type { TUIEventBase } from "@tuidom/core/dom/events/tuiEventBase";
|
|
3
|
+
import type { RenderContext } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
import { ScrollableElement, type ScrollViewportInfo } from "../scrollbar/scrollableElement.js";
|
|
5
|
+
import type { ITreeDataProvider } from "./iTreeDataProvider.js";
|
|
6
|
+
export declare class TreeViewElement<T> extends ScrollableElement {
|
|
7
|
+
private provider;
|
|
8
|
+
private expandedKeys;
|
|
9
|
+
private childrenCache;
|
|
10
|
+
private flatNodes;
|
|
11
|
+
private selectedIndex;
|
|
12
|
+
private selectionAnchor;
|
|
13
|
+
private hoveredIndex;
|
|
14
|
+
private selectedKeys;
|
|
15
|
+
private cutKeys;
|
|
16
|
+
private maxRowWidth;
|
|
17
|
+
/**
|
|
18
|
+
* Отступ контента строк от левого края элемента. В отличие от внешнего
|
|
19
|
+
* паддинг-контейнера, входит в саму строку, поэтому фон курсора/выделения
|
|
20
|
+
* заливает строку от края, а текст начинается со сдвигом.
|
|
21
|
+
*/
|
|
22
|
+
private leftPadding;
|
|
23
|
+
private typeaheadBuffer;
|
|
24
|
+
private typeaheadTimer;
|
|
25
|
+
onSelect: ((item: T) => void) | null;
|
|
26
|
+
onActivate: ((item: T) => void) | null;
|
|
27
|
+
onExpandedChanged: ((element: T, expanded: boolean) => void) | null;
|
|
28
|
+
onContextMenu: ((element: T, screenX: number, screenY: number) => void) | null;
|
|
29
|
+
constructor(provider: ITreeDataProvider<T>, options?: {
|
|
30
|
+
leftPadding?: number;
|
|
31
|
+
});
|
|
32
|
+
get contentHeight(): number;
|
|
33
|
+
get contentWidth(): number;
|
|
34
|
+
refresh(element?: T): Promise<void>;
|
|
35
|
+
toggleExpand(element: T): Promise<void>;
|
|
36
|
+
/** Раскрывает узел (идемпотентно, без прокрутки/выделения) и перестраивает список. */
|
|
37
|
+
expand(element: T): Promise<void>;
|
|
38
|
+
/** Раскрывает узел (загружая детей в кеш), если он ещё не раскрыт. No-op для уже раскрытого. */
|
|
39
|
+
private expandElement;
|
|
40
|
+
/**
|
|
41
|
+
* Раскрывает цепочку предков и ставит курсор на целевой узел, проматывая к нему.
|
|
42
|
+
* `chain` — путь от корня к цели включительно: все элементы кроме последнего
|
|
43
|
+
* трактуются как предки-контейнеры и раскрываются, последний — цель выделения.
|
|
44
|
+
* Если цель не находится в дереве после раскрытия (например, путь вне корня),
|
|
45
|
+
* выделение не меняется.
|
|
46
|
+
*/
|
|
47
|
+
reveal(chain: T[]): Promise<void>;
|
|
48
|
+
setCutKeys(keys: Set<string>): void;
|
|
49
|
+
clearCutKeys(): void;
|
|
50
|
+
/** Узел под курсором, либо null если дерево пусто. */
|
|
51
|
+
getSelectedNode(): T | null;
|
|
52
|
+
/**
|
|
53
|
+
* Глобальные экранные координаты выделенной строки — якорь для контекстного меню,
|
|
54
|
+
* открытого с клавиатуры (Shift+F10). X сдвинут на отступ по глубине узла, чтобы
|
|
55
|
+
* меню появлялось у имени, а не у левого края панели. Возвращает `null`, если
|
|
56
|
+
* дерево пусто.
|
|
57
|
+
*/
|
|
58
|
+
getSelectedRowGlobalPosition(): Point | null;
|
|
59
|
+
/**
|
|
60
|
+
* Все выбранные узлы. Если множественный выбор пуст, возвращает узел под курсором
|
|
61
|
+
* (в виде массива из одного элемента). Порядок — как в дереве.
|
|
62
|
+
*/
|
|
63
|
+
getSelectedNodes(): T[];
|
|
64
|
+
protected performDefaultAction(event: TUIEventBase): void;
|
|
65
|
+
protected renderViewport(context: RenderContext, viewport: ScrollViewportInfo): void;
|
|
66
|
+
private resolveRowColors;
|
|
67
|
+
private loadRootChildren;
|
|
68
|
+
private reloadExpandedChildren;
|
|
69
|
+
private invalidateSubtreeCache;
|
|
70
|
+
private rebuildFlatList;
|
|
71
|
+
private appendChildren;
|
|
72
|
+
/** Колонка (в координатах контента), с которой начинается строка узла данной глубины. */
|
|
73
|
+
private rowIndentX;
|
|
74
|
+
private calculateRowWidth;
|
|
75
|
+
private formatRow;
|
|
76
|
+
private getSelectedKey;
|
|
77
|
+
private restoreSelection;
|
|
78
|
+
focusPageDown(): void;
|
|
79
|
+
focusPageUp(): void;
|
|
80
|
+
focusFirst(): void;
|
|
81
|
+
focusLast(): void;
|
|
82
|
+
private setSelectedIndex;
|
|
83
|
+
/** Двигает курсор и расширяет множественный выбор диапазоном от якоря (Shift+стрелки). */
|
|
84
|
+
private extendSelectionTo;
|
|
85
|
+
private selectRange;
|
|
86
|
+
/** Переключает выбор одной строки (Ctrl/Cmd+клик), не сбрасывая остальное. */
|
|
87
|
+
private toggleSelectionAt;
|
|
88
|
+
private applyCursor;
|
|
89
|
+
private ensureVisible;
|
|
90
|
+
private findElementByKey;
|
|
91
|
+
private handleKeypress;
|
|
92
|
+
/**
|
|
93
|
+
* Быстрый поиск по набору: печатаешь символы — курсор перепрыгивает к первому узлу,
|
|
94
|
+
* чья метка начинается с набранной последовательности. Повтор одного и того же
|
|
95
|
+
* символа циклически перебирает совпадения; набор из разных символов уточняет
|
|
96
|
+
* префикс, начиная с текущей строки. Буфер копится, пока не выйдет тайм-аут.
|
|
97
|
+
*/
|
|
98
|
+
private handleTypeahead;
|
|
99
|
+
private restartTypeaheadTimer;
|
|
100
|
+
private jumpToTypeaheadMatch;
|
|
101
|
+
private handleExpandOrMoveToChild;
|
|
102
|
+
private handleCollapseOrMoveToParent;
|
|
103
|
+
private activateSelected;
|
|
104
|
+
private toggleSelected;
|
|
105
|
+
/**
|
|
106
|
+
* Единый вход контекстного меню: от мыши — узел под кликом (мультивыбор по
|
|
107
|
+
* выбранной строке не сбрасывается), от клавиатуры — узел курсора с якорем
|
|
108
|
+
* на его глобальной позиции. Виджет только сигналит {@link onContextMenu}.
|
|
109
|
+
*/
|
|
110
|
+
private handleContextMenu;
|
|
111
|
+
private handleClick;
|
|
112
|
+
private handleDblClick;
|
|
113
|
+
private handleMouseMove;
|
|
114
|
+
private handleMouseLeave;
|
|
115
|
+
private handleWheel;
|
|
116
|
+
}
|