1 /++
2 	Some widgets that are included in the package.
3 +/
4 module qui.widgets;
5 
6 import qui.qui;
7 import qui.utils;
8 import utils.misc;
9 import utils.ds;
10 
11 import std.conv : to;
12 
13 /// for testing only.
14 class ScrollTestingWidget : QWidget{
15 protected:
16 	/// text and background colors
17 	Color textColor, backgroundColor, emptyColor;
18 	/// whether to display debug info
19 	bool _debugInfo;
20 
21 	override bool updateEvent(){
22 		foreach (y; viewportY .. viewportY + viewportHeight){
23 			moveTo(viewportX, y);
24 			foreach (x; viewportX .. viewportX + viewportWidth){
25 				write(((x+y) % 10).to!dstring[0], textColor,
26 					y < height && x < width ?
27 						backgroundColor : emptyColor);
28 			}
29 		}
30 		if (_debugInfo){
31 			moveTo(viewportX, viewportY);
32 			write("size, width x height:     " ~
33 				to!dstring(width) ~ "x" ~ to!dstring(height) ~ '|',
34 				DEFAULT_FG, DEFAULT_BG);
35 			moveTo(viewportX, viewportY+1);
36 			write("min size, width x height: " ~
37 				to!dstring(width) ~ "x" ~ to!dstring(height) ~ '|',
38 				DEFAULT_FG, DEFAULT_BG);
39 			moveTo(viewportX, viewportY+2);
40 			write("max size, width x height: " ~
41 				to!dstring(width) ~ "x" ~ to!dstring(height) ~ '|',
42 				DEFAULT_FG, DEFAULT_BG);
43 			moveTo(viewportX, viewportY+3);
44 			write("scroll X, Y: " ~
45 				to!dstring(scrollX) ~ "," ~ to!dstring(scrollY) ~ '|',
46 				DEFAULT_FG, DEFAULT_BG);
47 			moveTo(viewportX, viewportY+4);
48 			write("view X, Y: " ~
49 				to!dstring(viewportX) ~ "," ~ to!dstring(viewportY) ~
50 				'|', DEFAULT_FG, DEFAULT_BG);
51 			moveTo(viewportX, viewportY+5);
52 			write("view width x height: " ~
53 				to!dstring(viewportWidth) ~ "x" ~
54 				to!dstring(viewportHeight) ~ '|',
55 				DEFAULT_FG, DEFAULT_BG);
56 		}
57 		return true;
58 	}
59 public:
60 	/// constructor
61 	this(Color textColor = DEFAULT_FG,
62 			Color backgroundColor = DEFAULT_BG,
63 			Color emptyColor = Color.green, bool debugInfo=false){
64 		eventSubscribe(EventMask.Resize | EventMask.Scroll |
65 			EventMask.Update);
66 		this.textColor = textColor;
67 		this.backgroundColor = backgroundColor;
68 		this.emptyColor = emptyColor;
69 		this._debugInfo = debugInfo;
70 	}
71 }
72 
73 /// Displays some text
74 ///
75 /// doesnt handle new-line characters
76 class TextLabelWidget : QWidget{
77 private:
78 	/// text and background colors
79 	Color _fg = DEFAULT_FG, _bg = DEFAULT_BG;
80 	/// the text to display
81 	dstring _caption;
82 protected:
83 	override bool updateEvent(){
84 		moveTo(0,0);
85 		write(_caption, _fg, _bg);
86 		return true;
87 	}
88 public:
89 	/// constructor
90 	this(dstring caption = ""){
91 		eventSubscribe(EventMask.Resize | EventMask.Scroll |
92 			EventMask.Update);
93 		_caption = caption;
94 		height = 1;
95 		width = cast(uint)caption.length;
96 	}
97 	/// the text to display
98 	@property dstring caption(){
99 		return _caption;
100 	}
101 	/// ditto
102 	@property dstring caption(dstring newCaption){
103 		_caption = newCaption;
104 		width = cast(uint)newCaption.length;
105 		requestUpdate();
106 		return _caption;
107 	}
108 	/// text color
109 	@property Color textColor(){
110 		return _fg;
111 	}
112 	/// ditto
113 	@property Color textColor(Color newColor){
114 		_fg = newColor;
115 		requestUpdate();
116 		return _fg;
117 	}
118 	/// background color
119 	@property Color backColor(){
120 		return _bg;
121 	}
122 	/// ditto
123 	@property Color backColor(Color newColor){
124 		_bg = newColor;
125 		requestUpdate();
126 		return _bg;
127 	}
128 }
129 
130 /// To get single-line input from keyboard
131 class EditLineWidget : QWidget{
132 private:
133 	/// text that's been input-ed
134 	dchar[] _text;
135 	/// position of cursor
136 	uint _x;
137 	/// Foreground and background colors
138 	Color _fg = DEFAULT_FG, _bg = DEFAULT_BG;
139 protected:
140 	/// override resize to re-scroll
141 	override bool resizeEvent(){
142 		requestUpdate();
143 		return true;
144 	}
145 	override bool mouseEvent(MouseEvent mouse){
146 		if (mouse.button == MouseEvent.Button.Left &&
147 		mouse.state == MouseEvent.State.Click)
148 			_x = mouse.x;
149 		requestUpdate();
150 		return true;
151 	}
152 	override bool keyboardEvent(KeyboardEvent key, bool cycle){
153 		if (cycle)
154 			return false;
155 		if (key.key == '\b'){ // backspace
156 			if (_x > 0){
157 				if (_x < _text.length)
158 					_text[_x - 1 .. $ - 1] = _text[_x .. $];
159 				_text.length --;
160 				_x --;
161 			}
162 		}else if (key.key == Key.Delete){ // delete
163 			if (_x < _text.length){
164 				_text[_x .. $ - 1] = _text[_x + 1 .. $];
165 				_text.length --;
166 			}
167 		}else if (key.key == Key.LeftArrow){ // <-
168 			_x = _x - (1 * (_x > 0));
169 		}else if (key.key == Key.RightArrow){ // ->
170 			_x = _x + (1 * (_x < _text.length));
171 		}else if (key.isChar && key.key != '\n'){ // insert character
172 			if (_x == _text.length)
173 				_text ~= key.key;
174 			else
175 				_text = _text[0 .. _x] ~ key.key ~ _text[_x .. $];
176 		}
177 		requestUpdate();
178 		return true;
179 	}
180 	override bool updateEvent(){
181 		moveTo(0,0);
182 		write(cast(dstring)_text, _fg, _bg);
183 		requestCursorPos(_x, 0);
184 		return true;
185 	}
186 public:
187 	/// constructor
188 	this(dstring text = ""){
189 		eventSubscribe(EventMask.Resize | EventMask.Scroll |
190 			EventMask.MousePress | EventMask.KeyboardPress |
191 			EventMask.Update);
192 		this._text = cast(dchar[])text.dup;
193 		height = 1;
194 	}
195 
196 	///The text that has been input-ed.
197 	@property dstring text(){
198 		return cast(dstring)_text.dup;
199 	}
200 	///The text that has been input-ed.
201 	@property dstring text(dstring newText){
202 		_text = cast(dchar[])newText.dup;
203 		// request update
204 		requestUpdate();
205 		return cast(dstring)newText;
206 	}
207 
208 	/// text color
209 	@property Color textColor(){
210 		return _fg;
211 	}
212 	/// ditto
213 	@property Color textColor(Color newColor){
214 		_fg = newColor;
215 		requestUpdate();
216 		return _fg;
217 	}
218 
219 	/// background color
220 	@property Color backColor(){
221 		return _bg;
222 	}
223 	/// ditto
224 	@property Color backColor(Color newColor){
225 		_bg = newColor;
226 		requestUpdate();
227 		return _bg;
228 	}
229 }
230 /*
231 /// Can be used as a simple text editor, or to just display text
232 class MemoWidget : QWidget{
233 private:
234 	List!dstring _lines;
235 	/// how many characters/lines are skipped
236 	uint _scrollX, _scrollY;
237 	/// whether the cursor is, relative to line#0 character#0
238 	uint _cursorX, _cursorY;
239 	/// whether the text in it will be editable
240 	bool _enableEditing = true;
241 	/// used by widget itself to recalculate scrolling
242 	void reScroll(){
243 		// _scrollY
244 		adjustScrollingOffset(_cursorY, this.height, lineCount, _scrollY);
245 		// _scrollX
246 		adjustScrollingOffset(_cursorX, this.width, cast(uint)readLine(_cursorY).length, _scrollX);
247 	}
248 	/// used by widget itself to move cursor
249 	void moveCursor(uint x, uint y){
250 		_cursorX = x;
251 		_cursorY = y;
252 		
253 		if (_cursorY > lineCount)
254 			_cursorY = lineCount-1;
255 		if (_cursorX > readLine(_cursorY).length)
256 			_cursorX = cast(uint)readLine(_cursorY).length;
257 	}
258 	/// Reads a line from widgetLines
259 	dstring readLine(uint index){
260 		if (index >= _lines.length)
261 			return "";
262 		return _lines.read(index);
263 	}
264 	/// overwrites a line
265 	void overwriteLine(uint index, dstring line){
266 		if (index == _lines.length)
267 			_lines.append(line);
268 		else
269 			_lines.set(index,line);
270 		
271 	}
272 	/// deletes a line
273 	void removeLine(uint index){
274 		if (index < _lines.length)
275 			_lines.remove(index);
276 	}
277 	/// inserts a line
278 	void insertLine(uint index, dstring line){
279 		if (index == _lines.length)
280 			_lines.append(line);
281 		else
282 			_lines.insert(index, line);
283 	}
284 	/// adds a line
285 	void addLine(dstring line){
286 		_lines.append(line);
287 	}
288 	/// returns lines count
289 	@property uint lineCount(){
290 		return cast(uint)_lines.length+1;
291 	}
292 protected:
293 	override void updateEvent(){
294 		const uint count = lineCount;
295 		if (count > 0){
296 			//write lines to memo
297 			for (uint i = _scrollY; i < count && _display.cursor.y < this.height; i++){
298 				_display.write(readLine(i).scrollHorizontal(_scrollX, this.width), 
299 					textColor, backgroundColor);
300 			}
301 		}
302 		_display.fill(' ', textColor, backgroundColor);
303 		// cursor position, in case this is active
304 		_cursorPosition = Position(_cursorX - _scrollX, _cursorY - _scrollY);
305 	}
306 
307 	override void resizeEvent(){
308 		requestUpdate();
309 		reScroll();
310 	}
311 	
312 	override void mouseEvent(MouseEvent mouse){
313 		//calculate mouse position, relative to scroll
314 		mouse.x = mouse.x + cast(int)_scrollX;
315 		mouse.y = mouse.y + cast(int)_scrollY;
316 		if (mouse.button == mouse.Button.Left){
317 			requestUpdate();
318 			moveCursor(mouse.x, mouse.y);
319 		}else if (mouse.button == mouse.Button.ScrollDown){
320 			if (_cursorY+1 < lineCount){
321 				requestUpdate();
322 				moveCursor(_cursorX, _cursorY + 4);
323 				reScroll();
324 			}
325 		}else if (mouse.button == mouse.Button.ScrollUp){
326 			if (_cursorY > 0){
327 				requestUpdate();
328 				if (_cursorY < 4){
329 					moveCursor(_cursorX, 0);
330 				}else{
331 					moveCursor(_cursorX, _cursorY - 4);
332 				}
333 				reScroll();
334 			}
335 		}
336 	}
337 	// too big of a mess to be dealt with right now, TODO try to make this shorter
338 	override void keyboardEvent(KeyboardEvent key){
339 		if (key.isChar){
340 			if (_enableEditing){
341 				requestUpdate();
342 				dstring currentLine = readLine(_cursorY);
343 				//check if backspace
344 				if (key.key == '\b'){
345 					//make sure that it's not the first line, first line cannot be removed
346 					if (_cursorY > 0){
347 						//check if has to remove a '\n'
348 						if (_cursorX == 0){
349 							_cursorY --;
350 							//if line's not empty, append it to previous line
351 							_cursorX = cast(uint)readLine(_cursorY).length;
352 							if (currentLine != ""){
353 								//else, append this line to previous
354 								overwriteLine(_cursorY, readLine(_cursorY)~currentLine);
355 							}
356 							removeLine(_cursorY+1);
357 						}else{
358 							overwriteLine(_cursorY, cast(dstring)deleteElement(cast(dchar[])currentLine,_cursorX-1));
359 							_cursorX --;
360 						}
361 					}else if (_cursorX > 0){
362 						overwriteLine(_cursorY, cast(dstring)deleteElement(cast(dchar[])currentLine,_cursorX-1));
363 						_cursorX --;
364 					}
365 					
366 				}else if (key.key == '\n'){
367 					//insert a newline
368 					if (_cursorX == readLine(_cursorY).length){
369 						if (_cursorY >= lineCount - 1){
370 							_lines.append("");
371 						}else{
372 							insertLine(_cursorY + 1,"");
373 						}
374 					}else{
375 						dstring[2] line;
376 						line[0] = readLine(_cursorY);
377 						line[1] = line[0][_cursorX .. line[0].length];
378 						line[0] = line[0][0 .. _cursorX];
379 						overwriteLine(_cursorY, line[0]);
380 						if (_cursorY >= lineCount - 1){
381 							_lines.append(line[1]);
382 						}else{
383 							insertLine(_cursorY + 1, line[1]);
384 						}
385 					}
386 					_cursorY ++;
387 					_cursorX = 0;
388 				}else if (key.key == '\t'){
389 					//convert it to 4 spaces
390 					overwriteLine(_cursorY, cast(dstring)insertElement(cast(dchar[])currentLine,cast(dchar[])"    ",_cursorX));
391 					_cursorX += 4;
392 				}else{
393 					//insert that char
394 					overwriteLine(_cursorY, cast(dstring)insertElement(cast(dchar[])currentLine,[cast(dchar)key.key],_cursorX));
395 					_cursorX ++;
396 				}
397 			}
398 		}else{
399 			if (key.key == Key.Delete && _enableEditing){
400 				requestUpdate();
401 				//check if is deleting \n
402 				if (_cursorX == readLine(_cursorY).length && _cursorY+1 < lineCount){
403 					//merge next line with this one
404 					dchar[] line = cast(dchar[])readLine(_cursorY)~readLine(_cursorY+1);
405 					overwriteLine(_cursorY, cast(dstring)line);
406 					//remove next line
407 					removeLine(_cursorY+1);
408 				}else if (_cursorX < readLine(_cursorY).length){
409 					dchar[] line = cast(dchar[])readLine(_cursorY);
410 					line = line.deleteElement(_cursorX);
411 					overwriteLine(_cursorY, cast(dstring)line);
412 				}
413 			}else if (key.key == Key.DownArrow){
414 				if (_cursorY+1 < lineCount){
415 					requestUpdate();
416 					_cursorY ++;
417 				}
418 			}else if (key.key == Key.UpArrow){
419 				if (_cursorY > 0){
420 					requestUpdate();
421 					_cursorY --;
422 				}
423 			}else if (key.key == Key.LeftArrow){
424 				if ((_cursorY >= 0 && _cursorX > 0) || (_cursorY > 0 && _cursorX == 0)){
425 					requestUpdate();
426 					if (_cursorX == 0){
427 						_cursorY --;
428 						_cursorX = cast(uint)readLine(_cursorY).length;
429 					}else{
430 						_cursorX --;
431 					}
432 				}
433 			}else if (key.key == Key.RightArrow){
434 				requestUpdate();
435 				if (_cursorX == readLine(_cursorY).length){
436 					if (_cursorY+1 < lineCount){
437 						_cursorX = 0;
438 						_cursorY ++;
439 						_scrollX = 0;
440 					}
441 				}else{
442 					_cursorX ++;
443 				}
444 			}
445 		}
446 		// I'll use this this time not to move the cursor, but to fix the cursor position
447 		moveCursor(_cursorX,_cursorY);
448 		reScroll();
449 	}
450 public:
451 	/// background and text colors
452 	Color backgroundColor, textColor;
453 	/// constructor
454 	this(bool allowEditing = true){
455 		_lines = new List!dstring;
456 		_scrollX = 0;
457 		_scrollY = 0;
458 		_cursorX = 0;
459 		_cursorY = 0;
460 		eventSubscribe(EventMask.Initialize | EventMask.MouseAll | EventMask.KeyboardPress |
461 		EventMask.Resize | EventMask.Update);
462 
463 		textColor = DEFAULT_FG;
464 		backgroundColor = DEFAULT_BG;
465 	}
466 	~this(){
467 		.destroy(_lines);
468 	}
469 	
470 	///returns a list of lines in memo
471 	///
472 	///To modify the content, just modify it in the returned list
473 	///
474 	///class `List` is defined in `utils.lists.d`
475 	@property List!dstring lines(){
476 		return _lines;
477 	}
478 }*/
479 
480 /// Displays an un-scrollable log
481 class LogWidget : QWidget{
482 private:
483 	/// stores the logs
484 	List!dstring _logs;
485 	/// The index in _logs where the oldest added line is
486 	uint _startIndex;
487 	/// the maximum number of lines to store
488 	uint _maxLines;
489 	/// Returns: line at an index
490 	dstring getLine(uint index){
491 		if (index >= _logs.length)
492 			return "";
493 		return _logs.read((index + _startIndex) % _maxLines);
494 	}
495 	/// wrap a line
496 	dstring[] wrapLine(dstring str){
497 		dstring[] r;
498 		while (str.length > 0){
499 			if (width > str.length){
500 				r ~= str;
501 				str = [];
502 			}else{
503 				r ~= str[0 .. width];
504 				str = str[width .. $];
505 			}
506 		}
507 		return r;
508 	}
509 protected:
510 	override bool updateEvent(){
511 		int hLeft = this.height;
512 		for (int i = cast(int)_logs.length-1; i >= 0; i --){
513 			dstring line = getLine(i);
514 			if (!line.length)
515 				continue;
516 			dstring[] wrappedLine = wrapLine(line);
517 			if (hLeft < wrappedLine.length)
518 				wrappedLine = wrappedLine[wrappedLine.length - hLeft .. $];
519 			immutable int startY = hLeft - cast(int)wrappedLine.length;
520 			foreach (lineno, currentLine; wrappedLine){
521 				moveTo(0, cast(int)lineno + startY);
522 				write(currentLine, textColor, backgroundColor);
523 				if (currentLine.length < width)
524 					fillLine(' ', textColor, backgroundColor);
525 			}
526 			hLeft = startY;
527 		}
528 		foreach (y; 0 .. hLeft){
529 			moveTo(0, y);
530 			fillLine(' ', textColor, backgroundColor);
531 		}
532 		return true;
533 	}
534 	
535 	override bool resizeEvent() {
536 		requestUpdate();
537 		return true;
538 	}
539 public:
540 	/// background and text color
541 	Color backgroundColor, textColor;
542 	/// constructor
543 	this(uint maxLen = 100){
544 		_maxLines = maxLen;
545 		_logs = new List!dstring;
546 		_startIndex = 0;
547 		eventSubscribe(EventMask.Resize | EventMask.Update);
548 		textColor = DEFAULT_FG;
549 		backgroundColor = DEFAULT_BG;
550 	}
551 	~this(){
552 		.destroy(_logs);
553 	}
554 	
555 	/// adds string to the log, and scrolls down to it.
556 	/// newline character is not allowed
557 	void add(dstring item){
558 		//check if needs to overwrite
559 		if (_logs.length > _maxLines){
560 			_startIndex = (_startIndex + 1) % _maxLines;
561 			_logs.set(_startIndex, item);
562 		}else
563 			_logs.append(item);
564 		requestUpdate();
565 	}
566 	/// ditto
567 	void add(string item){
568 		add(item.to!dstring);
569 	}
570 
571 	/// clears the log
572 	void clear(){
573 		_logs.clear;
574 		requestUpdate();
575 	}
576 }
577 
578 /// Just occupies some space. Use this to put space between widgets
579 class SplitterWidget : QWidget{
580 private:
581 	Color _color;
582 protected:
583 	override bool updateEvent(){
584 		foreach (y; viewportY .. viewportY + viewportHeight){
585 			moveTo(viewportX, y);
586 			fillLine(' ', DEFAULT_FG, _color);
587 		}
588 		return true;
589 	}
590 public:
591 	/// constructor
592 	this(){
593 		_color = DEFAULT_BG;
594 		eventSubscribe(EventMask.Scroll | EventMask.Resize |
595 			EventMask.Update);
596 	}
597 	/// color
598 	@property Color color(){
599 		return _color;
600 	}
601 	/// ditto
602 	@property Color color(Color newColor){
603 		_color = newColor;
604 		requestUpdate();
605 		return _color;
606 	}
607 }