UNPKG

ascii-ui

Version:

Graphic terminal emulator for HTML canvas elements

368 lines 12.7 kB
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