@teaui/core
Version:
A high-level terminal UI library for Node
298 lines • 10.3 kB
JavaScript
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