From 39c6954062fe8fafac951cee2b6d3ebaaf8531b9 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:12:05 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/term/widgets/layout.js | 259 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 js/term/widgets/layout.js diff --git a/js/term/widgets/layout.js b/js/term/widgets/layout.js new file mode 100644 index 0000000..a6d381f --- /dev/null +++ b/js/term/widgets/layout.js @@ -0,0 +1,259 @@ +/** + ** ============================== + ** O O O OOOO + ** O O O O O O + ** O O O O O O + ** OOOO OOOO O OOO OOOO + ** O O O O O O O + ** O O O O O O O + ** OOOO OOOO O O OOOO + ** ============================== + ** Dr. Stefan Bosse http://www.bsslab.de + ** + ** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED + ** BY THE AUTHOR(S). + ** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED, + ** MODIFIED, OR OTHERWISE USED IN A CONTEXT + ** OUTSIDE OF THE SOFTWARE SYSTEM. + ** + ** $AUTHORS: Christopher Jeffrey, Stefan Bosse + ** $INITIAL: (C) 2013-2015, Christopher Jeffrey and contributors + ** $MODIFIED: by sbosse + ** $REVESIO: 1.2.1 + ** + ** $INFO: + ** + ** layout.js - layout element for blessed + ** + ** $ENDOFINFO + */ + +/** + * Modules + */ +var Comp = Require('com/compat'); + +var Node = Require('term/widgets/node'); +var Element = Require('term/widgets/element'); + +/** + * Layout + */ + +function Layout(options) { + if (!instanceOf(this,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!'); + } + + options.layout = options.layout || 'inline'; + + Element.call(this, options); + + if (options.renderer) { + this.renderer = options.renderer; + } +} + +//Layout.prototype.__proto__ = Element.prototype; +inheritPrototype(Layout,Element); + +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 + , yi = coords.yi; + + // 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; + var lastRowIndex = 0; + + // Figure out the highest width child + if (this.options.layout === 'grid') { + var highWidth = this.children.reduce(function(out, el) { + out = Math.max(out, el.width); + return out; + }, 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; + + // 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, el) { + 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') { + var above = null; + var abovea = Infinity; + for (var j = lastRowIndex; j < rowIndex; j++) { + var l = self.children[j]; + if (!self.isRendered(l)) continue; + var 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._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;