diff options
Diffstat (limited to 'ui/form')
-rw-r--r-- | ui/form/Button.js | 49 | ||||
-rw-r--r-- | ui/form/CancelDialog.js | 63 | ||||
-rw-r--r-- | ui/form/ConfirmDialog.js | 79 | ||||
-rw-r--r-- | ui/form/FocusBox.js | 32 | ||||
-rw-r--r-- | ui/form/FocusElement.js | 38 | ||||
-rw-r--r-- | ui/form/Form.js | 51 | ||||
-rw-r--r-- | ui/form/HorizontalForm.js | 4 | ||||
-rw-r--r-- | ui/form/ListScrollForm.js | 137 | ||||
-rw-r--r-- | ui/form/TextInput.js | 114 |
9 files changed, 567 insertions, 0 deletions
diff --git a/ui/form/Button.js b/ui/form/Button.js new file mode 100644 index 0000000..9a3d2f7 --- /dev/null +++ b/ui/form/Button.js @@ -0,0 +1,49 @@ +const ansi = require('../../ansi') +const telc = require('../../telchars') + +const FocusElement = require('./FocusElement') + +module.exports = class ButtonInput extends FocusElement { + // A button. + + constructor(text) { + super() + + this.text = text + + this.cursorX = null + this.cursorY = null + } + + // Setting the text of the button should change the width of the button to + // fit the text. + // + // TODO: Make this happen in fixLayout + set text(newText) { + this._text = newText + this.w = newText.length + } + + get text() { + return this._text + } + + drawTo(writable) { + if (this.isSelected) { + writable.write(ansi.invert()) + } + + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write(this.text) + + writable.write(ansi.resetAttributes()) + + super.drawTo(writable) + } + + keyPressed(keyBuf) { + if (telc.isSelect(keyBuf)) { + this.emit('pressed') + } + } +} diff --git a/ui/form/CancelDialog.js b/ui/form/CancelDialog.js new file mode 100644 index 0000000..ba9faf8 --- /dev/null +++ b/ui/form/CancelDialog.js @@ -0,0 +1,63 @@ +const telc = require('../../telchars') + +const FocusElement = require('./FocusElement') + +const Button = require('./Button') +const Form = require('./Form') +const Label = require('../Label') +const Pane = require('../Pane') + +module.exports = class ConfirmDialog extends FocusElement { + // A basic cancel dialog. Has one buttons, cancel, and a label. + // The escape (esc) key can be used to exit the dialog (which sends a + // 'cancelled' event, as the cancel button also does). + + constructor(text) { + super() + + this.pane = new Pane() + this.addChild(this.pane) + + this.cancelBtn = new Button('Cancel') + this.pane.addChild(this.cancelBtn) + + this.label = new Label(text) + this.pane.addChild(this.label) + + this.initEventListeners() + } + + initEventListeners() { + this.cancelBtn.on('pressed', () => this.cancelPressed()) + } + + fixLayout() { + this.w = this.parent.contentW + this.h = this.parent.contentH + + this.pane.w = Math.max(40, 4 + this.label.w) + this.pane.h = 7 + this.pane.centerInParent() + + this.label.x = Math.floor((this.pane.contentW - this.label.w) / 2) + this.label.y = 1 + + this.cancelBtn.x = Math.floor( + (this.pane.contentW - this.cancelBtn.w) / 2) + this.cancelBtn.y = this.pane.contentH - 2 + } + + focus() { + this.root.select(this.cancelBtn) + } + + keyPressed(keyBuf) { + if (telc.isCancel(keyBuf)) { + this.emit('cancelled') + } + } + + cancelPressed() { + this.emit('cancelled') + } +} diff --git a/ui/form/ConfirmDialog.js b/ui/form/ConfirmDialog.js new file mode 100644 index 0000000..614dede --- /dev/null +++ b/ui/form/ConfirmDialog.js @@ -0,0 +1,79 @@ +const telc = require('../../telchars') + +const FocusElement = require('./FocusElement') + +const Button = require('./Button') +const Form = require('./Form') +const Label = require('../Label') +const Pane = require('../Pane') + +module.exports = class ConfirmDialog extends FocusElement { + // A basic yes/no dialog. Has two buttons, confirm/cancel, and a label. + // The escape (esc) key can be used to exit the dialog (which sends a + // 'cancelled' event, as the cancel button also does). + + constructor(text) { + super() + + this.pane = new Pane() + this.addChild(this.pane) + + this.form = new Form() + this.pane.addChild(this.form) + + this.confirmBtn = new Button('Confirm') + this.form.addInput(this.confirmBtn) + + this.cancelBtn = new Button('Cancel') + this.form.addInput(this.cancelBtn) + + this.label = new Label(text) + this.form.addChild(this.label) + + this.initEventListeners() + } + + initEventListeners() { + this.confirmBtn.on('pressed', () => this.confirmPressed()) + this.cancelBtn.on('pressed', () => this.cancelPressed()) + } + + fixLayout() { + this.w = this.parent.contentW + this.h = this.parent.contentH + + this.pane.w = Math.max(40, 2 + this.label.w) + this.pane.h = 7 + this.pane.centerInParent() + + this.form.w = this.pane.contentW + this.form.h = this.pane.contentH + + this.label.x = Math.floor((this.form.contentW - this.label.w) / 2) + this.label.y = 1 + + this.confirmBtn.x = 1 + this.confirmBtn.y = this.form.contentH - 2 + + this.cancelBtn.x = this.form.right - this.cancelBtn.w - 1 + this.cancelBtn.y = this.form.contentH - 2 + } + + focus() { + this.root.select(this.form) + } + + keyPressed(keyBuf) { + if (telc.isCancel(keyBuf)) { + this.emit('cancelled') + } + } + + confirmPressed() { + this.emit('confirmed') + } + + cancelPressed() { + this.emit('cancelled') + } +} diff --git a/ui/form/FocusBox.js b/ui/form/FocusBox.js new file mode 100644 index 0000000..c259f23 --- /dev/null +++ b/ui/form/FocusBox.js @@ -0,0 +1,32 @@ +const ansi = require('../../ansi') + +const FocusElement = require('./FocusElement') + +module.exports = class FocusBox extends FocusElement { + // A box (not to be confused with Pane!) that can be selected. When it's + // selected, it applies an invert effect to its children. (This won't work + // well if you have elements inside of it that have their own attributes, + // since they're likely to reset all effects after drawing - including the + // invert from the FocusBox! Bad ANSI limitations; it's relatively likely + // I'll implement maaaaaagic to help deal with this - maybe something + // similar to 'pushMatrix' from Processing - at some point... [TODO]) + + constructor() { + super() + + this.cursorX = null + this.cursorY = null + } + + drawTo(writable) { + if (this.isSelected) { + writable.write(ansi.invert()) + } + } + + didRenderTo(writable) { + if (this.isSelected) { + writable.write(ansi.resetAttributes()) + } + } +} diff --git a/ui/form/FocusElement.js b/ui/form/FocusElement.js new file mode 100644 index 0000000..25a0693 --- /dev/null +++ b/ui/form/FocusElement.js @@ -0,0 +1,38 @@ +const DisplayElement = require('../DisplayElement') + +module.exports = class FocusElement extends DisplayElement { + // A basic element that can receive cursor focus. + + constructor() { + super() + + this.cursorX = 0 + this.cursorY = 0 + + this.isSelected = false + } + + focus(socket) { + // Do something with socket. Should be overridden in subclasses. + + this.isSelected = true + } + + unfocus() { + // Should be overridden in subclasses. + + this.isSelected = false + } + + keyPressed(keyBuf) { + // Do something with a buffer containing the key pressed (that is, + // telnet data sent). Should be overridden in subclasses. + // + // Keyboard characters are sent as a buffer in the form of + // ESC[# where # is A, B, C or D. See more here: + // http://stackoverflow.com/a/11432632/4633828 + } + + get absCursorX() { return this.absX + this.cursorX } + get absCursorY() { return this.absY + this.cursorY } +} diff --git a/ui/form/Form.js b/ui/form/Form.js new file mode 100644 index 0000000..49fa075 --- /dev/null +++ b/ui/form/Form.js @@ -0,0 +1,51 @@ +const telc = require('../../telchars') + +const FocusElement = require('./FocusElement') + +module.exports = class Form extends FocusElement { + constructor() { + super() + + this.inputs = [] + this.curIndex = 0 + } + + addInput(input, asChild = true) { + // Adds the given input as a child element and pushes it to the input + // list. If the second optional, asChild, is false, it won't add the + // input element as a child of the form. + + this.inputs.push(input) + + if (asChild) { + this.addChild(input) + } + } + + keyPressed(keyBuf) { + if (telc.isTab(keyBuf) || telc.isBackTab(keyBuf)) { + // No inputs to tab through, so do nothing. + if (this.inputs.length < 2) { + return + } + + if (telc.isTab(keyBuf)) { + this.curIndex = (this.curIndex + 1) % this.inputs.length + } else { + this.curIndex = (this.curIndex - 1) + if (this.curIndex < 0) { + this.curIndex = (this.inputs.length - 1) + } + } + + const nextInput = this.inputs[this.curIndex] + this.root.select(nextInput) + + return false + } + } + + focus() { + this.root.select(this.inputs[this.curIndex]) + } +} diff --git a/ui/form/HorizontalForm.js b/ui/form/HorizontalForm.js new file mode 100644 index 0000000..141bb17 --- /dev/null +++ b/ui/form/HorizontalForm.js @@ -0,0 +1,4 @@ +const Form = require('./DisplayElement') + +module.exports = class HorizontalBox extends Box { +} diff --git a/ui/form/ListScrollForm.js b/ui/form/ListScrollForm.js new file mode 100644 index 0000000..b1484b5 --- /dev/null +++ b/ui/form/ListScrollForm.js @@ -0,0 +1,137 @@ +const Form = require('./Form') + +module.exports = class ListScrollForm extends Form { + // A form that lets the user scroll through a list of items. It + // automatically adjusts to always allow the selected item to be visible. + + constructor(layoutType = 'vertical') { + super() + + this.layoutType = layoutType + + this.scrollItems = 0 + } + + fixLayout() { + // The scrollItems property represents the item to the very left of where + // we've scrolled, so we know right away that none of those will be + // visible and we won't bother iterating over them. + const itemsPastScroll = this.inputs.slice(this.scrollItems) + + // This variable stores how far along the respective axis (as defined by + // posProp) the next element should be. + let nextPos = 0 + + for (let item of itemsPastScroll) { + item[this.posProp] = nextPos + nextPos += item[this.sizeProp] + + // By default, the item should be visible.. + item.visible = true + + // ..but the item's far edge is past the form's far edge, it isn't + // fully visible and should be hidden. + if (item[this.posProp] + item[this.sizeProp] > this.formEdge) { + item.visible = false + } + + // Same deal goes for the close edge. We can check it against 0 since + // the close edge of the form's content is going to be 0, of course! + if (item[this.posProp] < 0) { + item.visible = false + } + } + } + + keyPressed(keyBuf) { + super.keyPressed(keyBuf) + + const sel = this.inputs[this.curIndex] + + // If the item is ahead of our view (either to the right of or below), + // we should move the view so that the item is the farthest right (of all + // the visible items). + if (this.getItemPos(sel) > this.formEdge + this.scrollSize) { + // We can decide how many items to scroll past by moving forward until + // our item's far edge is visible. + + let i + let edge = this.formEdge + + for (i = 0; i < this.inputs.length; i++) { + if (this.getItemPos(sel) <= edge) break + edge += this.inputs[i][this.sizeProp] + } + + // Now that we have the right index to scroll to, apply it! + this.scrollItems = i + } + + // Adjusting the number of scroll items is much simpler to deal with if + // the item is behind our view. Since the item's behind, we need to move + // the scroll to be immediately behind it, which is simple since we + // already have its index. + if (this.getItemPos(sel) <= this.scrollSize) { + this.scrollItems = this.curIndex + } + + this.fixLayout() + } + + getItemPos(item) { + // Gets the position of the item in an unscrolled view. + + return this.inputs.slice(0, this.inputs.indexOf(item) + 1) + .reduce((a, b) => a + b[this.sizeProp], 0) + } + + get sizeProp() { + // The property used to measure the size of an item. If the layoutType + // isn't valid (that is, 'horizontal' or 'vertical'), it'll return null. + + return ( + this.layoutType === 'horizontal' ? 'w' : + this.layoutType === 'vertical' ? 'h' : + null + ) + } + + get posProp() { + // The property used to position an item. Like sizeProp, returns null if + // the layoutType isn't valid. + + return ( + this.layoutType === 'horizontal' ? 'x' : + this.layoutType === 'vertical' ? 'y' : + null) + } + + get edgeProp() { + // The property used to get the far edge of the property. As with + // sizeProp, if the layoutType doesn't have an expected value, it'll + // return null. + + return ( + this.layoutType === 'horizontal' ? 'right' : + this.layoutType === 'vertical' ? 'bottom' : + null) + } + + get formEdge() { + // Returns the value of the far edge of this form. Items farther in the + // list (up to the edge) will be closer to this edge. + + return ( + this.layoutType === 'horizontal' ? this.contentW : + this.layoutType === 'vertical' ? this.contentH : + null) + } + + get scrollSize() { + // Gets the actual length made up by all of the items currently scrolled + // past. + + return this.inputs.slice(0, this.scrollItems) + .reduce((a, b) => a + b[this.sizeProp], 0) + } +} diff --git a/ui/form/TextInput.js b/ui/form/TextInput.js new file mode 100644 index 0000000..d09480f --- /dev/null +++ b/ui/form/TextInput.js @@ -0,0 +1,114 @@ +const ansi = require('../../ansi') +const unic = require('../../unichars') +const telc = require('../../telchars') + +const FocusElement = require('./FocusElement') + +module.exports = class TextInput extends FocusElement { + // An element that the user can type in. + + constructor() { + super() + + this.value = '' + this.cursorIndex = 0 + this.scrollChars = 0 + } + + drawTo(writable) { + // There should be room for the cursor so move the "right edge" left a + // single character. + + const startRange = this.scrollChars + const endRange = this.scrollChars + this.w - 3 + + let str = this.value.slice(startRange, endRange) + + writable.write(ansi.moveCursor(this.absTop, this.absLeft + 1)) + writable.write(str) + + // Ellipsis on left side, if there's more characters behind the visible + // area. + if (startRange > 0) { + writable.write(ansi.moveCursor(this.absTop, this.absLeft)) + writable.write(unic.ELLIPSIS) + } + + // Ellipsis on the right side, if there's more characters ahead of the + // visible area. + if (endRange < this.value.length) { + writable.write(ansi.moveCursor(this.absTop, this.absRight - 1)) + writable.write(unic.ELLIPSIS.repeat(2)) + } + + this.cursorX = this.cursorIndex - this.scrollChars + 1 + + super.drawTo(writable) + } + + keyPressed(keyBuf) { + if (keyBuf[0] === 127) { + this.value = ( + this.value.slice(0, this.cursorIndex - 1) + + this.value.slice(this.cursorIndex) + ) + this.cursorIndex-- + this.root.cursorMoved() + } else if (keyBuf[0] === 13) { + this.emit('value', this.value) + } else if (keyBuf[0] === 0x1b && keyBuf[1] === 0x5b) { + // Keyboard navigation + if (keyBuf[2] === 0x44) { + this.cursorIndex-- + this.root.cursorMoved() + } else if (keyBuf[2] === 0x43) { + this.cursorIndex++ + this.root.cursorMoved() + } + } else if (telc.isEscape(keyBuf)) { + // ESC is bad and we don't want that in the text input! + return + } else { + // console.log(keyBuf, keyBuf[0], keyBuf[1], keyBuf[2]) + this.value = ( + this.value.slice(0, this.cursorIndex) + keyBuf.toString() + + this.value.slice(this.cursorIndex) + ) + this.cursorIndex++ + this.root.cursorMoved() + } + + this.keepCursorInRange() + } + + keepCursorInRange() { + // Keep the cursor inside or at the end of the input value. + + if (this.cursorIndex < 0) { + this.cursorIndex = 0 + } + + if (this.cursorIndex > this.value.length) { + this.cursorIndex = this.value.length + } + + // Scroll right, if the cursor is past the right edge of where text is + // displayed. + if (this.cursorIndex - this.scrollChars > this.w - 3) { + this.scrollChars++ + } + + // Scroll left, if the cursor is behind the left edge of where text is + // displayed. + if (this.cursorIndex - this.scrollChars < 0) { + this.scrollChars-- + } + + // Scroll left, if we can see past the end of the text. + if (this.scrollChars > 0 && ( + this.scrollChars + this.w - 3 > this.value.length) + ) { + this.scrollChars-- + } + } +} |