improve position, attribute parsing. misc.

This commit is contained in:
Christopher Jeffrey 2013-07-18 20:55:16 -05:00
parent 3ae01237be
commit df317bb472
2 changed files with 289 additions and 222 deletions

View File

@ -257,6 +257,7 @@ The base element.
(`0-100%`), or keyword (`half` or `shrink`).
- **left, right, top, bottom** - offsets of the element **relative to its
parent**. can be a number, percentage (`0-100%`), or keyword (`center`).
`right` and `bottom` do not accept keywords.
##### Properties:

View File

@ -9,6 +9,7 @@
*/
var EventEmitter = require('events').EventEmitter
, assert = require('assert')
, path = require('path')
, fs = require('fs')
, colors = require('./colors')
@ -150,6 +151,39 @@ Node.prototype.detach = function() {
if (this.parent) this.parent.remove(this);
};
Node.prototype.forDescendants = function(iter, s) {
if (s) iter(this);
el.children.forEach(function emit(el) {
iter(el);
el.children.forEach(emit);
});
};
Node.prototype.forAncestors = function(iter, s) {
var el = this;
if (s) iter(this);
while (el = el.parent) {
iter(el);
el.emit.apply(el, args);
}
};
Node.prototype.collectDescendants = function(s) {
var out = [];
this.forDescendants(function(el) {
out.push(el);
}, s);
return out;
};
Node.prototype.collectAncestors = function(s) {
var out = [];
this.forAncestors(function(el) {
out.push(el);
}, s);
return out;
};
Node.prototype.emitDescendants = function() {
var args = Array.prototype.slice(arguments)
, iter;
@ -158,31 +192,28 @@ Node.prototype.emitDescendants = function() {
iter = args.pop();
}
(function emit(el) {
return this.forDescendants(function(el) {
if (iter) iter(el);
el.emit.apply(el, args);
if (el.children) {
el.children.forEach(emit);
}
})(this);
}, true);
};
Node.prototype.emitAncestors = function() {
var args = Array.prototype.slice(arguments)
, el = this
, iter;
if (typeof args[args.length-1] === 'function') {
iter = args.pop();
}
do {
return this.forAncestors(function(el) {
if (iter) iter(el);
el.emit.apply(el, args);
} while (el = el.parent);
}, true);
};
Node.prototype.hasDescendant = function(target) {
// return this.collectDescendants().indexOf(target) !== -1;
return (function find(el) {
for (var i = 0; i < el.children.length; i++) {
if (el.children[i] === target) {
@ -197,6 +228,7 @@ Node.prototype.hasDescendant = function(target) {
};
Node.prototype.hasAncestor = function(target) {
// return this.collectAncestors().indexOf(target) !== -1;
var el = this;
while (el = el.parent) {
if (el === target) return true;
@ -1557,7 +1589,6 @@ Element.prototype.hide = function() {
Element.prototype.show = function() {
if (!this.hidden) return;
this.hidden = false;
// this.render();
this.emit('show');
};
@ -1602,11 +1633,14 @@ Element.prototype.parseContent = function() {
this._clines = this._wrapContent(content, width);
this._clines.width = width;
this._clines.content = this.content;
this._clines.attr = this._parseAttr(this._clines);
this._pcontent = this._clines.join('\n');
this.emit('parsed content');
return true;
}
this._clines.attr = this._parseAttr(this._clines) || this._clines.attr;
return false;
};
@ -1629,6 +1663,38 @@ Element.prototype._parseTags = function(text) {
});
};
Element.prototype._parseAttr = function(lines) {
var attr = this.sattr(this.style, this.style.fg, this.style.bg)
, attrs = []
, line
, i
, j
, c;
if (lines[0].attr === attr) {
return;
}
if (this.contentIndex == null || this.childBase == null) {
return;
}
for (j = 0; j < lines.length; j++) {
line = lines[j];
attrs[j] = attr;
for (i = 0; i < line.length; i++) {
if (line[i] === '\x1b') {
if (c = /^\x1b\[[\d;]*m/.exec(line.substring(i))) {
attr = this.screen.attrCode(c[0], attr);
i += c[0].length - 1;
}
}
}
}
return attrs;
};
Element.prototype._align = function(line, width, align) {
if (!align) return line;
@ -1798,241 +1864,223 @@ Element.prototype.clearPos = function() {
* Positioning
*/
// Help center shrunken nested elements.
Screen.prototype.spos =
Element.prototype.spos = function() {
// The below methods are a bit confusing: basically
// whenever Box.render is called `lpos` gets set on
// the element, an object containing the rendered
// coordinates. Since these don't update if the
// element is moved somehow, they're unreliable in
// that situation. However, if we can guarantee that
// lpos is good and up to date, it can be more
// accurate than the calculated positions below.
// In this case, if the element is being rendered,
// it's guaranteed that the parent will have been
// rendered first, in which case we can use the
// parant's lpos instead of recalculating it's
// position (since that might be wrong because
// it doesn't handle content shrinkage).
Screen.prototype._getPos = function() {
return this;
};
Element.prototype._getPos = function() {
var pos = this.lpos;
if (!pos || !this.shrink) return this;
pos.width = pos.xl - pos.xi;
pos.height = pos.yl - pos.yi;
assert.ok(pos && !pos.changed);
if (pos.left != null) return pos;
pos.left = pos.xi;
pos.top = pos.yi;
pos.right = this.screen.cols - pos.xl;
pos.bottom = this.screen.rows - pos.yl;
pos.width = pos.xl - pos.xi;
pos.height = pos.yl - pos.yi;
return pos;
};
// NOTE: When coords are entered in the Element constructor, all of the coords
// are *relative* to their parent, when retrieving them from `.left`, `.right`,
// etc members, the coords are absolute. To see the *relative* coords again,
// use `.rleft`, `.rright`, etc.
Element.prototype.__defineGetter__('left', function() {
var left = this.position.left;
var parent = this.parent.spos();
if (typeof left === 'string') {
if (left === 'center') left = '50%';
left = +left.slice(0, -1) / 100;
left = this.parent.width * left | 0;
if (this.position.left === 'center') {
left -= this.width / 2 | 0;
}
Element.prototype._bindPosChanged = function() {
function changed() {
self.forDescendants(function(el) {
if (el.lpos) el.lpos.changed = true;
}, true);
}
if (this.options.left == null && this.options.right != null) {
return this.screen.cols - this.width - this.right;
}
this.on('move', changed);
// Resize might be tricky because it's
// emitted recursively for a screen resize.
this.on('resize', changed);
this.on('reparent', changed);
};
return (parent.left || 0) + left;
});
/**
* Position Getters
*/
Element.prototype.__defineGetter__('right', function() {
if (this.options.right == null && this.options.left != null) {
return this.screen.cols - (this.left + this.width);
}
return (this.parent.right || 0) + this.position.right;
});
Element.prototype.__defineGetter__('top', function() {
var top = this.position.top;
var parent = this.parent.spos();
if (typeof top === 'string') {
if (top === 'center') top = '50%';
top = +top.slice(0, -1) / 100;
top = this.parent.height * top | 0;
if (this.position.top === 'center') {
top -= this.height / 2 | 0;
}
}
if (this.options.top == null && this.options.bottom != null) {
return this.screen.rows - this.height - this.bottom;
}
return (parent.top || 0) + top;
});
Element.prototype.__defineGetter__('bottom', function() {
if (this.options.bottom == null && this.options.top != null) {
return this.screen.rows - (this.top + this.height);
}
return (this.parent.bottom || 0) + this.position.bottom;
});
// TODO: Move _getShrinkSize calculation here. This will in turn fix .left.
Element.prototype.__defineGetter__('width', function() {
Element.prototype._getWidth = function(get) {
var parent = get ? this.parent._getPos() : this.parent;
var width = this.position.width;
if (typeof width === 'string') {
if (width === 'half') width = '50%';
width = +width.slice(0, -1) / 100;
return this.parent.width * width | 0;
return parent.width * width | 0;
}
// This is for if the element is being streched or shrunken.
// Although the width for shrunken elements is calculated
// in the render function, it may be calculated based on
// the content width, and the content width is initially
// decided by the width the element, so it needs to be
// calculated here.
if (!width) {
// Problem if .left is 'center', we can't calculate the width
// NOTE: This assume `right` cannot be a string.
var left = this.position.left;
if (typeof left === 'string') {
if (left === 'center') left = '50%';
left = +left.slice(0, -1) / 100;
left = this.parent.width * left | 0;
left = parent.width * left | 0;
}
width = this.parent.width - this.position.right - left;
width = parent.width - this.position.right - left;
}
return width;
};
Element.prototype.__defineGetter__('width', function() {
return this._getWidth(false);
});
// TODO: Move _getShrinkSize calculation here. This will in turn fix .top.
Element.prototype.__defineGetter__('height', function() {
Element.prototype._getHeight = function(get) {
var parent = get ? this.parent._getPos() : this.parent;
var height = this.position.height;
if (typeof height === 'string') {
if (height === 'half') height = '50%';
height = +height.slice(0, -1) / 100;
return this.parent.height * height | 0;
return parent.height * height | 0;
}
// This is for if the element is being streched or shrunken.
// Although the width for shrunken elements is calculated
// in the render function, it may be calculated based on
// the content width, and the content width is initially
// decided by the width the element, so it needs to be
// calculated here.
if (!height) {
// Problem if .top is 'center', we can't calculate the height
// NOTE: This assume `bottom` cannot be a string.
var top = this.position.top;
if (typeof top === 'string') {
if (top === 'center') top = '50%';
top = +top.slice(0, -1) / 100;
top = this.parent.height * top | 0;
top = parent.height * top | 0;
}
height = this.parent.height - this.position.bottom - top;
height = parent.height - this.position.bottom - top;
}
return height;
};
Element.prototype.__defineGetter__('height', function() {
return this._getHeight(false);
});
Element.prototype.__defineGetter__('rleft', function() {
Element.prototype._getLeft = function(get) {
var parent = get ? this.parent._getPos() : this.parent;
var left = this.position.left;
if (typeof left === 'string') {
if (left === 'center') left = '50%';
left = +left.slice(0, -1) / 100;
left = this.parent.width * left | 0;
left = parent.width * left | 0;
if (this.position.left === 'center') {
left -= this.width / 2 | 0;
left -= this._getWidth(get) / 2 | 0;
}
}
if (this.options.left == null && this.options.right != null) {
return this.parent.width - this.width - this.right;
return this.screen.cols - this._getWidth(get) - this._getRight(get);
}
return left;
return (parent.left || 0) + left;
};
Element.prototype.__defineGetter__('left', function() {
return this._getLeft(false);
});
Element.prototype.__defineGetter__('rright', function() {
Element.prototype._getRight = function(get) {
var parent = get ? this.parent._getPos() : this.parent;
if (this.options.right == null && this.options.left != null) {
return this.parent.width - (this.rleft + this.width);
return this.screen.cols - (this._getLeft(get) + this._getWidth(get));
}
return this.position.right;
return (parent.right || 0) + this.position.right;
};
Element.prototype.__defineGetter__('right', function() {
return this._getRight(false);
});
Element.prototype.__defineGetter__('rtop', function() {
Element.prototype._getTop = function(get) {
var parent = get ? this.parent._getPos() : this.parent;
var top = this.position.top;
if (typeof top === 'string') {
if (top === 'center') top = '50%';
top = +top.slice(0, -1) / 100;
top = this.parent.height * top | 0;
top = parent.height * top | 0;
if (this.position.top === 'center') {
top -= this.height / 2 | 0;
top -= this._getHeight(get) / 2 | 0;
}
}
if (this.options.top == null && this.options.bottom != null) {
return this.parent.height - this.height - this.bottom;
return this.screen.rows - this._getHeight(get) - this._getBottom(get);
}
return top;
return (parent.top || 0) + top;
};
Element.prototype.__defineGetter__('top', function() {
return this._getTop(false);
});
Element.prototype._getBottom = function(get) {
var parent = get ? this.parent._getPos() : this.parent;
if (this.options.bottom == null && this.options.top != null) {
return this.screen.rows - (this._getTop(get) + this._getHeight(get));
}
return (parent.bottom || 0) + this.position.bottom;
};
Element.prototype.__defineGetter__('bottom', function() {
return this._getBottom(false);
});
Element.prototype.__defineGetter__('rleft', function() {
return this.left - this.parent.left;
});
Element.prototype.__defineGetter__('rright', function() {
return this.right - this.parent.right;
});
Element.prototype.__defineGetter__('rtop', function() {
return this.top - this.parent.top;
});
Element.prototype.__defineGetter__('rbottom', function() {
if (this.options.bottom == null && this.options.top != null) {
return this.parent.height - (this.rtop + this.height);
}
return this.position.bottom;
return this.bottom - this.parent.bottom;
});
// TODO: Reconcile the fact the `position.left` is actually `.rleft`. etc.
// TODO: Allow string values for absolute coords below.
// TODO: Optimize clearing to only clear what is necessary.
Element.prototype.__defineSetter__('left', function(val) {
if (typeof val === 'string') {
if (val === 'center') val = '50%';
val = +val.slice(0, -1) / 100;
val = this.screen.width * val | 0;
}
val -= this.parent.left;
if (this.position.left === val) return;
this.emit('move');
this.clearPos();
return this.options.left = this.position.left = val;
});
Element.prototype.__defineSetter__('right', function(val) {
if (typeof val === 'string') {
if (val === 'center') val = '50%';
val = +val.slice(0, -1) / 100;
val = this.screen.width * val | 0;
}
val -= this.parent.right;
if (this.position.right === val) return;
this.emit('move');
this.clearPos();
//if (this.options.right == null) {
// return this.options.left = this.position.left =
// this.screen.width - 1 - val;
//}
return this.options.right = this.position.right = val;
});
Element.prototype.__defineSetter__('top', function(val) {
if (typeof val === 'string') {
if (val === 'center') val = '50%';
val = +val.slice(0, -1) / 100;
val = this.screen.height * val | 0;
}
val -= this.parent.top;
if (this.position.top === val) return;
this.emit('move');
this.clearPos();
return this.options.top = this.position.top = val;
});
Element.prototype.__defineSetter__('bottom', function(val) {
if (typeof val === 'string') {
if (val === 'center') val = '50%';
val = +val.slice(0, -1) / 100;
val = this.screen.height * val | 0;
}
val -= this.parent.bottom;
if (this.position.bottom === val) return;
this.emit('move');
this.clearPos();
//if (this.options.bottom == null) {
// return this.options.top = this.position.top =
// this.screen.height - 1 - val;
//}
return this.options.bottom = this.position.bottom = val;
});
/**
* Position Setters
*/
// NOTE:
// For right, bottom, rright, and rbottom:
// If position.bottom is null, we could simply set top instead.
// But it wouldn't replicate bottom behavior appropriately if
// the parent was resized, etc.
Element.prototype.__defineSetter__('width', function(val) {
if (this.position.width === val) return;
this.emit('resize');
@ -2047,6 +2095,56 @@ Element.prototype.__defineSetter__('height', function(val) {
return this.options.height = this.position.height = val;
});
Element.prototype.__defineSetter__('left', function(val) {
if (typeof val === 'string') {
if (val === 'center') {
val = this.screen.width / 2 | 0;
val -= this.width / 2 | 0;
} else {
val = +val.slice(0, -1) / 100;
val = this.screen.width * val | 0;
}
}
val -= this.parent.left;
if (this.position.left === val) return;
this.emit('move');
this.clearPos();
return this.options.left = this.position.left = val;
});
Element.prototype.__defineSetter__('right', function(val) {
val -= this.parent.right;
if (this.position.right === val) return;
this.emit('move');
this.clearPos();
return this.options.right = this.position.right = val;
});
Element.prototype.__defineSetter__('top', function(val) {
if (typeof val === 'string') {
if (val === 'center') {
val = this.screen.height / 2 | 0;
val -= this.height / 2 | 0;
} else {
val = +val.slice(0, -1) / 100;
val = this.screen.height * val | 0;
}
}
val -= this.parent.top;
if (this.position.top === val) return;
this.emit('move');
this.clearPos();
return this.options.top = this.position.top = val;
});
Element.prototype.__defineSetter__('bottom', function(val) {
val -= this.parent.bottom;
if (this.position.bottom === val) return;
this.emit('move');
this.clearPos();
return this.options.bottom = this.position.bottom = val;
});
Element.prototype.__defineSetter__('rleft', function(val) {
if (this.position.left === val) return;
this.emit('move');
@ -2058,10 +2156,6 @@ Element.prototype.__defineSetter__('rright', function(val) {
if (this.position.right === val) return;
this.emit('move');
this.clearPos();
//if (this.options.right == null) {
// return this.options.left = this.position.left =
// this.parent.width - 1 - val;
//}
return this.options.right = this.position.right = val;
});
@ -2076,10 +2170,6 @@ Element.prototype.__defineSetter__('rbottom', function(val) {
if (this.position.bottom === val) return;
this.emit('move');
this.clearPos();
//if (this.options.bottom == null) {
// return this.options.top = this.position.top =
// this.parent.height - 1 - val;
//}
return this.options.bottom = this.position.bottom = val;
});
@ -2123,7 +2213,7 @@ Box.prototype.__proto__ = Element.prototype;
Box.prototype.type = 'box';
Box.prototype._getShrinkSize = function(content) {
Box.prototype._getShrinkSize = function() {
// TODO: Possibly move this to parseContent.
return {
height: this._clines.length,
@ -2146,7 +2236,7 @@ Box.prototype._getShrinkBox = function(xi, xl, yi, yl) {
for (i = 0; i < this.children.length; i++) {
el = this.children[i];
ret = el.getCoords();
ret = el._getCoords();
if (!ret) continue;
if (ret.xi < mxi) mxi = ret.xi;
@ -2183,8 +2273,8 @@ Box.prototype._getShrinkBox = function(xi, xl, yi, yl) {
return { xi: xi, xl: xl, yi: yi, yl: yl };
};
Box.prototype._getShrinkContent = function(xi, xl, yi, yl, content) {
var hw = this._getShrinkSize(content)
Box.prototype._getShrinkContent = function(xi, xl, yi, yl) {
var hw = this._getShrinkSize()
, h = hw.height
, w = hw.width;
@ -2212,9 +2302,9 @@ Box.prototype._getShrinkContent = function(xi, xl, yi, yl, content) {
return { xi: xi, xl: xl, yi: yi, yl: yl };
};
Box.prototype._getShrink = function(xi, xl, yi, yl, content) {
Box.prototype._getShrink = function(xi, xl, yi, yl) {
var shrinkBox = this._getShrinkBox(xi, xl, yi, yl)
, shrinkContent = this._getShrinkContent(xi, xl, yi, yl, content)
, shrinkContent = this._getShrinkContent(xi, xl, yi, yl)
, xll = xl
, yll = yl;
@ -2251,48 +2341,30 @@ Box.prototype._getShrink = function(xi, xl, yi, yl, content) {
return { xi: xi, xl: xl, yi: yi, yl: yl };
};
Box.prototype.getCoords = function() {
Box.prototype._getCoords = function(get) {
if (this.hidden) return;
var lines = this.screen.lines
, xi = this.left
//, xl = this.screen.cols - this.right
, xl = xi + this.width
, yi = this.top
//, yl = this.screen.rows - this.bottom
, yl = yi + this.height
var xi = this._getLeft(get)
, xl = xi + this._getWidth(get)
, yi = this._getTop(get)
, yl = yi + this._getHeight(get)
, rtop
, visible
, coords;
//if (this.position.width) {
// xl = xi + this.width;
//}
//if (this.position.height) {
// yl = yi + this.height;
//}
// Check to make sure we're visible and
// inside of the visible scroll area.
if (this.parent.childBase != null
&& (!this.parent.items
|| ~this.parent.items.indexOf(this))) {
rtop = this.rtop
rtop = yi - this.parent._getTop(get)
- (this.parent.border ? 1 : 0)
- this.parent.padding;
visible = this.parent.height
visible = this.parent._getHeight(get)
- (this.parent.border ? 2 : 0)
- this.parent.padding * 2;
yi -= this.parent.childBase;
yl = Math.min(yl, this.screen.rows
- this.parent.bottom
- (this.parent.border ? 1 : 0)
- this.parent.padding);
if (rtop - this.parent.childBase < 0) {
return;
}
@ -2300,6 +2372,9 @@ Box.prototype.getCoords = function() {
if (rtop - this.parent.childBase >= visible) {
return;
}
yi -= this.parent.childBase;
yl -= this.parent.childBase;
}
// Attempt to shrink the element base on the
@ -2319,12 +2394,10 @@ Box.prototype.getCoords = function() {
};
// Here be dragons.
// TODO: Potentially move all calculations performed on
// xi/xl/yi/yl here to Element offset and size getters.
Box.prototype.render = function() {
this.parseContent();
var coords = this.getCoords();
var coords = this._getCoords(true);
if (!coords) return;
var lines = this.screen.lines
@ -2351,17 +2424,10 @@ Box.prototype.render = function() {
dattr = this.sattr(this.style, this.style.fg, this.style.bg);
attr = dattr;
// Check previous line for escape codes.
if (this.contentIndex != null && this.childBase > 0 && this._clines) {
i = ci - (this._clines[this.childBase - 1].length + 1);
for (; i < ci; i++) {
if (content[i] === '\x1b') {
if (c = /^\x1b\[[\d;]*m/.exec(content.substring(i))) {
attr = this.screen.attrCode(c[0], attr);
i += c[0].length - 1;
}
}
}
// If we're in a scrollable text box, check to
// see which attributes this line starts with.
if (this.contentIndex != null && this.childBase != null) {
attr = this._clines.attr[this.childBase];
}
if (this.border) xi++, xl--, yi++, yl--;
@ -2564,7 +2630,7 @@ Box.prototype.render = function() {
// screen.render();
// box.left++;
// box.insertTop('foobar');
// Things will break because we're using lpos instead of getCoords().
// Things will break because we're using lpos instead of _getCoords().
// Maybe lpos could be updated on .left, .right, etc setters?
Box.prototype.insertLine = function(i, line) {
@ -4241,9 +4307,9 @@ function Checkbox(options) {
}
this.on('focus', function(old) {
if (!this.lpos) return;
if (!self.lpos) return;
self.screen.program.saveCursor();
self.screen.program.cup(this.lpos.yi, this.lpos.xi + 1);
self.screen.program.cup(self.lpos.yi, self.lpos.xi + 1);
self.screen.program.showCursor();
});