UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

312 lines 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Tabs = void 0; const Container_1 = require("../Container"); const geometry_1 = require("../geometry"); const Style_1 = require("../Style"); const Text_1 = require("./Text"); const events_1 = require("../events"); const util_1 = require("../util"); // tabs = new Tabs() // tabs.addTab('title', tab) // tabs.addTab(new Text({text: 'title', style: …}), tab) // // tabs.add() class Tabs extends Container_1.Container { static Section; #selectedTab = 0; #separatorLocation; #separatorWidths = []; #border = false; static create(tabs, extraProps = {}) { const tabsView = new Tabs(extraProps); for (const tab of tabs) { if (tab instanceof Section) { tabsView.addTab(tab); } else { const [title, view] = tab; tabsView.addTab(title, view); } } return tabsView; } constructor(props = {}) { super(props); this.#update(props); } get tabs() { return this.children.filter(view => view instanceof Section); } get tabTitles() { return this.children.filter(view => view instanceof TabTitle); } update(props) { super.update(props); this.#update(props); } #update({ border }) { this.#border = border ?? true; } addTab(titleOrTab, child) { let tabView; if (titleOrTab instanceof Section) { tabView = titleOrTab; } else { tabView = Section.create(titleOrTab, child); } this.add(tabView); } add(child, at) { if (child instanceof Section) { child.titleView.onClick = tab => this.#selected(tab); super.add(child.titleView); } super.add(child, at); } removeChild(child) { if (child instanceof Section && child.titleView) { super.removeChild(child.titleView); } super.removeChild(child); } #selected(tab) { const tabTitles = this.tabTitles; const index = tabTitles.indexOf(tab); if (index === -1) { return; } this.#selectedTab = index; } naturalSize(available) { const remainingSize = available.mutableCopy(); const tabTitleSize = this.tabTitles.reduce((size, tab, index) => { const tabSize = tab.naturalSize(remainingSize).mutableCopy(); size.width += tabSize.width; size.height = Math.max(size.height, tabSize.height); remainingSize.width = Math.max(0, remainingSize.width - tabSize.width); return size; }, geometry_1.Size.zero.mutableCopy()); const childSize = geometry_1.Size.zero.mutableCopy(); const availableChildSize = available.shrink(0, tabTitleSize.height); for (const tab of this.tabs) { const tabSize = tab.naturalSize(availableChildSize); childSize.width = Math.max(childSize.width, tabSize.width); childSize.height = Math.max(childSize.height, tabSize.height); } return new geometry_1.Size(Math.max(tabTitleSize.width, childSize.width) + (this.#border ? 3 : 0), tabTitleSize.height + childSize.height + (this.#border ? 1 : 0)); } receiveTick(dt) { if (this.#separatorLocation === undefined || this.#selectedTab >= this.#separatorWidths.length) { return false; } const [start, stop] = this.#separatorWidths.reduce(([start, stop, prev], width, index) => index === this.#selectedTab ? [start, stop + width, 0] : index > this.#selectedTab ? [start, stop, 0] : [start + width, stop + width, width], [0, 0, 0]); const dx = dt / 20; if (start < this.#separatorLocation[0]) { this.#separatorLocation[0] = Math.max(start, this.#separatorLocation[0] - dx); } else if (start > this.#separatorLocation[0]) { this.#separatorLocation[0] = Math.min(start, this.#separatorLocation[0] + dx); } if (stop > this.#separatorLocation[1]) { this.#separatorLocation[1] = Math.min(stop, this.#separatorLocation[1] + dx); } else if (stop < this.#separatorLocation[1]) { this.#separatorLocation[1] = Math.max(stop, this.#separatorLocation[1] - dx); } else { return false; } if (this.#separatorLocation[1] <= this.#separatorLocation[0] + 1) { this.#separatorLocation[1] = Math.min(stop, this.#separatorLocation[0] + 1); } return true; } render(viewport) { viewport.registerTick(); const remainingSize = viewport.contentSize.mutableCopy(); const tabInfo = []; const separatorWidths = []; let x = this.#border ? 2 : 0, tabHeight = 0; this.tabTitles.forEach((tab, index) => { const tabRect = new geometry_1.Rect(new geometry_1.Point(x, 0), tab.naturalSize(remainingSize)); tabInfo.push([tabRect, tab]); remainingSize.width -= tabRect.size.width; if (this.#separatorLocation === undefined && this.#selectedTab === index) { this.#separatorLocation = [x, x + tabRect.size.width]; } x += tabRect.size.width; tabHeight = Math.max(tabHeight, tabRect.size.height - TAB_SEPARATOR_HEIGHT); separatorWidths.push(tabRect.size.width); }); this.#selectedTab = Math.min(separatorWidths.length - 1, this.#selectedTab); this.#separatorWidths = separatorWidths; if (this.#separatorLocation) { this.#renderSeparator(viewport, tabHeight, separatorWidths, this.#separatorLocation); } if (this.#border) { const borderRect = viewport.contentRect.inset(tabHeight + TAB_SEPARATOR_HEIGHT - 1, 0, 0); viewport.clipped(borderRect, inner => this.#renderBorder(inner, this.#separatorWidths)); } tabInfo.forEach(([tabRect, tab]) => { viewport.clipped(tabRect, inner => tab.render(inner)); }); const selectedTab = this.tabs.at(this.#selectedTab); if (selectedTab) { const childRect = viewport.contentRect.inset(tabHeight + TAB_SEPARATOR_HEIGHT, this.#border ? 1 : 0, this.#border ? 1 : 0); viewport.clipped(childRect, inner => { selectedTab.render(inner); }); } } #renderBorder(viewport, separatorWidths) { const totalWidth = separatorWidths.reduce((a, b) => a + b, 2); for (let x = viewport.contentSize.width - 2; x > 0; x--) { if (x === totalWidth) { viewport.write('╶', new geometry_1.Point(x, 0)); } else if (x > totalWidth) { viewport.write('─', new geometry_1.Point(x, 0)); } viewport.write('─', new geometry_1.Point(x, viewport.contentSize.height - 1)); } for (let y = viewport.contentSize.height - 2; y > 0; y--) { viewport.write('│', new geometry_1.Point(0, y)); viewport.write('│', new geometry_1.Point(viewport.contentSize.width - 1, y)); } viewport.write('┌╴', new geometry_1.Point(0, 0)); viewport.write('┐', new geometry_1.Point(viewport.contentSize.width - 1, 0)); viewport.write('└', new geometry_1.Point(0, viewport.contentSize.height - 1)); viewport.write('┘', new geometry_1.Point(viewport.contentSize.width - 1, viewport.contentSize.height - 1)); } #renderSeparator(viewport, tabHeight, separatorWidths, separatorLocation) { // separatorLocation is rounded down in this function let xLeft = this.#border ? 2 : 0, xRight = 0, didDrawSeparator = false; const [separatorStart, separatorStop] = [ ~~separatorLocation[0] + xLeft, ~~separatorLocation[1] + xLeft, ]; viewport.paint(new Style_1.Style({ background: this.theme.ui().background, }), new geometry_1.Rect([separatorStart, 0], [separatorStop - separatorStart, tabHeight + 1])); separatorWidths.forEach((separatorWidth, index) => { const tab = this.tabs.at(index); const isHover = tab?.isHover ?? false; xRight = xLeft + separatorWidth; let underline; if (xLeft >= separatorStart && xLeft <= separatorStop) { const xMid = Math.min(separatorStop, xRight); const u1 = '━'.repeat(xMid - xLeft); const u2 = dashesLeft(xRight - separatorStop, isHover); underline = u1 + u2; didDrawSeparator = false; } else if (xRight >= separatorStart && xLeft < separatorStop) { const xMid = Math.min(separatorStop, xRight); const u0 = dashesRight(separatorStart - xLeft, isHover); const u1 = '━'.repeat(xMid - separatorStart); const u2 = dashesLeft(xRight - separatorStop, isHover); underline = u0 + u1 + u2; didDrawSeparator = xRight > separatorStop; } else if (didDrawSeparator) { underline = dashesLeft(separatorWidth, isHover); didDrawSeparator = false; } else if (xRight === separatorStart) { underline = dashesRight(separatorWidth, isHover); } else { underline = dashes(separatorWidth, isHover); } viewport.write(underline, new geometry_1.Point(xLeft, tabHeight)); xLeft += separatorWidth; }); } } exports.Tabs = Tabs; class TabTitle extends Container_1.Container { #textView; onClick; constructor(title) { super({}); this.#textView = new Text_1.Text({ text: title ?? '', style: this.titleStyle, }); this.add(this.#textView); } get title() { return this.#textView.text; } set title(value) { this.#textView.text = value; } get titleStyle() { return new Style_1.Style({ bold: this.isHover }); } naturalSize(available) { return this.#textView .naturalSize(available) .grow(TAB_TITLE_PAD, TAB_SEPARATOR_HEIGHT); } receiveMouse(event, system) { super.receiveMouse(event, system); if ((0, events_1.isMouseClicked)(event)) { this.onClick?.(this); } this.#textView.style = this.titleStyle; } render(viewport) { viewport.registerMouse(['mouse.button.left', 'mouse.move']); viewport.clipped(new geometry_1.Rect([1, 0], viewport.contentSize.shrink(TAB_TITLE_PAD, 0)), inner => { this.#textView.render(inner); }); } } class Section extends Container_1.Container { titleView = new TabTitle(''); static create(title, child, extraProps = {}) { return new Section({ title, child, ...extraProps }); } constructor({ title, ...props }) { super(props); this.titleView.title = title ?? ''; (0, util_1.define)(this, 'title', { enumerable: true }); } get title() { return this.titleView.title; } set title(value) { this.titleView.title = value; } } function dashesLeft(w, isHover) { if (w <= 0) { return ''; } return (isHover ? '╺' : '╶') + dashes(w - 1, isHover); } function dashesRight(w, isHover) { if (w <= 0) { return ''; } return dashes(w - 1, isHover) + (isHover ? '╸' : '╴'); } function dashes(w, isHover) { if (w <= 0) { return ''; } return (isHover ? '━' : '─').repeat(w); } Tabs.Section = Section; const TAB_TITLE_PAD = 2; const TAB_SEPARATOR_HEIGHT = 1; //# sourceMappingURL=Tabs.js.map