diff options
Diffstat (limited to 'ui/presentation')
-rw-r--r-- | ui/presentation/HorizontalBox.js | 13 | ||||
-rw-r--r-- | ui/presentation/Label.js | 67 | ||||
-rw-r--r-- | ui/presentation/Pane.js | 101 | ||||
-rw-r--r-- | ui/presentation/Sprite.js | 69 | ||||
-rw-r--r-- | ui/presentation/WrapLabel.js | 41 | ||||
-rw-r--r-- | ui/presentation/index.js | 15 |
6 files changed, 306 insertions, 0 deletions
diff --git a/ui/presentation/HorizontalBox.js b/ui/presentation/HorizontalBox.js new file mode 100644 index 0000000..d396ec3 --- /dev/null +++ b/ui/presentation/HorizontalBox.js @@ -0,0 +1,13 @@ +import {DisplayElement} from 'tui-lib/ui/primitives' + +export default class HorizontalBox extends DisplayElement { + // A box that will automatically lay out its children in a horizontal row. + + fixLayout() { + let nextX = 0 + for (const child of this.children) { + child.x = nextX + nextX = child.right + 1 + } + } +} diff --git a/ui/presentation/Label.js b/ui/presentation/Label.js new file mode 100644 index 0000000..ed45601 --- /dev/null +++ b/ui/presentation/Label.js @@ -0,0 +1,67 @@ +import {DisplayElement} from 'tui-lib/ui/primitives' + +import * as ansi from 'tui-lib/util/ansi' + +export default class Label extends DisplayElement { + // A simple text display. Automatically adjusts size to fit text. + // + // Supports formatted text in two ways: + // 1) Modify the textAttributes to be an array containing the ANSI numerical + // codes for any wanted attributes, and/or + // 2) Supply full ANSI escape codes within the text itself. (The reset + // attributes code, ESC[0m, will be processed to reset to the provided + // values in textAttributes. + // + // Subclasses overriding the writeTextTo function should be sure to call + // processFormatting before actually writing text. + + constructor(text = '') { + super() + + this.text = text + this.textAttributes = [] + } + + fixLayout() { + this.w = ansi.measureColumns(this.text) + } + + drawTo(writable) { + if (this.textAttributes.length) { + writable.write(ansi.setAttributes(this.textAttributes)) + } + + this.writeTextTo(writable) + + if (this.textAttributes.length) { + writable.write(ansi.resetAttributes()) + } + + super.drawTo(writable) + } + + writeTextTo(writable) { + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write(this.processFormatting(this.text)) + } + + processFormatting(text) { + return text.replace(new RegExp(ansi.ESC + '\\[0m', 'g'), + ansi.setAttributes([ansi.A_RESET, ...this.textAttributes])) + } + + set text(newText) { + const ret = this.setDep('text', newText) + this.fixLayout() + return ret + } + + get text() { + return this.getDep('text') + } + + // Kinda bad, but works as long as you're overwriting the array instead of + // mutating it. + set textAttributes(val) { return this.setDep('textAttributes', val) } + get textAttributes() { return this.getDep('textAttributes') } +} diff --git a/ui/presentation/Pane.js b/ui/presentation/Pane.js new file mode 100644 index 0000000..4769cf9 --- /dev/null +++ b/ui/presentation/Pane.js @@ -0,0 +1,101 @@ +import {DisplayElement} from 'tui-lib/ui/primitives' + +import * as ansi from 'tui-lib/util/ansi' +import unic from 'tui-lib/util/unichars' + +import Label from './Label.js' + +export default class Pane extends DisplayElement { + // A simple rectangular framed pane. + + constructor() { + super() + + this.frameColor = null + + this.hPadding = 1 + this.vPadding = 1 + } + + drawTo(writable) { + this.drawFrame(writable) + super.drawTo(writable) + } + + drawFrame(writable, debug=false) { + writable.write(ansi.setForeground(this.frameColor)) + + const left = this.absLeft + const right = this.absRight + const top = this.absTop + const bottom = this.absBottom + + // Background + // (TODO) Transparent background (that dimmed everything behind it) would + // be cool at some point! + for (let y = top + 1; y <= bottom - 1; y++) { + writable.write(ansi.moveCursor(y, left)) + writable.write(' '.repeat(this.w)) + } + + // Left/right edges + for (let x = left + 1; x <= right - 1; x++) { + writable.write(ansi.moveCursor(top, x)) + writable.write(unic.BOX_H) + writable.write(ansi.moveCursor(bottom, x)) + writable.write(unic.BOX_H) + } + + // Top/bottom edges + for (let y = top + 1; y <= bottom - 1; y++) { + writable.write(ansi.moveCursor(y, left)) + writable.write(unic.BOX_V) + writable.write(ansi.moveCursor(y, right)) + writable.write(unic.BOX_V) + } + + // Corners + writable.write(ansi.moveCursor(top, left)) + writable.write(unic.BOX_CORNER_TL) + writable.write(ansi.moveCursor(top, right)) + writable.write(unic.BOX_CORNER_TR) + writable.write(ansi.moveCursor(bottom, left)) + writable.write(unic.BOX_CORNER_BL) + writable.write(ansi.moveCursor(bottom, right)) + writable.write(unic.BOX_CORNER_BR) + + // Debug info + if (debug) { + writable.write(ansi.moveCursor(6, 8)) + writable.write( + `x: ${this.x}; y: ${this.y}; w: ${this.w}; h: ${this.h}`) + writable.write(ansi.moveCursor(7, 8)) + writable.write(`AbsX: ${this.absX}; AbsY: ${this.absY}`) + writable.write(ansi.moveCursor(8, 8)) + writable.write(`Left: ${this.left}; Right: ${this.right}`) + writable.write(ansi.moveCursor(9, 8)) + writable.write(`Top: ${this.top}; Bottom: ${this.bottom}`) + } + + writable.write(ansi.setForeground(ansi.C_RESET)) + } + + static alert(parent, text) { + // Show an alert pane in the bottom left of the given parent element for + // a couple seconds. + + const pane = new Pane() + pane.frameColor = ansi.C_WHITE + pane.w = ansi.measureColumns(text) + 2 + pane.h = 3 + parent.addChild(pane) + + const label = new Label(text) + label.textAttributes = [ansi.C_WHITE] + pane.addChild(label) + + setTimeout(() => { + parent.removeChild(pane) + }, 2000) + } +} diff --git a/ui/presentation/Sprite.js b/ui/presentation/Sprite.js new file mode 100644 index 0000000..49ee450 --- /dev/null +++ b/ui/presentation/Sprite.js @@ -0,0 +1,69 @@ +import {DisplayElement} from 'tui-lib/ui/primitives' + +import * as ansi from 'tui-lib/util/ansi' + +export default class Sprite extends DisplayElement { + // "A sprite is a two-dimensional bitmap that is integrated into a larger + // scene." - Wikipedia + // + // Sprites are display objects that have a single texture that will not + // render outside of their parent. + // + // Sprites have a "real" position which overrides their "integer" position. + // This is so that motion can be more fluid (i.e., sprites don't need to + // move a whole number of terminal characters at a time). + + constructor() { + super() + + this.texture = [] + + this.realX = 0 + this.realY = 0 + } + + set x(newX) { this.realX = newX } + set y(newY) { this.realY = newY } + get x() { return Math.round(this.realX) } + get y() { return Math.round(this.realY) } + + drawTo(writable) { + if (this.textureAttributes) { + writable.write(ansi.setAttributes(this.textureAttributes)) + } + + for (let y = 0; y < this.textureHeight; y++) { + // Don't render above or below the parent's content area. + if (this.y + y >= this.parent.contentH || this.y + y < 0) continue + + const right = this.x + this.textureWidth + + const start = (this.x < 0) ? -this.x : 0 + const end = ( + (right > this.parent.contentW) + ? this.parent.contentW - right + : right) + const text = this.texture[y].slice(start, end) + + writable.write(ansi.moveCursor(this.absY + y, this.absX + start)) + writable.write(text) + } + + if (this.textureAttributes) { + writable.write(ansi.resetAttributes()) + } + } + + fixLayout() { + this.w = this.textureWidth + this.h = this.textureHeight + } + + get textureWidth() { + return Math.max(...this.texture.map(row => ansi.measureColumns(row))) + } + + get textureHeight() { + return this.texture.length + } +} diff --git a/ui/presentation/WrapLabel.js b/ui/presentation/WrapLabel.js new file mode 100644 index 0000000..eae8960 --- /dev/null +++ b/ui/presentation/WrapLabel.js @@ -0,0 +1,41 @@ +import * as ansi from 'tui-lib/util/ansi' + +import Label from './Label.js' + +export default class WrapLabel extends Label { + // A word-wrapping text display. Given a width, wraps text to fit. + + constructor(...args) { + super(...args) + } + + fixLayout() { + // Override Label.fixLayout to do nothing. We don't want to make the + // width of this label be set to the content of the text! (That would + // defeat the entire point of word wrapping.) + } + + writeTextTo(writable) { + const lines = this.getWrappedLines() + for (let i = 0; i < lines.length; i++) { + writable.write(ansi.moveCursor(this.absTop + i, this.absLeft)) + writable.write(this.processFormatting(lines[i])) + } + } + + getWrappedLines() { + if (this.text.trim().length === 0) { + return [] + } + + return ansi.wrapToColumns(this.text, this.w - 1).map(l => l.trim()) + } + + get h() { + return this.getWrappedLines().length + } + + set h(newHeight) { + // Do nothing. Height is computed on the fly. + } +} diff --git a/ui/presentation/index.js b/ui/presentation/index.js new file mode 100644 index 0000000..9605d25 --- /dev/null +++ b/ui/presentation/index.js @@ -0,0 +1,15 @@ +// +// Import mapping: +// +// primitives -> +// HorizontalBox +// Sprite +// +// Label -> Pane, WrapLabel +// + +export {default as HorizontalBox} from './HorizontalBox.js' +export {default as Label} from './Label.js' +export {default as Pane} from './Pane.js' +export {default as Sprite} from './Sprite.js' +export {default as WrapLabel} from './WrapLabel.js' |