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