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