add a customizable Layout element. see #23.
This commit is contained in:
parent
583fa4f6f7
commit
b21e535391
171
README.md
171
README.md
|
@ -168,6 +168,7 @@ nasty low-level terminal stuff.
|
|||
- [ListTable](#listtable-from-list)
|
||||
- [Terminal](#terminal-from-box)
|
||||
- [Image](#image-from-box)
|
||||
- [Layout](#layout-from-element)
|
||||
|
||||
|
||||
#### Node (from EventEmitter)
|
||||
|
@ -1313,6 +1314,176 @@ terminals.
|
|||
- __getPixelRatio(callback)__ - get the pixel to cell ratio for the terminal.
|
||||
|
||||
|
||||
#### Layout (from Element)
|
||||
|
||||
A layout which can position children automatically based on a `renderer` method
|
||||
(__experimental__ - the mechanics of this element may be changed in the
|
||||
future!).
|
||||
|
||||
By default, the Layout element automatically positions children as if they were
|
||||
`display: inline-block;` in CSS.
|
||||
|
||||
##### Options:
|
||||
|
||||
- inherits all from Element.
|
||||
- __renderer__ - a callback which is called right before the children are
|
||||
iterated over to be rendered. should return an iterator callback which is
|
||||
called on each child element: __iterator(el, i)__.
|
||||
|
||||
##### Properties:
|
||||
|
||||
- inherits all from Element.
|
||||
|
||||
##### Events:
|
||||
|
||||
- inherits all from Element.
|
||||
|
||||
##### Methods:
|
||||
|
||||
- inherits all from Element.
|
||||
- __renderer(coords)__ - a callback which is called right before the children
|
||||
are iterated over to be rendered. should return an iterator callback which is
|
||||
called on each child element: __iterator(el, i)__.
|
||||
- __isRendered(el)__ - check to see if a previous child element has been
|
||||
rendered and is visible on screen. this is __only__ useful for checking child
|
||||
elements that have already been attempted to be rendered! see the example
|
||||
below.
|
||||
- __getLast(i)__ - get the last rendered and visible child element based on an
|
||||
index. this is useful for basing the position of the current child element on
|
||||
the position of the last child element.
|
||||
- __getLastCoords(i)__ - get the last rendered and visible child element coords
|
||||
based on an index. this is useful for basing the position of the current
|
||||
child element on the position of the last child element. see the example
|
||||
below.
|
||||
|
||||
##### Rendering a Layout for child elements
|
||||
|
||||
###### Notes
|
||||
|
||||
You must __always__ give `Layout` a width and height. This is a chicken-and-egg
|
||||
problem: blessed cannot calculate the width and height dynamically _before_ the
|
||||
children are positioned.
|
||||
|
||||
`border` and `padding` are already calculated into the `coords` object the
|
||||
`renderer` receives, so there is no need to account for it in your renderer.
|
||||
|
||||
Try to set position for children using `el.position`. `el.position` is the most
|
||||
primitive "to-be-rendered" way to set coordinates. Setting `el.left` directly
|
||||
has more dynamic behavior which may interfere with rendering.
|
||||
|
||||
Some definitions for `coords` (otherwise known as `el.lpos`):
|
||||
|
||||
- `coords.xi` - the absolute x coordinate of the __left__ side of a rendered
|
||||
element. it is absolute: relative to the screen itself.
|
||||
- `coords.xl` - the absolute x coordinate of the __right__ side of a rendered
|
||||
element. it is absolute: relative to the screen itself.
|
||||
- `coords.yi` - the absolute y coordinate of the __top__ side of a rendered
|
||||
element. it is absolute: relative to the screen itself.
|
||||
- `coords.yl` - the absolute y coordinate of the __bottom__ side of a rendered
|
||||
element. it is absolute: relative to the screen itself.
|
||||
|
||||
Note again: the `coords` the renderer receives for the Layout already has
|
||||
border and padding subtracted, so you do not have to account for these. The
|
||||
children do not.
|
||||
|
||||
###### Example
|
||||
|
||||
Here is an example of how to provide a renderer. Note that this is also the
|
||||
default renderer if none is provided. This renderer will render each child as
|
||||
though they were `display: inline-block;` in CSS, as if there were a
|
||||
dynamically sized horizontal grid from left to right.
|
||||
|
||||
``` js
|
||||
var layout = blessed.layout({
|
||||
parent: screen,
|
||||
top: 'center',
|
||||
left: 'center',
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
border: 'line',
|
||||
style: {
|
||||
bg: 'red',
|
||||
border: {
|
||||
fg: 'blue'
|
||||
}
|
||||
},
|
||||
// NOTE: This is already the default renderer if none is provided!
|
||||
renderer: function(coords) {
|
||||
var self = this;
|
||||
|
||||
// The coordinates of the layout element
|
||||
var width = coords.xl - coords.xi
|
||||
, height = coords.yl - coords.yi
|
||||
, xi = coords.xi
|
||||
, xl = coords.xl
|
||||
, yi = coords.yi
|
||||
, yl = coords.yl;
|
||||
|
||||
// The current row offset in cells (which row are we on?)
|
||||
var rowOffset = 0;
|
||||
|
||||
// The index of the first child in the row
|
||||
var rowIndex = 0;
|
||||
|
||||
return function iterator(el, i) {
|
||||
// Make our children shrinkable. If they don't have a height, for
|
||||
// example, calculate it for them.
|
||||
el.shrink = true;
|
||||
|
||||
// Find the previous rendered child's coordinates
|
||||
var last = self.getLastCoords(i);
|
||||
|
||||
// If there is no previously rendered element, we are on the first child.
|
||||
if (!last) {
|
||||
el.position.left = 0;
|
||||
el.position.top = 0;
|
||||
} else {
|
||||
// Otherwise, figure out where to place this child. We'll start by
|
||||
// setting it's `left`/`x` coordinate to right after the previous
|
||||
// rendered element. This child will end up directly to the right of it.
|
||||
el.position.left = last.xl - xi;
|
||||
|
||||
// If our child does not overlap the right side of the Layout, set it's
|
||||
// `top`/`y` to the current `rowOffset` (the coordinate for the current
|
||||
// row).
|
||||
if (el.position.left + el.width <= width) {
|
||||
el.position.top = rowOffset;
|
||||
} else {
|
||||
// Otherwise we need to start a new row and calculate a new
|
||||
// `rowOffset` and `rowIndex` (the index of the child on the current
|
||||
// row).
|
||||
rowOffset += self.children.slice(rowIndex, i).reduce(function(out, el) {
|
||||
if (!self.isRendered(el)) return out;
|
||||
out = Math.max(out, el.lpos.yl - el.lpos.yi);
|
||||
return out;
|
||||
}, 0);
|
||||
rowIndex = i;
|
||||
el.position.left = 0;
|
||||
el.position.top = rowOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// If our child overflows the Layout, do not render it!
|
||||
// Disable this feature for now.
|
||||
if (el.position.top + el.height > height) {
|
||||
// Returning false tells blessed to ignore this child.
|
||||
// return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
blessed.box({
|
||||
parent: layout,
|
||||
width: i % 2 === 0 ? 10 : 20,
|
||||
height: i % 2 === 0 ? 5 : 10,
|
||||
border: 'line'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Artificial Cursors
|
||||
|
||||
Terminal cursors can be tricky. They all have different custom escape codes to
|
||||
|
|
|
@ -35,7 +35,8 @@ widget.classes = [
|
|||
'Table',
|
||||
'ListTable',
|
||||
'Terminal',
|
||||
'Image'
|
||||
'Image',
|
||||
'Layout'
|
||||
];
|
||||
|
||||
widget.classes.forEach(function(name) {
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* layout.js - layout element for blessed
|
||||
* Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
|
||||
* https://github.com/chjj/blessed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Modules
|
||||
*/
|
||||
|
||||
var helpers = require('../helpers');
|
||||
|
||||
var Node = require('./node');
|
||||
var Element = require('./element');
|
||||
|
||||
/**
|
||||
* Layout
|
||||
*/
|
||||
|
||||
function Layout(options) {
|
||||
var self = this;
|
||||
|
||||
if (!(this instanceof Node)) {
|
||||
return new Layout(options);
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if ((options.width == null
|
||||
&& (options.left == null && options.right == null))
|
||||
|| (options.height == null
|
||||
&& (options.top == null && options.bottom == null))) {
|
||||
throw new Error('`Layout` must have a width and height!');
|
||||
}
|
||||
|
||||
Element.call(this, options);
|
||||
|
||||
if (options.renderer) {
|
||||
this.renderer = options.renderer;
|
||||
}
|
||||
}
|
||||
|
||||
Layout.prototype.__proto__ = Element.prototype;
|
||||
|
||||
Layout.prototype.type = 'layout';
|
||||
|
||||
Layout.prototype.isRendered = function(el) {
|
||||
if (!el.lpos) return false;
|
||||
return (el.lpos.xl - el.lpos.xi) > 0
|
||||
&& (el.lpos.yl - el.lpos.yi) > 0;
|
||||
};
|
||||
|
||||
Layout.prototype.getLast = function(i) {
|
||||
while (this.children[--i]) {
|
||||
var el = this.children[i];
|
||||
if (this.isRendered(el)) return el;
|
||||
}
|
||||
};
|
||||
|
||||
Layout.prototype.getLastCoords = function(i) {
|
||||
var last = this.getLast(i);
|
||||
if (last) return last.lpos;
|
||||
};
|
||||
|
||||
Layout.prototype._renderCoords = function() {
|
||||
var coords = this._getCoords(true);
|
||||
var children = this.children;
|
||||
this.children = [];
|
||||
this._render();
|
||||
this.children = children;
|
||||
return coords;
|
||||
};
|
||||
|
||||
Layout.prototype.renderer = function(coords) {
|
||||
var self = this;
|
||||
|
||||
// The coordinates of the layout element
|
||||
var width = coords.xl - coords.xi
|
||||
, height = coords.yl - coords.yi
|
||||
, xi = coords.xi
|
||||
, xl = coords.xl
|
||||
, yi = coords.yi
|
||||
, yl = coords.yl;
|
||||
|
||||
// The current row offset in cells (which row are we on?)
|
||||
var rowOffset = 0;
|
||||
|
||||
// The index of the first child in the row
|
||||
var rowIndex = 0;
|
||||
|
||||
return function iterator(el, i) {
|
||||
// Make our children shrinkable. If they don't have a height, for
|
||||
// example, calculate it for them.
|
||||
el.shrink = true;
|
||||
|
||||
// Find the previous rendered child's coordinates
|
||||
var last = self.getLast(i);
|
||||
|
||||
// If there is no previously rendered element, we are on the first child.
|
||||
if (!last) {
|
||||
el.position.left = 0;
|
||||
el.position.top = 0;
|
||||
} else {
|
||||
// Otherwise, figure out where to place this child. We'll start by
|
||||
// setting it's `left`/`x` coordinate to right after the previous
|
||||
// rendered element. This child will end up directly to the right of it.
|
||||
el.position.left = last.lpos.xl - xi;
|
||||
|
||||
// If our child does not overlap the right side of the Layout, set it's
|
||||
// `top`/`y` to the current `rowOffset` (the coordinate for the current
|
||||
// row).
|
||||
if (el.position.left + el.width <= width) {
|
||||
el.position.top = rowOffset;
|
||||
} else {
|
||||
// Otherwise we need to start a new row and calculate a new
|
||||
// `rowOffset` and `rowIndex` (the index of the child on the current
|
||||
// row).
|
||||
rowOffset += self.children.slice(rowIndex, i).reduce(function(out, el) {
|
||||
if (!self.isRendered(el)) return out;
|
||||
out = Math.max(out, el.lpos.yl - el.lpos.yi);
|
||||
return out;
|
||||
}, 0);
|
||||
rowIndex = i;
|
||||
el.position.left = 0;
|
||||
el.position.top = rowOffset;
|
||||
}
|
||||
}
|
||||
|
||||
// If our child overflows the Layout, do not render it!
|
||||
// Disable this feature for now.
|
||||
if (el.position.top + el.height > height) {
|
||||
// Returning false tells blessed to ignore this child.
|
||||
// return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Layout.prototype.render = function() {
|
||||
var self = this;
|
||||
|
||||
this._emit('prerender');
|
||||
|
||||
var coords = this._renderCoords();
|
||||
if (!coords) {
|
||||
delete this.lpos;
|
||||
return;
|
||||
}
|
||||
|
||||
if (coords.xl - coords.xi <= 0) {
|
||||
coords.xl = Math.max(coords.xl, coords.xi);
|
||||
return;
|
||||
}
|
||||
|
||||
if (coords.yl - coords.yi <= 0) {
|
||||
coords.yl = Math.max(coords.yl, coords.yi);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lpos = coords;
|
||||
|
||||
if (this.border) coords.xi++, coords.xl--, coords.yi++, coords.yl--;
|
||||
if (this.tpadding) {
|
||||
coords.xi += this.padding.left, coords.xl -= this.padding.right;
|
||||
coords.yi += this.padding.top, coords.yl -= this.padding.bottom;
|
||||
}
|
||||
|
||||
var iterator = this.renderer(coords);
|
||||
|
||||
if (this.border) coords.xi--, coords.xl++, coords.yi--, coords.yl++;
|
||||
if (this.tpadding) {
|
||||
coords.xi -= this.padding.left, coords.xl += this.padding.right;
|
||||
coords.yi -= this.padding.top, coords.yl += this.padding.bottom;
|
||||
}
|
||||
|
||||
this.children.forEach(function(el, i) {
|
||||
if (el.screen._ci !== -1) {
|
||||
el.index = el.screen._ci++;
|
||||
}
|
||||
var rendered = iterator(el, i);
|
||||
if (rendered === false) {
|
||||
delete el.lpos;
|
||||
return;
|
||||
}
|
||||
// if (el.screen._rendering) {
|
||||
// el._rendering = true;
|
||||
// }
|
||||
el.render();
|
||||
// if (el.screen._rendering) {
|
||||
// el._rendering = false;
|
||||
// }
|
||||
});
|
||||
|
||||
this._emit('render', [coords]);
|
||||
|
||||
return coords;
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose
|
||||
*/
|
||||
|
||||
module.exports = Layout;
|
|
@ -0,0 +1,130 @@
|
|||
var blessed = require('../')
|
||||
, screen;
|
||||
|
||||
screen = blessed.screen({
|
||||
dump: __dirname + '/logs/layout.log',
|
||||
smartCSR: true,
|
||||
autoPadding: true
|
||||
});
|
||||
|
||||
var layout = blessed.layout({
|
||||
parent: screen,
|
||||
top: 'center',
|
||||
left: 'center',
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
border: 'line',
|
||||
style: {
|
||||
bg: 'red',
|
||||
border: {
|
||||
fg: 'blue'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var box1 = blessed.box({
|
||||
parent: layout,
|
||||
top: 'center',
|
||||
left: 'center',
|
||||
width: 20,
|
||||
height: 10,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box1 = blessed.box({
|
||||
parent: layout,
|
||||
top: 'center',
|
||||
left: 'center',
|
||||
width: 20,
|
||||
height: 10,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box2 = blessed.box({
|
||||
parent: layout,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 10,
|
||||
height: 5,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
var box1 = blessed.box({
|
||||
parent: layout,
|
||||
top: 'center',
|
||||
left: 'center',
|
||||
width: 20,
|
||||
height: 10,
|
||||
border: 'line'
|
||||
});
|
||||
|
||||
/*
|
||||
for (var i = 0; i < 10; i++) {
|
||||
blessed.box({
|
||||
parent: layout,
|
||||
width: i % 2 === 0 ? 10 : 20,
|
||||
height: i % 2 === 0 ? 5 : 10,
|
||||
border: 'line'
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
screen.key('q', function() {
|
||||
return process.exit(0);
|
||||
});
|
||||
|
||||
screen.render();
|
Loading…
Reference in New Issue