PageRenderTime 124ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/cui/textbox.d

http://github.com/wilkie/djehuty
D | 823 lines | 592 code | 132 blank | 99 comment | 156 complexity | 65be936191383de7d211d6cf00cbc959 MD5 | raw file
  1. /*
  2. * textbox.d
  3. *
  4. * This module implements a large editable text area for TUI apps.
  5. *
  6. * Author: Dave Wilkinson
  7. * Originated: August 6th 2009
  8. *
  9. */
  10. module cui.textbox;
  11. import djehuty;
  12. import data.list;
  13. import cui.widget;
  14. import io.console;
  15. class CuiTextBox : CuiWidget {
  16. this(uint x, uint y, uint width, uint height) {
  17. super(x,y,width,height);
  18. _lines = new List!(LineInfo);
  19. LineInfo newItem = new LineInfo();
  20. newItem.value = "if (something) { /* in comment block */ init(); }";
  21. _lines.add(newItem);
  22. onLineChanged(_lines.length - 1);
  23. for (int o; o < 500; o++) {
  24. LineInfo subItem = new LineInfo();
  25. subItem.value = new string(o);
  26. _lines.add(subItem);
  27. onLineChanged(_lines.length - 1);
  28. }
  29. _tabWidth = 4;
  30. _lineCont = '$';
  31. _scrollH = ScrollType.Skip;
  32. _scrollV = ScrollType.Skip;
  33. }
  34. override void onKeyDown(Key key) {
  35. switch (key.code) {
  36. case Key.Backspace:
  37. if (_column == 0) {
  38. _row--;
  39. if (_row < 0) {
  40. _row = 0;
  41. break;
  42. }
  43. _column = _lines[_row].value.length;
  44. _lines[_row] ~= _lines[_row+1].dup();
  45. LineInfo oldLine;
  46. oldLine = _lines.removeAt(_row+1);
  47. _lineColumn = _column;
  48. onLineChanged(_row);
  49. refresh();
  50. break;
  51. }
  52. else if (_column == 1) {
  53. _lines[_row].value = _lines[_row].value.substring(1);
  54. if (_lines[_row].format !is null) {
  55. // The first section has one less length
  56. if (_lines[_row].format[0].len <= 1) {
  57. // The section has been destroyed
  58. if (_lines[_row].format.length == 1) {
  59. _lines[_row].format = null;
  60. }
  61. else {
  62. _lines[_row].format = _lines[_row].format[1..$];
  63. }
  64. }
  65. else {
  66. // Just subtract one
  67. _lines[_row].format[0].len--;
  68. }
  69. }
  70. }
  71. else if (_column == _lines[_row].value.length) {
  72. _lines[_row].value = _lines[_row].value.substring(0, _lines[_row].value.length - 1);
  73. // The last section has one less length
  74. if (_lines[_row].format !is null) {
  75. if (_lines[_row].format[$-1].len <= 1) {
  76. // The last section has been destroyed
  77. if (_lines[_row].format.length == 1) {
  78. // All sections have been destroyed
  79. _lines[_row].format = null;
  80. }
  81. else {
  82. _lines[_row].format = _lines[_row].format[0..$-1];
  83. }
  84. }
  85. else {
  86. // Just subtract one
  87. _lines[_row].format[$-1].len--;
  88. }
  89. }
  90. }
  91. else {
  92. _lines[_row].value = _lines[_row].value.substring(0, _column-1) ~ _lines[_row].value.substring(_column);
  93. // Reduce the count of the current format index
  94. if (_lines[_row].format !is null) {
  95. if (_lines[_row].format[_formatIndex].len <= 1) {
  96. // This format section has been depleted
  97. _lines[_row].format = _lines[_row].format[0.._formatIndex] ~ _lines[_row].format[_formatIndex+1..$];
  98. }
  99. else {
  100. // Just subtract
  101. _lines[_row].format[_formatIndex].len--;
  102. }
  103. }
  104. }
  105. _column--;
  106. _lineColumn = _column;
  107. onLineChanged(_row);
  108. drawLine(_row);
  109. positionCaret();
  110. break;
  111. case Key.Delete:
  112. if (_column == _lines[_row].value.length) {
  113. if (_row + 1 >= _lines.length) {
  114. // Last column of last row. Do nothing.
  115. } else {
  116. // Last column with more rows beneath, so suck next row up.
  117. _lines[_row] ~= _lines[_row+1].dup();
  118. LineInfo oldLine;
  119. oldLine = _lines.removeAt(_row+1);
  120. onLineChanged(_row);
  121. refresh();
  122. }
  123. } else {
  124. // Not the last column, so delete the character to the right.
  125. _lines[_row].value = _lines[_row].value.substring(0, _column) ~ _lines[_row].value.substring(_column + 1);
  126. if (_lines[_row].format !is null) {
  127. _formatIndex = calculateFormatIndex(_lines[_row], _column + 1);
  128. if (_lines[_row].format[_formatIndex].len < 2) {
  129. // This format section has been depleted
  130. _lines[_row].format = _lines[_row].format[0.._formatIndex] ~ _lines[_row].format[_formatIndex+1..$];
  131. }
  132. else {
  133. // One fewer character with this format
  134. _lines[_row].format[_formatIndex].len--;
  135. }
  136. _formatIndex = calculateFormatIndex(_lines[_row], _column);
  137. }
  138. refresh();
  139. }
  140. break;
  141. case Key.Left:
  142. _column--;
  143. if (_column < 0) {
  144. _row--;
  145. if (_row < 0) {
  146. _row = 0;
  147. _column = 0;
  148. }
  149. else {
  150. _column = _lines[_row].value.length;
  151. }
  152. }
  153. _lineColumn = _column;
  154. positionCaret();
  155. break;
  156. case Key.Right:
  157. _column++;
  158. if (_column > _lines[_row].value.length) {
  159. _row++;
  160. if (_row >= _lines.length) {
  161. _row = _lines.length - 1;
  162. _column = _lines[_row].value.length;
  163. _lineColumn = _column;
  164. }
  165. else {
  166. _column = 0;
  167. }
  168. }
  169. _lineColumn = _column;
  170. positionCaret();
  171. break;
  172. case Key.Up:
  173. _row--;
  174. _column = _lineColumn;
  175. if (_row < 0) {
  176. _row = 0;
  177. _column = 0;
  178. _lineColumn = _column;
  179. }
  180. if (_column > _lines[_row].value.length) {
  181. _column = _lines[_row].value.length;
  182. }
  183. positionCaret();
  184. break;
  185. case Key.Down:
  186. _row++;
  187. _column = _lineColumn;
  188. if (_row >= _lines.length) {
  189. _row = _lines.length - 1;
  190. _column = _lines[_row].value.length;
  191. }
  192. if (_column > _lines[_row].value.length) {
  193. _column = _lines[_row].value.length;
  194. }
  195. positionCaret();
  196. break;
  197. case Key.PageUp:
  198. _row -= this.height;
  199. _firstVisible -= this.height;
  200. if (_firstVisible < 0) {
  201. _firstVisible = 0;
  202. }
  203. if (_row < 0) {
  204. _row = 0;
  205. _column = 0;
  206. _lineColumn = _column;
  207. }
  208. if (_column > _lines[_row].value.length) {
  209. _column = _lines[_row].value.length;
  210. }
  211. refresh();
  212. break;
  213. case Key.PageDown:
  214. _row += this.height;
  215. _firstVisible += this.height;
  216. if (_firstVisible >= _lines.length) {
  217. _firstVisible = _lines.length - 1;
  218. }
  219. if (_row >= _lines.length) {
  220. _row = _lines.length - 1;
  221. _column = _lines[_row].value.length;
  222. }
  223. if (_column > _lines[_row].value.length) {
  224. _column = _lines[_row].value.length;
  225. }
  226. refresh();
  227. break;
  228. case Key.End:
  229. _column = _lines[_row].value.length;
  230. _lineColumn = _column;
  231. positionCaret();
  232. break;
  233. case Key.Home:
  234. _column = 0;
  235. _lineColumn = 0;
  236. positionCaret();
  237. break;
  238. default:
  239. break;
  240. }
  241. }
  242. override void onKeyChar(dchar chr) {
  243. if (chr == 0x8) {
  244. // Ignore character generation for backspace
  245. return;
  246. }
  247. else if (chr == 0xa) {
  248. // Ignore
  249. return;
  250. }
  251. else if (chr == 0xd) {
  252. // Pressing enter
  253. LineInfo newLine = new LineInfo();
  254. newLine.value = _lines[_row].value.substring(_column);
  255. // Splitting format field
  256. if (_lines[_row].format !is null) {
  257. if (_column == 0) {
  258. // At the beginning of the line; shift the format to the new line
  259. newLine.format = _lines[_row].format;
  260. _lines[_row].format = null;
  261. } else if (_column == _lines[_row].value.length) {
  262. // At the end of the line; formats remain unchanged
  263. newLine.format = null;
  264. } else {
  265. // In the middle of the line; current format may need cutting
  266. uint pos = 0;
  267. uint last;
  268. for (uint i = 0; i <= _formatIndex; i++) {
  269. last = pos;
  270. pos += _lines[_row].format[i].len;
  271. }
  272. if (_column == pos) {
  273. // Clean break
  274. newLine.format = _lines[_row].format[_formatIndex+1..$];
  275. } else {
  276. // Unclean break
  277. newLine.format = [_lines[_row].format[_formatIndex].dup];
  278. newLine.format ~= _lines[_row].format[_formatIndex+1..$];
  279. // Determine lengths for the format being cut
  280. newLine.format[0].len = pos - _column;
  281. _lines[_row].format[_formatIndex].len = _column - last;
  282. }
  283. _lines[_row].format = _lines[_row].format[0.._formatIndex+1];
  284. }
  285. _formatIndex = 0;
  286. }
  287. _lines.addAt(newLine, _row+1);
  288. _lines[_row].value = _lines[_row].value.substring(0, _column);
  289. _column = 0;
  290. _row++;
  291. _lineColumn = _column;
  292. onLineChanged(_row);
  293. refresh();
  294. return;
  295. }
  296. // Normal character append
  297. _lines[_row].value = _lines[_row].value.substring(0, _column) ~ Unicode.toUtf8([chr]) ~ _lines[_row].value.substring(_column);
  298. // Increase the length of the current format index
  299. if (_lines[_row].format !is null) {
  300. // Just add
  301. _lines[_row].format[_formatIndex].len++;
  302. }
  303. _column++;
  304. _lineColumn = _column;
  305. onLineChanged(_row);
  306. drawLine(_row);
  307. positionCaret();
  308. }
  309. override void onGotFocus() {
  310. positionCaret();
  311. }
  312. // Events
  313. void onLineChanged(uint lineNumber) {
  314. }
  315. // Properties
  316. uint row() {
  317. return _row;
  318. }
  319. uint column() {
  320. return _column;
  321. }
  322. // Description: This property returns the backcolor color of the text
  323. Color backcolor() {
  324. return _backcolor;
  325. }
  326. // Description: This property sets the backcolor of the text
  327. // value: the color to set backcolor to
  328. void backcolor(Color value) {
  329. _backcolor = value;
  330. }
  331. // Description: This property returns the forecolor color of the text
  332. Color forecolor() {
  333. return _forecolor;
  334. }
  335. // Description: This property sets the forecolor of the text
  336. // value: the color to set forecolor to
  337. void forecolor(Color value) {
  338. _forecolor = value;
  339. }
  340. // Description: This property returns the backcolor color of the line numbers
  341. Color backcolorNum() {
  342. return _backcolorNum;
  343. }
  344. // Description: This property sets the backcolor of the line numbers
  345. // value: the color to set backcolor to
  346. void backcolorNum(Color value) {
  347. _backcolorNum = value;
  348. }
  349. // Description: returns the forecolor color of the line numbers
  350. Color forecolorNum() {
  351. return _forecolorNum;
  352. }
  353. // Description: This property sets the forecolor of the line numbers
  354. // value: the color to set forecolor to
  355. void forecolorNum(Color value) {
  356. _forecolorNum = value;
  357. }
  358. // Description: This property returns the true if linenumbers are enabled, false if disabled
  359. bool lineNumbers() {
  360. return _lineNumbers;
  361. }
  362. // Description: This property enables or disables line numbers
  363. // value: true to enable the line numbers, false to disable
  364. void lineNumbers(bool value) {
  365. _lineNumbers = value;
  366. calculateLineNumbersWidth();
  367. }
  368. void refresh() {
  369. onDraw();
  370. positionCaret();
  371. }
  372. override void onDraw() {
  373. // Draw each line and pad any remaining spaces
  374. Console.hideCaret();
  375. uint i;
  376. for (i = _firstVisible; i < _lines.length && i < _firstVisible + this.height; i++) {
  377. // Draw line
  378. drawLine(i);
  379. }
  380. for (; i < _firstVisible + this.height; i++) {
  381. drawEmptyLine(i);
  382. }
  383. }
  384. override bool isTabStop() {
  385. return true;
  386. }
  387. protected:
  388. void drawLine(uint lineNumber) {
  389. Console.hideCaret();
  390. Console.position(0, lineNumber - _firstVisible);
  391. if (_lineNumbers) {
  392. if (_lineNumbersWidth == 0) {
  393. calculateLineNumbersWidth();
  394. }
  395. string strLineNumber = new string(lineNumber);
  396. Console.forecolor = _forecolorNum;
  397. Console.backcolor = _backcolorNum;
  398. Console.putSpaces(_lineNumbersWidth - 2 - strLineNumber.length);
  399. Console.put(strLineNumber);
  400. Console.put(": ");
  401. }
  402. uint[] formatTabExtension;
  403. uint curFormat, untilNextFormat;
  404. if (_lines[lineNumber].format !is null) {
  405. formatTabExtension.length = _lines[lineNumber].format.length;
  406. untilNextFormat = _lines[lineNumber].format[0].len;
  407. }
  408. string actualLine = _lines[lineNumber].value;
  409. string visibleLine = "";
  410. if (_tabWidth > 0) {
  411. for (uint i = 0; i < actualLine.length; i++) {
  412. while (curFormat + 1 < formatTabExtension.length && untilNextFormat == 0) {
  413. ++curFormat;
  414. untilNextFormat = _lines[lineNumber].format[curFormat].len;
  415. }
  416. if (curFormat < formatTabExtension.length)
  417. untilNextFormat--;
  418. string c = actualLine.charAt(i);
  419. if ("\t" == c) {
  420. uint tabSpaces = _tabWidth - visibleLine.length % _tabWidth;
  421. if (curFormat < formatTabExtension.length)
  422. formatTabExtension[curFormat] += tabSpaces - 1;
  423. visibleLine ~= " ".times(tabSpaces);
  424. } else {
  425. visibleLine ~= c;
  426. }
  427. }
  428. } else {
  429. visibleLine = actualLine;
  430. }
  431. uint pos = 0;
  432. // Make space for the line continuation symbol
  433. if (visibleLine.length > _firstColumn && _firstColumn > 0) {
  434. visibleLine = visibleLine.insertAt(" ", _firstColumn);
  435. pos++;
  436. }
  437. if (_lines[lineNumber].format is null) {
  438. // No formatting, this line is just a simple regular line
  439. Console.forecolor = _forecolor;
  440. Console.backcolor = _backcolor;
  441. if (_firstColumn >= _lines[lineNumber].value.length) {
  442. }
  443. else {
  444. Console.put(visibleLine.substring(_firstColumn));
  445. }
  446. }
  447. else {
  448. // Splitting up the line due to formatting
  449. for (uint i = 0; i < _lines[lineNumber].format.length; i++) {
  450. Console.forecolor = _lines[lineNumber].format[i].fgCol;
  451. Console.backcolor = _lines[lineNumber].format[i].bgCol;
  452. //Console.Console.put("[", _lines[lineNumber].format[i].length, "]");
  453. uint formatLength = _lines[lineNumber].format[i].len + formatTabExtension[i];
  454. if (formatLength + pos < _firstColumn) {
  455. // draw nothing
  456. }
  457. else if (pos >= _firstColumn) {
  458. Console.put(visibleLine[pos..pos + formatLength]);
  459. }
  460. else {
  461. Console.put(visibleLine[_firstColumn..pos + formatLength]);
  462. }
  463. pos += formatLength;
  464. }
  465. }
  466. Console.forecolor = _forecolor;
  467. Console.backcolor = _backcolor;
  468. // Pad with spaces
  469. uint num = (visibleLine.length - _firstColumn);
  470. //uint num = (_lines[lineNumber].value.length - _firstColumn);
  471. if (_firstColumn >= _lines[lineNumber].value.length) {
  472. num = this.width - _lineNumbersWidth;
  473. }
  474. else if (num > this.width - _lineNumbersWidth) {
  475. num = 0;
  476. }
  477. else {
  478. num = (this.width - _lineNumbersWidth) - num;
  479. }
  480. if (num != 0) {
  481. Console.putSpaces(num);
  482. }
  483. // Output the necessary line continuation symbols.
  484. Console.forecolor = Color.White;
  485. Console.backcolor = Color.Black;
  486. if (visibleLine.length > _firstColumn && _firstColumn > 0) {
  487. Console.position(_lineNumbersWidth, lineNumber - _firstVisible);
  488. Console.put(_lineCont);
  489. }
  490. if (visibleLine.length > _firstColumn && visibleLine.length - _firstColumn > this.width - _lineNumbersWidth) {
  491. Console.position(this.width - 1, lineNumber - _firstVisible);
  492. Console.put(_lineCont);
  493. }
  494. }
  495. void drawEmptyLine(uint lineNumber) {
  496. Console.hideCaret();
  497. Console.position(0, lineNumber - _firstVisible);
  498. // Pad with spaces
  499. Console.putSpaces(this.width);
  500. }
  501. void positionCaret() {
  502. bool shouldDraw;
  503. // Count the tabs to the left of the caret.
  504. uint leftTabSpaces = 0;
  505. if (_tabWidth > 0) {
  506. for (uint i = 0; i < _column; i++) {
  507. if ("\t" == _lines[_row].value.charAt(i)) {
  508. leftTabSpaces += _tabWidth - (i + leftTabSpaces) % _tabWidth - 1;
  509. }
  510. }
  511. }
  512. if (_column < _firstColumn) {
  513. // scroll horizontally
  514. if (_scrollH == ScrollType.Skip) {
  515. // If scrolling left, go to the start of the line and let the next section do the work.
  516. if (_column + leftTabSpaces < _firstColumn)
  517. _firstColumn = 0;
  518. } else { // ScrollType.Step
  519. _firstColumn = _column + leftTabSpaces;
  520. if (_firstColumn <= 1)
  521. _firstColumn = 0;
  522. }
  523. shouldDraw = true;
  524. }
  525. // _firstColumn > 0 means the characters are shifted 1 to the right thanks to the line continuation symbol
  526. if (_column + leftTabSpaces - _firstColumn + (_firstColumn > 0 ? 1 : 0) >= this.width - _lineNumbersWidth - 1) {
  527. // scroll horizontally
  528. if (_scrollH == ScrollType.Skip) {
  529. _firstColumn = _column + leftTabSpaces - (this.width - _lineNumbersWidth) / 2;
  530. } else { // ScrollType.Step
  531. _firstColumn = _column + leftTabSpaces - (this.width - _lineNumbersWidth) + 3;
  532. }
  533. shouldDraw = true;
  534. }
  535. if (_row < _firstVisible) {
  536. // scroll vertically
  537. if (_scrollV == ScrollType.Skip) {
  538. // If scrolling up, go to the first row and let the next section do the work.
  539. _firstVisible = 0;
  540. } else { // ScrollType.Step
  541. _firstVisible = _row;
  542. if (_firstVisible < 0)
  543. _firstVisible = 0;
  544. }
  545. shouldDraw = true;
  546. }
  547. if (this.top + (_row - _firstVisible) >= this.bottom) {
  548. // scroll vertically
  549. if (_scrollV == ScrollType.Skip) {
  550. _firstVisible = _row - this.height / 2;
  551. } else { // ScrollType.Step
  552. _firstVisible = _row - this.height + 1;
  553. }
  554. if (_firstVisible >= _lines.length) {
  555. _firstVisible = _lines.length - 1;
  556. }
  557. shouldDraw = true;
  558. }
  559. if (shouldDraw) {
  560. onDraw();
  561. }
  562. _formatIndex = calculateFormatIndex(_lines[_row], _column);
  563. // Is the caret on the screen?
  564. if ((this.left + _lineNumbersWidth + (_column - _firstColumn) >= this.right) || (this.top + (_row - _firstVisible) >= this.bottom)) {
  565. // The caret is outside of the bounds of the widget
  566. Console.hideCaret();
  567. }
  568. else {
  569. // Move cursor to where the edit caret is
  570. Console.position(_lineNumbersWidth + (_column - _firstColumn) + leftTabSpaces + (_firstColumn > 0 ? 1 : 0), _row - _firstVisible);
  571. // The caret is within the bounds of the widget
  572. Console.showCaret();
  573. }
  574. }
  575. // Description: Calculates the formatIndex given a LineInfo and column.
  576. // Returns: The calculated formatIndex.
  577. int calculateFormatIndex(LineInfo line, int column) {
  578. int formatIndex = 0;
  579. if (line.format !is null) {
  580. uint pos;
  581. for (uint i = 0; i < line.format.length; i++) {
  582. pos += line.format[i].len;
  583. if (pos >= column) {
  584. formatIndex = i;
  585. break;
  586. }
  587. }
  588. }
  589. return formatIndex;
  590. }
  591. void calculateLineNumbersWidth() {
  592. if (_lineNumbers) {
  593. // The width of the maximum line (in decimal as a string)
  594. // summed with two for the ': '
  595. _lineNumbersWidth = (new string(_lines.length)).length + 2;
  596. }
  597. else {
  598. _lineNumbers = 0;
  599. }
  600. }
  601. // The behavior when a line is scrolled via the keyboard.
  602. enum ScrollType {
  603. Step,
  604. Skip,
  605. }
  606. // The formatting of a line segment
  607. static class LineFormat {
  608. this () {}
  609. this (Color f, Color b, uint l) {
  610. fgCol = f;
  611. bgCol = b;
  612. len = l;
  613. }
  614. LineFormat dup() {
  615. return new LineFormat(fgCol, bgCol, len);
  616. }
  617. int opEquals(LineFormat lf) {
  618. return cast(int)(this.fgCol == lf.fgCol && this.bgCol == lf.bgCol);
  619. }
  620. Color fgCol;
  621. Color bgCol;
  622. uint len;
  623. }
  624. // The information about each line
  625. class LineInfo {
  626. this() {
  627. }
  628. this(string v, LineFormat[] f) {
  629. value = v;
  630. format = f;
  631. this();
  632. }
  633. LineInfo dup() {
  634. return new LineInfo(this.value, this.format.dup);
  635. }
  636. void opCatAssign(LineInfo li) {
  637. if (this.format !is null && li.format !is null) {
  638. // Merge format lines
  639. if (this.format[$-1] == li.format[0]) {
  640. this.format[$-1].len += li.format[0].len;
  641. this.format ~= li.format[1..$];
  642. } else {
  643. this.format ~= li.format;
  644. }
  645. } else if (this.format !is null) {
  646. // Make a format for the 2nd line
  647. this.format ~= [new LineFormat(_forecolor, _backcolor, li.value.length)];
  648. } else if (li.format !is null) {
  649. // Make a format for the 1st line
  650. this.format = [new LineFormat(_forecolor, _backcolor, this.value.length)] ~ li.format;
  651. } else {
  652. // Ignore formats if none exist
  653. }
  654. this.value ~= li.value;
  655. }
  656. LineInfo opCat(LineInfo li) {
  657. LineInfo li_new = this.dup();
  658. li_new ~= li;
  659. return li_new;
  660. }
  661. string value;
  662. LineFormat[] format;
  663. }
  664. // Stores the buffer of lines
  665. List!(LineInfo) _lines;
  666. // The top left corner
  667. int _firstVisible; // Row
  668. int _firstColumn; // Column
  669. // The current caret position
  670. int _row;
  671. int _column;
  672. // The current caret position within the format array
  673. int _formatIndex;
  674. // The column that the caret is in while pressing up and down or scrolling.
  675. int _lineColumn;
  676. // Whether or not line numbers are rendered
  677. bool _lineNumbers;
  678. // The width of the line numbers column
  679. uint _lineNumbersWidth;
  680. // The width of a single tab character expressed in spaces
  681. uint _tabWidth;
  682. // The default text colors
  683. Color _forecolor = Color.Gray;
  684. Color _backcolor = Color.Black;
  685. Color _forecolorNum = Color.DarkYellow;
  686. Color _backcolorNum = Color.Black;
  687. // The symbol to use to show a line continuation
  688. dchar _lineCont;
  689. // How to scroll horizontally and vertically
  690. ScrollType _scrollH, _scrollV;
  691. }