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)
|
- [ListTable](#listtable-from-list)
|
||||||
- [Terminal](#terminal-from-box)
|
- [Terminal](#terminal-from-box)
|
||||||
- [Image](#image-from-box)
|
- [Image](#image-from-box)
|
||||||
|
- [Layout](#layout-from-element)
|
||||||
|
|
||||||
|
|
||||||
#### Node (from EventEmitter)
|
#### Node (from EventEmitter)
|
||||||
|
@ -1313,6 +1314,176 @@ terminals.
|
||||||
- __getPixelRatio(callback)__ - get the pixel to cell ratio for the terminal.
|
- __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
|
### Artificial Cursors
|
||||||
|
|
||||||
Terminal cursors can be tricky. They all have different custom escape codes to
|
Terminal cursors can be tricky. They all have different custom escape codes to
|
||||||
|
|
|
@ -35,7 +35,8 @@ widget.classes = [
|
||||||
'Table',
|
'Table',
|
||||||
'ListTable',
|
'ListTable',
|
||||||
'Terminal',
|
'Terminal',
|
||||||
'Image'
|
'Image',
|
||||||
|
'Layout'
|
||||||
];
|
];
|
||||||
|
|
||||||
widget.classes.forEach(function(name) {
|
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