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