diff options
-rw-r--r-- | ui/DisplayElement.js | 85 | ||||
-rw-r--r-- | ui/Root.js | 65 | ||||
-rw-r--r-- | ui/form/Form.js | 3 | ||||
-rw-r--r-- | ui/form/TextInput.js | 3 | ||||
-rw-r--r-- | util/ansi.js | 21 |
5 files changed, 166 insertions, 11 deletions
diff --git a/ui/DisplayElement.js b/ui/DisplayElement.js index b7e07b9..87a9cb5 100644 --- a/ui/DisplayElement.js +++ b/ui/DisplayElement.js @@ -14,10 +14,14 @@ module.exports = class DisplayElement extends EventEmitter { constructor() { super() - this.visible = true + this[DisplayElement.drawValues] = {} + this[DisplayElement.lastDrawValues] = {} + this[DisplayElement.scheduledDraw] = false - this.parent = null this.children = [] + this.parent = null + + this.visible = true this.x = 0 this.y = 0 @@ -82,6 +86,68 @@ module.exports = class DisplayElement extends EventEmitter { } } + getDep(key) { + return this[DisplayElement.drawValues][key] + } + + setDep(key, value) { + const oldValue = this[DisplayElement.drawValues][key] + // TODO: new map for old values. we only compare value !== oldValue LATER, at render time, not when choosing to schedule - otherwise intermediate sets e.g. f.y = 1; f.y++, which always has a net effect of f.y = 2, will count as a redraw even though the final value isn't changing between frames + if (value !== this[DisplayElement.drawValues][key]) { + this[DisplayElement.drawValues][key] = value + this.scheduleDraw() + // Grumble: technically it's possible for a root element to not be an + // actual Root. While we don't check for this case most of the time (even + // though we ought to), we do here because it's not unlikely for draw + // dependency values to be changed before the element is actually added + // to a Root element. + if (this.root.scheduleRender) { + this.root.scheduleRender() + } + } + return value + } + + scheduleDrawWithoutPropertyChange() { + // Utility function for when you need to schedule a draw without updating + // any particular draw-dependency property on the element. Works by setting + // an otherwise unused dep to a unique object. (We can't use a symbol here, + // because then Object.entries doesn't notice it.) + this.setDep('drawWithoutProperty', Math.random()) + } + + scheduleDraw() { + this[DisplayElement.scheduledDraw] = true + } + + unscheduleDraw() { + this[DisplayElement.scheduledDraw] = false + } + + hasScheduledDraw() { + if (this[DisplayElement.scheduledDraw]) { + for (const [ key, value ] of Object.entries(this[DisplayElement.drawValues])) { + if (value !== this[DisplayElement.lastDrawValues][key]) { + return true + } + } + } + return false + } + + updateLastDrawValues() { + Object.assign(this[DisplayElement.lastDrawValues], this[DisplayElement.drawValues]) + } + + eachDescendant(fn) { + // Run a function on this element, all of its children, all of their + // children, etc. + fn(this) + for (const child of this.children) { + child.eachDescendant(fn) + } + } + addChild(child, afterIndex = this.children.length, {fixLayout = true} = {}) { // TODO Don't let a direct ancestor of this be added as a child. Don't // let itself be one of its childs either! @@ -230,6 +296,17 @@ module.exports = class DisplayElement extends EventEmitter { return null } + get x() { return this.getDep('x') } + set x(v) { return this.setDep('x', v) } + get y() { return this.getDep('y') } + set y(v) { return this.setDep('y', v) } + get hPadding() { return this.getDep('hPadding') } + set hPadding(v) { return this.setDep('hPadding', v) } + get vPadding() { return this.getDep('vPadding') } + set vPadding(v) { return this.setDep('vPadding', v) } + get visible() { return this.getDep('visible') } + set visible(v) { return this.setDep('visible', v) } + get absX() { if (this.parent) { return this.parent.contentX + this.x @@ -262,3 +339,7 @@ module.exports = class DisplayElement extends EventEmitter { get absTop() { return this.absY } get absBottom() { return this.absY + this.h - 1 } } + +module.exports.drawValues = Symbol('drawValues') +module.exports.lastDrawValues = Symbol('lastDrawValues') +module.exports.scheduledDraw = Symbol('scheduledDraw') diff --git a/ui/Root.js b/ui/Root.js index 8242f7a..fd81fed 100644 --- a/ui/Root.js +++ b/ui/Root.js @@ -9,10 +9,11 @@ module.exports = class Root extends DisplayElement { // An element to be used as the root of a UI. Handles lots of UI and // socket stuff. - constructor(interfacer) { + constructor(interfacer, writable = null) { super() this.interfacer = interfacer + this.writable = writable || interfacer this.selectedElement = null @@ -21,10 +22,8 @@ module.exports = class Root extends DisplayElement { this.oldSelectionStates = [] interfacer.on('inputData', buf => this.handleData(buf)) - } - render() { - this.renderTo(this.interfacer) + this.renderCount = 0 } handleData(buffer) { @@ -74,7 +73,60 @@ module.exports = class Root extends DisplayElement { writable.write(' '.repeat(this.w * this.h)) } + scheduleRender() { + if (!this.scheduledRender) { + setTimeout(() => { + this.scheduledRender = false + this.render() + }) + this.scheduledRender = true + } + } + + render() { + this.renderTo(this.writable) + } + + renderNow() { + this.renderNowTo(this.writable) + } + + renderTo(writable) { + if (this.shouldRenderTo(writable)) { + this.renderNowTo(writable) + } + } + + renderNowTo(writable) { + if (writable) { + this.renderCount++ + super.renderTo(writable) + } + } + + shouldRenderTo(writable) { + let render = false + this.eachDescendant(el => { + // If we already know we're going to render, checking the element's + // scheduled-draw status (which involves iterating over each of its draw + // dependency properties) is redundant. + if (render) { + el.unscheduleDraw() + } else if (el.hasScheduledDraw()) { + render = true + el.unscheduleDraw() + } + el.updateLastDrawValues() + }) + return render + } + didRenderTo(writable) { + /* + writable.write(ansi.moveCursorRaw(1, 1)) + writable.write('Renders: ' + this.renderCount) + */ + // Render the cursor, based on the cursorX and cursorY of the currently // selected element. if (this.selectedElement && this.selectedElement.cursorVisible) { @@ -94,6 +146,8 @@ module.exports = class Root extends DisplayElement { } else { writable.write(ansi.hideCursor()) } + + this.emit('rendered') } cursorMoved() { @@ -209,4 +263,7 @@ module.exports = class Root extends DisplayElement { if (this.selectedElement.directAncestors.includes(el)) return true return false } + + get selectedElement() { return this.getDep('selectedElement') } + set selectedElement(v) { return this.setDep('selectedElement', v) } } diff --git a/ui/form/Form.js b/ui/form/Form.js index 3c59cf5..74a1065 100644 --- a/ui/form/Form.js +++ b/ui/form/Form.js @@ -122,4 +122,7 @@ module.exports = class Form extends FocusElement { this.updateSelectedElement() } } + + get curIndex() { return this.getDep('curIndex') } + set curIndex(v) { return this.setDep('curIndex', v) } } diff --git a/ui/form/TextInput.js b/ui/form/TextInput.js index 08bbbb6..78d3b6d 100644 --- a/ui/form/TextInput.js +++ b/ui/form/TextInput.js @@ -139,4 +139,7 @@ module.exports = class TextInput extends FocusElement { this.scrollChars-- } } + + get value() { return this.getDep('value') } + set value(v) { return this.setDep('value', v) } } diff --git a/util/ansi.js b/util/ansi.js index c786db5..ac511ed 100644 --- a/util/ansi.js +++ b/util/ansi.js @@ -182,7 +182,8 @@ const ansi = { interpret(text, scrRows, scrCols, { oldChars = null, oldLastChar = null, - oldCursorRow = 0, oldCursorCol = 0, oldShowCursor = true + oldScrRows = null, oldScrCols = null, + oldCursorRow = 1, oldCursorCol = 1, oldShowCursor = true } = {}) { // Interprets the given ansi code, more or less. @@ -193,9 +194,17 @@ const ansi = { const chars = new Array(scrRows * scrCols).fill(blank) - let showCursor = true - let cursorRow = 1 - let cursorCol = 1 + if (oldChars) { + for (let row = 0; row < scrRows && row < oldScrRows; row++) { + for (let col = 0; col < scrCols && col < oldScrCols; col++) { + chars[row * scrCols + col] = oldChars[row * oldScrCols + col] + } + } + } + + let showCursor = oldShowCursor + let cursorRow = oldCursorRow + let cursorCol = oldCursorCol let attributes = [] for (let charI = 0; charI < text.length; charI++) { @@ -475,10 +484,12 @@ const ansi = { return { oldChars: newChars.slice(), + oldLastChar: Object.assign({}, lastChar), + oldScrRows: scrRows, + oldScrCols: scrCols, oldCursorRow: cursorRow, oldCursorCol: cursorCol, oldShowCursor: showCursor, - oldLastChar: Object.assign({}, lastChar), screen: result.join('') } } |