buffer_reflow.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import 'dart:math';
  2. import 'buffer.dart';
  3. import 'buffer_line.dart';
  4. class LayoutResult {
  5. LayoutResult(this.layout, this.removedCount);
  6. final List<int> layout;
  7. final int removedCount;
  8. }
  9. class InsertionSet {
  10. InsertionSet({this.lines, this.start, this.isNull = false});
  11. final List<BufferLine>? lines;
  12. final int? start;
  13. final bool isNull;
  14. static InsertionSet nullValue = InsertionSet(isNull: true);
  15. }
  16. class BufferReflow {
  17. BufferReflow(this._buffer);
  18. final Buffer _buffer;
  19. void doReflow(int colsBefore, int colsAfter) {
  20. if (colsBefore == colsAfter) {
  21. return;
  22. }
  23. if (colsAfter > colsBefore) {
  24. //got larger
  25. _reflowLarger(colsBefore, colsAfter);
  26. } else {
  27. //got smaller
  28. _reflowSmaller(colsBefore, colsAfter);
  29. }
  30. }
  31. void _reflowLarger(int colsBefore, int colsAfter) {
  32. var toRemove = _reflowLargerGetLinesToRemove(colsBefore, colsAfter);
  33. if (toRemove.length > 0) {
  34. var newLayoutResult =
  35. _reflowLargerCreateNewLayout(_buffer.lines, toRemove);
  36. _reflowLargerApplyNewLayout(newLayoutResult.layout);
  37. _reflowLargerAdjustViewport(
  38. colsBefore, colsAfter, newLayoutResult.removedCount);
  39. }
  40. }
  41. void _reflowSmaller(int colsBefore, int colsAfter) {
  42. // Gather all BufferLines that need to be inserted into the Buffer here so that they can be
  43. // batched up and only committed once
  44. List<InsertionSet> toInsert = [];
  45. int countToInsert = 0;
  46. // Go backwards as many lines may be trimmed and this will avoid considering them
  47. for (int y = _buffer.lines.length - 1; y >= 0; y--) {
  48. // Check whether this line is a problem or not, if not skip it
  49. BufferLine nextLine = _buffer.lines[y];
  50. int lineLength = nextLine.getTrimmedLength();
  51. if (!nextLine.isWrapped && lineLength <= colsAfter) {
  52. continue;
  53. }
  54. // Gather wrapped lines and adjust y to be the starting line
  55. List<BufferLine> wrappedLines = [];
  56. wrappedLines.add(nextLine);
  57. while (nextLine.isWrapped && y > 0) {
  58. nextLine = _buffer.lines[--y];
  59. wrappedLines.insert(0, nextLine);
  60. }
  61. // If these lines contain the cursor don't touch them, the program will handle fixing up
  62. // wrapped lines with the cursor
  63. final absoluteY = _buffer.cursorY + _buffer.scrollOffsetFromTop;
  64. if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
  65. continue;
  66. }
  67. int lastLineLength = wrappedLines.last.getTrimmedLength();
  68. List<int> destLineLengths =
  69. _getNewLineLengths(wrappedLines, colsBefore, colsAfter);
  70. int linesToAdd = destLineLengths.length - wrappedLines.length;
  71. // Add the new lines
  72. List<BufferLine> newLines = [];
  73. for (int i = 0; i < linesToAdd; i++) {
  74. BufferLine newLine = BufferLine(isWrapped: true);
  75. newLines.add(newLine);
  76. }
  77. if (newLines.length > 0) {
  78. toInsert.add(InsertionSet(
  79. start: y + wrappedLines.length + countToInsert, lines: newLines));
  80. countToInsert += newLines.length;
  81. }
  82. newLines.forEach((l) => wrappedLines.add(l));
  83. // Copy buffer data to new locations, this needs to happen backwards to do in-place
  84. int destLineIndex =
  85. destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);
  86. int destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;
  87. if (destCol == 0) {
  88. destLineIndex--;
  89. destCol = destLineLengths[destLineIndex];
  90. }
  91. int srcLineIndex = wrappedLines.length - linesToAdd - 1;
  92. int srcCol = lastLineLength;
  93. while (srcLineIndex >= 0) {
  94. int cellsToCopy = min(srcCol, destCol);
  95. wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex],
  96. srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy);
  97. destCol -= cellsToCopy;
  98. if (destCol == 0) {
  99. destLineIndex--;
  100. if (destLineIndex >= 0) destCol = destLineLengths[destLineIndex];
  101. }
  102. srcCol -= cellsToCopy;
  103. if (srcCol == 0) {
  104. srcLineIndex--;
  105. int wrappedLinesIndex = max(srcLineIndex, 0);
  106. srcCol = _getWrappedLineTrimmedLengthRow(
  107. wrappedLines, wrappedLinesIndex, colsBefore);
  108. }
  109. }
  110. // Null out the end of the line ends if a wide character wrapped to the following line
  111. for (int i = 0; i < wrappedLines.length; i++) {
  112. if (destLineLengths[i] < colsAfter) {
  113. wrappedLines[i].removeRange(destLineLengths[i]);
  114. }
  115. }
  116. _buffer.adjustSavedCursor(0, linesToAdd);
  117. //TODO: maybe row count has to be handled here?
  118. }
  119. _rearrange(toInsert, countToInsert);
  120. }
  121. void _rearrange(List<InsertionSet> toInsert, int countToInsert) {
  122. // Rearrange lines in the buffer if there are any insertions, this is done at the end rather
  123. // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
  124. // costly calls to CircularList.splice.
  125. if (toInsert.length > 0) {
  126. // Record buffer insert events and then play them back backwards so that the indexes are
  127. // correct
  128. List<int> insertEvents = [];
  129. // Record original lines so they don't get overridden when we rearrange the list
  130. List<BufferLine> originalLines = List<BufferLine>.from(_buffer.lines);
  131. _buffer.lines.addAll(
  132. List<BufferLine>.generate(countToInsert, (index) => BufferLine()));
  133. int originalLinesLength = originalLines.length;
  134. int originalLineIndex = originalLinesLength - 1;
  135. int nextToInsertIndex = 0;
  136. InsertionSet nextToInsert = toInsert[nextToInsertIndex];
  137. //TODO: remove rows that now are "too much"
  138. int countInsertedSoFar = 0;
  139. for (int i = originalLinesLength + countToInsert - 1; i >= 0; i--) {
  140. if (!nextToInsert.isNull &&
  141. nextToInsert.start != null &&
  142. nextToInsert.lines != null &&
  143. nextToInsert.start! > originalLineIndex + countInsertedSoFar) {
  144. // Insert extra lines here, adjusting i as needed
  145. for (int nextI = nextToInsert.lines!.length - 1;
  146. nextI >= 0;
  147. nextI--) {
  148. if (i < 0) {
  149. // if we reflow and the content has to be scrolled back past the beginning
  150. // of the buffer then we end up loosing those lines
  151. break;
  152. }
  153. _buffer.lines[i--] = nextToInsert.lines![nextI];
  154. }
  155. i++;
  156. countInsertedSoFar += nextToInsert.lines!.length;
  157. if (nextToInsertIndex < toInsert.length - 1) {
  158. nextToInsert = toInsert[++nextToInsertIndex];
  159. } else {
  160. nextToInsert = InsertionSet.nullValue; //TODO: just break?
  161. }
  162. } else {
  163. _buffer.lines[i] = originalLines[originalLineIndex--];
  164. }
  165. }
  166. }
  167. }
  168. /// <summary>
  169. /// Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
  170. /// compute the wrapping points since wide characters may need to be wrapped onto the following line.
  171. /// This function will return an array of numbers of where each line wraps to, the resulting array
  172. /// will only contain the values `newCols` (when the line does not end with a wide character) and
  173. /// `newCols - 1` (when the line does end with a wide character), except for the last value which
  174. /// will contain the remaining items to fill the line.
  175. /// Calling this with a `newCols` value of `1` will lock up.
  176. /// </summary>
  177. List<int> _getNewLineLengths(
  178. List<BufferLine> wrappedLines, int oldCols, int newCols) {
  179. List<int> newLineLengths = [];
  180. int cellsNeeded = 0;
  181. for (int i = 0; i < wrappedLines.length; i++) {
  182. cellsNeeded += _getWrappedLineTrimmedLengthRow(wrappedLines, i, oldCols);
  183. }
  184. // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
  185. // linesNeeded
  186. int srcCol = 0;
  187. int srcLine = 0;
  188. int cellsAvailable = 0;
  189. while (cellsAvailable < cellsNeeded) {
  190. if (cellsNeeded - cellsAvailable < newCols) {
  191. // Add the final line and exit the loop
  192. newLineLengths.add(cellsNeeded - cellsAvailable);
  193. break;
  194. }
  195. srcCol += newCols;
  196. int oldTrimmedLength =
  197. _getWrappedLineTrimmedLengthRow(wrappedLines, srcLine, oldCols);
  198. if (srcCol > oldTrimmedLength) {
  199. srcCol -= oldTrimmedLength;
  200. srcLine++;
  201. }
  202. bool endsWithWide = wrappedLines[srcLine].getWidthAt(srcCol - 1) == 2;
  203. if (endsWithWide) {
  204. srcCol--;
  205. }
  206. int lineLength = endsWithWide ? newCols - 1 : newCols;
  207. newLineLengths.add(lineLength);
  208. cellsAvailable += lineLength;
  209. }
  210. return newLineLengths;
  211. }
  212. void _reflowLargerAdjustViewport(
  213. int colsBefore, int colsAfter, int countRemoved) {
  214. // Adjust viewport based on number of items removed
  215. var viewportAdjustments = countRemoved;
  216. while (viewportAdjustments-- > 0) {
  217. //viewport is at the top
  218. if (_buffer.lines.length <= _buffer.terminal.viewHeight) {
  219. //cursor is not at the top
  220. if (_buffer.cursorY > 0) {
  221. _buffer.moveCursorY(-1);
  222. }
  223. //buffer doesn't have enough lines
  224. if (_buffer.lines.length < _buffer.terminal.viewHeight) {
  225. // Add an extra row at the bottom of the viewport
  226. _buffer.lines.add(BufferLine());
  227. }
  228. }
  229. }
  230. //TODO: adjust buffer content to max length
  231. _buffer.adjustSavedCursor(0, -countRemoved);
  232. }
  233. void _reflowLargerApplyNewLayout(List<int> newLayout) {
  234. var newLayoutLines = List<BufferLine>.generate(
  235. newLayout.length, (index) => _buffer.lines[newLayout[index]]);
  236. // Rearrange the list
  237. for (int i = 0; i < newLayoutLines.length; i++) {
  238. _buffer.lines[i] = newLayoutLines[i];
  239. }
  240. _buffer.lines
  241. .removeRange(newLayoutLines.length - 1, _buffer.lines.length - 1);
  242. }
  243. LayoutResult _reflowLargerCreateNewLayout(
  244. List<BufferLine> lines, List<int> toRemove) {
  245. var layout = List<int>.empty(growable: true);
  246. // First iterate through the list and get the actual indexes to use for rows
  247. int nextToRemoveIndex = 0;
  248. int nextToRemoveStart = toRemove[nextToRemoveIndex];
  249. int countRemovedSoFar = 0;
  250. for (int i = 0; i < lines.length; i++) {
  251. if (nextToRemoveStart == i) {
  252. int countToRemove = toRemove[++nextToRemoveIndex];
  253. i += countToRemove - 1;
  254. countRemovedSoFar += countToRemove;
  255. nextToRemoveStart = lines.length + 1;
  256. if (nextToRemoveIndex < toRemove.length - 1)
  257. nextToRemoveStart = toRemove[++nextToRemoveIndex];
  258. } else {
  259. layout.add(i);
  260. }
  261. }
  262. return LayoutResult(layout, countRemovedSoFar);
  263. }
  264. List<int> _reflowLargerGetLinesToRemove(int colsBefore, int colsAfter) {
  265. List<int> toRemove = List<int>.empty(growable: true);
  266. for (int y = 0; y < _buffer.lines.length - 1; y++) {
  267. // Check if this row is wrapped
  268. int i = y;
  269. BufferLine nextLine = _buffer.lines[++i];
  270. if (!nextLine.isWrapped) {
  271. continue;
  272. }
  273. // Check how many lines it's wrapped for
  274. List<BufferLine> wrappedLines = List<BufferLine>.empty(growable: true);
  275. wrappedLines.add(_buffer.lines[y]);
  276. while (i < _buffer.lines.length && nextLine.isWrapped) {
  277. wrappedLines.add(nextLine);
  278. nextLine = _buffer.lines[++i];
  279. }
  280. final bufferAbsoluteY = _buffer.cursorY + _buffer.scrollOffsetFromTop;
  281. // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
  282. // lines with the cursor
  283. if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
  284. y += wrappedLines.length - 1;
  285. continue;
  286. }
  287. // Copy buffer data to new locations
  288. int destLineIndex = 0;
  289. int destCol = _getWrappedLineTrimmedLengthRow(
  290. _buffer.lines, destLineIndex, colsBefore);
  291. int srcLineIndex = 1;
  292. int srcCol = 0;
  293. while (srcLineIndex < wrappedLines.length) {
  294. int srcTrimmedTineLength = _getWrappedLineTrimmedLengthRow(
  295. wrappedLines, srcLineIndex, colsBefore);
  296. int srcRemainingCells = srcTrimmedTineLength - srcCol;
  297. int destRemainingCells = colsAfter - destCol;
  298. int cellsToCopy = min(srcRemainingCells, destRemainingCells);
  299. wrappedLines[destLineIndex].copyCellsFrom(
  300. wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy);
  301. destCol += cellsToCopy;
  302. if (destCol == colsAfter) {
  303. destLineIndex++;
  304. destCol = 0;
  305. }
  306. srcCol += cellsToCopy;
  307. if (srcCol == srcTrimmedTineLength) {
  308. srcLineIndex++;
  309. srcCol = 0;
  310. }
  311. // Make sure the last cell isn't wide, if it is copy it to the current dest
  312. if (destCol == 0 && destLineIndex != 0) {
  313. if (wrappedLines[destLineIndex - 1].getWidthAt(colsAfter - 1) == 2) {
  314. wrappedLines[destLineIndex].copyCellsFrom(
  315. wrappedLines[destLineIndex - 1], colsAfter - 1, destCol++, 1);
  316. // Null out the end of the last row
  317. wrappedLines[destLineIndex - 1].erase(
  318. _buffer.terminal.cellAttr.value,
  319. colsAfter - 1,
  320. colsAfter,
  321. false);
  322. }
  323. }
  324. }
  325. // Clear out remaining cells or fragments could remain;
  326. wrappedLines[destLineIndex]
  327. .erase(_buffer.terminal.cellAttr.value, destCol, colsAfter, false);
  328. // Work backwards and remove any rows at the end that only contain null cells
  329. int countToRemove = 0;
  330. for (int ix = wrappedLines.length - 1; ix > 0; ix--) {
  331. if (ix > destLineIndex || wrappedLines[ix].getTrimmedLength() == 0) {
  332. countToRemove++;
  333. } else {
  334. break;
  335. }
  336. }
  337. if (countToRemove > 0) {
  338. toRemove.add(y + wrappedLines.length - countToRemove); // index
  339. toRemove.add(countToRemove);
  340. }
  341. y += wrappedLines.length - 1;
  342. }
  343. return toRemove;
  344. }
  345. int _getWrappedLineTrimmedLengthRow(
  346. List<BufferLine> lines, int row, int cols) {
  347. return _getWrappedLineTrimmedLength(
  348. lines[row], row == lines.length - 1 ? null : lines[row + 1], cols);
  349. }
  350. int _getWrappedLineTrimmedLength(
  351. BufferLine line, BufferLine? nextLine, int cols) {
  352. // If this is the last row in the wrapped line, get the actual trimmed length
  353. if (nextLine == null) {
  354. return line.getTrimmedLength();
  355. }
  356. // Detect whether the following line starts with a wide character and the end of the current line
  357. // is null, if so then we can be pretty sure the null character should be excluded from the line
  358. // length]
  359. bool endsInNull =
  360. !(line.hasContentAt(cols - 1)) && line.getWidthAt(cols - 1) == 1;
  361. bool followingLineStartsWithWide = nextLine.getWidthAt(0) == 2;
  362. if (endsInNull && followingLineStartsWithWide) {
  363. return cols - 1;
  364. }
  365. return cols;
  366. }
  367. }