diff --git a/lib/widget.js b/lib/widget.js index be3150a..8967662 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -1633,7 +1633,7 @@ Element.prototype._focus = function() { , visible = el.height - el.iheight; if (ryi < el.childBase) { - el.scrollTo(ryi); + el.scrollTo(ryi - 0); this.screen.render(); } else if (ryi >= el.childBase + visible) { el.scrollTo(ryi); @@ -1720,7 +1720,7 @@ Element.prototype._parseAttr = function(lines) { return; } - if (this.contentIndex == null || this.childBase == null) { + if (this.childBase == null) { return; } @@ -1896,13 +1896,19 @@ Element.prototype.removeKey = function() { }; Element.prototype.clearPos = function() { - if (this.lpos && !this.lpos.cleared) { - // optimize by making sure we only clear once. - this.lpos.cleared = true; - this.screen.clearRegion( - this.lpos.xi, this.lpos.xl, - this.lpos.yi, this.lpos.yl); - } + // if (this.lpos && !this.lpos.cleared) { + // // optimize by making sure we only clear once. + // this.lpos.cleared = true; + // this.screen.clearRegion( + // this.lpos.xi, this.lpos.xl, + // this.lpos.yi, this.lpos.yl); + // } + if (this.detached) return; + var lpos = this._getCoords(); + if (!lpos) return; + this.screen.clearRegion( + lpos.xi, lpos.xl, + lpos.yi, lpos.yl); }; /** @@ -2412,27 +2418,52 @@ Box.prototype._getCoords = function(get) { // Check to make sure we're visible and // inside of the visible scroll area. + // NOTE: Lists have a property where only + // the list items are obfuscated. if (el && (!el.items || ~el.items.indexOf(this))) { + // This is unfortunate because this._getTop(true) is based on its + // this.parent.lpos.yi - which was saved after it's render() **with the + // childBase subtracted!!!!*** This means nested elements within + // a scrollable box are screwy unless we do this. + var cb = el.childBase; + if (get) { + var el_ = this; + // Do we need to get every parent? + while (el_ = el_.parent) { + if (el_ === el) break; + yi += el_.lpos.cb; + yl += el_.lpos.cb; + } + } + ryi = yi - el._getTop(get) - el.itop; ryl = yl - el._getTop(get) - el.ibottom; visible = el._getHeight(get) - el.iheight; + // if (ryi < el.childBase) { + // if (ryl > el.childBase) { + // // Is partially covered above. + // v = ryl - el.childBase; + // yi += (ryl - ryi) - v; + // } else { + // // Is above. + // return; + // } + // } else if (ryi >= el.childBase + visible) { + // // Is below. + // return; + // } else if (ryl >= el.childBase + visible) { + // // Is partially covered below. + // v = el.childBase + visible + (yl - yi) - ryl; + // yl = yi + v; + // } + if (ryi < el.childBase) { - if (ryl > el.childBase) { - // Is partially covered above. - v = ryl - el.childBase; - yi += (ryl - ryi) - v; - } else { - // Is above. - return; - } + // Is above. + return; } else if (ryi >= el.childBase + visible) { // Is below. return; - } else if (ryl >= el.childBase + visible) { - // Is partially covered below. - v = el.childBase + visible + (yl - yi) - ryl; - yl = yi + v; } yi -= el.childBase; @@ -2451,7 +2482,8 @@ Box.prototype._getCoords = function(get) { xi: xi, xl: xl, yi: yi, - yl: yl + yl: yl, + cb: cb || 0 }; }; @@ -2488,13 +2520,8 @@ Box.prototype.render = function() { // If we're in a scrollable text box, check to // see which attributes this line starts with. - if (this.contentIndex != null && this.childBase != null) { - // if (this._clines.length > this.childBase) { - // attr = this._clines.attr[this.childBase]; - // } else { - // attr = this._clines.attr[this._clines.length - 1]; - // } - attr = this._clines.attr[this.childBase]; + if (this.childBase != null) { + attr = this._clines.attr[Math.min(this.childBase, this._clines.length - 1)]; } if (this.border) xi++, xl--, yi++, yl--; @@ -2582,18 +2609,14 @@ Box.prototype.render = function() { // Draw the scrollbar. if (this.scrollbar) { - i = this.type === 'scrollable-text' - ? this._clines.length + 1 - : this._scrollBottom() + (yl - yi) + 1; - //i = Math.max(this._clines.length + 1, this._scrollBottom() + (yl - yi) + 1); + i = Math.max(this._clines.length + 1, + this._scrollBottom() + (yl - yi) + 1); } if (this.scrollbar && (yl - yi) < i) { i -= yl - yi; x = xl - 1; if (this.scrollbar.ignoreBorder && this.border) x++; - y = this.selected == null - ? this.childBase - : this.selected; + y = this.childBase + this.childOffset; y = y / i; y = yi + ((yl - yi) * y | 0); cell = lines[y] && lines[y][x]; @@ -2871,6 +2894,8 @@ Line.prototype.type = 'line'; */ function ScrollableBox(options) { + var self = this; + if (!(this instanceof ScrollableBox)) { return new ScrollableBox(options); } @@ -2882,6 +2907,7 @@ function ScrollableBox(options) { this.scrollable = true; this.childOffset = 0; this.childBase = 0; + this.contentIndex = 0; this.baseLimit = options.baseLimit || Infinity; this.alwaysScroll = options.alwaysScroll; @@ -2900,6 +2926,68 @@ function ScrollableBox(options) { } this.scrollbar.style = this.style.scrollbar; } + + if (options.mouse) { + this.on('wheeldown', function(data) { + self.scroll(self.height / 2 | 0 || 1); + self.screen.render(); + }); + this.on('wheelup', function(data) { + self.scroll(-(self.height / 2 | 0) || -1); + self.screen.render(); + }); + } + + if (options.keys && !options.ignoreKeys) { + this.on('keypress', function(ch, key) { + if (key.name === 'up' || (options.vi && key.name === 'k')) { + self.scroll(-1); + self.screen.render(); + return; + } + if (key.name === 'down' || (options.vi && key.name === 'j')) { + self.scroll(1); + self.screen.render(); + return; + } + if (options.vi && key.name === 'u' && key.ctrl) { + self.scroll(-(self.height / 2 | 0) || -1); + self.screen.render(); + return; + } + if (options.vi && key.name === 'd' && key.ctrl) { + self.scroll(self.height / 2 | 0 || 1); + self.screen.render(); + return; + } + if (options.vi && key.name === 'b' && key.ctrl) { + self.scroll(-self.height || -1); + self.screen.render(); + return; + } + if (options.vi && key.name === 'f' && key.ctrl) { + self.scroll(self.height || 1); + self.screen.render(); + return; + } + if (options.vi && key.name === 'g' && !key.shift) { + self.scroll(-self._clines.length); + self.screen.render(); + return; + } + if (options.vi && key.name === 'g' && key.shift) { + self.scroll(self._clines.length); + self.screen.render(); + return; + } + }); + } + + this.on('parsed content', function() { + self._recalculateIndex(); + }); + + self._recalculateIndex(); } ScrollableBox.prototype.__proto__ = Box.prototype; @@ -2907,6 +2995,8 @@ ScrollableBox.prototype.__proto__ = Box.prototype; ScrollableBox.prototype.type = 'scrollable-box'; ScrollableBox.prototype._scrollBottom = function() { + if (!this.scrollable) return 0; + // We could just calculate the children, but we can // optimize for lists by just returning the items.length. if (this.type === 'list') { @@ -2921,6 +3011,7 @@ ScrollableBox.prototype._scrollBottom = function() { return Math.max(current, el.rtop + el.height); }, 0); + //bottom -= this.height; if (this.lpos) this.lpos._scrollBottom = bottom; return bottom; @@ -2931,6 +3022,9 @@ ScrollableBox.prototype.scrollTo = function(offset) { }; ScrollableBox.prototype.scroll = function(offset) { + if (!this.scrollable) return; + + // Handle scrolling. var visible = this.height - this.iheight , base = this.childBase , d @@ -2938,8 +3032,6 @@ ScrollableBox.prototype.scroll = function(offset) { , t , b; - // Maybe do for lists: - // if (this.items) visible = Math.min(this.items.length, visible); if (this.alwaysScroll) { // Semi-workaround this.childOffset = offset > 0 @@ -2965,12 +3057,47 @@ ScrollableBox.prototype.scroll = function(offset) { this.childBase = this.baseLimit; } - // This code works for scrollable box and list, but it - // makes scrollable text choke because it can't diff properly. - if (this.type !== 'scrollable-text') { - var bottom = this._scrollBottom(); - if (this.childBase > bottom) { - this.childBase = bottom; + // Find max "bottom" value for + // content and descendant elements. + // Scroll the content if necessary. + var diff = this.childBase - base + , w + , i + , max + , emax + , t; + + if (diff === 0) { + return this.emit('scroll'); + } + + // When scrolling text, we want to be able to handle SGR codes as well as line + // feeds. This allows us to take preformatted text output from other programs + // and put it in a scrollable text box. + this.parseContent(); + + max = this._clines.length - (this.height - this.iheight); + if (max < 0) max = 0; + emax = this._scrollBottom() - (this.height - this.iheight); + + this.childBase = Math.min(this.childBase, Math.max(emax, max)); + diff = this.childBase - base; + + if (this.childBase < 0) { + this.childBase = 0; + } else if (this.childBase > this.baseLimit) { + this.childBase = this.baseLimit; + } + + if (diff > 0) { + for (i = base; i < this.childBase; i++) { + if (!this._clines[i]) continue; + this.contentIndex += this._clines[i].length + 1; + } + } else { + for (i = base - 1; i >= this.childBase; i--) { + if (!this._clines[i]) continue; + this.contentIndex -= this._clines[i].length + 1; } } @@ -2981,10 +3108,6 @@ ScrollableBox.prototype.scroll = function(offset) { b = p.yl - this.ibottom - 1; d = this.childBase - base; - // var i = p.xi + this.ileft; - // var attr = this.screen.olines[t][i][0]; - // this.screen.program.write(this.screen.codeAttr(attr, this.screen)); - if (d > 0 && d < visible) { // scrolled down this.screen.deleteLine(d, t, t, b); @@ -2993,16 +3116,46 @@ ScrollableBox.prototype.scroll = function(offset) { d = -d; this.screen.insertLine(d, t, t, b); } - - // this.screen.program.sgr0(); } - this.emit('scroll'); + return this.emit('scroll'); +}; + +ScrollableBox.prototype._recalculateIndex = function() { + var max, emax, i, t; + + if (this.detached || !this.scrollabe) { + this.contentIndex = 0; + return 0; + } + + max = this._clines.length - (this.height - this.iheight); + if (max < 0) max = 0; + emax = this._scrollBottom() - (this.height - this.iheight); + + this.childBase = Math.min(this.childBase, Math.max(emax, max)); + + if (this.childBase < 0) { + this.childBase = 0; + } else if (this.childBase > this.baseLimit) { + this.childBase = this.baseLimit; + } + + for (i = 0, t = 0; i < this.childBase; i++) { + if (!this._clines[i]) continue; + t += this._clines[i].length + 1; + } + + this.contentIndex = t; + + return t; }; ScrollableBox.prototype.resetScroll = function() { + this.contentIndex = 0; this.childOffset = 0; this.childBase = 0; + return this.emit('scroll'); }; /** @@ -3018,6 +3171,7 @@ function List(options) { options = options || {}; + options.ignoreKeys = true; ScrollableBox.call(this, options); this.value = ''; @@ -3080,7 +3234,7 @@ function List(options) { // this.select(0); } - if (this.mouse) { + if (options.mouse) { this.on('wheeldown', function(data) { self.select(self.selected + 2); self.screen.render(); @@ -3289,10 +3443,8 @@ List.prototype.select = function(index) { if (this.selected === index && this._listInitialized) return; this._listInitialized = true; - //var diff = index - this.selected; this.selected = index; this.value = this.ritems[this.selected]; - //this.scroll(diff); this.scrollTo(this.selected); }; @@ -3313,153 +3465,18 @@ List.prototype.down = function(offset) { */ function ScrollableText(options) { - var self = this; - if (!(this instanceof ScrollableText)) { return new ScrollableText(options); } - options = options || {}; - options.alwaysScroll = true; - ScrollableBox.call(this, options); - - if (options.mouse) { - var self = this; - this.on('wheeldown', function(data) { - self.scroll(self.height / 2 | 0 || 1); - self.screen.render(); - }); - this.on('wheelup', function(data) { - self.scroll(-(self.height / 2 | 0) || -1); - self.screen.render(); - }); - } - - if (options.keys) { - this.on('keypress', function(ch, key) { - if (key.name === 'up' || (options.vi && key.name === 'k')) { - self.scroll(-1); - self.screen.render(); - return; - } - if (key.name === 'down' || (options.vi && key.name === 'j')) { - self.scroll(1); - self.screen.render(); - return; - } - if (options.vi && key.name === 'u' && key.ctrl) { - self.scroll(-(self.height / 2 | 0) || -1); - self.screen.render(); - return; - } - if (options.vi && key.name === 'd' && key.ctrl) { - self.scroll(self.height / 2 | 0 || 1); - self.screen.render(); - return; - } - if (options.vi && key.name === 'b' && key.ctrl) { - self.scroll(-self.height || -1); - self.screen.render(); - return; - } - if (options.vi && key.name === 'f' && key.ctrl) { - self.scroll(self.height || 1); - self.screen.render(); - return; - } - if (options.vi && key.name === 'g' && !key.shift) { - self.scroll(-self._clines.length); - self.screen.render(); - return; - } - if (options.vi && key.name === 'g' && key.shift) { - self.scroll(self._clines.length); - self.screen.render(); - return; - } - }); - } - - this.on('parsed content', function() { - self._recalculateIndex(); - }); - - self._recalculateIndex(); } ScrollableText.prototype.__proto__ = ScrollableBox.prototype; ScrollableText.prototype.type = 'scrollable-text'; -ScrollableText.prototype._scroll = ScrollableText.prototype.scroll; -ScrollableText.prototype.scroll = function(offset) { - var base = this.childBase - , ret = this._scroll(offset) - , cb = this.childBase - , diff = cb - base - , w - , i - , max - , t; - - if (diff === 0) return ret; - - // When scrolling text, we want to be able to handle SGR codes as well as line - // feeds. This allows us to take preformatted text output from other programs - // and put it in a scrollable text box. - if (this.content != null) { - this.parseContent(); - - max = this._clines.length - (this.height - this.iheight); - if (max < 0) max = 0; - - if (cb > max) { - this.childBase = cb = max; - diff = cb - base; - } - - // this.childBase = Math.max(this.childBase, max); - // cb = this.childBase; - // diff = cb - base; - - if (diff > 0) { - for (i = base; i < cb; i++) { - // if (!this._clines[i]) continue; - this.contentIndex += this._clines[i].length + 1; - } - } else { - for (i = base - 1; i >= cb; i--) { - // if (!this._clines[i]) continue; - this.contentIndex -= this._clines[i].length + 1; - } - } - } - - return ret; -}; - -ScrollableText.prototype._recalculateIndex = function() { - if (this.detached) return; - - var max = this._clines.length - (this.height - this.iheight); - if (max < 0) max = 0; - - if (this.childBase > max) { - this.childBase = max; - } - - //this.childBase = Math.max(this.childBase, max); - - for (var i = 0, t = 0; i < this.childBase; i++) { - // if (!this._clines[i]) continue; - t += this._clines[i].length + 1; - } - - this.contentIndex = t; -}; - /** * Form */ @@ -3473,6 +3490,7 @@ function Form(options) { options = options || {}; + options.ignoreKeys = true; ScrollableBox.call(this, options); if (options.keys) { @@ -3723,8 +3741,12 @@ Textbox.prototype.type = 'textbox'; Textbox.prototype.updateCursor = function() { if (this.screen.focused !== this) return; - this.screen.program.cup(this.top + this.itop, - this.left + this.ileft + this.value.length); + //this.screen.program.cup(this.top + this.itop, + // this.left + this.ileft + this.value.length); + var lpos = this._getCoords(); + if (!lpos) return; + this.screen.program.cup(lpos.yi + this.itop, + lpos.xi + this.ileft + this.value.length); }; Textbox.prototype.input = @@ -3887,6 +3909,9 @@ Textarea.prototype.updateCursor = function() { return; } + var lpos = this.getCoords(); + if (!lpos) return; + var last = this._clines[this._clines.length-1] , program = this.screen.program , line @@ -3903,10 +3928,10 @@ Textarea.prototype.updateCursor = function() { line = Math.min( this._clines.length - 1 - this.childBase, - this.height - this.iheight - 1); + (lpos.yl - lpos.yi) - this.iheight - 1); - cy = this.top + this.itop + line; - cx = this.left + this.ileft + last.length; + cy = lpos.yi + this.itop + line; + cx = lpos.xi + this.ileft + last.length; if (cy === program.y && cx === program.x) { return; @@ -4421,9 +4446,10 @@ function Checkbox(options) { } this.on('focus', function(old) { - if (!self.lpos) return; + var lpos = self._getCoords(); + if (!lpos) return; self.screen.program.saveCursor(); - self.screen.program.cup(self.lpos.yi, self.lpos.xi + 1); + self.screen.program.cup(lpos.yi, lpos.xi + 1); self.screen.program.showCursor(); }); @@ -4439,7 +4465,7 @@ Checkbox.prototype.type = 'checkbox'; Checkbox.prototype._render = Checkbox.prototype.render; Checkbox.prototype.render = function() { - if (!this._getCoords(true)) return; + //if (!this._getCoords(true)) return; if (this.type === 'radio-button') { this.setContent('(' + (this.checked ? '*' : ' ') + ') ' + this.text); } else { @@ -4501,8 +4527,15 @@ function RadioButton(options) { Checkbox.call(this, options); this.on('check', function() { - if (self.parent.type !== 'radio-set') return; - self.parent.children.forEach(function(el) { + var el = self; + while (el = el.parent) { + if (self.parent.type === 'radio-set' + || self.parent.type === 'form') { + break; + } + } + if (!el) el = self.parent; + el.children.forEach(function(el) { if (el === self) return; el.uncheck(); }); diff --git a/test/widget-form.js b/test/widget-form.js index def57b8..04288c6 100644 --- a/test/widget-form.js +++ b/test/widget-form.js @@ -9,9 +9,19 @@ var form = blessed.form({ left: 0, top: 0, width: '100%', - height: 5, + height: 12, bg: 'green', - content: 'foobar' + content: 'foobar', + border: { + type: 'ch', + ch: ' ', + style: { inverse: true } + }, + scrollbar: { + ch: ' ', + inverse: true + } + //alwaysScroll: true }); form.on('submit', function(data) { @@ -19,10 +29,20 @@ form.on('submit', function(data) { screen.render(); }); +form.key('d', function() { + form.scroll(1); + screen.render(); +}); + +form.key('u', function() { + form.scroll(-1); + screen.render(); +}); + var set = blessed.radioset({ parent: form, - left: 0, - top: 0, + left: 1, + top: 1, shrink: true, //padding: 1, //content: 'f', @@ -63,7 +83,7 @@ var text = blessed.textbox({ height: 1, width: 20, left: 1, - top: 2, + top: 3, name: 'text' }); @@ -79,7 +99,7 @@ var check = blessed.checkbox({ bg: 'magenta', height: 1, left: 28, - top: 0, + top: 1, name: 'check', content: 'check' }); @@ -92,7 +112,7 @@ var check2 = blessed.checkbox({ bg: 'magenta', height: 1, left: 28, - top: 10, + top: 14, name: 'foooooooo2', content: 'foooooooo2' }); @@ -106,8 +126,8 @@ var submit = blessed.button({ left: 1, right: 1 }, - left: 30, - top: 2, + left: 29, + top: 3, shrink: true, name: 'submit', content: 'submit', @@ -128,7 +148,7 @@ var output = blessed.scrollabletext({ mouse: true, keys: true, left: 0, - top: 5, + top: 12, width: '100%', bg: 'red', content: 'foobar' diff --git a/test/widget.js b/test/widget.js index 5833663..59afb29 100644 --- a/test/widget.js +++ b/test/widget.js @@ -150,7 +150,10 @@ var stext = blessed.scrollabletext({ //height: 4, height: 6, left: 0, - bottom: 0 + bottom: 0, + scrollbar: { + inverse: true + } }); screen.append(stext);