@tuidom/elements 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuidom/elements",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Experimental terminal UI DOM — widgets: lists, trees, menus, inputs, quick pick. API is unstable.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"./*": "./dist/*.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@tuidom/core": "0.5.
|
|
31
|
+
"@tuidom/core": "0.5.1"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
-
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
-
/** A view hosted in the bottom Panel (e.g. Problems, Output). */
|
|
4
|
-
export interface PanelView {
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly title: string;
|
|
7
|
-
/** The view's content element; null renders {@link placeholder} instead. */
|
|
8
|
-
content: TUIElement | null;
|
|
9
|
-
/**
|
|
10
|
-
* View-specific controls pinned to the right of the tab row (VS Code's
|
|
11
|
-
* `MenuId.ViewTitle` area) — e.g. the Output channel selector.
|
|
12
|
-
*/
|
|
13
|
-
actions?: TUIElement | null;
|
|
14
|
-
/** Empty-state message shown when `content` is null (à la VS Code view welcome). */
|
|
15
|
-
readonly placeholder?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* The bottom **Panel** part (VS Code `ViewContainerLocation.Panel`): a top border
|
|
19
|
-
* strip + a header row of view tabs (PROBLEMS, OUTPUT, …), a left border that
|
|
20
|
-
* separates it from the sidebar, and the active view's content below. Views are
|
|
21
|
-
* registered via {@link addView}; the active one is shown. A view with no content
|
|
22
|
-
* element renders its {@link PanelView.placeholder} empty-state message.
|
|
23
|
-
*
|
|
24
|
-
* Tab labels are drawn dim (`panelTitle.inactiveForeground`); the active tab is
|
|
25
|
-
* marked with an underline. Colours are pushed by the controller (`panel.*` /
|
|
26
|
-
* `panelTitle.*`), mirroring how `EditorElement` receives its theme colours.
|
|
27
|
-
*/
|
|
28
|
-
export declare class PanelContainerElement extends TUIElement {
|
|
29
|
-
/** Fired when a tab is clicked (after the active view has switched). */
|
|
30
|
-
onActivateView?: (id: string) => void;
|
|
31
|
-
private views;
|
|
32
|
-
private activeId;
|
|
33
|
-
constructor();
|
|
34
|
-
addView(view: PanelView): void;
|
|
35
|
-
/** Replaces a registered view's title-row controls (null removes them). */
|
|
36
|
-
setViewActions(id: string, actions: TUIElement | null): void;
|
|
37
|
-
/** Replaces a registered view's content element (e.g. swapping a placeholder for the real view). */
|
|
38
|
-
setViewContent(id: string, content: TUIElement | null): void;
|
|
39
|
-
setActiveView(id: string): void;
|
|
40
|
-
/**
|
|
41
|
-
* Все вкладки живут в дереве постоянно; неактивные — hidden (root и стили
|
|
42
|
-
* доходят до них всегда). Раньше getChildren() отдавал только активную, и
|
|
43
|
-
* контент/actions, прицепленные до укоренения панели, оставались с
|
|
44
|
-
* протухшим root — модель бага #204 (селектор каналов Output молча не
|
|
45
|
-
* открывал выпадашку после restore сессии).
|
|
46
|
-
*/
|
|
47
|
-
private syncChildren;
|
|
48
|
-
getActiveViewId(): string | null;
|
|
49
|
-
getViewIds(): string[];
|
|
50
|
-
/**
|
|
51
|
-
* Observable panel state for the inspector: which tabs exist, which is
|
|
52
|
-
* active, and each tab's absolute hit geometry. Replaces the e2e helper that
|
|
53
|
-
* re-derived tab coordinates from TAB_INDENT/TAB_PAD by hand — a test clicks
|
|
54
|
-
* `tabs[i].centerX` on `tabRow` instead.
|
|
55
|
-
*/
|
|
56
|
-
inspectState(): Record<string, unknown>;
|
|
57
|
-
/** Правая граница таб-строки — за неё контролы вкладки заезжать не должны. */
|
|
58
|
-
private tabsEnd;
|
|
59
|
-
private activeView;
|
|
60
|
-
/** Header tab layout: ` Title ` segments after the indent, with hit ranges. */
|
|
61
|
-
private tabSegments;
|
|
62
|
-
protected performLayout(constraints: BoxConstraints): Size;
|
|
63
|
-
render(context: RenderContext): void;
|
|
64
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { BoxConstraints, Offset, Point, Rect, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
-
import { StyleFlags } from "@tuidom/core/common/styleFlags";
|
|
3
|
-
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
-
/** One space of padding on each side of a tab title. */
|
|
5
|
-
const TAB_PAD = 1;
|
|
6
|
-
/** Indent of the tab strip from the left edge. */
|
|
7
|
-
const TAB_INDENT = 1;
|
|
8
|
-
/** Row the tab header sits on (below the top border strip). */
|
|
9
|
-
const TAB_ROW = 1;
|
|
10
|
-
/** First content row (below the top border strip + tab header). */
|
|
11
|
-
const CONTENT_TOP = 2;
|
|
12
|
-
/** Left indent of the content area / placeholder (aligns under the tab label). */
|
|
13
|
-
const CONTENT_LEFT = 2;
|
|
14
|
-
/**
|
|
15
|
-
* The bottom **Panel** part (VS Code `ViewContainerLocation.Panel`): a top border
|
|
16
|
-
* strip + a header row of view tabs (PROBLEMS, OUTPUT, …), a left border that
|
|
17
|
-
* separates it from the sidebar, and the active view's content below. Views are
|
|
18
|
-
* registered via {@link addView}; the active one is shown. A view with no content
|
|
19
|
-
* element renders its {@link PanelView.placeholder} empty-state message.
|
|
20
|
-
*
|
|
21
|
-
* Tab labels are drawn dim (`panelTitle.inactiveForeground`); the active tab is
|
|
22
|
-
* marked with an underline. Colours are pushed by the controller (`panel.*` /
|
|
23
|
-
* `panelTitle.*`), mirroring how `EditorElement` receives its theme colours.
|
|
24
|
-
*/
|
|
25
|
-
export class PanelContainerElement extends TUIElement {
|
|
26
|
-
/** Fired when a tab is clicked (after the active view has switched). */
|
|
27
|
-
onActivateView;
|
|
28
|
-
views = [];
|
|
29
|
-
activeId = null;
|
|
30
|
-
constructor() {
|
|
31
|
-
super();
|
|
32
|
-
this.style = { bg: "panel.background" };
|
|
33
|
-
this.addEventListener("mousedown", (event) => {
|
|
34
|
-
if (event.button !== "left")
|
|
35
|
-
return;
|
|
36
|
-
// Событие, всплывшее из дочернего контрола (селектор в шапке), не наше:
|
|
37
|
-
// без этой проверки клик по нему ещё и переключал бы вкладку.
|
|
38
|
-
if (event.target !== this)
|
|
39
|
-
return;
|
|
40
|
-
const localY = event.screenY - this.globalPosition.y;
|
|
41
|
-
if (localY !== TAB_ROW)
|
|
42
|
-
return; // only the tab header row switches tabs
|
|
43
|
-
const localX = event.screenX - this.globalPosition.x;
|
|
44
|
-
const segment = this.tabSegments().find((s) => localX >= s.start && localX < s.end);
|
|
45
|
-
if (segment === undefined)
|
|
46
|
-
return;
|
|
47
|
-
this.setActiveView(segment.id);
|
|
48
|
-
this.onActivateView?.(segment.id);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
addView(view) {
|
|
52
|
-
this.views.push(view);
|
|
53
|
-
this.activeId ??= view.id;
|
|
54
|
-
this.syncChildren();
|
|
55
|
-
this.markDirty();
|
|
56
|
-
}
|
|
57
|
-
/** Replaces a registered view's title-row controls (null removes them). */
|
|
58
|
-
setViewActions(id, actions) {
|
|
59
|
-
const view = this.views.find((v) => v.id === id);
|
|
60
|
-
if (view === undefined)
|
|
61
|
-
return;
|
|
62
|
-
view.actions = actions;
|
|
63
|
-
this.syncChildren();
|
|
64
|
-
this.markDirty();
|
|
65
|
-
}
|
|
66
|
-
/** Replaces a registered view's content element (e.g. swapping a placeholder for the real view). */
|
|
67
|
-
setViewContent(id, content) {
|
|
68
|
-
const view = this.views.find((v) => v.id === id);
|
|
69
|
-
if (view === undefined)
|
|
70
|
-
return;
|
|
71
|
-
view.content = content;
|
|
72
|
-
this.syncChildren();
|
|
73
|
-
this.markDirty();
|
|
74
|
-
}
|
|
75
|
-
setActiveView(id) {
|
|
76
|
-
if (this.views.every((v) => v.id !== id) || this.activeId === id)
|
|
77
|
-
return;
|
|
78
|
-
this.activeId = id;
|
|
79
|
-
this.syncChildren();
|
|
80
|
-
this.markDirty();
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Все вкладки живут в дереве постоянно; неактивные — hidden (root и стили
|
|
84
|
-
* доходят до них всегда). Раньше getChildren() отдавал только активную, и
|
|
85
|
-
* контент/actions, прицепленные до укоренения панели, оставались с
|
|
86
|
-
* протухшим root — модель бага #204 (селектор каналов Output молча не
|
|
87
|
-
* открывал выпадашку после restore сессии).
|
|
88
|
-
*/
|
|
89
|
-
syncChildren() {
|
|
90
|
-
const children = [];
|
|
91
|
-
for (const view of this.views) {
|
|
92
|
-
const isActive = view.id === this.activeId;
|
|
93
|
-
if (view.actions != null) {
|
|
94
|
-
view.actions.hidden = !isActive;
|
|
95
|
-
children.push(view.actions);
|
|
96
|
-
}
|
|
97
|
-
if (view.content !== null) {
|
|
98
|
-
view.content.hidden = !isActive;
|
|
99
|
-
children.push(view.content);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
this.setChildren(children);
|
|
103
|
-
}
|
|
104
|
-
getActiveViewId() {
|
|
105
|
-
return this.activeId;
|
|
106
|
-
}
|
|
107
|
-
getViewIds() {
|
|
108
|
-
return this.views.map((v) => v.id);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Observable panel state for the inspector: which tabs exist, which is
|
|
112
|
-
* active, and each tab's absolute hit geometry. Replaces the e2e helper that
|
|
113
|
-
* re-derived tab coordinates from TAB_INDENT/TAB_PAD by hand — a test clicks
|
|
114
|
-
* `tabs[i].centerX` on `tabRow` instead.
|
|
115
|
-
*/
|
|
116
|
-
inspectState() {
|
|
117
|
-
const originX = this.globalPosition.x;
|
|
118
|
-
let x = TAB_INDENT;
|
|
119
|
-
const tabs = this.views.map((view) => {
|
|
120
|
-
const width = view.title.length + TAB_PAD * 2;
|
|
121
|
-
const start = x;
|
|
122
|
-
x += width;
|
|
123
|
-
return {
|
|
124
|
-
id: view.id,
|
|
125
|
-
title: view.title,
|
|
126
|
-
active: view.id === this.activeId,
|
|
127
|
-
x: originX + start,
|
|
128
|
-
width,
|
|
129
|
-
centerX: originX + start + Math.floor(width / 2),
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
return { activeId: this.activeId, tabRow: this.globalPosition.y + TAB_ROW, tabs };
|
|
133
|
-
}
|
|
134
|
-
/** Правая граница таб-строки — за неё контролы вкладки заезжать не должны. */
|
|
135
|
-
tabsEnd() {
|
|
136
|
-
return this.views.reduce((x, view) => x + view.title.length + TAB_PAD * 2, TAB_INDENT);
|
|
137
|
-
}
|
|
138
|
-
activeView() {
|
|
139
|
-
return this.views.find((v) => v.id === this.activeId);
|
|
140
|
-
}
|
|
141
|
-
/** Header tab layout: ` Title ` segments after the indent, with hit ranges. */
|
|
142
|
-
tabSegments() {
|
|
143
|
-
const segments = [];
|
|
144
|
-
let x = TAB_INDENT;
|
|
145
|
-
for (const view of this.views) {
|
|
146
|
-
const width = view.title.length + TAB_PAD * 2;
|
|
147
|
-
segments.push({ id: view.id, start: x, end: x + width });
|
|
148
|
-
x += width;
|
|
149
|
-
}
|
|
150
|
-
return segments;
|
|
151
|
-
}
|
|
152
|
-
performLayout(constraints) {
|
|
153
|
-
const containerSize = super.performLayout(constraints);
|
|
154
|
-
// Контролы вкладки прижаты вправо на строке табов — как в шапке Panel у
|
|
155
|
-
// VS Code. Ширину берём интринсиковую и не даём заехать на сами табы.
|
|
156
|
-
const actions = this.activeView()?.actions;
|
|
157
|
-
if (actions != null) {
|
|
158
|
-
const actionsWidth = Math.min(actions.getMaxIntrinsicWidth(1), containerSize.width);
|
|
159
|
-
const x = Math.max(this.tabsEnd(), containerSize.width - actionsWidth - TAB_INDENT);
|
|
160
|
-
this.layoutChild(actions, x, TAB_ROW, BoxConstraints.tight(new Size(Math.max(0, containerSize.width - x), 1)));
|
|
161
|
-
}
|
|
162
|
-
const content = this.activeView()?.content;
|
|
163
|
-
if (content != null) {
|
|
164
|
-
const contentWidth = Math.max(0, containerSize.width - CONTENT_LEFT);
|
|
165
|
-
const contentHeight = Math.max(0, containerSize.height - CONTENT_TOP);
|
|
166
|
-
this.layoutChild(content, CONTENT_LEFT, CONTENT_TOP, BoxConstraints.tight(new Size(contentWidth, contentHeight)));
|
|
167
|
-
}
|
|
168
|
-
return containerSize;
|
|
169
|
-
}
|
|
170
|
-
render(context) {
|
|
171
|
-
const { width, height } = this.layoutSize;
|
|
172
|
-
// Fill the panel with its background first.
|
|
173
|
-
for (let y = 0; y < height; y++) {
|
|
174
|
-
for (let x = 0; x < width; x++) {
|
|
175
|
-
context.setCell(x, y, { char: " ", bg: this.resolvedStyle.bg });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
// Top border strip (row 0).
|
|
179
|
-
for (let x = 0; x < width; x++) {
|
|
180
|
-
context.setCell(x, 0, { char: "─", fg: this.styleVar("panel.border"), bg: this.resolvedStyle.bg });
|
|
181
|
-
}
|
|
182
|
-
// Tab header (dim). The active tab is underlined — but only under the title
|
|
183
|
-
// glyphs, leaving the surrounding padding un-underlined.
|
|
184
|
-
const segments = this.tabSegments();
|
|
185
|
-
for (let i = 0; i < this.views.length; i++) {
|
|
186
|
-
const view = this.views[i];
|
|
187
|
-
const segment = segments[i];
|
|
188
|
-
const isActive = view.id === this.activeId;
|
|
189
|
-
for (let x = segment.start; x < segment.end && x < width; x++) {
|
|
190
|
-
const textIndex = x - segment.start - TAB_PAD;
|
|
191
|
-
const isGlyph = textIndex >= 0 && textIndex < view.title.length;
|
|
192
|
-
const char = isGlyph ? view.title[textIndex] : " ";
|
|
193
|
-
const style = isActive && isGlyph ? StyleFlags.Underline : StyleFlags.None;
|
|
194
|
-
context.setCell(x, TAB_ROW, {
|
|
195
|
-
char,
|
|
196
|
-
fg: this.styleVar("panelTitle.inactiveForeground"),
|
|
197
|
-
bg: this.resolvedStyle.bg,
|
|
198
|
-
style,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// View-specific controls in the title row (drawn after the tabs so they win
|
|
203
|
-
// the shared row), then the active view's content below — renderChildren
|
|
204
|
-
// рисует только видимых детей, скрытые вкладки пропускаются базой.
|
|
205
|
-
this.renderChildren(context);
|
|
206
|
-
// Placeholder empty-state message, если у активной вкладки нет контента.
|
|
207
|
-
const active = this.activeView();
|
|
208
|
-
if (active?.content == null && active?.placeholder !== undefined && height > CONTENT_TOP) {
|
|
209
|
-
const message = active.placeholder;
|
|
210
|
-
for (let i = 0; i < message.length && i + CONTENT_LEFT < width; i++) {
|
|
211
|
-
context.setCell(i + CONTENT_LEFT, CONTENT_TOP, {
|
|
212
|
-
char: message[i],
|
|
213
|
-
fg: this.styleVar("panelTitle.inactiveForeground"),
|
|
214
|
-
bg: this.resolvedStyle.bg,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|