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