1 /* UI elements module. 2 3 Provides some UI element classes. 4 5 It publicly imports both nice.curses and nice.ui.base. 6 7 */ 8 9 module nice.ui.elements; 10 11 public import nice.curses; 12 public import nice.ui.base; 13 14 private alias W = WChar; /* There's a lot of default keys below. */ 15 16 class Menu(T): UIElement 17 { 18 protected: 19 string delegate() header; 20 int choice; 21 int curScroll; 22 string delegate()[] entries; 23 T[] values; 24 Config cfg; 25 bool signalChange; 26 27 public: 28 struct Config 29 { 30 WChar[] down = [W('j'), W(Key.down)]; 31 WChar[] up = [W('k'), W(Key.up)]; 32 WChar[] enter = [W('\n'), W('\r'), W(Key.enter)]; 33 Align alignment = Align.center; 34 bool signalChange = false; 35 } 36 37 /* Thrown when 'Enter' is pressed while in the menu or when the chosen 38 element changes (if signalChange is set). */ 39 class Signal: UISignal 40 { 41 T value; 42 43 this() 44 { 45 auto m = this.outer; 46 super(m); 47 value = m.values[m.choice]; 48 } 49 } 50 51 this(UI ui, int nlines, int ncols, int y, int x, 52 string delegate() header, Config cfg = Config()) 53 { 54 super(ui, nlines, ncols, y, x); 55 this.header = header; 56 this.cfg = cfg; 57 } 58 59 this(UI ui, int nlines, int ncols, int y, int x, string header, 60 Config cfg = Config()) 61 { 62 string dg() { return header; } 63 this(ui, nlines, ncols, y, x, &dg, cfg); 64 } 65 66 auto chosenValue() const @property { return values[choice]; } 67 68 void addEntry(T value, string delegate() text) 69 { 70 values ~= value; 71 entries ~= text; 72 } 73 74 void addEntry(T value, string text) 75 { 76 values ~= value; 77 string dg() { return text; } 78 entries ~= &dg; 79 } 80 81 /* ---------- internal things ---------- */ 82 83 private: 84 85 void scroll(int by) 86 { 87 curScroll += by; 88 if (curScroll < 0) curScroll = 0; 89 /* The cast is most likely safe. I can't imagine a menu where difference 90 between size_t and int is significant. */ 91 if (curScroll >= entries.length) curScroll = cast(int) entries.length - 1; 92 } 93 94 void choose(int shift) 95 { 96 choice += shift; 97 /* The cast is safe since menus are generally short. */ 98 if (choice < 0) choice = 0; 99 if (choice >= entries.length) choice = cast(int) entries.length - 1; 100 if (choice >= curScroll + window.height || choice < curScroll) scroll(shift); 101 } 102 103 public: 104 105 /* ---------- inherited stuff ---------- */ 106 107 override void draw(bool active) 108 { 109 window.erase; 110 auto headerAttr = active ? Attr.reverse : Attr.normal; 111 int w = window.width; 112 int h = window.height; 113 window.addAligned(0, header(), cfg.alignment, headerAttr); 114 int offset = 5; 115 for (int i = 0; i < h - offset; i++) { 116 int entry = i + curScroll; 117 if (entry >= entries.length) break; 118 auto attr = entry == choice ? Attr.reverse : Attr.normal; 119 window.addAligned(offset + i, entries[entry](), cfg.alignment, attr); 120 } 121 } 122 123 override bool keystroke(WChar key) 124 { 125 import std.algorithm; 126 if (values == []) return false; 127 if (cfg.down.canFind(key)) { 128 choose(+1); 129 if (cfg.signalChange) 130 throw new Signal(); 131 return true; 132 } else if (cfg.up.canFind(key)) { 133 choose(-1); 134 if (cfg.signalChange) 135 throw new Signal(); 136 return true; 137 } else if (cfg.enter.canFind(key)) { 138 throw new Signal(); 139 } 140 return false; 141 } 142 } 143 144 class Button: UIElement 145 { 146 protected: 147 string delegate() text; 148 Config cfg; 149 150 public: 151 /* Thrown when the button is pressed. */ 152 class Signal: UISignal 153 { 154 this() { super(this.outer); } 155 } 156 157 struct Config 158 { 159 Align alignment = Align.left; 160 WChar[] enter = [W('\n'), W('\r'), W(Key.enter)]; 161 } 162 163 this(UI ui, int nlines, int ncols, int y, int x, 164 string delegate() text, 165 Config cfg = Config()) 166 { 167 super(ui, nlines, ncols, y, x); 168 this.text = text; 169 this.cfg = cfg; 170 } 171 172 this(UI ui, int nlines, int ncols, int y, int x, string text, 173 Config cfg = Config()) 174 { 175 string dg() { return text; } 176 this(ui, nlines, ncols, y, x, &dg, cfg); 177 } 178 179 /* ---------- inherited stuff ---------- */ 180 181 override void draw(bool active) 182 { 183 window.erase; 184 auto attr = active ? Attr.reverse : Attr.normal; 185 window.addAligned(0, text(), cfg.alignment, attr); 186 } 187 188 override bool keystroke(WChar key) 189 { 190 import std.algorithm; 191 if (cfg.enter.canFind(key)) 192 throw new Signal(); 193 return false; 194 } 195 } 196 197 class Label: UIElement 198 { 199 protected: 200 string delegate() text; 201 Config cfg; 202 203 public: 204 struct Config 205 { 206 Align alignment = Align.left; 207 chtype attribute = Attr.normal; 208 } 209 210 this(UI ui, int nlines, int ncols, int y, int x, 211 string delegate() text, 212 Config cfg = Config()) 213 { 214 super(ui, nlines, ncols, y, x); 215 this.text = text; 216 this.cfg = cfg; 217 focusable = false; 218 } 219 220 this(UI ui, int nlines, int ncols, int y, int x, string text, 221 Config cfg = Config()) 222 { 223 string dg() { return text; } 224 this(ui, nlines, ncols, y, x, &dg, cfg); 225 } 226 227 /* ---------- inherited stuff ---------- */ 228 229 override void draw(bool active) 230 { 231 window.erase; 232 window.addAligned(0, text(), cfg.alignment, cfg.attribute); 233 } 234 } 235 236 class ProgressBar: UIElement 237 { 238 protected: 239 Config cfg; 240 double percentage_ = 0; 241 242 public: 243 struct Config 244 { 245 wint_t empty = '-'; 246 wint_t filled = '#'; 247 chtype emptyAttr = Attr.normal; 248 chtype filledAttr = Attr.normal; 249 bool vertical = false; 250 bool reverse = false; 251 } 252 253 this(UI ui, int nlines, int ncols, int y, int x, Config cfg = Config()) 254 { 255 super(ui, nlines, ncols, y, x); 256 this.cfg = cfg; 257 focusable = false; 258 } 259 260 double percentage() const @property { return percentage_; } 261 void percentage(double p) @property 262 { 263 if (p < 0) p = 0; 264 if (p > 1) p = 1; 265 percentage_ = p; 266 } 267 268 /* ---------- inherited stuff ---------- */ 269 270 override void draw(bool active) 271 { 272 window.erase; 273 if (cfg.vertical) { 274 int n = cast(int) (window.height * percentage); 275 if (cfg.reverse) { 276 /* Fill from the top downwards. */ 277 foreach (y; 0 .. n) 278 foreach (x; 0 .. window.width) 279 window.addch(y, x, cfg.filled, cfg.filledAttr); 280 foreach (y; n .. window.height) 281 foreach (x; 0 .. window.width) 282 window.addch(y, x, cfg.empty, cfg.emptyAttr); 283 } else { 284 /* Fill from the bottom up. */ 285 foreach (k; 1 .. n + 1) 286 foreach (x; 0 .. window.width) 287 window.addch(window.height - k, x, cfg.filled, cfg.filledAttr); 288 foreach (k; n + 1 .. window.height + 1) 289 foreach (x; 0 .. window.width) 290 window.addch(window.height - k, x, cfg.empty, cfg.emptyAttr); 291 } 292 } else { 293 int n = cast(int) (window.width * percentage); 294 if (cfg.reverse) { 295 /* Fill from the right to the left. */ 296 foreach (k; 1 .. n) 297 foreach (y; 0 .. window.height) 298 window.addch(y, window.width - k, cfg.filled, cfg.filledAttr); 299 foreach (k; n + 1 .. window.width + 1) 300 foreach (y; 0 .. window.height) 301 window.addch(y, window.width - k, cfg.empty, cfg.emptyAttr); 302 } else { 303 /* Fill from the left to the right. */ 304 foreach (x; 0 .. n) 305 foreach (y; 0 .. window.height) 306 window.addch(y, x, cfg.filled, cfg.filledAttr); 307 foreach (x; n .. window.width) 308 foreach (y; 0 .. window.height) 309 window.addch(y, x, cfg.empty, cfg.emptyAttr); 310 } 311 } /* if vertical */ 312 } /* draw */ 313 } 314 315 class TextInput: UIElement 316 { 317 protected: 318 string text; 319 int scroll; 320 Config cfg; 321 322 public: 323 /* Thrown when the user finishes typing. */ 324 class Signal: UISignal 325 { 326 string text; 327 328 this(string text) 329 { 330 super(this.outer); 331 this.text = text; 332 } 333 } 334 335 struct Config 336 { 337 WChar[] start = [W('\n'), W('\r'), W('i'), W(Key.enter)]; 338 string emptyText = "<empty>"; 339 } 340 341 this(UI ui, int nlines, int ncols, int y, int x, string initialText, 342 Config cfg = Config()) 343 { 344 super(ui, nlines, ncols, y, x); 345 text = initialText; 346 this.cfg = cfg; 347 } 348 349 /* ---------- inherited stuff ---------- */ 350 351 override bool keystroke(WChar key) 352 { 353 import std.algorithm; 354 if (!cfg.start.canFind(key)) return false; 355 356 window.erase; 357 window.move(0, 0); 358 text = window.getstr; 359 throw new Signal(text); 360 } 361 362 override void draw(bool active) 363 { 364 window.erase; 365 auto attr = active ? Attr.reverse : Attr.normal; 366 if (text != "") 367 window.addstr(0, 0, text, attr); 368 else 369 window.addstr(0, 0, cfg.emptyText, attr); 370 } 371 } 372 373 class CheckBox: UIElement 374 { 375 protected: 376 string delegate() text; 377 Config cfg; 378 Window textWindow; 379 Window markWindow; 380 381 public: 382 bool checked; 383 384 /* Thrown when the user checks/unchecks the box. */ 385 class Signal: UISignal 386 { 387 bool checked; 388 this() 389 { 390 super(this.outer); 391 checked = this.outer.checked; 392 } 393 } 394 395 struct Config 396 { 397 wint_t whenChecked = '+'; 398 wint_t whenUnchecked = '-'; 399 WChar[] switchKeys = [W('\n'), W('\r'), W(Key.enter)]; 400 /* Denotes the position of checked/unchecked mark. Note that the 401 element should be at least 4 cells wide for left and right 402 alignments and at least 2 cells high for central alignment. */ 403 Align alignment = Align.left; 404 } 405 406 this(UI ui, int nlines, int ncols, int y, int x, 407 string delegate() text, Config cfg = Config()) 408 { 409 super(ui, nlines, ncols, y, x); 410 this.text = text; 411 this.cfg = cfg; 412 enum width = 3; 413 final switch (cfg.alignment) { 414 case Align.left: 415 markWindow = window.derwin(nlines, width, 0, 0); 416 textWindow = window.derwin(nlines, ncols - width, 0, width); 417 break; 418 case Align.center: 419 markWindow = window.derwin(1, ncols, nlines - 1, 0); 420 textWindow = window.derwin(nlines - 1, ncols, 0, 0); 421 break; 422 case Align.right: 423 markWindow = window.derwin(nlines, width, 0, ncols - width); 424 textWindow = window.derwin(nlines, ncols - width, 0, 0); 425 break; 426 } 427 } 428 429 this(UI ui, int nlines, int ncols, int y, int x, 430 string text, Config cfg = Config()) 431 { 432 string dg() { return text; } 433 this(ui, nlines, ncols, y, x, &dg, cfg); 434 } 435 436 /* ---------- inherited stuff ---------- */ 437 438 override bool keystroke(W key) 439 { 440 import std.algorithm; 441 if (cfg.switchKeys.canFind(key)) { 442 checked = !checked; 443 throw new Signal(); 444 } 445 return false; 446 } 447 448 override void draw(bool active) 449 { 450 auto attr = active ? Attr.reverse : Attr.normal; 451 window.erase; 452 wint_t mark = checked ? cfg.whenChecked : cfg.whenUnchecked; 453 final switch (cfg.alignment) { 454 case Align.left: 455 markWindow.addch(markWindow.height / 2, 1, mark); 456 textWindow.addAligned(textWindow.height / 2, text(), Align.left, attr); 457 break; 458 case Align.center: 459 markWindow.addch(0, markWindow.width / 2, mark); 460 textWindow.addAligned(0, text(), Align.center, attr); 461 break; 462 case Align.right: 463 markWindow.addch(markWindow.height / 2, 1, mark); 464 textWindow.addAligned(textWindow.height / 2, text(), Align.right, attr); 465 break; 466 } 467 } 468 } 469 470 class NumberBox: UIElement 471 { 472 protected: 473 int value_; 474 Config cfg; 475 476 public: 477 int value() const @property { return value_; } 478 int value(int k) @property 479 { 480 int old = value_; 481 if (k > cfg.max) k = cfg.max; 482 if (k < cfg.min) k = cfg.min; 483 value_ = k; 484 return old; 485 } 486 487 /* Thrown when the user changes value - either through typing or 488 pressing an increment/decrement button. */ 489 class Signal: UISignal 490 { 491 int value; 492 int delta; 493 int old; 494 495 this(int old) 496 { 497 super(this.outer); 498 this.old = old; 499 this.value = this.outer.value; 500 delta = value - old; 501 } 502 } 503 504 struct Config 505 { 506 WChar[] start = [W('\n'), W('\r'), W('i'), W(Key.enter)]; 507 WChar[] smallIncr = [W('k'), W('l'), W(Key.up), W(Key.right)]; 508 WChar[] bigIncr = [W('K'), W('L'), W(Key.sright)]; 509 WChar[] smallDecr = [W('j'), W('h'), W(Key.down), W(Key.left)]; 510 WChar[] bigDecr = [W('J'), W('H'), W(Key.sleft)]; 511 int min = int.min; 512 int max = int.max; 513 int smallStep = 1; 514 int bigStep = 5; 515 Align alignment = Align.center; 516 } 517 518 this(UI ui, int nlines, int ncols, int y, int x, int startingValue, 519 Config cfg = Config()) 520 { 521 super(ui, nlines, ncols, y, x); 522 value = startingValue; 523 this.cfg = cfg; 524 } 525 526 /* ---------- inherited stuff ---------- */ 527 528 override bool keystroke(WChar key) 529 { 530 import std.algorithm; 531 532 int old = value; 533 if (cfg.start.canFind(key)) { 534 /* Read a number from the keyboard. */ 535 import std.conv; 536 window.erase; 537 window.move(0, 0); 538 string str = window.getstr(ch => '0' <= ch && ch <= '9'); 539 try { 540 value = str.to!int; 541 } catch (ConvException e) { 542 /* Can happen on an empty string. */ 543 value = cfg.min; 544 } 545 throw new Signal(old); 546 } else if (cfg.smallIncr.canFind(key)) { 547 /* Small step increment. */ 548 throw new Signal(value = value + cfg.smallStep); 549 } else if (cfg.bigIncr.canFind(key)) { 550 /* Big step increment .*/ 551 throw new Signal(value = value + cfg.bigStep); 552 } else if (cfg.smallDecr.canFind(key)) { 553 /* Small step decrement. */ 554 throw new Signal(value = value - cfg.smallStep); 555 } else if (cfg.bigDecr.canFind(key)) { 556 /* Big step decrement. */ 557 throw new Signal(value = value - cfg.bigStep); 558 } 559 return false; 560 } /* keystroke */ 561 562 override void draw(bool active) 563 { 564 import std.conv; 565 window.erase; 566 auto attr = active ? Attr.reverse : Attr.normal; 567 window.addAligned(window.height / 2, value.to!string, cfg.alignment, 568 attr); 569 } 570 }