ascii-ui
Version:
Graphic terminal emulator for HTML canvas elements
368 lines • 12.7 kB
JavaScript
import { Terminal } from '../Terminal';
import { coalesce } from '../util/coalesce';
import { Widget } from '../Widget';
export class Grid extends Widget {
constructor(terminal, options, parent) {
super(terminal, options, parent);
this.attachedWidgets = [];
this.columnStarts = [];
this.rowStarts = [];
this.options = Object.assign({}, Grid.defaultOptions, this.options, options);
if (typeof options.fullSize === 'undefined') {
this.options.fullSize = true;
}
this.resizedEventHandler = this.resizedEventHandler.bind(this);
if (parent instanceof Terminal && this.options.fullSize) {
const parentSize = parent.getSize();
this.setOptions({
col: 0,
line: 0,
width: parentSize.columns,
height: parentSize.rows,
});
terminal.eventManager.addListener('resized', this.resizedEventHandler);
}
else {
this.setOptions({
col: options.col || 0,
line: options.line || 0,
width: options.width,
height: options.height,
});
}
this.recalculateCellSizes();
}
destruct() {
this.terminal.eventManager.removeListener('resized', this.resizedEventHandler);
}
render() {
if (this.options.borders) {
this.renderBorders();
}
this.attachedWidgets.forEach((instance) => {
instance.widget.render();
});
}
align() {
this.alignWidgets();
}
attachWidget(col, line, width, height, WidgetClass, options) {
const widget = Reflect.construct(WidgetClass, [
this.terminal,
options,
this,
]);
const attachedWidget = {
widget,
col,
line,
width,
height,
};
this.attachedWidgets.push(attachedWidget);
this.alignWidgets(attachedWidget);
return widget;
}
dettachWidget(widget) {
const index = this.attachedWidgets.findIndex((instance) => instance.widget === widget);
if (index !== -1) {
this.attachedWidgets.splice(index, 1);
const position = widget.getPosition();
const size = widget.getSize();
this.terminal.clear(position.col, position.line, size.columns, size.rows);
}
return index !== -1;
}
getWidgetAt(column, line) {
for (const instance of this.attachedWidgets) {
if (instance.widget.isAt(column, line)) {
return instance.widget;
}
}
return;
}
[Symbol.iterator](startWidget) {
const data = this.attachedWidgets;
let index;
const it = {
next: () => {
const attachedWidget = data[++index];
if (index > this.attachedWidgets.length) {
index = this.attachedWidgets.length;
}
return {
value: attachedWidget && attachedWidget.widget,
done: !(index in data),
};
},
prev: () => {
const attachedWidget = data[--index];
if (index < -1) {
index = -1;
}
return {
value: attachedWidget && attachedWidget.widget,
done: !(index in data),
};
},
seek: (value) => {
index = typeof value === 'number'
? (value < 0 ? this.attachedWidgets.length - value - 1 : value)
: data.findIndex((attachedWidget) => attachedWidget.widget === value);
},
};
if (startWidget) {
it.seek(startWidget);
}
else {
index = -1;
}
return it;
}
getWidgetGrid(column, line) {
const attachedWidget = this.attachedWidgets.filter((instance) => instance.col >= column
&& instance.col < column + instance.width
&& instance.line >= line
&& instance.line < line + instance.height)[0];
return attachedWidget ? attachedWidget.widget : undefined;
}
getCellSize(column, line) {
return {
columns: this.columnStarts[column + 1] - this.columnStarts[column],
rows: this.rowStarts[line + 1] - this.rowStarts[line],
};
}
updateOptions(changes) {
const options = this.options;
if (!options.calculateGridSpace && !changes.calculateGridSpace) {
options.calculateGridSpace = calculateGridSpace;
}
if (coalesce(changes.width, changes.height, changes.col, changes.line, changes.borders) !== undefined) {
if (this.parent instanceof Terminal && options.fullSize) {
const terminalSize = this.parent.getSize();
options.col = 0;
options.line = 0;
options.width = terminalSize.columns;
options.height = terminalSize.rows;
}
this.recalculateCellSizes();
}
}
calculateBorders() {
if (!this.attachedWidgets || this.attachedWidgets.length === 0) {
this.borderTiles = [];
return;
}
const borders = {};
const borderStyle = this.options.borderStyle;
const newTiles = [];
function checkTile(col, line, ...deltas) {
return deltas.every(([dx, dy]) => {
const row = borders[line + dy];
return row !== undefined && row[col + dx] !== undefined;
});
}
function getBorderTile(col, line, char) {
let c;
if (checkTile(col, line, [0, -1], [1, 0], [0, 1], [-1, 0])) {
c = borderStyle.cross;
}
else if (checkTile(col, line, [1, 0], [0, 1], [-1, 0])) {
c = borderStyle.noTop;
}
else if (checkTile(col, line, [0, -1], [0, 1], [-1, 0])) {
c = borderStyle.noLeft;
}
else if (checkTile(col, line, [0, -1], [1, 0], [-1, 0])) {
c = borderStyle.noBottom;
}
else if (checkTile(col, line, [0, -1], [1, 0], [0, 1])) {
c = borderStyle.noRight;
}
else if (checkTile(col, line, [1, 0], [0, 1])) {
c = borderStyle.topLeft;
}
else if (checkTile(col, line, [-1, 0], [0, 1])) {
c = borderStyle.topRight;
}
else if (checkTile(col, line, [-1, 0], [0, -1])) {
c = borderStyle.bottomRight;
}
else if (checkTile(col, line, [1, 0], [0, -1])) {
c = borderStyle.bottomLeft;
}
else {
c = char;
}
return {
char: c,
font: borderStyle.font,
offsetX: borderStyle.offsetX,
offsetY: borderStyle.offsetY,
fg: borderStyle.fg,
bg: borderStyle.bg,
};
}
function addTile(col, line, char) {
if (!borders[line]) {
borders[line] = {};
}
borders[line][col] = char;
}
function addBox(x0, y0, x1, y1) {
if (x0 === undefined || y0 === undefined || x1 === undefined || y1 === undefined) {
return;
}
for (let i = x0; i < x1; i++) {
addTile(i, y0, borderStyle.top);
addTile(i, y1 - 1, borderStyle.bottom);
}
for (let i = y0 + 1; i < y1 - 1; i++) {
addTile(x0, i, borderStyle.left);
addTile(x1 - 1, i, borderStyle.right);
}
}
for (const widget of this.attachedWidgets) {
addBox(this.columnStarts[widget.col] - 1, this.rowStarts[widget.line] - 1, this.columnStarts[widget.col + widget.width], this.rowStarts[widget.line + widget.height]);
}
Object.keys(borders)
.forEach((keyY) => {
const y = Number(keyY);
const line = borders[y];
let lastCol = -1;
let chunkStart = -1;
let chunk;
Object.keys(line)
.forEach((keyX) => {
const x = Number(keyX);
if (lastCol === -1 || x - lastCol !== 1) {
if (chunk) {
newTiles.push({
tiles: chunk,
col: chunkStart,
line: y,
});
}
chunk = [];
chunkStart = x;
}
lastCol = x;
chunk.push(getBorderTile(x, y, borders[y][x]));
});
newTiles.push({
tiles: chunk,
col: chunkStart,
line: y,
});
});
this.borderTiles = newTiles;
}
renderBorders() {
if (!this.borderTiles) {
this.calculateBorders();
}
this.borderTiles.forEach((chunk) => {
this.terminal.setTiles(chunk.tiles, chunk.col, chunk.line);
});
}
alignWidgets(attachedWidget) {
const columnStarts = this.columnStarts;
const rowStarts = this.rowStarts;
this.borderTiles = undefined;
const alignOne = (w) => {
const col = columnStarts[w.col];
const line = rowStarts[w.line];
const borders = this.options.borders ? 1 : 0;
const width = columnStarts[w.col + w.width] - col - borders;
const height = rowStarts[w.line + w.height] - line - borders;
w.widget.setOptions({ col, line, width, height });
w.widget.render();
};
if (this.options.borders) {
this.renderBorders();
}
if (attachedWidget) {
alignOne(attachedWidget);
}
else if (this.attachedWidgets) {
this.attachedWidgets.forEach(alignOne);
}
}
recalculateCellSizes() {
const options = this.options;
function spaceToStarts(first, spaces, max) {
const starts = [];
let acc = first;
spaces.forEach((space, i) => {
starts.push(acc + (options.borders ? i + 1 : 0));
acc += space;
});
starts.push(max);
return starts;
}
let availableWidth = options.width;
let availableHeight = options.height;
if (options.borders) {
availableWidth -= options.columns + 1;
availableHeight -= options.rows + 1;
}
this.columnStarts = spaceToStarts(options.col, options.calculateGridSpace(availableWidth, options.columns, false, this.terminal), options.width);
this.rowStarts = spaceToStarts(options.line, options.calculateGridSpace(availableHeight, options.rows, true, this.terminal), options.height);
this.alignWidgets();
}
resizedEventHandler(event) {
if (this.parent instanceof Terminal && this.options.fullSize) {
const terminalSize = this.parent.getSize();
this.setOptions({
col: 0,
line: 0,
width: terminalSize.columns,
height: terminalSize.rows,
});
}
}
}
function calculateGridSpace(available, cells) {
if (cells === 0) {
return [];
}
if (cells === 1) {
return [available];
}
const tilesPerSlot = available / cells;
let acc = Math.round(tilesPerSlot);
const res = [acc];
for (let i = 1; i < cells - 1; i++) {
const size = Math.round(tilesPerSlot * (i + 1)) - acc;
res.push(size);
acc += size;
}
res.push(available - acc);
return res;
}
Grid.defaultOptions = {
calculateGridSpace,
rows: undefined,
columns: undefined,
borderStyle: {
offsetX: 0,
offsetY: 0,
bg: '#000000',
fg: '#00ff00',
font: '20pt Terminal_VT220',
top: '─',
bottom: '─',
left: '│',
right: '│',
topLeft: '┌',
topRight: '┐',
bottomRight: '┘',
bottomLeft: '└',
noTop: '┬',
noRight: '├',
noBottom: '┴',
noLeft: '┤',
cross: '┼',
},
};
//# sourceMappingURL=Grid.js.map