1 /++ 2 This module contains most of the functions you'll need. 3 All the 'base' classes, like QWidget are defined in this. 4 +/ 5 module qui.qui; 6 7 import std.datetime.stopwatch; 8 import utils.misc; 9 import utils.lists; 10 import std.conv : to; 11 import qui.termwrap; 12 13 import qui.utils; 14 15 /// the default foreground color 16 const Color DEFAULT_FG = Color.white; 17 /// the default background color 18 const Color DEFAULT_BG = Color.black; 19 20 /// Available colors are in this enum 21 public alias Color = qui.termwrap.Color; 22 /// Availabe Keys (keyboard) for input 23 public alias Key = qui.termwrap.Event.Keyboard.Key; 24 /// Mouse Event 25 public alias MouseEvent = Event.Mouse; 26 /// Keyboard Event 27 public alias KeyboardEvent = Event.Keyboard; 28 29 /// Used to store position for widgets 30 struct Position{ 31 /// x and y position 32 integer x = 0, y = 0; 33 /// Returns: a string representation of Position 34 string tostring(){ 35 return "{x:"~to!string(x)~",y:"~to!string(y)~"}"; 36 } 37 } 38 39 /// To store size for widgets 40 /// 41 /// zero in min/max means no limit 42 struct Size{ 43 private{ 44 uinteger _w = 0, _h = 0; 45 /// width 46 @property uinteger width(uinteger newWidth){ 47 if (newWidth == 0) 48 return _w = 0; 49 if (minWidth > 0 && newWidth < minWidth){ 50 return _w = minWidth; 51 }else if (maxWidth > 0 && newWidth > maxWidth){ 52 return _w = maxWidth; 53 } 54 return _w = newWidth; 55 } 56 /// height 57 @property uinteger height(uinteger newHeight){ 58 if (newHeight == 0) 59 return _h = 0; 60 if (minHeight > 0 && newHeight < minHeight){ 61 return _h = minHeight; 62 }else if (maxHeight > 0 && newHeight > maxHeight){ 63 return _h = maxHeight; 64 } 65 return _h = newHeight; 66 } 67 } 68 /// width 69 @property uinteger width(){ 70 return _w; 71 } 72 /// height 73 @property uinteger height(){ 74 return _h; 75 } 76 77 /// minimun width & height. These are "applied" automatically when setting value using `width` or `height` 78 uinteger minWidth = 0, minHeight = 0; 79 /// maximum width & height. These are "applied" automatically when setting value using `width` or `height` 80 uinteger maxWidth = 0, maxHeight = 0; 81 /// Returns: a string representation of KeyPress, in JSON 82 string tostring(){ 83 return "{width:"~to!string(_w)~",height:"~to!string(_h)~ 84 ",minWidth:"~to!string(minWidth)~",maxWidth:"~to!string(maxWidth)~ 85 ",minHeight:"~to!string(minHeight)~",maxHeight:"~to!string(maxHeight)~"}"; 86 } 87 } 88 89 /// mouseEvent function. Return true if the event should be dropped 90 alias MouseEventFuction = bool delegate(QWidget, MouseEvent); 91 ///keyboardEvent function. Return true if the event should be dropped 92 alias KeyboardEventFunction = bool delegate(QWidget, KeyboardEvent); 93 /// resizeEvent function. Return true if the event should be dropped 94 alias ResizeEventFunction = bool delegate(QWidget); 95 /// activateEvent function. Return true if the event should be dropped 96 alias ActivateEventFunction = bool delegate(QWidget, bool); 97 /// TimerEvent function. Return true if the event should be dropped 98 alias TimerEventFunction = bool delegate(QWidget, uinteger); 99 /// Init function. Return true if the event should be dropped 100 alias InitFunction = bool delegate(QWidget); 101 102 103 /// Base class for all widgets, including layouts and QTerminal 104 /// 105 /// Use this as parent-class for new widgets 106 abstract class QWidget{ 107 private: 108 /// stores the position of this widget, relative to it's parent widget's position 109 Position _position; 110 /// stores if this widget is the active widget 111 bool _isActive = false; 112 /// stores what child widgets want updates 113 bool[] _requestingUpdate; 114 /// what key handlers are registered for what keys (key/index) 115 QWidget[dchar] _keyHandlers; 116 /// the parent widget 117 QWidget _parent = null; 118 /// the index it is stored at in _parent. -1 if no parent asigned yet 119 integer _indexInParent = -1; 120 /// called by owner for initialize event 121 void initializeCall(){ 122 if (!_customInitEvent || !_customInitEvent(this)) 123 this.initialize(); 124 } 125 /// called by owner for mouseEvent 126 void mouseEventCall(MouseEvent mouse){ 127 mouse.x = mouse.x - cast(int)this._position.x; 128 mouse.y = mouse.y - cast(int)this._position.y; 129 if (!_customMouseEvent || !_customMouseEvent(this, mouse)) 130 this.mouseEvent(mouse); 131 } 132 /// called by owner for keyboardEvent 133 void keyboardEventCall(KeyboardEvent key){ 134 if (!_customKeyboardEvent || !_customKeyboardEvent(this, key)) 135 this.keyboardEvent(key); 136 } 137 /// called by owner for resizeEvent 138 void resizeEventCall(){ 139 if (!_customResizeEvent || !_customResizeEvent(this)) 140 this.resizeEvent(); 141 } 142 /// called by owner for activateEvent 143 void activateEventCall(bool isActive){ 144 _isActive = isActive; 145 if (!_customActivateEvent || !_customActivateEvent(this, isActive)) 146 this.activateEvent(isActive); 147 } 148 /// called by owner for mouseEvent 149 void timerEventCall(uinteger msecs){ 150 if (!_customTimerEvent || !_customTimerEvent(this, msecs)) 151 this.timerEvent(msecs); 152 } 153 /// Called by children of this widget to request updates 154 void requestUpdate(uinteger index){ 155 if (index < _requestingUpdate.length){ 156 _requestingUpdate[index] = true; 157 requestUpdate(); 158 } 159 } 160 /// Called by children of this widget to register key handlers 161 bool registerKeyHandler(QWidget widget, dchar key){ 162 if (key in _keyHandlers || _parent is null) 163 return false; 164 _keyHandlers[key] = widget; 165 this.registerKeyHandler(key); 166 return true; 167 } 168 protected: 169 ///size of this widget 170 Size _size; 171 ///whether this widget should be drawn or not 172 bool _show = true; 173 /// specifies that how much height (in horizontal layout) or width (in vertical) is given to this widget. 174 /// The ratio of all widgets is added up and height/width for each widget is then calculated using this 175 uinteger _sizeRatio = 1; 176 /// specifies whether this widget should receive the Tab key press, default is false, and should only be changed to true 177 /// if only required, for example, in text editors 178 bool _wantsTab = false; 179 /// whether the widget wants input 180 bool _wantsInput = false; 181 /// used to write to terminal 182 Display _display = null; 183 184 /// For cycling between widgets. Returns false, always. 185 bool cycleActiveWidget(){ 186 return false; 187 } 188 189 /// custom onInit event, if not null, it should be called before doing anything else in init(); 190 InitFunction _customInitEvent; 191 /// custom mouse event, if not null, it should be called before doing anything else in mouseEvent. 192 MouseEventFuction _customMouseEvent; 193 /// custom keyboard event, if not null, it should be called before doing anything else in keyboardEvent. 194 KeyboardEventFunction _customKeyboardEvent; 195 /// custom resize event, if not null, it should be called before doing anything else in the resizeEvent 196 ResizeEventFunction _customResizeEvent; 197 /// custom onActivate event, if not null, it should be called before doing anything else in activateEvent 198 ActivateEventFunction _customActivateEvent; 199 /// custom onTimer event, if not null, it should be called before doing anything else in timerEvent 200 TimerEventFunction _customTimerEvent; 201 202 /// Called by parent to update this widget 203 void update(){} 204 205 /// Called after `_display` has been set and this widget is ready to be used 206 void initialize(){} 207 /// Called when mouse is clicked with cursor on this widget. 208 void mouseEvent(MouseEvent mouse){} 209 /// Called when key is pressed and this widget is active. 210 void keyboardEvent(KeyboardEvent key){} 211 /// Called when widget size is changed, or widget should recalculate it's child widgets' sizes; 212 void resizeEvent(){} 213 /// called right after this widget is activated, or de-activated, i.e: is made _activeWidget, or un-made _activeWidget 214 void activateEvent(bool isActive){} 215 /// called often. `msecs` is the msecs since last timerEvent, not accurate 216 void timerEvent(uinteger msecs){} 217 public: 218 /// Called by itself when it needs to request an update 219 void requestUpdate(){ 220 if (_parent && _indexInParent > -1 && _indexInParent < _parent._requestingUpdate.length && 221 _parent._requestingUpdate[_indexInParent] == false) 222 _parent.requestUpdate(_indexInParent); 223 } 224 /// Called to request this widget to resize at next update 225 void requestResize(){ 226 if (_parent) 227 _parent.requestResize(); 228 } 229 /// Called by itself (not necessarily) to register itself as a key handler 230 bool registerKeyHandler(dchar key){ 231 return _parent && _indexInParent > -1 && _parent.registerKeyHandler(this, key); 232 } 233 /// use to change the custom initialize event 234 @property InitFunction onInitEvent(InitFunction func){ 235 return _customInitEvent = func; 236 } 237 /// use to change the custom mouse event 238 @property MouseEventFuction onMouseEvent(MouseEventFuction func){ 239 return _customMouseEvent = func; 240 } 241 /// use to change the custom keyboard event 242 @property KeyboardEventFunction onKeyboardEvent(KeyboardEventFunction func){ 243 return _customKeyboardEvent = func; 244 } 245 /// use to change the custom resize event 246 @property ResizeEventFunction onResizeEvent(ResizeEventFunction func){ 247 return _customResizeEvent = func; 248 } 249 /// use to change the custom activate event 250 @property ActivateEventFunction onActivateEvent(ActivateEventFunction func){ 251 return _customActivateEvent = func; 252 } 253 /// use to change the custom timer event 254 @property TimerEventFunction onTimerEvent(TimerEventFunction func){ 255 return _customTimerEvent = func; 256 } 257 /// Returns: this widget's parent 258 @property QWidget parent(){ 259 return _parent; 260 } 261 /// Returns: true if this widget is the current active widget 262 @property bool isActive(){ 263 return _isActive; 264 } 265 /// Returns: whether the widget is receiving the Tab key press or not 266 @property bool wantsTab(){ 267 return _wantsTab; 268 } 269 /// ditto 270 @property bool wantsTab(bool newStatus){ 271 return _wantsTab = newStatus; 272 } 273 /// Returns: true if the widget wants input 274 @property bool wantsInput(){ 275 return _wantsInput; 276 } 277 /// Returns: position of cursor to be shown on terminal. (-1,-1) if dont show 278 @property Position cursorPosition(){ 279 return Position(-1,-1); 280 } 281 /// size of width (height/width, depending of Layout.Type it is in) of this widget, in ratio to other widgets in that layout 282 @property uinteger sizeRatio(){ 283 return _sizeRatio; 284 } 285 /// ditto 286 @property uinteger sizeRatio(uinteger newRatio){ 287 requestResize; 288 return _sizeRatio = newRatio; 289 } 290 /// visibility of the widget. getter 291 @property bool show(){ 292 return _show; 293 } 294 /// visibility of the widget. setter 295 @property bool show(bool visibility){ 296 requestResize; 297 return _show = visibility; 298 } 299 /// size of the widget. getter 300 /// **NOTE: call this.requestResize() if you change the size!** 301 @property ref Size size(){ 302 return _size; 303 } 304 /// size of the widget. setter 305 @property ref Size size(Size newSize){ 306 requestResize; 307 return _size = newSize; 308 } 309 } 310 311 ///Used to place widgets in an order (i.e vertical or horizontal) 312 class QLayout : QWidget{ 313 private: 314 /// array of all the widgets that have been added to this layout 315 QWidget[] _widgets; 316 /// stores the layout type, horizontal or vertical 317 QLayout.Type _type; 318 /// stores index of active widget. -1 if none. This is useful only for Layouts. For widgets, this stays 0 319 integer _activeWidgetIndex = -1; 320 321 /// gets height/width of a widget using it's sizeRatio and min/max-height/width 322 static uinteger calculateWidgetSize(QLayout.Type type)(QWidget widget, uinteger ratioTotal, uinteger totalSpace, 323 ref bool free){ 324 Size wSize = widget.size; 325 immutable calculatedSize = cast(uinteger)((widget.sizeRatio*totalSpace)/ratioTotal); 326 static if (type == QLayout.Type.Horizontal){ 327 wSize.width = calculatedSize; 328 free = wSize.minWidth == 0 && wSize.maxWidth == 0; 329 return wSize.width; 330 }else{ // this else just exists to shut up compiler about "statement not reachable" 331 wSize.height = calculatedSize; 332 free = wSize.minHeight == 0 && wSize.maxHeight == 0; 333 return wSize.height; 334 } 335 } 336 337 /// recalculates the size of every widget inside layout 338 void recalculateWidgetsSize(QLayout.Type T)(){ 339 static if (T != QLayout.Type.Horizontal && T != QLayout.Type.Vertical) 340 assert(false); 341 FIFOStack!QWidget widgetStack = new FIFOStack!QWidget; 342 uinteger totalRatio = 0; 343 uinteger totalSpace = T == QLayout.Type.Horizontal ? _size.width : _size.height; 344 foreach (widget; _widgets){ 345 if (!widget.show) 346 continue; 347 totalRatio += widget.sizeRatio; 348 widget._size.height = _size.height; 349 widget._size.width = _size.width; 350 widgetStack.push(widget); 351 } 352 // do widgets with size limits 353 uinteger limitWRatio, limitWSize; /// totalRatio, and space used of widgets with limits 354 for (integer i = 0; i < widgetStack.count; i ++){ 355 QWidget widget = widgetStack.pop; 356 bool free; 357 immutable uinteger space = calculateWidgetSize!(T)(widget, totalRatio, totalSpace, free); 358 if (free){ 359 widgetStack.push(widget); 360 continue; 361 } 362 static if (T == QLayout.Type.Horizontal) 363 widget._size.width = space; 364 else 365 widget._size.height = space; 366 limitWRatio += widget.sizeRatio; 367 limitWSize += space; 368 } 369 totalSpace -= limitWSize; 370 totalRatio -= limitWRatio; 371 while (widgetStack.count){ 372 QWidget widget = widgetStack.pop; 373 bool free; 374 immutable uinteger space = calculateWidgetSize!(T)(widget, totalRatio, totalSpace, free); 375 static if (T == QLayout.Type.Horizontal) 376 widget._size.width = space; 377 else 378 widget._size.height = space; 379 totalRatio -= widget.sizeRatio; 380 totalSpace -= space; 381 } 382 .destroy(widgetStack); 383 } 384 /// calculates and assigns widgets positions based on their sizes 385 void recalculateWidgetsPosition(QLayout.Type T)(){ 386 static if (T != QLayout.Type.Horizontal && T != QLayout.Type.Vertical) 387 assert(false); 388 uinteger previousSpace = 0; 389 foreach(widget; _widgets){ 390 if (widget.show){ 391 static if (T == QLayout.Type.Horizontal){ 392 widget._position.y = 0; 393 widget._position.x = previousSpace; 394 previousSpace += widget.size.width; 395 }else{ 396 widget._position.x = 0; 397 widget._position.y = previousSpace; 398 previousSpace += widget.size.height; 399 } 400 } 401 } 402 } 403 protected: 404 /// Recalculates size and position for all visible widgets 405 /// If a widget is too large to fit in, it's visibility is marked false 406 override void resizeEvent(){ 407 uinteger ratioTotal; 408 foreach(w; _widgets){ 409 if (w.show){ 410 ratioTotal += w._sizeRatio; 411 } 412 } 413 if (_type == QLayout.Type.Horizontal){ 414 recalculateWidgetsSize!(QLayout.Type.Horizontal); 415 recalculateWidgetsPosition!(QLayout.Type.Horizontal); 416 }else{ 417 recalculateWidgetsSize!(QLayout.Type.Vertical); 418 recalculateWidgetsPosition!(QLayout.Type.Vertical); 419 } 420 foreach (widget; _widgets){ 421 this._display.getSlice(widget._display, widget.size.width, widget.size.height,widget._position.x,widget._position.y); 422 widget.resizeEventCall(); 423 } 424 } 425 426 /// Redirects the mouseEvent to the appropriate widget 427 override public void mouseEvent(MouseEvent mouse) { 428 /// first check if it's already inside active widget, might not have to search through each widget 429 QWidget activeWidget = null; 430 if (_activeWidgetIndex > -1) 431 activeWidget = _widgets[_activeWidgetIndex]; 432 if (activeWidget && mouse.x >= activeWidget._position.x && mouse.x < activeWidget._position.x +activeWidget.size.width 433 && mouse.y >= activeWidget._position.y && mouse.y < activeWidget._position.y + activeWidget.size.height){ 434 activeWidget.mouseEventCall(mouse); 435 }else{ 436 foreach (i, widget; _widgets){ 437 if (widget.show && widget.wantsInput && 438 mouse.x >= widget._position.x && mouse.x < widget._position.x + widget.size.width && 439 mouse.y >= widget._position.y && mouse.y < widget._position.y + widget.size.height){ 440 // make it active only if this layout is itself active 441 if (this.isActive){ 442 if (activeWidget) 443 activeWidget.activateEventCall(false); 444 widget.activateEventCall(true); 445 _activeWidgetIndex = i; 446 } 447 widget.mouseEventCall(mouse); 448 break; 449 } 450 } 451 } 452 } 453 454 /// Redirects the keyboardEvent to appropriate widget 455 override public void keyboardEvent(KeyboardEvent key){ 456 // check if key handler registered 457 if (key.key in _keyHandlers) 458 _keyHandlers[key.key].keyboardEventCall(key); 459 // check if need to cycle 460 if ((key.key == '\t' && (_activeWidgetIndex == -1 || !_widgets[_activeWidgetIndex].wantsTab)) || 461 key.key == KeyboardEvent.Key.Escape){ 462 this.cycleActiveWidget(); 463 }else if (_activeWidgetIndex > -1){ 464 _widgets[_activeWidgetIndex].keyboardEventCall(key); 465 } 466 } 467 468 /// override initialize to initliaze child widgets 469 override void initialize(){ 470 foreach (widget; _widgets){ 471 widget._display = _display.getSlice(1,1, _position.x, _position.y); // just throw in dummy size/position, resize event will fix that 472 widget.initializeCall(); 473 } 474 } 475 476 /// override timer event to call child widgets' timers 477 override void timerEvent(uinteger msecs){ 478 foreach (widget; _widgets) 479 widget.timerEventCall(msecs); 480 } 481 482 /// override activate event 483 override void activateEvent(bool isActive){ 484 if (isActive){ 485 _activeWidgetIndex = -1; 486 this.cycleActiveWidget(); 487 }else if (_activeWidgetIndex > -1){ 488 _widgets[_activeWidgetIndex].activateEventCall(isActive); 489 } 490 } 491 492 /// called by owner widget to update 493 override void update(){ 494 foreach(i, widget; _widgets){ 495 if (_requestingUpdate[i] && widget.show){ 496 widget._display.cursor = Position(0,0); 497 widget.update(); 498 _requestingUpdate[i] = false; 499 } 500 } 501 } 502 /// called to cycle between actveWidgets. This is called by owner widget 503 /// 504 /// Returns: true if cycled to another widget, false if _activeWidgetIndex set to -1 505 override bool cycleActiveWidget(){ 506 // check if need to cycle within current active widget 507 if (_activeWidgetIndex == -1 || !(_widgets[_activeWidgetIndex].cycleActiveWidget())){ 508 integer lastActiveWidgetIndex = _activeWidgetIndex; 509 for (_activeWidgetIndex ++; _activeWidgetIndex < _widgets.length; _activeWidgetIndex ++){ 510 if (_widgets[_activeWidgetIndex].wantsInput && _widgets[_activeWidgetIndex].show) 511 break; 512 } 513 if (_activeWidgetIndex >= _widgets.length) 514 _activeWidgetIndex = -1; 515 516 if (lastActiveWidgetIndex != _activeWidgetIndex){ 517 if (lastActiveWidgetIndex > -1) 518 _widgets[lastActiveWidgetIndex].activateEventCall(false); 519 if (_activeWidgetIndex > -1) 520 _widgets[_activeWidgetIndex].activateEventCall(true); 521 } 522 } 523 return _activeWidgetIndex != -1; 524 } 525 public: 526 /// Layout type 527 enum Type{ 528 Vertical, 529 Horizontal, 530 } 531 /// constructor 532 this(QLayout.Type type){ 533 _type = type; 534 } 535 /// Returns: whether the widget is receiving the Tab key press or not 536 override @property bool wantsTab(){ 537 foreach (widget; _widgets){ 538 if (widget.wantsTab) 539 return true; 540 } 541 return false; 542 } 543 /// Returns: true if the widget wants input 544 override @property bool wantsInput(){ 545 foreach (widget; _widgets){ 546 if (widget.wantsInput) 547 return true; 548 } 549 return false; 550 } 551 /// Returns: true if the cursor should be visible if this widget is active 552 override @property Position cursorPosition(){ 553 // just do a hack, and check only for active widget 554 if (_activeWidgetIndex == -1) 555 return Position(-1, -1); 556 Position cursorPosition = _widgets[_activeWidgetIndex].cursorPosition; 557 if (cursorPosition == Position(-1, -1)) 558 return cursorPosition; 559 return Position(_widgets[_activeWidgetIndex]._position.x + cursorPosition.x, 560 _widgets[_activeWidgetIndex]._position.y + cursorPosition.y); 561 } 562 563 /// adds (appends) a widget to the widgetList, and makes space for it 564 /// 565 /// If there a widget is too large, it's marked as not visible 566 void addWidget(QWidget widget){ 567 widget._parent = this; 568 widget._indexInParent = _widgets.length; 569 //add it to array 570 _widgets ~= widget; 571 // make space in _requestingUpdate 572 _requestingUpdate ~= true; 573 } 574 /// adds (appends) widgets to the widgetList, and makes space for them 575 /// 576 /// If there a widget is too large, it's marked as not visible 577 void addWidget(QWidget[] widgets){ 578 foreach (i, widget; widgets){ 579 widget._parent = this; 580 widget._indexInParent = _widgets.length+i; 581 } 582 // add to array 583 _widgets ~= widgets.dup; 584 // make space in _requestingUpdate 585 bool[] reqUpdates; 586 reqUpdates.length = widgets.length; 587 reqUpdates[] = true; 588 _requestingUpdate ~= reqUpdates; 589 } 590 } 591 592 /// Used to write to display by widgets 593 class Display{ 594 private: 595 /// width & height 596 uinteger _width, _height; 597 /// x and y offsets 598 uinteger _xOff, _yOff; 599 /// cursor position, relative to _xOff and _yOff 600 Position _cursor; 601 /// the terminal 602 TermWrapper _term; 603 /// constructor for when this buffer is just a slice of the actual buffer 604 this(uinteger w, uinteger h, uinteger xOff, uinteger yOff, TermWrapper terminal){ 605 _xOff = xOff; 606 _yOff = yOff; 607 _width = w; 608 _height = h; 609 _term = terminal; 610 } 611 /// Returns: a "slice" of this buffer, that is only limited to some rectangular area 612 Display getSlice(uinteger w, uinteger h, uinteger x, uinteger y){ 613 return new Display(w, h, _xOff + x, _yOff + y, _term); 614 } 615 /// modifies an existing Display to act as a "slice" 616 void getSlice(Display sliced, uinteger w, uinteger h, uinteger x, uinteger y){ 617 sliced._width = w; 618 sliced._height = h; 619 sliced._xOff = _xOff + x; 620 sliced._yOff = _yOff + y; 621 } 622 public: 623 /// constructor 624 this(uinteger w, uinteger h, TermWrapper terminal){ 625 _width = w; 626 _height = h; 627 _term = terminal; 628 } 629 ~this(){ 630 631 } 632 /// Returns: cursor position 633 @property Position cursor(){ 634 return _cursor; 635 } 636 /// ditto 637 @property Position cursor(Position newPos){ 638 if (newPos.x >= _width) 639 newPos.x = _width-1; 640 if (newPos.y >= _height) 641 newPos.y = _height-1; 642 if (newPos.x < 0) 643 newPos.x = 0; 644 if (newPos.y < 0) 645 newPos.y = 0; 646 return _cursor = newPos; 647 } 648 /// sets background and foreground color 649 void colors(Color fg, Color bg){ 650 _term.color(fg, bg); 651 } 652 /// Writes a line. if string has more characters than there is space for, extra characters will be ignored. 653 /// Tab character is converted to a single space character 654 void write(dstring str, Color fg, Color bg){ 655 _term.color(fg, bg); 656 this.write(str); 657 } 658 /// ditto 659 void write(dstring str){ 660 foreach (c; str){ 661 if (_cursor.x >= _width){ 662 _cursor.x = 0; 663 _cursor.y ++; 664 } 665 if (_cursor.x < _width && _cursor.y < _height) 666 _term.put(cast(int)(_cursor.x + _xOff), cast(int)(_cursor.y + _yOff), c == '\t' || c == '\n' ? ' ' : c); 667 else 668 break; 669 _cursor.x ++; 670 } 671 } 672 /// fills all remaining cells with a character 673 void fill(dchar c, Color fg, Color bg){ 674 dchar[] line; 675 line.length = _width; 676 line[] = c; 677 _term.color(fg, bg); 678 while (_cursor.y < _height){ 679 _term.write(cast(int)(_cursor.x + _xOff), cast(int)(_cursor.y + _yOff), cast(dstring)line[0 .. _width - _cursor.x]); 680 _cursor.y ++; 681 _cursor.x = 0; 682 } 683 } 684 /// fills rest of current line with a character 685 void fillLine(dchar c, Color fg, Color bg, uinteger max = 0){ 686 dchar[] line; 687 line.length = max < _width - _cursor.x && max > 0 ? max : _width - _cursor.x; 688 line[] = c; 689 _term.color(fg, bg); 690 _term.write(cast(int)(_cursor.x + _xOff), cast(int)(_cursor.y + _yOff), cast(dstring)line); 691 _cursor.x += line.length; 692 if (_cursor.x >= _width -1){ 693 _cursor.y ++; 694 _cursor.x = 0; 695 } 696 } 697 } 698 699 /// A terminal (as the name says). 700 class QTerminal : QLayout{ 701 private: 702 /// To actually access the terminal 703 TermWrapper _termWrap; 704 /// Whether to call resize before next update 705 bool _requestingResize; 706 /// set to false to stop UI loop in run() 707 bool _isRunning; 708 709 /// Reads InputEvent and calls appropriate functions to address those events 710 void readEvent(Event event){ 711 if (event.type == Event.Type.HangupInterrupt){ 712 _isRunning = false; 713 }else if (event.type == Event.Type.Keyboard){ 714 KeyboardEvent kPress = event.keyboard; 715 this.keyboardEventCall(kPress); 716 }else if (event.type == Event.Type.Mouse){ 717 this.mouseEventCall(event.mouse); 718 }else if (event.type == Event.Type.Resize){ 719 //update self size 720 _size.height = event.resize.height; 721 _size.width = event.resize.width; 722 //call size change on all widgets 723 resizeEventCall(); 724 } 725 } 726 727 protected: 728 729 override void resizeEvent(){ 730 _requestingResize = false; 731 super.resizeEvent(); 732 } 733 734 override void update(){ 735 // resize if needed 736 if (_requestingResize) 737 this.resizeEventCall(); 738 super.update(); 739 // check if need to show/hide cursor 740 Position cursorPos = this.cursorPosition; 741 if (cursorPos == Position(-1, -1)){ 742 _termWrap.cursorVisible = false; 743 }else{ 744 _termWrap.moveCursor(cast(int)cursorPos.x, cast(int)cursorPos.y); 745 _termWrap.cursorVisible = true; 746 } 747 _termWrap.flush; 748 } 749 750 public: 751 /// text color, and background color 752 Color textColor, backgroundColor; 753 /// time to wait between timer events (milliseconds) 754 ushort timerMsecs; 755 /// constructor 756 this(QLayout.Type displayType = QLayout.Type.Vertical, ushort timerDuration = 500){ 757 super(displayType); 758 759 textColor = DEFAULT_FG; 760 backgroundColor = DEFAULT_BG; 761 timerMsecs = timerDuration; 762 763 _termWrap = new TermWrapper(); 764 _display = new Display(1,1, _termWrap); 765 this._isActive = true; // so it can make other widgets active on mouse events 766 } 767 ~this(){ 768 .destroy(_termWrap); 769 .destroy(_display); 770 } 771 772 /// Called to make container widgets (QLayouts) recalcualte widgets' sizes before update; 773 override void requestResize(){ 774 _requestingResize = true; 775 } 776 777 /// stops UI loop. **not instantly**, if it is in-between updates, calling event functions, or timers, it will complete those first 778 void terminate(){ 779 _isRunning = false; 780 } 781 782 /// starts the UI loop 783 void run(){ 784 // init termbox 785 _size.width = _termWrap.width(); 786 _size.height = _termWrap.height(); 787 //ready 788 initializeCall(); 789 resizeEventCall(); 790 //draw the whole thing 791 update(); 792 _isRunning = true; 793 // the stop watch, to count how much time has passed after each timerEvent 794 StopWatch sw = StopWatch(AutoStart.yes); 795 while (_isRunning){ 796 int timeout = cast(int)(timerMsecs - sw.peek.total!"msecs"); 797 Event event; 798 while (_termWrap.getEvent(timeout, event) > 0){ 799 readEvent(event); 800 timeout = cast(int)(timerMsecs - sw.peek.total!"msecs"); 801 update(); 802 } 803 if (sw.peek.total!"msecs" >= timerMsecs){ 804 this.timerEventCall(sw.peek.total!"msecs"); 805 sw.reset; 806 sw.start; 807 update(); 808 } 809 } 810 } 811 }