UNPKG

@teaui/core

Version:

A high-level terminal UI library for Node

298 lines 10.3 kB
import * as unicode from '@teaui/term'; import { Style } from '../Style.js'; import { Container } from '../Container.js'; import { Rect, Point, Size } from '../geometry.js'; import { isMouseClicked, isMouseEnter, isMouseExit, isMouseMove, } from '../events/index.js'; export class ToggleGroup extends Container { #multiple = false; #padding = 1; #offAxisPadding = 0; #direction = 'horizontal'; #titles = []; #titlesCache = []; #sizeCache = Size.zero; #selected = new Set(); #hover; #onChange; constructor(props) { super(props); this.#update(props); } update(props) { super.update(props); this.#update(props); } get titles() { return this.#titles; } set titles(value) { this.#titles = value; this.#updateTitles(value); this.invalidateSize(); } #update({ multiple, padding, direction, titles, selected, onChange }) { this.#multiple = multiple ?? false; this.#padding = Math.max(0, padding ?? 1); this.#offAxisPadding = Math.max(0, this.#padding - 1); this.#direction = direction ?? 'horizontal'; this.#selected = new Set(selected); this.#onChange = onChange; this.#updateTitles(titles); } #updateTitles(titles) { if (titles.length == 0) { this.#titlesCache = []; return; } const sizeCache = Size.zero.mutableCopy(); this.#titlesCache = titles.map(title => { const size = unicode.stringSize(title); if (this.#direction === 'horizontal') { const textWidth = size.width + 2 * this.#padding; sizeCache.width += BORDER.size + textWidth; sizeCache.height = Math.max(sizeCache.height, size.height); } else { const textHeight = size.height + 2 * this.#padding; sizeCache.width = Math.max(sizeCache.width, size.width); sizeCache.height += BORDER.size + textHeight; } return [title, size]; }); if (this.#direction === 'horizontal') { sizeCache.width += BORDER.size; sizeCache.height += BORDER.size * 2 + 2 * this.#offAxisPadding; } else { sizeCache.width += BORDER.size * 2 + 2 * this.#offAxisPadding; sizeCache.height += BORDER.size; } this.#sizeCache = sizeCache; } naturalSize(_available) { return this.#sizeCache; } receiveMouse(event, _system) { let x = 0; if (this.#direction === 'horizontal') { if (event.position.y >= this.#sizeCache.height) { this.#hover = undefined; return; } let hoverIndex = undefined; for (const [index, [_, size]] of this.#titlesCache.entries()) { const textWidth = size.width + 2 * this.#padding; x += 2 * BORDER.size + textWidth; if (event.position.x < x) { hoverIndex = index; break; } x -= BORDER.size; } if (isMouseExit(event)) { this.#hover = undefined; } else if (isMouseEnter(event) || isMouseMove(event)) { this.#hover = hoverIndex; } else if (isMouseClicked(event) && hoverIndex !== undefined) { if (this.#selected.has(hoverIndex)) { this.#selected.delete(hoverIndex); } else if (this.#multiple) { this.#selected.add(hoverIndex); } else { this.#selected = new Set([hoverIndex]); } this.#onChange?.(hoverIndex, [...this.#selected]); } } } render(viewport) { if (viewport.isEmpty) { return; } viewport.registerMouse(['mouse.button.left', 'mouse.move']); if (this.#direction === 'horizontal') { let x = 0; for (const [index, [text, size]] of this.#titlesCache.entries()) { const rect = new Rect([x, 0], [size.width + 2 + 2 * this.#padding, this.#sizeCache.height]); viewport.clipped(rect, inner => { this.#renderGroupHorizontal(inner, text, size, index); }); x += rect.size.width - 1; } } else { let y = 0; for (const [_index, [_text, size]] of this.#titlesCache.entries()) { const rect = new Rect([0, y], [this.#sizeCache.width, size.height + 2 + 2 * this.#padding]).offset(BORDER.size, 0); viewport.clipped(rect, _inner => { // this.#renderGroupVertical( // inner, // text, // size, // index, // ) }); } } } #renderGroupHorizontal(viewport, text, size, index) { const maxIndex = this.#titlesCache.length - 1; const isFirst = index === 0; const isLast = index === maxIndex; const isSelected = this.#selected.has(index); const isHovered = this.#hover === index; const textWidth = size.width + 2 * this.#padding; const bottomPoint = Point.zero.offset(0, this.#sizeCache.height - 1); let border; if (this.#selected.has(index - 1) && this.#selected.has(index)) { border = BORDER_BOTH; } else if (this.#selected.has(index - 1)) { border = BORDER_PREV; } else if (this.#selected.has(index)) { border = BORDER_CURR; } else { border = BORDER; } if (isHovered && isSelected) { border = { ...border, top: '━', bottom: '━', left: '┃', right: '┃', joinerHorizTop: '┏', joinerHorizBottom: '┗', }; } else if (isHovered) { border = { ...border, top: '─', bottom: '─', left: '│', right: '│', joinerHorizTop: '┌', joinerHorizBottom: '└', }; } else if (this.#hover === index - 1 && this.#selected.has(index - 1)) { border = { ...border, joinerHorizTop: '┓', joinerHorizBottom: '┛', }; } else if (this.#hover === index - 1) { border = { ...border, left: '│', joinerHorizTop: '┐', joinerHorizBottom: '┘', }; } if (isFirst && isLast) { const top = border.tl + border.bottom.repeat(textWidth) + border.tr; const bottom = border.bl + border.bottom.repeat(textWidth) + border.br; viewport.write(top, Point.zero); viewport.write(bottom, bottomPoint); } else if (isFirst) { const top = border.tl + border.bottom.repeat(textWidth); const bottom = border.bl + border.bottom.repeat(textWidth); viewport.write(top, Point.zero); viewport.write(bottom, bottomPoint); } else if (isLast) { const top = border.joinerHorizTop + border.bottom.repeat(textWidth) + border.tr; const bottom = border.joinerHorizBottom + border.bottom.repeat(textWidth) + border.br; viewport.write(top, Point.zero); viewport.write(bottom, bottomPoint); } else { const top = border.joinerHorizTop + border.bottom.repeat(textWidth); const bottom = border.joinerHorizBottom + border.bottom.repeat(textWidth); viewport.write(top, Point.zero); viewport.write(bottom, bottomPoint); } let offsetY = 1; const backgroundStyle = this.#backgroundStyle(isSelected); const fill = FILL.repeat(textWidth); for (let i = this.#sizeCache.height - 2 * BORDER.size; i-- > 0;) { viewport.write(border.left, Point.zero.offset(0, offsetY)); viewport.write(fill, Point.zero.offset(BORDER.size, offsetY), backgroundStyle); viewport.write(border.right, Point.zero.offset(textWidth + BORDER.size, offsetY)); offsetY += 1; } viewport.clipped(viewport.contentRect.offset(BORDER.size + this.#padding, BORDER.size + this.#offAxisPadding), inner => { inner.write(text, Point.zero, this.#textStyle(isSelected)); }); } #backgroundStyle(isSelected) { if (!isSelected) { return undefined; } return new Style({ background: this.purpose.dimBackgroundColor }); } #textStyle(isSelected) { const style = this.purpose.text(); if (!isSelected) { return style; } return style.merge({ background: this.purpose.dimBackgroundColor }); } } const FILL = ' '; const BORDER = { size: 1, top: '─', bottom: '─', left: '│', right: '│', joinerHorizTop: '┬', joinerHorizBottom: '┴', joinerVertRight: '┤', joinerVertLeft: '├', tl: '╭', tr: '╮', bl: '╰', br: '╯', }; const BORDER_BOTH = { ...BORDER, top: '━', bottom: '━', left: '┃', right: '┃', joinerHorizTop: '┳', joinerHorizBottom: '┻', joinerVertRight: '┫', joinerVertLeft: '┣', tl: '┏', tr: '┓', bl: '┗', br: '┛', }; const BORDER_PREV = { ...BORDER, top: '━', left: '┃', joinerHorizTop: '┱', joinerHorizBottom: '┹', joinerVertRight: '┩', joinerVertLeft: '┡', }; const BORDER_CURR = { ...BORDER_BOTH, joinerHorizTop: '┲', joinerHorizBottom: '┺', joinerVertRight: '┪', joinerVertLeft: '┢', }; //# sourceMappingURL=ToggleGroup.js.map