diff --git a/lib/program.js b/lib/program.js index 5182b2f..3df8a56 100644 --- a/lib/program.js +++ b/lib/program.js @@ -2638,7 +2638,7 @@ exports = Program; exports.Program = Program; exports.Tput = Tput; -['Screen', 'Box', 'Text', 'List'].forEach(function(key) { +['Screen', 'Box', 'Text', 'List', 'Line', 'ProgressBar', 'ScrollableBox', 'ScrollableText'].forEach(function(key) { exports.__defineGetter__(key, function() { return (exports._widget || (exports._widget = require('./widget')))[key]; }); diff --git a/lib/widget.js b/lib/widget.js index f4b3b03..61a255a 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -39,25 +39,39 @@ screen.render(); */ +/** + * Modules + */ + +var EventEmitter = require('events').EventEmitter; + /** * Node */ function Node(options) { + EventEmitter.call(this); + this.options = options || {}; this.children = options.children || []; + if (this._isScreen && !this.focused) this.focused = this.children[0]; } +Node.prototype.__proto__ = EventEmitter.prototype; + Node.prototype.prepend = function(element) { + if (this._isScreen && !this.focused) this.focused = element; this.children.unshift(element); }; Node.prototype.append = function(element) { + if (this._isScreen && !this.focused) this.focused = element; this.children.push(element); }; Node.prototype.remove = function(element) { var i = this.children.indexOf(element); if (~i) this.children.splice(i, 1); + if (this._isScreen && this.focused === element) this.focused = this.children[0]; }; Node.prototype.detach = function(element) { @@ -73,6 +87,7 @@ function Screen(options) { Node.call(this, options); + this._isScreen = true; this.children = options.children || []; this.program = options.program; this.tput = this.program.tput; @@ -89,11 +104,76 @@ function Screen(options) { this.program.on('resize', function() { self.alloc(); self.render(); + self.emit('resize'); }); + + this.focused = null; + this.clickable = []; + this.input = []; } Screen.prototype.__proto__ = Node.prototype; +// TODO: Bubble events. +Screen.prototype._listenMouse = function(el, hover) { + var self = this; + + if (el) { + if (!hover) this.clickable.push(el); + else this.hover.push(el); + } + + if (this._listenedMouse) return; + this._listenedMouse = true; + + this.program.enableMouse(); + + process.on('exit', function() { + self.program.disableMouse(); + }); + + this.program.on('mouse', function(data) { + var i = 0, left, top, el; + for (; i < self.clickable.length; i++) { + el = self.clickable[i]; + left = el.left + (el.border ? 1 : 0); + top = el.top + (el.border ? 1 : 0); + if (el.parent.childBase != null) top -= el.parent.childBase; + if (data.x >= left && data.x <= left + el.width + && data.y >= top && data.y <= top + el.height) { + el.emit('mouse', data); + if (data.action === 'mouseup') { + el.emit('click', data); + } else if (data.action === 'movement') { + el.emit('hover', data); + } + el.emit(data.action, data); + break; + } + } + self.emit('mouse', data); + }); +}; + +// TODO: Bubble events. +Screen.prototype._listenKeys = function(el) { + var self = this; + + if (el) { + this.input.push(el); + } + + if (this._listenedKeys) return; + this._listenedKeys = true; + + this.program.on('keypress', function(ch, key) { + if (~self.input.indexOf(self.focused)) { + self.focused.emit('keypress', ch, key); + } + self.emit('keypress', ch, key); + }); +}; + Screen.prototype.__defineGetter__('cols', function() { return this.program.cols; }); @@ -156,7 +236,7 @@ Screen.prototype.draw = function(start, end) { data = line[x][0]; ch = line[x][1]; - if (data && data !== attr) { + if (data !== attr) { if (attr !== this.dattr) { out += '\x1b[m'; } @@ -193,7 +273,7 @@ Screen.prototype.draw = function(start, end) { } if (bgColor !== 0x1ff) { - if (bgColor < 16) { //|| this.tput.colors <= 16) { + if (bgColor < 16 || this.tput.colors <= 16) { if (bgColor < 8) { bgColor += 40; } else if (bgColor < 16) { @@ -206,7 +286,7 @@ Screen.prototype.draw = function(start, end) { } if (fgColor !== 0x1ff) { - if (fgColor < 16) { //|| this.tput.colors <= 16) { + if (fgColor < 16 || this.tput.colors <= 16) { if (fgColor < 8) { fgColor += 30; } else if (fgColor < 16) { @@ -228,7 +308,7 @@ Screen.prototype.draw = function(start, end) { attr = data; } - if (attr && attr !== this.dattr) { + if (attr !== this.dattr) { out += '\x1b[m'; } @@ -240,6 +320,31 @@ Screen.prototype.draw = function(start, end) { this.program.restoreCursor(); }; +Screen.prototype.focus = function(offset) { + if (!this.input.length || !offset) return; + var i = this.input.indexOf(this.focused); + if (!~i) return; + if (!this.input[i + offset]) { + if (offset > 0) { + while (offset--) if (++i > this.input.length - 1) i = 0; + } else { + offset = -offset; + while (offset--) if (--i < 0) i = this.input.length - 1; + } + } else { + i += offset; + } + return this.input[i].focus(); +}; + +Screen.prototype.focusPrev = function() { + return this.focus(-1); +}; + +Screen.prototype.focusNext = function() { + return this.focus(1); +}; + /** * Element */ @@ -280,14 +385,48 @@ function Element(options) { this.children = options.children || []; - this.clickable = this.clickable || false; - this.hover = this.hover || false; + if (options.clickable) { + this.screen._listenMouse(this); + } + + if (options.input) { + this.screen._listenKeys(this); + } this.content = options.content || ''; } Element.prototype.__proto__ = Node.prototype; +Element._addListener = Element.prototype.addListener; +Element.prototype.on = +Element.prototype.addListener = function(type, listener) { + if (type === 'mouse' + || type === 'click' + || type === 'hover' + || type === 'mousedown' + || type === 'mouseup' + || type === 'mousewheel' + || type === 'wheeldown' + || type === 'wheelup' + || type === 'mousemove' + || type === 'movement') { + this.screen._listenMouse(this); + } else if (type === 'keypress') { + this.screen._listenKeys(this); + } + return Element._addListener.apply(this, arguments); +}; + +Element.prototype.focus = function() { + var old = this.screen.focused; + this.screen.focused = this; + old.emit('blur', this); + this.emit('focus', old); + this.screen.emit('element blur', old, this); + this.screen.emit('element focus', old, this); +}; + Element.prototype.__defineGetter__('left', function() { var left = this.position.left; @@ -298,6 +437,10 @@ Element.prototype.__defineGetter__('left', function() { left = (this.parent.width - len) * left | 0; } + if (this.options.left == null && this.options.right != null) { + return this.screen.cols - this.width - this.right; + } + return (this.parent.left || 0) + left; }); @@ -315,6 +458,10 @@ Element.prototype.__defineGetter__('top', function() { top = (this.parent.height - len) * top | 0; } + if (this.options.top == null && this.options.bottom != null) { + return this.screen.rows - this.height - this.bottom; + } + return (this.parent.top || 0) + top; }); @@ -372,6 +519,10 @@ Element.prototype.__defineGetter__('rleft', function() { left = len * left | 0; } + if (this.options.left == null && this.options.right != null) { + return this.parent.width - this.width - this.right; + } + return left; }); @@ -389,6 +540,10 @@ Element.prototype.__defineGetter__('rtop', function() { top = len * top | 0; } + if (this.options.top == null && this.options.bottom != null) { + return this.parent.height - this.height - this.bottom; + } + return top; }); @@ -396,50 +551,6 @@ Element.prototype.__defineGetter__('rbottom', function() { return this.position.bottom; }); -/** - * Border Measures - */ - -Element.prototype.__defineGetter__('bheight', function() { - return this.height - (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('bwidth', function() { - return this.width - (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('bleft', function() { - return this.left + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('bright', function() { - return this.right + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('btop', function() { - return this.top + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('bbottom', function() { - return this.bottom + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('brleft', function() { - return this.rleft + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('brright', function() { - return this.rright + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('brtop', function() { - return this.rtop + (this.parent.border ? 1 : 0); -}); - -Element.prototype.__defineGetter__('brbottom', function() { - return this.rbottom + (this.parent.border ? 1 : 0); -}); - /** * Box */ @@ -470,8 +581,43 @@ Box.prototype.render = function() { yl = yi + this.height; } - for (yi = this.top; yi < yl; yi++) { + if (this.parent.childBase != null && ~this.parent.items.indexOf(this)) { + var rtop = this.rtop - (this.parent.border ? 1 : 0) + , visible = this.parent.height - (this.parent.border ? 2 : 0); + + yi -= this.parent.childBase; + // if (noOverflow) ... + yl = Math.min(yl, this.screen.rows - this.parent.bottom); + + if (rtop - this.parent.childBase < 0) { + return; + } + if (rtop - this.parent.childBase >= visible) { + return; + } + } + + // Scroll the content + if (this.childBase != null) { + var cb = this.childBase + , xx; + while (cb--) { + for (xx = this.left; xx < xl; xx++) ci++; + } + } + + var ret = { + xi: xi, + xl: xl, + yi: yi, + yl: yl + }; + + for (; yi < yl; yi++) { + if (!lines[yi]) continue; for (xi = this.left; xi < xl; xi++) { + cell = lines[yi][xi]; + if (!cell) continue; if (this.border && (yi === this.top || xi === this.left || yi === yl - 1 || xi === xl - 1)) { attr = ((this.border.bold << 18) + (this.border.underline << 18)) | (this.border.fg << 9) | this.border.bg; if (this.border.type === 'ascii') { @@ -493,7 +639,6 @@ Box.prototype.render = function() { attr = ((this.bold << 18) + (this.underline << 18)) | (this.fg << 9) | this.bg; ch = this.content[ci++] || ' '; } - cell = lines[yi][xi]; if (attr !== cell[0] || ch !== cell[1]) { lines[yi][xi][0] = attr; lines[yi][xi][1] = ch; @@ -506,10 +651,7 @@ Box.prototype.render = function() { el.render(); }); - //var o = { x: 0, y: 0 }; - //this.children.forEach(function(el) { - // el.render(o); - //}); + return ret; }; /** @@ -553,6 +695,8 @@ Text.prototype.render = function() { , visible = this.parent.height - (this.parent.border ? 2 : 0); yi -= this.parent.childBase; + // if (noOverflow) ... + yl = Math.min(yl, this.screen.rows - this.parent.bottom); if (rtop - this.parent.childBase < 0) { return; @@ -562,10 +706,19 @@ Text.prototype.render = function() { } } + var ret = { + xi: xi, + xl: xl, + yi: yi, + yl: yl + }; + + var dattr = ((this.bold << 18) + (this.underline << 18)) | (this.fg << 9) | this.bg; + for (; yi < yl; yi++) { for (xi = this.left; xi < xl; xi++) { cell = lines[yi][xi]; - attr = ((this.bold << 18) + (this.underline << 18)) | (this.fg << 9) | this.bg; + attr = dattr; ch = this.content[ci++]; if (!ch) { if (this.full) { @@ -586,25 +739,87 @@ Text.prototype.render = function() { } } } + + return ret; }; +/** + * Line + */ + +function Line(options) { + var orientation = options.orientation || 'vertical'; + delete options.orientation; + + if (orientation === 'vertical') { + options.width = 1; + } else { + options.height = 1; + } + + options.border = { + type: 'bg', + bg: options.bg || -1, + fg: options.fg || -1, + ch: !options.type || options.type === 'ascii' + ? orientation === 'horizontal' ? '─' : '│' + : options.ch || ' ' + }; + + delete options.bg; + delete options.fg; + delete options.ch; + + Box.call(this, options); +} + +Line.prototype.__proto__ = Box.prototype; + /** * ScrollableBox */ function ScrollableBox(options) { Box.call(this, options); + this.scrollable = true; this.childOffset = 0; this.childBase = 0; + this.baseLimit = options.baseLimit || Infinity; + this.alwaysScroll = options.alwaysScroll; } ScrollableBox.prototype.__proto__ = Box.prototype; +ScrollableBox.prototype.scroll = function(offset) { + var visible = this.height - (this.border ? 2 : 0); + if (this.alwaysScroll) { + // Semi-workaround + this.childOffset = offset > 0 + ? visible - 1 + offset + : offset; + } else { + this.childOffset += offset; + } + if (this.childOffset > visible - 1) { + var d = this.childOffset - (visible - 1); + this.childOffset -= d; + this.childBase += d; + } else if (this.childOffset < 0) { + var d = this.childOffset; + this.childOffset += -d; + this.childBase += d; + } + if (this.childBase < 0) this.childBase = 0; + else if (this.childBase > this.baseLimit) this.childBase = this.baseLimit; +}; + /** * List */ function List(options) { + var self = this; + ScrollableBox.call(this, options); this.items = []; @@ -618,6 +833,16 @@ function List(options) { if (this.selectedBg === -1) this.selectedBg = exports.NORMAL; if (this.selectedFg === -1) this.selectedFg = exports.NORMAL; + this.on('wheeldown', function(data) { + self.select(self.selected + 2); + self.screen.render(); + }); + + this.on('wheelup', function(data) { + self.select(self.selected - 2); + self.screen.render(); + }); + if (options.items) { options.items.forEach(this.add.bind(this)); } @@ -630,6 +855,8 @@ function List(options) { List.prototype.__proto__ = ScrollableBox.prototype; List.prototype.add = function(item) { + var self = this; + var item = new Text({ screen: this.screen, parent: this, @@ -641,8 +868,14 @@ List.prototype.add = function(item) { full: true, height: 1 }); + this.append(item); this.items.push(item); + + item.on('click', function(data) { + self.select(item); + self.screen.render(); + }); }; List.prototype._remove = List.prototype.remove; @@ -688,6 +921,7 @@ List.prototype.select = function(index) { this.items[index].underline = this.selectedUnderline; } +/* var diff = index - this.selected; this.childOffset += diff; @@ -703,6 +937,11 @@ List.prototype.select = function(index) { this.childOffset += -d; this.childBase += d; } +*/ + + var diff = index - this.selected; + this.selected = index; + this.scroll(diff); }; List.prototype.move = function(offset) { @@ -717,6 +956,17 @@ List.prototype.down = function(offset) { this.move(offset || 1); }; +/** + * ScrollableText + */ + +function ScrollableText(options) { + options.alwaysScroll = true; + ScrollableBox.call(this, options); +} + +ScrollableText.prototype.__proto__ = ScrollableBox.prototype; + /** * Input */ @@ -768,27 +1018,41 @@ ProgressBar.prototype.__proto__ = Input.prototype; ProgressBar.prototype._render = ProgressBar.prototype.render; ProgressBar.prototype.render = function() { - this._render(); + //var hash = this.filled + ':' + dattr; + //if (this._hash === hash) return; + //this._hash = hash; - var x, y; + var ret = this._render(); - var h = this.height - , w = this.width * (this.filled / 100) - , l = this.left - , t = this.top; + var xi = ret.xi + , xl = ret.xl + , yi = ret.yi + , yl = ret.yl; - for (y = t; y < t + h; y++) { - for (x = l; x < l + w; x++) { - attr = ((this.bold << 18) + (this.underline << 18)) | (this.barFg << 9) | this.barBg; + var lines = this.screen.lines; + + if (this.border) xi++, yi++, xl--, yl--; + + xl = xi + ((xl - xi) * (this.filled / 100)) | 0; + + var dattr = ((this.bold << 18) + (this.underline << 18)) | (this.barFg << 9) | this.barBg; + + for (y = yi; y < yl; y++) { + if (!lines[y]) continue; + for (x = xi; x < xl; x++) { + attr = dattr; ch = this.ch; - cell = lines[yi][xi]; + cell = lines[y][x]; + if (!cell) continue; if (attr !== cell[0] || ch !== cell[1]) { - lines[yi][xi][0] = attr; - lines[yi][xi][1] = ch; - lines[yi].dirty = true; + lines[y][x][0] = attr; + lines[y][x][1] = ch; + lines[y].dirty = true; } } } + + return ret; }; ProgressBar.prototype.progress = function(filled) { @@ -810,8 +1074,10 @@ exports.NORMAL = 0x1ff; exports.Screen = Screen; exports.Box = Box; exports.Text = Text; +exports.Line = Line; exports.ScrollableBox = ScrollableBox; exports.List = List; +exports.ScrollableText = ScrollableText; exports.Input = Input; exports.Textbox = Textbox; exports.Button = Button; diff --git a/test/widget.js b/test/widget.js index 4778f0f..533451d 100644 --- a/test/widget.js +++ b/test/widget.js @@ -2,10 +2,30 @@ var blessed = require('blessed') , program = blessed() , screen; +program.alternateBuffer(); +program.hideCursor(); + screen = new blessed.Screen({ program: program }); +screen.append(new blessed.Text({ + screen: screen, + parent: screen, + top: 0, + left: 2, + content: 'Welcome to my program' +})); + +screen.append(new blessed.Line({ + screen: screen, + parent: screen, + orientation: 'horizontal', + top: 1, + left: 0, + right: 0 +})); + /* screen.append(new blessed.Box({ screen: screen, @@ -43,7 +63,7 @@ screen.children[0].append(new blessed.Box({ })); */ -screen.append(new blessed.List({ +var list = new blessed.List({ screen: screen, parent: screen, fg: 4, @@ -70,25 +90,97 @@ screen.append(new blessed.List({ 'nine', 'ten' ] -})); +}); -screen.children[0].prepend(new blessed.Text({ +screen.append(list); + +list.prepend(new blessed.Text({ screen: screen, - parent: screen.children[0], + parent: list, left: 2, content: ' My list ' })); -program.on('keypress', function(ch, key) { +list.on('keypress', function(ch, key) { if (key.name === 'up') { - screen.children[0].up(); + list.up(); screen.render(); return; } else if (key.name === 'down') { - screen.children[0].down(); + list.down(); screen.render(); return; } +}); + +var progress = new blessed.ProgressBar({ + screen: screen, + parent: screen, + fg: 4, + bg: -1, + barBg: -1, + barFg: 4, + border: { + type: 'ascii', + fg: -1, + bg: -1 + }, + ch: ':', + width: '50%', + height: 3, + right: 0, + bottom: 0, + filled: 50 +}); + +screen.append(progress); + +var lorem = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; + +var stext = new blessed.ScrollableText({ + screen: screen, + parent: screen, + content: lorem, + fg: 4, + bg: -1, + barBg: -1, + barFg: 4, + border: { + type: 'ascii', + fg: -1, + bg: -1 + }, + width: '50%', + height: 4, + left: 0, + bottom: 0 +}); + +screen.append(stext); +stext.on('keypress', function(ch, key) { + if (key.name === 'up') { + stext.scroll(-1); + screen.render(); + return; + } else if (key.name === 'down') { + stext.scroll(1); + screen.render(); + return; + } +}); + +screen.on('element focus', function(old, cur) { + if (old.border) old.border.fg = 0x1ff; + if (cur.border) cur.border.fg = 2; + screen.render(); +}); + +program.on('keypress', function(ch, key) { + if (key.name === 'tab') { + return key.shift + ? screen.focusPrev() + : screen.focusNext(); + } if (key.name === 'escape' || key.name === 'q') { program.disableMouse(); program.clear(); @@ -98,7 +190,13 @@ program.on('keypress', function(ch, key) { } }); -program.alternateBuffer(); -program.hideCursor(); +list.focus(); screen.render(); + +(function fill() { + if (progress.filled === 100) progress.filled = 0; + progress.progress(2); + screen.render(); + setTimeout(fill, 300); +})();