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