@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.
Files changed (89) hide show
  1. package/dist/body/bodyElement.d.ts +33 -0
  2. package/dist/body/bodyElement.js +87 -0
  3. package/dist/button/buttonElement.d.ts +18 -0
  4. package/dist/button/buttonElement.js +68 -0
  5. package/dist/completionlist/completionDetailsElement.d.ts +49 -0
  6. package/dist/completionlist/completionDetailsElement.js +165 -0
  7. package/dist/completionlist/completionItemKindIcon.d.ts +2 -0
  8. package/dist/completionlist/completionItemKindIcon.js +43 -0
  9. package/dist/completionlist/completionListElement.d.ts +102 -0
  10. package/dist/completionlist/completionListElement.js +309 -0
  11. package/dist/completionlist/completionWidgetElement.d.ts +41 -0
  12. package/dist/completionlist/completionWidgetElement.js +102 -0
  13. package/dist/contextview/contextMenuController.d.ts +23 -0
  14. package/dist/contextview/contextMenuController.js +75 -0
  15. package/dist/contextview/overlayHostElement.d.ts +23 -0
  16. package/dist/contextview/overlayHostElement.js +47 -0
  17. package/dist/editorgroup/editorTabItemElement.d.ts +41 -0
  18. package/dist/editorgroup/editorTabItemElement.js +186 -0
  19. package/dist/editorgroup/editorTabStripElement.d.ts +33 -0
  20. package/dist/editorgroup/editorTabStripElement.js +118 -0
  21. package/dist/editorpart/editorPartElement.d.ts +95 -0
  22. package/dist/editorpart/editorPartElement.js +285 -0
  23. package/dist/inputbox/inputElement.d.ts +34 -0
  24. package/dist/inputbox/inputElement.js +207 -0
  25. package/dist/inputbox/inputState.d.ts +69 -0
  26. package/dist/inputbox/inputState.js +326 -0
  27. package/dist/layout/boxContainerElement.d.ts +27 -0
  28. package/dist/layout/boxContainerElement.js +121 -0
  29. package/dist/layout/boxElement.d.ts +4 -0
  30. package/dist/layout/boxElement.js +30 -0
  31. package/dist/layout/fillerElement.d.ts +12 -0
  32. package/dist/layout/fillerElement.js +16 -0
  33. package/dist/layout/fitContentElement.d.ts +18 -0
  34. package/dist/layout/fitContentElement.js +46 -0
  35. package/dist/layout/hFlexElement.d.ts +26 -0
  36. package/dist/layout/hFlexElement.js +134 -0
  37. package/dist/layout/paddingContainerElement.d.ts +30 -0
  38. package/dist/layout/paddingContainerElement.js +88 -0
  39. package/dist/layout/sizedBoxElement.d.ts +30 -0
  40. package/dist/layout/sizedBoxElement.js +74 -0
  41. package/dist/layout/vFlexElement.d.ts +32 -0
  42. package/dist/layout/vFlexElement.js +136 -0
  43. package/dist/layout/vStackElement.d.ts +18 -0
  44. package/dist/layout/vStackElement.js +74 -0
  45. package/dist/list/listViewElement.d.ts +232 -0
  46. package/dist/list/listViewElement.js +944 -0
  47. package/dist/menu/menuBarElement.d.ts +47 -0
  48. package/dist/menu/menuBarElement.js +285 -0
  49. package/dist/menu/menuBarItemElement.d.ts +20 -0
  50. package/dist/menu/menuBarItemElement.js +68 -0
  51. package/dist/menu/popupMenuElement.d.ts +81 -0
  52. package/dist/menu/popupMenuElement.js +318 -0
  53. package/dist/menu/popupMenuItemElement.d.ts +24 -0
  54. package/dist/menu/popupMenuItemElement.js +89 -0
  55. package/dist/panel/panelContainerElement.d.ts +64 -0
  56. package/dist/panel/panelContainerElement.js +219 -0
  57. package/dist/quickpick/quickPickElement.d.ts +161 -0
  58. package/dist/quickpick/quickPickElement.js +561 -0
  59. package/dist/sash/sashElement.d.ts +30 -0
  60. package/dist/sash/sashElement.js +78 -0
  61. package/dist/scrollbar/iScrollable.d.ts +8 -0
  62. package/dist/scrollbar/iScrollable.js +1 -0
  63. package/dist/scrollbar/scrollBarRenderer.d.ts +36 -0
  64. package/dist/scrollbar/scrollBarRenderer.js +76 -0
  65. package/dist/scrollbar/scrollContainerElement.d.ts +39 -0
  66. package/dist/scrollbar/scrollContainerElement.js +91 -0
  67. package/dist/scrollbar/scrollViewport.d.ts +28 -0
  68. package/dist/scrollbar/scrollViewport.js +65 -0
  69. package/dist/scrollbar/scrollableElement.d.ts +22 -0
  70. package/dist/scrollbar/scrollableElement.js +42 -0
  71. package/dist/selectbox/selectBoxElement.d.ts +48 -0
  72. package/dist/selectbox/selectBoxElement.js +161 -0
  73. package/dist/terminal/encodeKeyForPty.d.ts +3 -0
  74. package/dist/terminal/encodeKeyForPty.js +56 -0
  75. package/dist/terminal/terminalViewElement.d.ts +17 -0
  76. package/dist/terminal/terminalViewElement.js +172 -0
  77. package/dist/text/textBlockElement.d.ts +13 -0
  78. package/dist/text/textBlockElement.js +35 -0
  79. package/dist/text/textLabelElement.d.ts +28 -0
  80. package/dist/text/textLabelElement.js +77 -0
  81. package/dist/titledpanel/titledPanelElement.d.ts +14 -0
  82. package/dist/titledpanel/titledPanelElement.js +40 -0
  83. package/dist/tree/iTreeDataProvider.d.ts +19 -0
  84. package/dist/tree/iTreeDataProvider.js +1 -0
  85. package/dist/tree/treeViewElement.d.ts +116 -0
  86. package/dist/tree/treeViewElement.js +748 -0
  87. package/dist/workbenchlayout/workbenchLayoutElement.d.ts +60 -0
  88. package/dist/workbenchlayout/workbenchLayoutElement.js +203 -0
  89. package/package.json +33 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import type { RenderContext } from "@tuidom/core/dom/tuiElement";
2
+ /**
3
+ * Resolved scrollbar colours. `background` fills the half of each cell the bar
4
+ * itself does not paint — the scrollbar lives on a dedicated row/column that the
5
+ * child never draws into, so without it those cells keep the terminal's default
6
+ * background and the bar looks like a hole punched through the widget.
7
+ */
8
+ export interface ScrollBarColors {
9
+ readonly thumb: number;
10
+ readonly track: number;
11
+ readonly background: number;
12
+ }
13
+ export interface ScrollBarMetrics {
14
+ /** Thumb start position in half-cell units (0 = top of track). */
15
+ thumbStartHalves: number;
16
+ /** Thumb size in half-cell units (minimum 2 = 1 full cell). */
17
+ thumbSizeHalves: number;
18
+ }
19
+ export declare function computeScrollBarMetrics(trackHeight: number, contentHeight: number, scrollTop: number, viewportHeight: number): ScrollBarMetrics;
20
+ /**
21
+ * For each cell row, determine the character to display.
22
+ *
23
+ * Each cell covers 2 halves: top half and bottom half.
24
+ * - Both halves inside thumb → "█"
25
+ * - Only top half inside thumb → "▀"
26
+ * - Only bottom half inside thumb → "▄"
27
+ * - Neither half inside thumb → " "
28
+ */
29
+ export declare function getScrollBarCellChars(trackHeight: number, metrics: ScrollBarMetrics): string[];
30
+ export declare function renderScrollBar(context: RenderContext, x: number, trackHeight: number, contentHeight: number, scrollTop: number, viewportHeight: number, colors: ScrollBarColors): void;
31
+ /**
32
+ * Horizontal bar on its own row. Uses "▄" (LOWER half block) so the bar sits on
33
+ * the bottom edge of the row, hugging the widget's frame rather than floating a
34
+ * half-cell above it.
35
+ */
36
+ export declare function renderHorizontalScrollBar(context: RenderContext, y: number, trackWidth: number, contentWidth: number, scrollLeft: number, viewportWidth: number, colors: ScrollBarColors): void;
@@ -0,0 +1,76 @@
1
+ export function computeScrollBarMetrics(trackHeight, contentHeight, scrollTop, viewportHeight) {
2
+ const trackHalves = trackHeight * 2;
3
+ if (contentHeight <= viewportHeight) {
4
+ return { thumbStartHalves: 0, thumbSizeHalves: trackHalves };
5
+ }
6
+ const thumbSizeHalves = Math.max(2, Math.round((viewportHeight / contentHeight) * trackHalves));
7
+ const maxScroll = contentHeight - viewportHeight;
8
+ const scrollFraction = Math.min(1, Math.max(0, scrollTop / maxScroll));
9
+ const thumbStartHalves = Math.round(scrollFraction * (trackHalves - thumbSizeHalves));
10
+ return { thumbStartHalves, thumbSizeHalves };
11
+ }
12
+ /**
13
+ * For each cell row, determine the character to display.
14
+ *
15
+ * Each cell covers 2 halves: top half and bottom half.
16
+ * - Both halves inside thumb → "█"
17
+ * - Only top half inside thumb → "▀"
18
+ * - Only bottom half inside thumb → "▄"
19
+ * - Neither half inside thumb → " "
20
+ */
21
+ export function getScrollBarCellChars(trackHeight, metrics) {
22
+ const { thumbStartHalves, thumbSizeHalves } = metrics;
23
+ const thumbEndHalves = thumbStartHalves + thumbSizeHalves;
24
+ const result = [];
25
+ for (let row = 0; row < trackHeight; row++) {
26
+ const topHalf = row * 2;
27
+ const bottomHalf = topHalf + 1;
28
+ const topInThumb = topHalf >= thumbStartHalves && topHalf < thumbEndHalves;
29
+ const bottomInThumb = bottomHalf >= thumbStartHalves && bottomHalf < thumbEndHalves;
30
+ if (topInThumb && bottomInThumb) {
31
+ result.push("█");
32
+ }
33
+ else if (topInThumb) {
34
+ result.push("▀");
35
+ }
36
+ else if (bottomInThumb) {
37
+ result.push("▄");
38
+ }
39
+ else {
40
+ result.push("░");
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+ export function renderScrollBar(context, x, trackHeight, contentHeight, scrollTop, viewportHeight, colors) {
46
+ const metrics = computeScrollBarMetrics(trackHeight, contentHeight, scrollTop, viewportHeight);
47
+ const chars = getScrollBarCellChars(trackHeight, metrics);
48
+ for (let row = 0; row < trackHeight; row++) {
49
+ const char = chars[row];
50
+ const fg = char === "░" ? colors.track : colors.thumb;
51
+ context.setCell(x, row, { char, fg, bg: colors.background });
52
+ }
53
+ }
54
+ /**
55
+ * Horizontal bar on its own row. Uses "▄" (LOWER half block) so the bar sits on
56
+ * the bottom edge of the row, hugging the widget's frame rather than floating a
57
+ * half-cell above it.
58
+ */
59
+ export function renderHorizontalScrollBar(context, y, trackWidth, contentWidth, scrollLeft, viewportWidth, colors) {
60
+ if (contentWidth <= viewportWidth) {
61
+ // Content fits — only reachable under policy "always"; the thumb spans the track.
62
+ for (let col = 0; col < trackWidth; col++) {
63
+ context.setCell(col, y, { char: "▄", fg: colors.thumb, bg: colors.background });
64
+ }
65
+ return;
66
+ }
67
+ const thumbSize = Math.max(1, Math.round((viewportWidth / contentWidth) * trackWidth));
68
+ const maxScroll = contentWidth - viewportWidth;
69
+ const scrollFraction = Math.min(1, Math.max(0, scrollLeft / maxScroll));
70
+ const thumbStart = Math.round(scrollFraction * (trackWidth - thumbSize));
71
+ const thumbEnd = thumbStart + thumbSize;
72
+ for (let col = 0; col < trackWidth; col++) {
73
+ const inThumb = col >= thumbStart && col < thumbEnd;
74
+ context.setCell(col, y, { char: "▄", fg: inThumb ? colors.thumb : colors.track, bg: colors.background });
75
+ }
76
+ }
@@ -0,0 +1,39 @@
1
+ import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
3
+ import type { IScrollable } from "./iScrollable.js";
4
+ export type ScrollBarPolicy = "auto" | "always" | "never";
5
+ /**
6
+ * Draws scrollbars alongside a scrollable child.
7
+ *
8
+ * The child is rendered as-is — it must handle its own content rendering.
9
+ * For simple widgets that draw full content in local coordinates, wrap
10
+ * them in a ScrollViewport before passing to ScrollBarDecorator:
11
+ *
12
+ * new ScrollBarDecorator(new ScrollViewport(textBlock))
13
+ *
14
+ * For self-scrolling widgets (e.g. EditorElement) that already manage
15
+ * their own scroll offset, pass them directly:
16
+ *
17
+ * new ScrollBarDecorator(editorElement)
18
+ */
19
+ export declare class ScrollBarDecorator extends TUIElement {
20
+ private child;
21
+ verticalScrollBar: ScrollBarPolicy;
22
+ horizontalScrollBar: ScrollBarPolicy;
23
+ constructor(child: TUIElement & IScrollable);
24
+ getChild(): TUIElement & IScrollable;
25
+ setChild(child: TUIElement & IScrollable): void;
26
+ /**
27
+ * Бегунок — хром декоратора, рисуемый по состоянию РЕБЁНКА (scrollTop /
28
+ * contentHeight) в колонке вне его rect'а. Damage ребёнка обязан повреждать
29
+ * и колонку скроллбара — атомарность даёт это автоматически ценой +1
30
+ * строки/колонки к области.
31
+ */
32
+ protected get paintsSubtreeAtomically(): boolean;
33
+ protected performLayout(constraints: BoxConstraints): Size;
34
+ render(context: RenderContext): void;
35
+ private resolveScrollBarVisibility;
36
+ private resolvePolicy;
37
+ }
38
+ /** @deprecated Use ScrollBarDecorator instead */
39
+ export declare const ScrollContainerElement: typeof ScrollBarDecorator;
@@ -0,0 +1,91 @@
1
+ import { BoxConstraints, Offset, Point, Rect, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
3
+ import { renderHorizontalScrollBar, renderScrollBar } from "./scrollBarRenderer.js";
4
+ /**
5
+ * Draws scrollbars alongside a scrollable child.
6
+ *
7
+ * The child is rendered as-is — it must handle its own content rendering.
8
+ * For simple widgets that draw full content in local coordinates, wrap
9
+ * them in a ScrollViewport before passing to ScrollBarDecorator:
10
+ *
11
+ * new ScrollBarDecorator(new ScrollViewport(textBlock))
12
+ *
13
+ * For self-scrolling widgets (e.g. EditorElement) that already manage
14
+ * their own scroll offset, pass them directly:
15
+ *
16
+ * new ScrollBarDecorator(editorElement)
17
+ */
18
+ export class ScrollBarDecorator extends TUIElement {
19
+ child;
20
+ verticalScrollBar = "auto";
21
+ horizontalScrollBar = "auto";
22
+ constructor(child) {
23
+ super();
24
+ this.child = child;
25
+ this.appendChild(this.child);
26
+ }
27
+ getChild() {
28
+ return this.child;
29
+ }
30
+ setChild(child) {
31
+ // setChildren, а не appendChild: старый ребёнок обязан отцепиться. До
32
+ // рефакторинга владения он молча оставался с parent=декоратор навсегда.
33
+ this.child = child;
34
+ this.setChildren([this.child]);
35
+ this.markDirty();
36
+ }
37
+ /**
38
+ * Бегунок — хром декоратора, рисуемый по состоянию РЕБЁНКА (scrollTop /
39
+ * contentHeight) в колонке вне его rect'а. Damage ребёнка обязан повреждать
40
+ * и колонку скроллбара — атомарность даёт это автоматически ценой +1
41
+ * строки/колонки к области.
42
+ */
43
+ get paintsSubtreeAtomically() {
44
+ return true;
45
+ }
46
+ performLayout(constraints) {
47
+ const containerSize = super.performLayout(constraints);
48
+ const { showVertical, showHorizontal } = this.resolveScrollBarVisibility(containerSize);
49
+ const childWidth = containerSize.width - (showVertical ? 1 : 0);
50
+ const childHeight = containerSize.height - (showHorizontal ? 1 : 0);
51
+ this.layoutChild(this.child, 0, 0, BoxConstraints.tight(new Size(childWidth, childHeight)));
52
+ return containerSize;
53
+ }
54
+ render(context) {
55
+ const { showVertical, showHorizontal } = this.resolveScrollBarVisibility(this.layoutSize);
56
+ this.renderChildren(context);
57
+ const childWidth = this.child.layoutSize.width;
58
+ const childHeight = this.child.layoutSize.height;
59
+ // Токены темы + фон колонки от КАСКАДА: декоратор внутри панели/сайдбара
60
+ // наследует фон хозяина — бывшая параметризация getScrollBarStyles(theme,
61
+ // backgroundKey) стала свойством дерева.
62
+ const colors = {
63
+ thumb: this.styleVar("scrollbarSlider.background"),
64
+ track: this.styleVar("scrollbar.background"),
65
+ background: this.resolvedStyle.bg,
66
+ };
67
+ if (showVertical) {
68
+ renderScrollBar(context, this.layoutSize.width - 1, childHeight, this.child.contentHeight, this.child.scrollTop, childHeight, colors);
69
+ }
70
+ if (showHorizontal) {
71
+ renderHorizontalScrollBar(context, this.layoutSize.height - 1, childWidth, this.child.contentWidth, this.child.scrollLeft, childWidth, colors);
72
+ }
73
+ }
74
+ resolveScrollBarVisibility(containerSize) {
75
+ const showVertical = this.resolvePolicy(this.verticalScrollBar, this.child.contentHeight, containerSize.height);
76
+ const showHorizontal = this.resolvePolicy(this.horizontalScrollBar, this.child.contentWidth, containerSize.width - (showVertical ? 1 : 0));
77
+ return { showVertical, showHorizontal };
78
+ }
79
+ resolvePolicy(policy, contentSize, viewportSize) {
80
+ switch (policy) {
81
+ case "always":
82
+ return true;
83
+ case "never":
84
+ return false;
85
+ case "auto":
86
+ return contentSize > viewportSize;
87
+ }
88
+ }
89
+ }
90
+ /** @deprecated Use ScrollBarDecorator instead */
91
+ export const ScrollContainerElement = ScrollBarDecorator;
@@ -0,0 +1,28 @@
1
+ import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
3
+ import type { IContentSized, IScrollable } from "./iScrollable.js";
4
+ /**
5
+ * Scroll engine: wraps any content-sized child, owns the scroll state, and
6
+ * clips rendering to the viewport bounds. The child draws full content in
7
+ * local coordinates; ScrollViewport shifts by -scrollTop/-scrollLeft and
8
+ * clips so only the visible region produces cells on screen.
9
+ *
10
+ * The child only needs to report contentHeight/contentWidth (IContentSized).
11
+ * ScrollViewport itself implements IScrollable, so it can be nested inside
12
+ * ScrollBarDecorator or any other consumer of IScrollable.
13
+ */
14
+ export declare class ScrollViewport extends TUIElement implements IScrollable {
15
+ private child;
16
+ scrollTop: number;
17
+ scrollLeft: number;
18
+ constructor(child: TUIElement & IContentSized);
19
+ get contentHeight(): number;
20
+ get contentWidth(): number;
21
+ getMaxIntrinsicWidth(_height: number): number;
22
+ getMaxIntrinsicHeight(_width: number): number;
23
+ scrollBy(dx: number, dy: number): void;
24
+ scrollTo(left: number, top: number): void;
25
+ getChild(): TUIElement & IContentSized;
26
+ protected performLayout(constraints: BoxConstraints): Size;
27
+ render(context: RenderContext): void;
28
+ }
@@ -0,0 +1,65 @@
1
+ import { BoxConstraints, Offset, Rect, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
3
+ /**
4
+ * Scroll engine: wraps any content-sized child, owns the scroll state, and
5
+ * clips rendering to the viewport bounds. The child draws full content in
6
+ * local coordinates; ScrollViewport shifts by -scrollTop/-scrollLeft and
7
+ * clips so only the visible region produces cells on screen.
8
+ *
9
+ * The child only needs to report contentHeight/contentWidth (IContentSized).
10
+ * ScrollViewport itself implements IScrollable, so it can be nested inside
11
+ * ScrollBarDecorator or any other consumer of IScrollable.
12
+ */
13
+ export class ScrollViewport extends TUIElement {
14
+ child;
15
+ scrollTop = 0;
16
+ scrollLeft = 0;
17
+ constructor(child) {
18
+ super();
19
+ this.child = child;
20
+ this.appendChild(this.child);
21
+ }
22
+ get contentHeight() {
23
+ return this.child.contentHeight;
24
+ }
25
+ get contentWidth() {
26
+ return this.child.contentWidth;
27
+ }
28
+ getMaxIntrinsicWidth(_height) {
29
+ return this.child.contentWidth;
30
+ }
31
+ getMaxIntrinsicHeight(_width) {
32
+ return this.child.contentHeight;
33
+ }
34
+ scrollBy(dx, dy) {
35
+ this.scrollTo(this.scrollLeft + dx, this.scrollTop + dy);
36
+ }
37
+ scrollTo(left, top) {
38
+ const maxScrollTop = Math.max(0, this.contentHeight - this.layoutSize.height);
39
+ const maxScrollLeft = Math.max(0, this.contentWidth - this.layoutSize.width);
40
+ const top2 = Math.max(0, Math.min(maxScrollTop, top));
41
+ const left2 = Math.max(0, Math.min(maxScrollLeft, left));
42
+ if (top2 === this.scrollTop && left2 === this.scrollLeft)
43
+ return;
44
+ this.scrollTop = top2;
45
+ this.scrollLeft = left2;
46
+ // Симметрия со ScrollableElement.scrollTo: смена скролла — dirty-кадр.
47
+ this.markDirty();
48
+ }
49
+ getChild() {
50
+ return this.child;
51
+ }
52
+ performLayout(constraints) {
53
+ const size = super.performLayout(constraints);
54
+ // Give child the full viewport size — it draws all content in local coords
55
+ this.layoutChild(this.child, 0, 0, BoxConstraints.tight(size));
56
+ return size;
57
+ }
58
+ render(context) {
59
+ // Клип — собственный rect вьюпорта: он не может опустеть — пустой
60
+ // контекст отсёк бы уже прунинг renderChildren родителя.
61
+ const scrollOffset = new Offset(-this.scrollLeft, -this.scrollTop);
62
+ const viewportClip = new Rect(this.globalPosition, this.layoutSize);
63
+ this.child.render(context.withOffset(scrollOffset).withClip(viewportClip));
64
+ }
65
+ }
@@ -0,0 +1,22 @@
1
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
2
+ import type { IScrollable } from "./iScrollable.js";
3
+ export interface ScrollViewportInfo {
4
+ readonly scrollTop: number;
5
+ readonly scrollLeft: number;
6
+ readonly viewportWidth: number;
7
+ readonly viewportHeight: number;
8
+ }
9
+ export declare abstract class ScrollableElement extends TUIElement implements IScrollable {
10
+ scrollTop: number;
11
+ scrollLeft: number;
12
+ abstract get contentHeight(): number;
13
+ abstract get contentWidth(): number;
14
+ getMinIntrinsicWidth(_height: number): number;
15
+ getMaxIntrinsicWidth(_height: number): number;
16
+ getMinIntrinsicHeight(_width: number): number;
17
+ getMaxIntrinsicHeight(_width: number): number;
18
+ scrollBy(dx: number, dy: number): void;
19
+ scrollTo(left: number, top: number): void;
20
+ render(context: RenderContext): void;
21
+ protected abstract renderViewport(context: RenderContext, viewport: ScrollViewportInfo): void;
22
+ }
@@ -0,0 +1,42 @@
1
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
2
+ export class ScrollableElement extends TUIElement {
3
+ scrollTop = 0;
4
+ scrollLeft = 0;
5
+ getMinIntrinsicWidth(_height) {
6
+ return 0;
7
+ }
8
+ getMaxIntrinsicWidth(_height) {
9
+ return this.contentWidth;
10
+ }
11
+ getMinIntrinsicHeight(_width) {
12
+ return 0;
13
+ }
14
+ getMaxIntrinsicHeight(_width) {
15
+ return this.contentHeight;
16
+ }
17
+ scrollBy(dx, dy) {
18
+ this.scrollTo(this.scrollLeft + dx, this.scrollTop + dy);
19
+ }
20
+ scrollTo(left, top) {
21
+ const maxScrollTop = Math.max(0, this.contentHeight - this.layoutSize.height);
22
+ const maxScrollLeft = Math.max(0, this.contentWidth - this.layoutSize.width);
23
+ const top2 = Math.max(0, Math.min(maxScrollTop, top));
24
+ const left2 = Math.max(0, Math.min(maxScrollLeft, left));
25
+ if (top2 === this.scrollTop && left2 === this.scrollLeft)
26
+ return;
27
+ this.scrollTop = top2;
28
+ this.scrollLeft = left2;
29
+ // Скролл меняет картинку — сам помечает кадр грязным, не полагаясь на
30
+ // markDirty у вызывающих (кадр ввода рендерится только по dirty-флагу).
31
+ this.markDirty();
32
+ }
33
+ render(context) {
34
+ const viewport = {
35
+ scrollTop: this.scrollTop,
36
+ scrollLeft: this.scrollLeft,
37
+ viewportWidth: this.layoutSize.width,
38
+ viewportHeight: this.layoutSize.height,
39
+ };
40
+ this.renderViewport(context, viewport);
41
+ }
42
+ }
@@ -0,0 +1,48 @@
1
+ import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import type { TUIEventBase } from "@tuidom/core/dom/events/tuiEventBase";
3
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
4
+ /** Опция списка — форма зеркалит `ISelectOptionItem` из vscode. */
5
+ export interface ISelectOptionItem {
6
+ readonly text: string;
7
+ /** Разделитель: не выбирается, в списке рисуется чертой. */
8
+ readonly isSeparator?: boolean;
9
+ }
10
+ /** Результат выбора — зеркалит `ISelectData` из vscode. */
11
+ export interface ISelectData {
12
+ readonly selected: string;
13
+ readonly index: number;
14
+ }
15
+ /**
16
+ * Выпадающий список (аналог `SelectBox` из `base/browser/ui/selectBox/`).
17
+ * Закрытое состояние — одна строка `текст ⌄`; раскрытие строит
18
+ * {@link PopupMenuElement} и вешает его в overlay-слой корневой view — тем же
19
+ * путём, что контекст-меню Explorer'а, поэтому клик мимо и Escape закрывают
20
+ * список, а фокус возвращается сюда.
21
+ */
22
+ export declare class SelectBoxElement extends TUIElement {
23
+ private options;
24
+ private selectedIndex;
25
+ private session;
26
+ /** Выбор пользователя. Программный {@link select} события не порождает. */
27
+ onDidSelect?: (data: ISelectData) => void;
28
+ constructor();
29
+ setOptions(options: readonly ISelectOptionItem[], selected?: number): void;
30
+ /** Программно выставить выбор — без `onDidSelect`, как `select()` в vscode. */
31
+ select(index: number): void;
32
+ getSelected(): number;
33
+ /** Observable state: option texts, selected index/text, and open/closed. */
34
+ inspectState(): Record<string, unknown>;
35
+ isOpen(): boolean;
36
+ /** Текст закрытого состояния: выбранная опция (или пусто, если выбора нет). */
37
+ private selectedText;
38
+ private contentWidth;
39
+ getMinIntrinsicWidth(_height: number): number;
40
+ getMaxIntrinsicWidth(_height: number): number;
41
+ getMinIntrinsicHeight(_width: number): number;
42
+ getMaxIntrinsicHeight(_width: number): number;
43
+ protected performLayout(constraints: BoxConstraints): Size;
44
+ render(context: RenderContext): void;
45
+ protected performDefaultAction(event: TUIEventBase): void;
46
+ private toggleOpen;
47
+ private open;
48
+ }
@@ -0,0 +1,161 @@
1
+ import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
2
+ import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
3
+ import { PopupMenuElement } from "../menu/popupMenuElement.js";
4
+ /** Шеврон закрытого состояния — тот же глиф, что у `<select>` в VS Code. */
5
+ const CHEVRON = "⌄"; // ⌄ DOWN ARROWHEAD
6
+ /** Отметка выбранного пункта в раскрытом списке (VS Code рисует галочку). */
7
+ const CHECK = "✓"; // ✓
8
+ /**
9
+ * Выпадающий список (аналог `SelectBox` из `base/browser/ui/selectBox/`).
10
+ * Закрытое состояние — одна строка `текст ⌄`; раскрытие строит
11
+ * {@link PopupMenuElement} и вешает его в overlay-слой корневой view — тем же
12
+ * путём, что контекст-меню Explorer'а, поэтому клик мимо и Escape закрывают
13
+ * список, а фокус возвращается сюда.
14
+ */
15
+ export class SelectBoxElement extends TUIElement {
16
+ options = [];
17
+ selectedIndex = -1;
18
+ session = null;
19
+ /** Выбор пользователя. Программный {@link select} события не порождает. */
20
+ onDidSelect;
21
+ constructor() {
22
+ super();
23
+ this.style = { fg: "dropdown.foreground", bg: "dropdown.background" };
24
+ this.focusable = true;
25
+ this.addEventListener("mousedown", (event) => {
26
+ if (event.button !== "left")
27
+ return;
28
+ this.toggleOpen();
29
+ });
30
+ }
31
+ setOptions(options, selected) {
32
+ this.options = options;
33
+ if (selected !== undefined)
34
+ this.selectedIndex = selected;
35
+ // Выбранный индекс мог указывать за пределы нового списка (канал исчез).
36
+ if (this.selectedIndex >= options.length)
37
+ this.selectedIndex = options.length - 1;
38
+ this.markDirty();
39
+ }
40
+ /** Программно выставить выбор — без `onDidSelect`, как `select()` в vscode. */
41
+ select(index) {
42
+ if (index < 0 || index >= this.options.length)
43
+ return;
44
+ this.selectedIndex = index;
45
+ this.markDirty();
46
+ }
47
+ getSelected() {
48
+ return this.selectedIndex;
49
+ }
50
+ /** Observable state: option texts, selected index/text, and open/closed. */
51
+ inspectState() {
52
+ return {
53
+ open: this.isOpen(),
54
+ selectedIndex: this.selectedIndex,
55
+ selectedText: this.selectedText(),
56
+ options: this.options.map((o) => o.text),
57
+ };
58
+ }
59
+ isOpen() {
60
+ return this.session?.isOpen() ?? false;
61
+ }
62
+ /** Текст закрытого состояния: выбранная опция (или пусто, если выбора нет). */
63
+ selectedText() {
64
+ const option = this.selectedIndex >= 0 ? this.options[this.selectedIndex] : undefined;
65
+ return option?.text ?? "";
66
+ }
67
+ contentWidth() {
68
+ // Ширина не прыгает при смене выбора: держим её по самой длинной опции.
69
+ const longest = this.options.reduce((max, o) => Math.max(max, o.text.length), 0);
70
+ return Math.max(longest, this.selectedText().length) + CHEVRON.length + 1;
71
+ }
72
+ getMinIntrinsicWidth(_height) {
73
+ return this.contentWidth();
74
+ }
75
+ getMaxIntrinsicWidth(_height) {
76
+ return this.contentWidth();
77
+ }
78
+ getMinIntrinsicHeight(_width) {
79
+ return 1;
80
+ }
81
+ getMaxIntrinsicHeight(_width) {
82
+ return 1;
83
+ }
84
+ performLayout(constraints) {
85
+ const natural = new Size(this.getMaxIntrinsicWidth(1), this.getMaxIntrinsicHeight(0));
86
+ return super.performLayout(BoxConstraints.tight(constraints.constrain(natural)));
87
+ }
88
+ render(context) {
89
+ const width = this.layoutSize.width;
90
+ const { fg, bg } = this.resolvedStyle;
91
+ const text = this.selectedText();
92
+ for (let x = 0; x < width; x++) {
93
+ context.setCell(x, 0, { char: " ", fg, bg });
94
+ }
95
+ for (let i = 0; i < text.length && i < width; i++) {
96
+ context.setCell(i, 0, { char: text[i], fg, bg });
97
+ }
98
+ // Шеврон прижат вправо — так же, как стрелка нативного `<select>`.
99
+ if (width > 0) {
100
+ context.setCell(width - 1, 0, { char: CHEVRON, fg: this.styleVar("dropdown.border"), bg });
101
+ }
102
+ }
103
+ performDefaultAction(event) {
104
+ if (event.type === "keydown") {
105
+ const key = event.key;
106
+ if (key === "Enter" || key === " " || key === "ArrowDown") {
107
+ this.toggleOpen();
108
+ return;
109
+ }
110
+ }
111
+ super.performDefaultAction(event);
112
+ }
113
+ toggleOpen() {
114
+ if (this.isOpen()) {
115
+ this.session?.close();
116
+ return;
117
+ }
118
+ this.open();
119
+ }
120
+ open() {
121
+ const layer = this.getOverlayLayer();
122
+ if (layer === null || this.options.length === 0)
123
+ return;
124
+ const entries = this.options.map((option, index) => {
125
+ if (option.isSeparator === true)
126
+ return { type: "separator" };
127
+ return {
128
+ label: option.text,
129
+ // Галочка у активного пункта — то же, что `toggled` в меню VS Code.
130
+ icon: index === this.selectedIndex ? CHECK : " ",
131
+ onSelect: () => {
132
+ this.session?.close();
133
+ this.selectedIndex = index;
134
+ this.markDirty();
135
+ this.onDidSelect?.({ selected: option.text, index });
136
+ },
137
+ };
138
+ });
139
+ const menu = new PopupMenuElement(entries);
140
+ // Фон раскрытого списка — dropdown.listBackground; пункты наследуют
141
+ // (их обычное состояние объявлено сентинелами, не своим bg).
142
+ menu.style = { fg: "menu.foreground", bg: "dropdown.listBackground" };
143
+ menu.focusable = true;
144
+ // Список раскрывается ПОД контролом и прижимается к его левому краю —
145
+ // как раскрывается `<select>`.
146
+ this.session = layer.openPopupSession(menu, { screenX: this.globalPosition.x, screenY: this.globalPosition.y }, {
147
+ visible: true,
148
+ restoreFocus: true,
149
+ focusOnOpen: true,
150
+ closeOnEscape: true,
151
+ pointerPolicy: "close-on-outside",
152
+ disposeOnClose: true,
153
+ onClose: () => {
154
+ this.session = null;
155
+ },
156
+ });
157
+ menu.onClose = () => {
158
+ this.session?.close();
159
+ };
160
+ }
161
+ }
@@ -0,0 +1,3 @@
1
+ import type { TUIKeyboardEvent } from "@tuidom/core/dom/events/tuiKeyboardEvent";
2
+ /** Вернуть байты для PTY, либо "" если клавишу не транслируем. */
3
+ export declare function encodeKeyForPty(event: TUIKeyboardEvent): string;