text: /*
   * Scrollablebox.js - scrollable box element for blessed
   */
  
  /*
   * Modules
   */
  
  var Node = require("./node");
  var Box = require("./box");
  
  /*
   * ScrollableBox
   */
  
  function ScrollableBox(options) {
  	var self = this;
  
  	if (!(this instanceof Node)) {
  		return new ScrollableBox(options);
  	}
  
  	options = options || {};
  
  	Box.call(this, options);
  
  	if (options.scrollable === false) {
  		return this;
  	}
  
  	this.scrollable = true;
  	this.childOffset = 0;
  	this.childBase = 0;
  	this.baseLimit = options.baseLimit || Infinity;
  	this.alwaysScroll = options.alwaysScroll;
  
  	this.scrollbar = options.scrollbar;
  	if (this.scrollbar) {
  		this.scrollbar.ch = this.scrollbar.ch || " ";
  		this.style.scrollbar = this.style.scrollbar || this.scrollbar.style;
  		if (!this.style.scrollbar) {
  			this.style.scrollbar = {};
  			this.style.scrollbar.fg = this.scrollbar.fg;
  			this.style.scrollbar.bg = this.scrollbar.bg;
  			this.style.scrollbar.bold = this.scrollbar.bold;
  			this.style.scrollbar.underline = this.scrollbar.underline;
  			this.style.scrollbar.inverse = this.scrollbar.inverse;
  			this.style.scrollbar.invisible = this.scrollbar.invisible;
  		}
  		// This.scrollbar.style = this.style.scrollbar;
  		if (this.track || this.scrollbar.track) {
  			this.track = this.scrollbar.track || this.track;
  			this.style.track = this.style.scrollbar.track || this.style.track;
  			this.track.ch = this.track.ch || " ";
  			this.style.track = this.style.track || this.track.style;
  			if (!this.style.track) {
  				this.style.track = {};
  				this.style.track.fg = this.track.fg;
  				this.style.track.bg = this.track.bg;
  				this.style.track.bold = this.track.bold;
  				this.style.track.underline = this.track.underline;
  				this.style.track.inverse = this.track.inverse;
  				this.style.track.invisible = this.track.invisible;
  			}
  			this.track.style = this.style.track;
  		}
  		// Allow controlling of the scrollbar via the mouse:
  		if (options.mouse) {
  			this.on("mousedown", function(data) {
  				if (self._scrollingBar) {
  					// Do not allow dragging on the scrollbar:
  					delete self.screen._dragging;
  					delete self._drag;
  					return;
  				}
  				var x = data.x - self.aleft;
  				var y = data.y - self.atop;
  				if (x === self.width - self.iright - 1) {
  					// Do not allow dragging on the scrollbar:
  					delete self.screen._dragging;
  					delete self._drag;
  					var perc = (y - self.itop) / (self.height - self.iheight);
  					self.setScrollPerc(perc * 100 | 0);
  					self.screen.render();
  					var smd, smu;
  					self._scrollingBar = true;
  					self.onScreenEvent("mousedown", smd = function(data) {
  						var y = data.y - self.atop;
  						var perc = y / self.height;
  						self.setScrollPerc(perc * 100 | 0);
  						self.screen.render();
  					});
  					/*
             * If mouseup occurs out of the window, no mouseup event fires, and
             * scrollbar will drag again on mousedown until another mouseup
             * occurs.
             */
  					self.onScreenEvent("mouseup", smu = function() {
  						self._scrollingBar = false;
  						self.removeScreenEvent("mousedown", smd);
  						self.removeScreenEvent("mouseup", smu);
  					});
  				}
  			});
  		}
  	}
  
  	if (options.mouse) {
  		this.on("wheeldown", function() {
  			self.scroll(self.height / 2 | 0 || 1);
  			self.screen.render();
  		});
  		this.on("wheelup", function() {
  			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.scrollTo(0);
          self.screen.render();
          return;
        }
        if (options.vi && key.name === 'g' && key.shift) {
          self.scrollTo(self.getScrollHeight());
          self.screen.render();
          
        }
      });
  	}
  
  	this.on("parsed content", function() {
  		self._recalculateIndex();
  	});
  
  	self._recalculateIndex();
  }
  
  ScrollableBox.prototype.__proto__ = Box.prototype;
  
  ScrollableBox.prototype.type = "scrollable-box";
  
  // XXX Potentially use this in place of scrollable checks elsewhere.
  ScrollableBox.prototype.__defineGetter__("reallyScrollable", function() {
  	if (this.shrink) {return this.scrollable;}
  	return this.getScrollHeight() > this.height;
  });
  
  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._isList) {
  		return this.items ? this.items.length : 0;
  	}
  
  	if (this.lpos && this.lpos._scrollBottom) {
  		return this.lpos._scrollBottom;
  	}
  
  	var bottom = this.children.reduce(function(current, el) {
  		/*
       * el.height alone does not calculate the shrunken height, we need to use
       * getCoords. A shrunken box inside a scrollable element will not grow any
       * larger than the scrollable element's context regardless of how much
       * content is in the shrunken box, unless we do this (call getCoords
       * without the scrollable calculation):
       * See: $ node test/widget-shrink-fail-2.js
       */
  		if (!el.detached) {
  			var lpos = el._getCoords(false, true);
  			if (lpos) {
  				return Math.max(current, el.rtop + (lpos.yl - lpos.yi));
  			}
  		}
  		return Math.max(current, el.rtop + el.height);
  	}, 0);
  
  	/*
     * XXX Use this? Makes .getScrollHeight() useless!
     * if (bottom < this._clines.length) bottom = this._clines.length;
     */
  
  	if (this.lpos) {this.lpos._scrollBottom = bottom;}
  
  	return bottom;
  };
  
  ScrollableBox.prototype.setScroll =
  ScrollableBox.prototype.scrollTo = function(offset, always) {
  	/*
     * XXX
     * At first, this appeared to account for the first new calculation of childBase:
     */
  	this.scroll(0);
  	return this.scroll(offset - (this.childBase + this.childOffset), always);
  };
  
  ScrollableBox.prototype.getScroll = function() {
  	return this.childBase + this.childOffset;
  };
  
  ScrollableBox.prototype.scroll = function(offset, always) {
  	if (!this.scrollable) {return;}
  
  	if (this.detached) {return;}
  
  	// Handle scrolling.
  	var visible = this.height - this.iheight,
       base = this.childBase,
       d,
       p,
       t,
       b,
       max,
       emax;
  
  	if (this.alwaysScroll || always) {
  		// Semi-workaround
  		this.childOffset = offset > 0
  			? visible - 1 + offset
  			: offset;
  	} else {
  		this.childOffset += offset;
  	}
  
  	if (this.childOffset > visible - 1) {
  		d = this.childOffset - (visible - 1);
  		this.childOffset -= d;
  		this.childBase += d;
  	} else if (this.childOffset < 0) {
  		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;
  	}
  
  	/*
     * Find max "bottom" value for
     * content and descendant elements.
     * Scroll the content if necessary.
     */
  	if (this.childBase === base) {
  		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();
  
  	/*
     * XXX
     * max = this.getScrollHeight() - (this.height - this.iheight);
     */
  
  	max = this._clines.length - (this.height - this.iheight);
  	if (max < 0) {max = 0;}
  	emax = this._scrollBottom() - (this.height - this.iheight);
  	if (emax < 0) {emax = 0;}
  
  	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;
  	}
  
  	// Optimize scrolling with CSR + IL/DL.
  	p = this.lpos;
  	/*
     * Only really need _getCoords() if we want
     * to allow nestable scrolling elements...
     * or if we **really** want shrinkable
     * scrolling elements.
     * p = this._getCoords();
     */
  	if (p && this.childBase !== base && this.screen.cleanSides(this)) {
  		t = p.yi + this.itop;
  		b = p.yl - this.ibottom - 1;
  		d = this.childBase - base;
  
  		if (d > 0 && d < visible) {
  			// Scrolled down
  			this.screen.deleteLine(d, t, t, b);
  		} else if (d < 0 && -d < visible) {
  			// Scrolled up
  			d = -d;
  			this.screen.insertLine(d, t, t, b);
  		}
  	}
  
  	return this.emit("scroll");
  };
  
  ScrollableBox.prototype._recalculateIndex = function() {
  	var max, emax;
  
  	if (this.detached || !this.scrollable) {
  		return 0;
  	}
  
  	/*
     * XXX
     * max = this.getScrollHeight() - (this.height - this.iheight);
     */
  
  	max = this._clines.length - (this.height - this.iheight);
  	if (max < 0) {max = 0;}
  	emax = this._scrollBottom() - (this.height - this.iheight);
  	if (emax < 0) {emax = 0;}
  
  	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;
  	}
  };
  
  ScrollableBox.prototype.resetScroll = function() {
  	if (!this.scrollable) {return;}
  	this.childOffset = 0;
  	this.childBase = 0;
  	return this.emit("scroll");
  };
  
  ScrollableBox.prototype.getScrollHeight = function() {
  	return Math.max(this._clines.length, this._scrollBottom());
  };
  
  ScrollableBox.prototype.getScrollPerc = function(s) {
  	var pos = this.lpos || this._getCoords();
  	if (!pos) {return s ? -1 : 0;}
  
  	var height = pos.yl - pos.yi - this.iheight,
       i = this.getScrollHeight(),
       p;
  
  	if (height < i) {
  		if (this.alwaysScroll) {
  			p = this.childBase / (i - height);
  		} else {
  			p = (this.childBase + this.childOffset) / (i - 1);
  		}
  		return p * 100;
  	}
  
  	return s ? -1 : 0;
  };
  
  ScrollableBox.prototype.setScrollPerc = function(i) {
  	/*
     * XXX
     * var m = this.getScrollHeight();
     */
  	var m = Math.max(this._clines.length, this._scrollBottom());
  	return this.scrollTo(i / 100 * m | 0);
  };
  
  /*
   * Expose
   */
  
  module.exports = ScrollableBox;
  