UNPKG

neo-neo-bblessed

Version:

A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.

307 lines (258 loc) 8.15 kB
/** * layout.js - layout element for blessed * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ /** * Modules */ const Node = require('./node'); const Element = require('./element'); /** * Type definitions */ interface LayoutOptions { width?: number; height?: number; left?: number; right?: number; top?: number; bottom?: number; layout?: 'inline' | 'grid'; renderer?: (coords: LayoutCoords) => LayoutIterator; [key: string]: any; } interface LayoutCoords { xi: number; xl: number; yi: number; yl: number; } interface LayoutPosition { left: number; top: number; width?: number; } interface LayoutPadding { left: number; right: number; top: number; bottom: number; } interface LayoutChild { shrink?: boolean; width: number; height: number; position: LayoutPosition; lpos?: LayoutCoords; index?: number; screen: { _ci: number; }; render(): void; } interface LayoutIterator { (el: LayoutChild, i: number): boolean | void; } interface LayoutInterface extends Element { type: string; options: LayoutOptions; children: LayoutChild[]; lpos?: LayoutCoords; border?: boolean; tpadding?: boolean; padding?: LayoutPadding; renderer: (coords: LayoutCoords) => LayoutIterator; isRendered(el: LayoutChild): boolean; getLast(i: number): LayoutChild | undefined; getLastCoords(i: number): LayoutCoords | undefined; _renderCoords(): LayoutCoords | undefined; render(): LayoutCoords | undefined; _emit(event: string, args?: any[]): void; _getCoords(get?: boolean): LayoutCoords | undefined; _render(): any; } /** * Layout */ function Layout(this: LayoutInterface, options?: LayoutOptions) { if (!(this instanceof Node)) { return new (Layout as any)(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!'); } options.layout = options.layout || 'inline'; Element.call(this, options); if (options.renderer) { this.renderer = options.renderer; } } Layout.prototype.__proto__ = Element.prototype; Layout.prototype.type = 'layout'; Layout.prototype.isRendered = function(this: LayoutInterface, el: LayoutChild): boolean { if (!el.lpos) return false; return (el.lpos.xl - el.lpos.xi) > 0 && (el.lpos.yl - el.lpos.yi) > 0; }; Layout.prototype.getLast = function(this: LayoutInterface, i: number): LayoutChild | undefined { while (this.children[--i]) { const el = this.children[i]; if (this.isRendered(el)) return el; } }; Layout.prototype.getLastCoords = function(this: LayoutInterface, i: number): LayoutCoords | undefined { const last = this.getLast(i); if (last) return last.lpos; }; Layout.prototype._renderCoords = function(this: LayoutInterface): LayoutCoords | undefined { const coords = this._getCoords(true); const children = this.children; this.children = []; this._render(); this.children = children; return coords; }; Layout.prototype.renderer = function(this: LayoutInterface, coords: LayoutCoords): LayoutIterator { const self = this; // The coordinates of the layout element const width = coords.xl - coords.xi; const height = coords.yl - coords.yi; const xi = coords.xi; const yi = coords.yi; // The current row offset in cells (which row are we on?) let rowOffset = 0; // The index of the first child in the row let rowIndex = 0; let lastRowIndex = 0; // Figure out the highest width child let highWidth = 0; if (this.options.layout === 'grid') { highWidth = this.children.reduce(function(out: number, el: LayoutChild): number { out = Math.max(out, el.width); return out; }, 0); } return function iterator(el: LayoutChild, i: number): boolean | void { // 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 const 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; // Make sure the position matches the highest width element if (self.options.layout === 'grid') { // Compensate with width: // el.position.width = el.width + (highWidth - el.width); // Compensate with position: el.position.left += highWidth - (last.lpos!.xl - last.lpos!.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: number, el: LayoutChild): number { if (!self.isRendered(el)) return out; out = Math.max(out, el.lpos!.yl - el.lpos!.yi); return out; }, 0); lastRowIndex = rowIndex; rowIndex = i; el.position.left = 0; el.position.top = rowOffset; } } // Make sure the elements on lower rows graviatate up as much as possible if (self.options.layout === 'inline') { let above: LayoutChild | null = null; let abovea = Infinity; for (let j = lastRowIndex; j < rowIndex; j++) { const l = self.children[j]; if (!self.isRendered(l)) continue; const abs = Math.abs(el.position.left - (l.lpos!.xi - xi)); // if (abs < abovea && (l.lpos.xl - l.lpos.xi) <= el.width) { if (abs < abovea) { above = l; abovea = abs; } } if (above) { el.position.top = above.lpos!.yl - yi; } } // 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(this: LayoutInterface): LayoutCoords | undefined { this._emit('prerender'); const 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; } const 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: LayoutChild, i: number) { if (el.screen._ci !== -1) { el.index = el.screen._ci++; } const 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;