layout.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * layout.js - layout element for blessed
  3. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
  4. * https://github.com/chjj/blessed
  5. */
  6. /**
  7. * Modules
  8. */
  9. var Node = require('./node');
  10. var Element = require('./element');
  11. /**
  12. * Layout
  13. */
  14. function Layout(options) {
  15. if (!(this instanceof Node)) {
  16. return new Layout(options);
  17. }
  18. options = options || {};
  19. if ((options.width == null
  20. && (options.left == null && options.right == null))
  21. || (options.height == null
  22. && (options.top == null && options.bottom == null))) {
  23. throw new Error('`Layout` must have a width and height!');
  24. }
  25. options.layout = options.layout || 'inline';
  26. Element.call(this, options);
  27. if (options.renderer) {
  28. this.renderer = options.renderer;
  29. }
  30. }
  31. Layout.prototype.__proto__ = Element.prototype;
  32. Layout.prototype.type = 'layout';
  33. Layout.prototype.isRendered = function(el) {
  34. if (!el.lpos) return false;
  35. return (el.lpos.xl - el.lpos.xi) > 0
  36. && (el.lpos.yl - el.lpos.yi) > 0;
  37. };
  38. Layout.prototype.getLast = function(i) {
  39. while (this.children[--i]) {
  40. var el = this.children[i];
  41. if (this.isRendered(el)) return el;
  42. }
  43. };
  44. Layout.prototype.getLastCoords = function(i) {
  45. var last = this.getLast(i);
  46. if (last) return last.lpos;
  47. };
  48. Layout.prototype._renderCoords = function() {
  49. var coords = this._getCoords(true);
  50. var children = this.children;
  51. this.children = [];
  52. this._render();
  53. this.children = children;
  54. return coords;
  55. };
  56. Layout.prototype.renderer = function(coords) {
  57. var self = this;
  58. // The coordinates of the layout element
  59. var width = coords.xl - coords.xi
  60. , height = coords.yl - coords.yi
  61. , xi = coords.xi
  62. , yi = coords.yi;
  63. // The current row offset in cells (which row are we on?)
  64. var rowOffset = 0;
  65. // The index of the first child in the row
  66. var rowIndex = 0;
  67. var lastRowIndex = 0;
  68. // Figure out the highest width child
  69. if (this.options.layout === 'grid') {
  70. var highWidth = this.children.reduce(function(out, el) {
  71. out = Math.max(out, el.width);
  72. return out;
  73. }, 0);
  74. }
  75. return function iterator(el, i) {
  76. // Make our children shrinkable. If they don't have a height, for
  77. // example, calculate it for them.
  78. el.shrink = true;
  79. // Find the previous rendered child's coordinates
  80. var last = self.getLast(i);
  81. // If there is no previously rendered element, we are on the first child.
  82. if (!last) {
  83. el.position.left = 0;
  84. el.position.top = 0;
  85. } else {
  86. // Otherwise, figure out where to place this child. We'll start by
  87. // setting it's `left`/`x` coordinate to right after the previous
  88. // rendered element. This child will end up directly to the right of it.
  89. el.position.left = last.lpos.xl - xi;
  90. // Make sure the position matches the highest width element
  91. if (self.options.layout === 'grid') {
  92. // Compensate with width:
  93. // el.position.width = el.width + (highWidth - el.width);
  94. // Compensate with position:
  95. el.position.left += highWidth - (last.lpos.xl - last.lpos.xi);
  96. }
  97. // If our child does not overlap the right side of the Layout, set it's
  98. // `top`/`y` to the current `rowOffset` (the coordinate for the current
  99. // row).
  100. if (el.position.left + el.width <= width) {
  101. el.position.top = rowOffset;
  102. } else {
  103. // Otherwise we need to start a new row and calculate a new
  104. // `rowOffset` and `rowIndex` (the index of the child on the current
  105. // row).
  106. rowOffset += self.children.slice(rowIndex, i).reduce(function(out, el) {
  107. if (!self.isRendered(el)) return out;
  108. out = Math.max(out, el.lpos.yl - el.lpos.yi);
  109. return out;
  110. }, 0);
  111. lastRowIndex = rowIndex;
  112. rowIndex = i;
  113. el.position.left = 0;
  114. el.position.top = rowOffset;
  115. }
  116. }
  117. // Make sure the elements on lower rows graviatate up as much as possible
  118. if (self.options.layout === 'inline') {
  119. var above = null;
  120. var abovea = Infinity;
  121. for (var j = lastRowIndex; j < rowIndex; j++) {
  122. var l = self.children[j];
  123. if (!self.isRendered(l)) continue;
  124. var abs = Math.abs(el.position.left - (l.lpos.xi - xi));
  125. // if (abs < abovea && (l.lpos.xl - l.lpos.xi) <= el.width) {
  126. if (abs < abovea) {
  127. above = l;
  128. abovea = abs;
  129. }
  130. }
  131. if (above) {
  132. el.position.top = above.lpos.yl - yi;
  133. }
  134. }
  135. // If our child overflows the Layout, do not render it!
  136. // Disable this feature for now.
  137. if (el.position.top + el.height > height) {
  138. // Returning false tells blessed to ignore this child.
  139. // return false;
  140. }
  141. };
  142. };
  143. Layout.prototype.render = function() {
  144. this._emit('prerender');
  145. var coords = this._renderCoords();
  146. if (!coords) {
  147. delete this.lpos;
  148. return;
  149. }
  150. if (coords.xl - coords.xi <= 0) {
  151. coords.xl = Math.max(coords.xl, coords.xi);
  152. return;
  153. }
  154. if (coords.yl - coords.yi <= 0) {
  155. coords.yl = Math.max(coords.yl, coords.yi);
  156. return;
  157. }
  158. this.lpos = coords;
  159. if (this.border) coords.xi++, coords.xl--, coords.yi++, coords.yl--;
  160. if (this.tpadding) {
  161. coords.xi += this.padding.left, coords.xl -= this.padding.right;
  162. coords.yi += this.padding.top, coords.yl -= this.padding.bottom;
  163. }
  164. var iterator = this.renderer(coords);
  165. if (this.border) coords.xi--, coords.xl++, coords.yi--, coords.yl++;
  166. if (this.tpadding) {
  167. coords.xi -= this.padding.left, coords.xl += this.padding.right;
  168. coords.yi -= this.padding.top, coords.yl += this.padding.bottom;
  169. }
  170. this.children.forEach(function(el, i) {
  171. if (el.screen._ci !== -1) {
  172. el.index = el.screen._ci++;
  173. }
  174. var rendered = iterator(el, i);
  175. if (rendered === false) {
  176. delete el.lpos;
  177. return;
  178. }
  179. // if (el.screen._rendering) {
  180. // el._rendering = true;
  181. // }
  182. el.render();
  183. // if (el.screen._rendering) {
  184. // el._rendering = false;
  185. // }
  186. });
  187. this._emit('render', [coords]);
  188. return coords;
  189. };
  190. /**
  191. * Expose
  192. */
  193. module.exports = Layout;