1 /++
2 	This module contains most of the functions you'll need.
3 	All the 'base' classes, like QWidget are defined in this.
4 	There are some classes, like List, that are defined in 
5 	other modules.
6 +/
7 module qui.qui;
8 
9 import qui.misc;
10 import qui.lists;
11 import qui.baseconv;//used for hexadecimal colors
12 import std.stdio;//used by QTheme.themeToFile
13 import arsd.terminal;
14 
15 ///Mouse Click, or Ms Wheel scroll event
16 ///
17 ///The mouseEvent function is called with this.
18 struct MouseClick{
19 	///Types of buttons
20 	enum Button{
21 		Left,/// Left mouse button
22 		ScrollUp,/// MS wheel was scrolled up
23 		ScrollDown,/// MS wheel was scrolled down
24 		Right,/// Right mouse button was pressed
25 	}
26 	///Stores which button was pressed
27 	Button mouseButton;
28 	/// the x-axis of mouse cursor, 0 means left-most
29 	uinteger x;
30 	/// the y-axis of mouse cursor, 0 means top-most
31 	uinteger y;
32 }
33 
34 ///Key press event, keyboardEvent function is called with this
35 ///
36 ///A note: backspace (`\b`) and enter (`\n`) are not included in KeyPress.NonCharKey
37 struct KeyPress{
38 	dchar key;/// stores which key was pressed
39 
40 	/// Returns true if the key was a character.
41 	/// 
42 	/// A note: backspace (`\b`) and enter (`\n`) are not included in KeyPress.NonCharKey
43 	bool isChar(){
44 		return !(key >= NonCharKey.min && key <= NonCharKey.max);
45 	}
46 	/// Types of non-character keys
47 	enum NonCharKey{
48 		escape = 0x1b + 0xF0000,
49 		F1 = 0x70 + 0xF0000,
50 		F2 = 0x71 + 0xF0000,
51 		F3 = 0x72 + 0xF0000,
52 		F4 = 0x73 + 0xF0000,
53 		F5 = 0x74 + 0xF0000,
54 		F6 = 0x75 + 0xF0000,
55 		F7 = 0x76 + 0xF0000,
56 		F8 = 0x77 + 0xF0000,
57 		F9 = 0x78 + 0xF0000,
58 		F10 = 0x79 + 0xF0000,
59 		F11 = 0x7A + 0xF0000,
60 		F12 = 0x7B + 0xF0000,
61 		LeftArrow = 0x25 + 0xF0000,
62 		RightArrow = 0x27 + 0xF0000,
63 		UpArrow = 0x26 + 0xF0000,
64 		DownArrow = 0x28 + 0xF0000,
65 		Insert = 0x2d + 0xF0000,
66 		Delete = 0x2e + 0xF0000,
67 		Home = 0x24 + 0xF0000,
68 		End = 0x23 + 0xF0000,
69 		PageUp = 0x21 + 0xF0000,
70 		PageDown = 0x22 + 0xF0000,
71 	}
72 }
73 
74 /// A 24 bit, RGB, color
75 /// 
76 /// `r` represents amount of red, `g` is green, and `b` is blue.
77 /// the `a` is ignored
78 alias RGBColor = RGB;
79 
80 /// Used to store position for widgets
81 struct Position{
82 	uinteger x, y;
83 }
84 
85 /// To store size for widgets
86 /// 
87 /// zero in min/max means no limit
88 struct Size{
89 	uinteger width, height;
90 	uinteger minHeight = 0, minWidth = 0;
91 	uinteger maxHeight = 0, maxWidth = 0;
92 }
93 
94 /// The whole terminal is divided into cells, total number of cells = length * height of terminal
95 /// 
96 /// Each cell has it's own character, foreground color, and background color
97 struct Cell{
98 	char c;
99 	RGBColor textColor;
100 	RGBColor bgColor;
101 }
102 
103 /// mouseEvent function
104 alias MouseEventFuction = void delegate(MouseClick);
105 ///keyboardEvent function
106 alias KeyboardEventFunction = void delegate(KeyPress);
107 
108 
109 /// Base class for all widgets, including layouts and QTerminal
110 ///
111 /// Use this as parent-class for new widgets
112 abstract class QWidget{
113 protected:
114 	///specifies position of this widget
115 	Position widgetPosition;
116 	///size of this widget
117 	Size widgetSize;
118 	///caption of this widget, it's up to the widget how to use this, progressbarWidget shows this inside the bar...
119 	string widgetCaption;
120 	///whether this widget should be drawn or not
121 	bool widgetShow = true;
122 	///to specify if this widget needs to be updated or not, mark this as true when the widget has changed
123 	bool needsUpdate = true;
124 	///specifies name of this widget. must be unique, as it is used to identify widgets in theme
125 	string widgetName = null;
126 	/// specifies that how much height (in horizontal layout) or width (in vertical) is given to this widget.
127 	/// The ratio of all widgets is added up and height/width for each widget is then calculated using this
128 	uinteger widgetSizeRatio = 1;
129 
130 	///The theme that is currently used
131 	///
132 	///The widget is free to modify the theme
133 	QTheme widgetTheme;
134 
135 	/// Called by widget when a redraw is needed, but no redraw is scheduled
136 	/// 
137 	/// In other words: call this function using:
138 	/// ```
139 	/// if (forceUpdate !is null){
140 	/// 	forceUpdate();
141 	/// }
142 	/// ``` 
143 	///  when an update is needed, but it's not sure if an update will be called.
144 	/// Update is automatically called after mouseEvent and keyboardEvent
145 	bool delegate() forceUpdate;
146 
147 	/// Called by widgets (usually keyboard-input-taking) to position the cursor
148 	/// 
149 	/// It can only be called if the widget is active (i.e selected), in non-active widgets, it's null;
150 	void delegate(uinteger x, uinteger y) cursorPos;
151 
152 	/// custom mouse event, if not null, it should be called before doing anything else in mouseEvent.
153 	/// 
154 	/// Like:
155 	/// ```
156 	/// override void mouseEvent(MouseClick mouse){
157 	/// 	super.mouseEvent(mouse);
158 	/// 	// rest of the code here
159 	/// }
160 	/// ```
161 	MouseEventFuction customMouseEvent;
162 
163 	/// custom keyboard event, if not null, it should be called before doing anything else in keyboardEvent.
164 	/// 
165 	/// Like:
166 	/// ```
167 	/// override void keyboardEvent(KeyPress key){
168 	/// 	super.keyboardEvent(key);
169 	/// 	// rest of the code here
170 	/// }
171 	/// ```
172 	KeyboardEventFunction customKeyboardEvent;
173 public:
174 	/// Called by owner when mouse is clicked with cursor on this widget.
175 	/// 
176 	/// `forceUpdate` is not required after this
177 	void mouseEvent(MouseClick mouse){
178 		if (customMouseEvent !is null){
179 			customMouseEvent(mouse);
180 		}
181 	}
182 
183 	/// Called by owner when key is pressed and this widget is active.
184 	/// 
185 	/// `forceUpdate` is not required after this
186 	void keyboardEvent(KeyPress key){
187 		if (customKeyboardEvent !is null){
188 			customKeyboardEvent(key);
189 		}
190 	}
191 
192 	/// Called by owner to update.
193 	/// 
194 	/// Return false if no need to update, and true if an update is required, and the new display in `display` Matrix
195 	abstract bool update(ref Matrix display);///return true to indicate that it has to be redrawn, else, make changes in display
196 
197 	/// Called by owner to indicate that widget has to 're-fetch' colors from the theme.
198 	abstract void updateColors();
199 
200 	//event properties
201 	/// use to change the custom mouse event
202 	@property MouseEventFuction onMouseEvent(MouseEventFuction func){
203 		return customMouseEvent = func;
204 	}
205 	/// use to change the custom keyboard event
206 	@property KeyboardEventFunction onKeyboardEvent(KeyboardEventFunction func){
207 		return customKeyboardEvent = func;
208 	}
209 
210 
211 	//properties:
212 
213 	/// The name of the widget. Read-only, cannot be modified
214 	@property string name(){
215 		return widgetName;
216 	}
217 
218 	/// caption of the widget. setter
219 	@property string caption(){
220 		return widgetCaption;
221 	}
222 	/// caption of the widget. getter
223 	@property string caption(string newCaption){
224 		needsUpdate = true;
225 		widgetCaption = newCaption;
226 		if (forceUpdate !is null){
227 			forceUpdate();
228 		}
229 		return widgetCaption;
230 	}
231 
232 	/// position of the widget. getter
233 	@property Position position(){
234 		return widgetPosition;
235 	}
236 	/// position of the widget. setter
237 	@property Position position(Position newPosition){
238 		widgetPosition = newPosition;
239 		if (forceUpdate !is null){
240 			forceUpdate();
241 		}
242 		return widgetPosition;
243 	}
244 
245 	/// size (width/height) of the widget. getter
246 	@property uinteger sizeRatio(){
247 		return widgetSizeRatio;
248 	}
249 	/// size (width/height) of the widget. setter
250 	@property uinteger sizeRatio(uinteger newRatio){
251 		needsUpdate = true;
252 		widgetSizeRatio = newRatio;
253 		if (forceUpdate !is null){
254 			forceUpdate();
255 		}
256 		return widgetSizeRatio;
257 	}
258 
259 	/// visibility of the widget. getter
260 	@property bool visible(){
261 		return widgetShow;
262 	}
263 	/// visibility of the widget. setter
264 	@property bool visible(bool visibility){
265 		needsUpdate = true;
266 		widgetShow = visibility;
267 		if (forceUpdate !is null){
268 			forceUpdate();
269 		}
270 		return widgetShow;
271 	}
272 
273 	/// theme of the widget. getter
274 	@property QTheme theme(){
275 		return widgetTheme;
276 	}
277 	/// theme of the widget. setter
278 	@property QTheme theme(QTheme newTheme){
279 		needsUpdate = true;
280 		widgetTheme = newTheme;
281 		if (forceUpdate !is null){
282 			forceUpdate();
283 		}
284 		return widgetTheme;
285 	}
286 
287 	/// called by owner to set the `forceUpdate` function, which is used to force an update immediately.
288 	@property bool delegate() onForceUpdate(bool delegate() newOnForceUpdate){
289 		return forceUpdate = newOnForceUpdate;
290 	}
291 	/// called by the owner to set the `cursorPos` function, which is used to position the cursor on the terminal.
292 	@property void delegate(uinteger, uinteger) onCursorPosition(void delegate(uinteger, uinteger) newOnCursorPos){
293 		return cursorPos = newOnCursorPos;
294 	}
295 	/// size of the widget. getter
296 	@property Size size(){
297 		return widgetSize;
298 	}
299 	/// size of the widget. setter
300 	@property Size size(Size newSize){
301 		//check if height or width < min
302 		if (newSize.minWidth > 0 && newSize.width < newSize.minWidth){
303 			return widgetSize;
304 		}else if (newSize.maxWidth > 0 && newSize.width > newSize.maxWidth){
305 			return widgetSize;
306 		}else if (newSize.minHeight > 0 && newSize.height < newSize.minHeight){
307 			return widgetSize;
308 		}else if (newSize.maxHeight > 0 && newSize.height > newSize.maxHeight){
309 			return widgetSize;
310 		}else{
311 			needsUpdate = true;
312 			widgetSize = newSize;
313 			if (forceUpdate !is null){
314 				forceUpdate();
315 			}
316 			return widgetSize;
317 		}
318 	}
319 }
320 
321 ///Used to place widgets in an order (i.e vertical or horizontal)
322 ///
323 ///The QTerminal is also a layout, basically.
324 ///
325 ///Name in theme: 'layout';
326 class QLayout : QWidget{
327 private:
328 	// array of all the widgets that have been added to this layout
329 	QWidget[] widgetList;
330 	// contains reference to the active widget, null if no active widget
331 	QWidget activeWidget;
332 	// stores the layout type, horizontal or vertical
333 	LayoutDisplayType layoutType;
334 
335 	// background color
336 	RGBColor backColor;
337 	// foreground color
338 	RGBColor foreColor;
339 	Cell emptySpace;
340 	// stores whether an update is in progress
341 	bool isUpdating = false;
342 
343 	// recalculates the size and position of every widget inside layout
344 	void recalculateWidgetsSize(){
345 		uinteger ratioTotal = 0;
346 		Size newSize;
347 		//calculate total ratio
348 		foreach (currentWidget; widgetList){
349 			ratioTotal += currentWidget.sizeRatio;
350 		}
351 		Position newPosition;
352 		uinteger newWidth = 0, newHeight = 0;
353 		uinteger availableWidth = widgetSize.width;
354 		uinteger availableHeight = widgetSize.height;
355 		
356 		if (layoutType == LayoutDisplayType.Vertical){
357 			//make space for new widget
358 			foreach(w; widgetList){
359 				//if a widget is not visible, skip it
360 				if (w.visible){
361 					//recalculate position
362 					newPosition.x = widgetPosition.x;//x axis is always same, cause this is a vertical (not horizontal) layout
363 					if (newHeight > 0){
364 						newPosition.y += newHeight;//add previous widget's height to get new y axis position
365 					}else{
366 						newPosition.y = 0;
367 					}
368 					w.position = newPosition;
369 					//recalculate height
370 					newHeight = ratioToRaw(w.sizeRatio, ratioTotal, availableHeight);
371 					if (w.size.minHeight > 0 && newHeight < w.size.minHeight){
372 						newHeight = w.size.minHeight;
373 					}else if (w.size.maxHeight > 0 && newHeight > w.size.maxHeight){
374 						newHeight = w.size.maxHeight;
375 					}
376 					//recalculate width
377 					newWidth = widgetSize.width;//default is max
378 					//compare with min & max
379 					if (w.size.minWidth > 0 && newWidth < w.size.minWidth){
380 						//although there isn't that much width, still assign it, that will be dealt with later
381 						newWidth = w.size.minWidth;
382 					}else if (w.size.maxWidth > 0 && newWidth > w.size.maxWidth){
383 						newWidth = w.size.maxWidth;
384 					}
385 					//check if there's not enough space available, then make it invisible
386 					if (newWidth > availableWidth || newHeight > availableHeight){
387 						newWidth = 0;
388 						newHeight = 0;
389 						w.visible = false;
390 						continue;
391 					}
392 					//apply new size
393 					newSize = w.size;//to get min and max values
394 					newSize.height = newHeight;
395 					newSize.width = newWidth;
396 					w.size = newSize;
397 					//now the new size has been assigned, calculate amount of space & ratios left
398 					availableHeight -= newHeight;
399 					ratioTotal -= w.sizeRatio;
400 				}
401 			}
402 		}else if (layoutType == LayoutDisplayType.Horizontal){
403 			//make space for new widget
404 			foreach(w; widgetList){
405 				//if a widget is not visible, skip it
406 				if (w.visible){
407 					//recalculate position
408 					newPosition.y = widgetPosition.y;//x axis is always same, cause this is a vertical (not horizontal) layout
409 					if (newWidth > 0){
410 						newPosition.x += newWidth;//add previous widget's height to get new y axis position
411 					}else{
412 						newPosition.x = 0;
413 					}
414 					w.position = newPosition;
415 					//recalculate width
416 					newWidth = ratioToRaw(w.sizeRatio, ratioTotal, availableWidth);
417 					if (w.size.minWidth > 0 && newWidth < w.size.minWidth){
418 						newWidth = w.size.minWidth;
419 					}else if (w.size.maxWidth > 0 && newWidth > w.size.maxWidth){
420 						newWidth = w.size.maxWidth;
421 					}
422 					//recalculate height
423 					newHeight = widgetSize.height;//default is max
424 					//compare with min & max
425 					if (w.size.minHeight > 0 && newHeight < w.size.minHeight){
426 						//although there isn't that much width, still assign it, that will be dealt with later
427 						newHeight = w.size.minHeight;
428 					}else if (w.size.maxHeight > 0 && newHeight > w.size.maxHeight){
429 						newHeight = w.size.maxHeight;
430 					}
431 					//check if there's not enough space available, then make it invisible
432 					if (newWidth > availableWidth || newHeight > availableHeight){
433 						newWidth = 0;
434 						newHeight = 0;
435 						w.visible = false;
436 						continue;
437 					}
438 					//apply new size
439 					newSize = w.size;//to get min and max values
440 					newSize.height = newHeight;
441 					newSize.width = newWidth;
442 					w.size = newSize;
443 					//now the new size has been assigned, calculate amount of space & ratios left
444 					availableWidth -= newWidth;
445 					ratioTotal -= w.sizeRatio;
446 				}
447 			}
448 		}
449 	}
450 
451 public:
452 	/// Layout type
453 	enum LayoutDisplayType{
454 		Vertical,
455 		Horizontal,
456 	}
457 	this(LayoutDisplayType type){
458 		widgetName = "layout";
459 		layoutType = type;
460 		activeWidget = null;
461 		emptySpace.c = ' ';
462 	}
463 
464 	override void updateColors(){
465 		needsUpdate = true;
466 		if (&widgetTheme && widgetTheme.hasColors(name,["background","text"])){
467 			emptySpace.bgColor = widgetTheme.getColor(name, "background");
468 			emptySpace.textColor = widgetTheme.getColor(name, "text");
469 		}else{
470 			emptySpace.bgColor = hexToColor("000000");
471 			emptySpace.textColor = hexToColor("00FF00");
472 		}
473 		if (forceUpdate !is null){
474 			forceUpdate();
475 		}
476 	}
477 
478 	/// adds (appends) a widget to the widgetList, and makes space for it
479 	void addWidget(QWidget widget){
480 		widget.theme = widgetTheme;
481 		widget.updateColors();
482 		widget.onForceUpdate = forceUpdate;
483 		//add it to array
484 		widgetList.length++;
485 		widgetList[widgetList.length-1] = widget;
486 		//recalculate all widget's size to adjust
487 		recalculateWidgetsSize();
488 	}
489 
490 	override void mouseEvent(MouseClick mouse){
491 		super.mouseEvent(mouse);
492 		//check on which widget the cursor was on
493 		Position p;
494 		Size s;
495 		uinteger i;
496 		QWidget widget;
497 		//remove access to cursor from previous active widget
498 		if (activeWidget !is null){
499 			activeWidget.onCursorPosition = null;
500 		}
501 		for (i = 0; i < widgetList.length; i++){
502 			widget = widgetList[i];
503 			p = widget.position;
504 			s = widget.size;
505 			//check x-axis
506 			if (mouse.x >= p.x && mouse.x < p.x + s.width){
507 				//check y-axis
508 				if (mouse.y >= p.y && mouse.y < p.y + s.height){
509 					//give access to cursor position
510 					widget.onCursorPosition = cursorPos;
511 					//call mouseEvent
512 					widget.mouseEvent(mouse);
513 					//mark this widget as active
514 					activeWidget = widget;
515 					break;
516 				}
517 			}
518 		}
519 	}
520 	override void keyboardEvent(KeyPress key){
521 		super.keyboardEvent(key);
522 		//check active widget, call keyboardEvent
523 		if (activeWidget){
524 			activeWidget.keyboardEvent(key);
525 		}
526 	}
527 	override bool update(ref Matrix display){
528 		bool updated = false;
529 		//check if already updating, case yes, return false
530 		if (!isUpdating){
531 			isUpdating = true;
532 			//go through all widgets, check if they need update, update them
533 			Matrix wDisplay = new Matrix(1,1,emptySpace);
534 			foreach(widget; widgetList){
535 				if (widget.visible){
536 					wDisplay.changeSize(widget.size.width, widget.size.height, emptySpace);
537 					wDisplay.resetWritePosition();
538 					if (widget.update(wDisplay)){
539 						display.insert(wDisplay, widget.position.x, widget.position.y);
540 						updated = true;
541 					}
542 				}
543 			}
544 			isUpdating = false;
545 		}else{
546 			return false;
547 		}
548 		return updated;
549 	}
550 }
551 
552 /// A terminal (as the name says).
553 /// 
554 /// All widgets, receives events, runs UI loop...
555 /// 
556 /// Name in theme: 'terminal';
557 class QTerminal : QLayout{
558 private:
559 	Terminal terminal;
560 	RealTimeConsoleInput input;
561 	Matrix termDisplay;
562 
563 	Position cursorPos;
564 
565 	bool isRunning = false;
566 public:
567 	this(string caption = "QUI Text User Interface", LayoutDisplayType displayType = LayoutDisplayType.Vertical){
568 		super(displayType);
569 		widgetName = "terminal";
570 		//create terminal & input
571 		terminal = Terminal(ConsoleOutputType.cellular);
572 		input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.allInputEvents);
573 		terminal.showCursor();
574 		//init vars
575 		widgetSize.height = terminal.height;
576 		widgetSize.width = terminal.width;
577 		widgetCaption = caption;
578 		//set caption
579 		terminal.setTitle(widgetCaption);
580 		//create display matrix
581 		termDisplay = new Matrix(widgetSize.width, widgetSize.height, emptySpace);
582 		//create theme
583 		widgetTheme = new QTheme;
584 	}
585 	~this(){
586 		terminal.clear;
587 		delete termDisplay;
588 	}
589 
590 	override public void addWidget(QWidget widget) {
591 		super.addWidget(widget);
592 		widget.onForceUpdate = &updateDisplay;
593 	}
594 
595 	override public void mouseEvent(MouseClick mouse) {
596 		super.mouseEvent(mouse);
597 		//check on which widget the cursor was on
598 		Position p;
599 		Size s;
600 		uinteger i;
601 		QWidget widget;
602 		//remove access to cursor from previous active widget
603 		if (activeWidget){
604 			activeWidget.onCursorPosition = null;
605 		}
606 		for (i = 0; i < widgetList.length; i++){
607 			widget = widgetList[i];
608 			p = widget.position;
609 			s = widget.size;
610 			//check x-axis
611 			if (mouse.x >= p.x && mouse.x < p.x + s.width){
612 				//check y-axis
613 				if (mouse.y >= p.y && mouse.y < p.y + s.height){
614 					//give access to cursor position
615 					widget.onCursorPosition = &setCursorPos;
616 					//call mouseEvent
617 					widget.mouseEvent(mouse);
618 					//mark this widget as active
619 					activeWidget = widget;
620 					break;
621 				}
622 			}
623 		}
624 	}
625 
626 	/// Use this instead of `update` to forcefully update the terminal
627 	bool updateDisplay(){
628 		//termDisplay.clear(emptySpace);
629 		bool r = update(termDisplay);
630 		if (r){
631 			termDisplay.flushToTerminal(this);
632 		}
633 		//set cursor position
634 		terminal.moveTo(cast(int)cursorPos.x, cast(int)cursorPos.y);
635 		terminal.showCursor();
636 		return r;
637 	}
638 
639 	/// starts the UI loop
640 	void run(){
641 		InputEvent event;
642 		isRunning = true;
643 		//draw the whole thing
644 		recalculateWidgetsSize;
645 		updateDisplay();
646 		while (isRunning){
647 			event = input.nextEvent;
648 			//check event type
649 			if (event.type == event.Type.KeyboardEvent){
650 				KeyPress kPress;
651 				kPress.key = event.get!(event.Type.KeyboardEvent).which;
652 				this.keyboardEvent(kPress);
653 				updateDisplay;
654 			}else if (event.type == event.Type.MouseEvent){
655 				MouseEvent mEvent = event.get!(event.Type.MouseEvent);
656 				MouseClick mPos;
657 				mPos.x = mEvent.x;
658 				mPos.y = mEvent.y;
659 				switch (mEvent.buttons){
660 					case MouseEvent.Button.Left:
661 						mPos.mouseButton = mPos.Button.Left;
662 						break;
663 					case MouseEvent.Button.Right:
664 						mPos.mouseButton = mPos.Button.Right;
665 						break;
666 					case MouseEvent.Button.ScrollUp:
667 						mPos.mouseButton = mPos.Button.ScrollUp;
668 						break;
669 					case MouseEvent.Button.ScrollDown:
670 						mPos.mouseButton = mPos.Button.ScrollDown;
671 						break;
672 					default:
673 						continue;
674 				}
675 				this.mouseEvent(mPos);
676 				updateDisplay;
677 			}else if (event.type == event.Type.SizeChangedEvent){
678 				//change matrix size
679 				termDisplay.changeSize(cast(uinteger)terminal.width, cast(uinteger)terminal.height, emptySpace);
680 				//update self size
681 				terminal.updateSize;
682 				widgetSize.height = terminal.height;
683 				widgetSize.width = terminal.width;
684 				//call size change on all widgets
685 				recalculateWidgetsSize;
686 				updateDisplay;
687 			}else if (event.type == event.Type.UserInterruptionEvent){
688 				//die here
689 				terminal.clear;
690 				isRunning = false;
691 				break;
692 			}
693 		}
694 		//in case an exception prevents it from being set to false before
695 		isRunning = false;
696 	}
697 
698 	/// terminates the UI loop
699 	void terminate(){
700 		isRunning = false;
701 	}
702 
703 	//override write properties
704 	override @property Size size(Size newSize){
705 		//don't let anything modify the size
706 		return widgetSize;
707 	}
708 	override @property Position position(Position newPosition){
709 		return widgetPosition;
710 	}
711 	/// Called by active-widget(s?) to position the cursor
712 	void setCursorPos(uinteger x, uinteger y){
713 		cursorPos.x = x;
714 		cursorPos.y = y;
715 	}
716 
717 	///returns true if UI loop is running
718 	@property bool running(){
719 		return isRunning;
720 	}
721 
722 	//functions below are used by Matrix.flushToTerminal
723 	///flush changes to terminal, called by Matrix
724 	void flush(){
725 		terminal.flush;
726 	}
727 	///clear terminal, called before writing, called by Matrix
728 	void clear(){
729 		terminal.clear;
730 	}
731 	///change colors, called by Matrix
732 	void setColors(RGBColor textColor, RGBColor bgColor){
733 		terminal.setTrueColor(textColor, bgColor);
734 	}
735 	///move write-cursor to a position, called by Matrix
736 	void moveTo(int x, int y){
737 		terminal.moveTo(x, y);
738 	}
739 	///write chars to terminal, called by Matrix
740 	void writeChars(char[] c){
741 		terminal.write(c);
742 	}
743 	///write char to terminal, called by Matrix
744 	void writeChars(char c){
745 		terminal.write(c);
746 	}
747 }
748 
749 ///Theme class
750 class QTheme{
751 private:
752 	RGBColor[string][string] colors;
753 	RGBColor[string] globalColors;//i.e default colors
754 public:
755 	this(string themeFile = null){
756 		if (themeFile != null){
757 			loadTheme(themeFile);
758 		}
759 	}
760 	///returns color, provided the widgetName, and which-color (like textColor).
761 	///
762 	///if not found, returns a default color provided by theme. If that color
763 	///is also not found, throws exception
764 	RGBColor getColor(string widgetName, string which){
765 		if (widgetName in colors && which in colors[widgetName]){
766 			return colors[widgetName][which];
767 		}else{
768 			if (which in globalColors){
769 				return globalColors[which];
770 			}else{
771 				throw new Exception("Color "~which~" not defined (for "~widgetName~')');
772 			}
773 		}
774 	}
775 	///gets all colors for a widget.
776 	///
777 	///Throws exception if that widget has no colors defined in theme
778 	RGBColor[string] getColors(string widgetName){
779 		if (widgetName in colors){
780 			return colors[widgetName];
781 		}else{
782 			throw new Exception("Widget "~widgetName~" not defined");
783 		}
784 	}
785 	/// sets a  color for a widget
786 	void setColor(string widgetName, string which, RGBColor color){
787 		colors[widgetName][which] = color;
788 	}
789 	///sets a default value for a color
790 	///
791 	///i.e a color that is used when the color for widget is not found
792 	void setColor(string which, RGBColor color){
793 		globalColors[which] = color;
794 	}
795 	///sets all colors for a widget
796 	void setColors(string widgetName, RGBColor[string] widgetColors){
797 		colors[widgetName] = widgetColors;
798 	}
799 	///Saves current theme to a file, throws exception if failed
800 	bool saveTheme(string filename){
801 		bool r = true;
802 		try{
803 			File f = File(filename, "w");
804 			foreach(widgetName; colors.keys){
805 				foreach(colorName; colors[widgetName].keys){
806 					f.write(widgetName,' ',colorName,' ',colorToHex(colors[widgetName][colorName]),'\n');
807 				}
808 			}
809 			foreach(colorName; globalColors.keys){
810 				f.write("* ",colorName,' ',colorToHex(globalColors[colorName]),'\n');
811 			}
812 			f.close;
813 		}catch(Exception e){
814 			throw e;
815 		}
816 		return r;
817 	}
818 	///Loads a theme from file, throws exception if failed
819 	bool loadTheme(string filename){
820 		bool r = true;
821 		try{
822 			string[] fcontents = fileToArray(filename);
823 			string widgetName, colorName, colorCode, line;
824 			uinteger lEnd;
825 			for (uinteger lno = 0; lno < fcontents.length; lno++){
826 				line = fcontents[lno];
827 				uinteger readFrom = 0;
828 				lEnd = line.length - 1;
829 				for (uinteger i = 0; i < line.length; i++){
830 					if (line[i] == ' ' || i == lEnd){
831 						if (widgetName == null){
832 							widgetName = line[readFrom .. i];
833 						}
834 						if (colorName == null){
835 							colorName = line[readFrom .. i];
836 						}
837 						if (colorCode == null){
838 							colorCode = line[readFrom .. i];
839 						}
840 						readFrom = i+1;
841 					}
842 				}
843 				//add color, if any
844 				if (widgetName && colorName && colorCode){
845 					if (widgetName == "*"){
846 						globalColors[colorName] = hexToColor(colorCode);
847 					}else{
848 						colors[widgetName][colorName] = hexToColor(colorCode);
849 					}
850 					//clear name ...
851 					widgetName, colorName, colorCode = null;
852 				}
853 			}
854 		}catch(Exception e){
855 			throw e;
856 		}
857 		return r;
858 	}
859 
860 	///checks if theme has any color(s) for a widget
861 	bool hasWidget(string widgetName){
862 		if (widgetName in colors){
863 			return true;
864 		}else{
865 			return false;
866 		}
867 	}
868 	///checks if theme has a specific color for a specific widget
869 	bool hasColor(string widgetName, string colorName){
870 		if (widgetName in colors){
871 			if (colorName in colors[widgetName]){
872 				return true;
873 			}else{
874 				return false;
875 			}
876 		}else{
877 			return false;
878 		}
879 	}
880 	///checks if theme has specific colors for a specific widget
881 	bool hasColors(string widgetName, string[] colorNames){
882 		bool r = true;
883 		foreach(color; colorNames){
884 			if (hasColor(widgetName, color) == false){
885 				r = false;
886 				break;
887 			}
888 		}
889 		return r;
890 	}
891 	///checks if theme has a default color
892 	bool hasColor(string colorName){
893 		if (colorName in globalColors){
894 			return true;
895 		}else{
896 			return false;
897 		}
898 	}///checks if theme has default colors
899 	bool hasColors(string[] colorNames){
900 		bool r = true;
901 		foreach(color; colorNames){
902 			if (hasColor(color) == false){
903 				r = false;
904 				break;
905 			}
906 		}
907 		return r;
908 	}
909 }
910 
911 //misc functions:
912 ///Center-aligns text, returns that in an char[] with width as length. The empty part filled with ' '
913 char[] centerAlignText(char[] text, uinteger width, char fill = ' '){
914 	char[] r;
915 	if (text.length < width){
916 		r.length = width;
917 		uinteger offset = (width - text.length)/2;
918 		r[0 .. offset] = fill;
919 		r[offset .. offset+text.length][] = text;
920 		r[offset+text.length .. r.length] = fill;
921 	}else{
922 		r = text[0 .. width];
923 	}
924 	return r;
925 }
926 
927 ///used to calculate height/width using sizeRation
928 uinteger ratioToRaw(uinteger selectedRatio, uinteger ratioTotal, uinteger total){
929 	uinteger r;
930 	r = cast(uinteger)((cast(float)selectedRatio/cast(float)ratioTotal)*total);
931 	return r;
932 }
933 
934 ///Converts hex color code to RGBColor
935 RGBColor hexToColor(string hex){
936 	RGBColor r;
937 	r.r = cast(ubyte)hexToDen(hex[0..2]);
938 	r.g = cast(ubyte)hexToDen(hex[2..4]);
939 	r.b = cast(ubyte)hexToDen(hex[4..6]);
940 	return r;
941 }
942 
943 ///Converts RGBColor to hex color code
944 string colorToHex(RGBColor col){
945 	char[] r;
946 	char[] code;
947 	r.length = 6;
948 	r[0 .. 6] = '0';
949 	code = cast(char[])denToHex(col.r);
950 	r[2 - code.length .. 2] = code;
951 	code = cast(char[])denToHex(col.g);
952 	r[4 - code.length .. 4] = code;
953 	code = cast(char[])denToHex(col.b);
954 	r[6 - code.length .. 6] = code;
955 	return cast(string)r;
956 }