1 module tui.components; 2 3 import colored : forceStyle, Style; 4 import std.algorithm : max, min; 5 import std.exception : enforce; 6 import std.format : format; 7 import std.math.algebraic : abs; 8 import std.range : array, empty, front, split; 9 import std.signals; 10 import tui : clipTo, Component, Context, EventType, Key, KeyInput, Position, Viewport; 11 12 alias ButtonHandler = void delegate(); 13 14 class HSplit : Component 15 { 16 int split; 17 this(int split, Component top, Component bottom) 18 { 19 super([top, bottom]); 20 this.split = split; 21 } 22 23 override void resize(int left, int top, int width, int height) 24 { 25 super.resize(left, top, width, height); 26 27 int splitPos = split; 28 if (split < 0) 29 { 30 splitPos = height + split; 31 } 32 this.top.resize(0, 0, width, splitPos); 33 this.bottom.resize(0, splitPos, width, height - splitPos); 34 } 35 36 override void render(Context context) 37 { 38 this.top.render(context.forChild(this.top)); 39 this.bottom.render(context.forChild(this.bottom)); 40 } 41 42 private Component top() 43 { 44 return children[0]; 45 } 46 47 private Component bottom() 48 { 49 return children[1]; 50 } 51 } 52 53 class VSplit : Component 54 { 55 int split; 56 this(int split, Component left, Component right) 57 { 58 super([left, right]); 59 this.split = split; 60 } 61 62 override void resize(int left, int top, int width, int height) 63 { 64 super.resize(left, top, width, height); 65 66 int splitPos = split; 67 if (split < 0) 68 { 69 splitPos = width + split; 70 } 71 this.left.resize(0, 0, splitPos, height); 72 this.right.resize(splitPos, 0, width - split, height); 73 } 74 75 override void render(Context context) 76 { 77 left.render(context.forChild(left)); 78 right.render(context.forChild(right)); 79 } 80 81 private Component left() 82 { 83 return children[0]; 84 } 85 86 private Component right() 87 { 88 return children[1]; 89 } 90 } 91 92 class Filled : Component 93 { 94 string what; 95 this(string what) 96 { 97 this.what = what; 98 } 99 100 override void render(Context context) 101 { 102 for (int y = 0; y < height; y++) 103 { 104 for (int x = 0; x < width; x++) 105 { 106 context.putString(x, y, what); 107 } 108 } 109 context.putString(0, 0, "0"); 110 context.putString(width - 1, height - 1, "1"); 111 } 112 113 override bool handlesInput() 114 { 115 return false; 116 } 117 } 118 119 class Border : Component 120 { 121 Component child; 122 string title; 123 this(string title, Component child) 124 { 125 this.title = title; 126 this.child = child; 127 } 128 129 override void render(Context context) 130 { 131 enum TL = "╭"; 132 enum BR = "╯"; 133 enum TR = "╮"; 134 enum BL = "╰"; 135 enum H = "─"; 136 enum V = "│"; 137 context.line(Position(0, 0), Position(width, 0), H); 138 context.line(Position(0, height - 1), Position(width, height - 1), H); 139 context.line(Position(0, 0), Position(0, height - 1), V); 140 context.line(Position(width - 1, 0), Position(width - 1, height - 1), V); 141 context.putString(0, 0, TL); 142 context.putString(width - 1, 0, TR); 143 context.putString(0, height - 1, BL); 144 context.putString(width - 1, height - 1, BR); 145 context.putString(3, 0, " " ~ title ~ " "); 146 child.render(context.forChild(child)); 147 } 148 149 override void resize(int left, int top, int width, int height) 150 { 151 super.resize(left, top, width, height); 152 this.child.resize(1, 1, width - 2, height - 2); 153 } 154 } 155 156 class Text : Component 157 { 158 string content; 159 this(string content) 160 { 161 this.content = content; 162 } 163 164 override void render(Context context) 165 { 166 context.putString(0, 0, content); 167 } 168 169 override bool handlesInput() 170 { 171 return false; 172 } 173 } 174 175 class MultilineText : Component 176 { 177 string[] lines; 178 this(string content) 179 { 180 lines = content.split("\n"); 181 } 182 183 override void render(Context context) 184 { 185 foreach (idx, line; lines) 186 { 187 context.putString(0, cast(int) idx, line); 188 } 189 } 190 191 override bool handlesInput() 192 { 193 return false; 194 } 195 } 196 197 class Canvas : Component 198 { 199 class Graphics 200 { 201 import std.uni : unicode; 202 203 static braille = unicode.Braille.byCodepoint.array; 204 int[] pixels; 205 this() 206 { 207 pixels = new int[width * height]; 208 } 209 210 int getWidth() 211 { 212 return width * 2; 213 } 214 215 int getHeight() 216 { 217 return height * 4; 218 } 219 // see https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#All_cases 220 void line(const(Position) from, const(Position) to) 221 { 222 const int dx = (to.x - from.x).abs; 223 const int stepX = from.x < to.x ? 1 : -1; 224 225 const int dy = -(to.y - from.y).abs; 226 const int stepY = from.y < to.y ? 1 : -1; 227 228 int error = dx + dy; 229 int x = from.x; 230 int y = from.y; 231 while (true) 232 { 233 set(Position(x, y)); 234 if (x == to.x && y == to.y) 235 { 236 break; 237 } 238 const e2 = 2 * error; 239 if (e2 >= dy) 240 { 241 if (x == to.x) 242 { 243 break; 244 } 245 error += dy; 246 x += stepX; 247 } 248 if (e2 <= dx) 249 { 250 if (y == to.y) 251 { 252 break; 253 } 254 error += dx; 255 y += stepY; 256 } 257 } 258 } 259 // x and y in braille coords 260 void set(const(Position) p) 261 { 262 enforce(p.x < getWidth(), "x: %s needs to be smaller than %s".format(p.x, getWidth)); 263 enforce(p.y < getHeight(), "y: %s needs to be smaller than %s".format(p.y, getHeight)); 264 enforce(p.x >= 0, "x: %s needs to be >= 0".format(p.x)); 265 enforce(p.y >= 0, "y: %s needs to be >= 0".format(p.y)); 266 // bit nr 267 // 0 3 268 // 1 4 269 // 2 5 270 // 6 7 271 int xIdx = p.x / 2; 272 int yIdx = p.y / 4; 273 274 int brailleX = p.x % 2; 275 int brailleY = p.y % 4; 276 static brailleBits = [0, 1, 2, 6, 3, 4, 5, 7]; 277 int idx = xIdx + yIdx * width; 278 pixels[idx] |= 1 << brailleBits[brailleY + brailleX * 4]; 279 } 280 281 void render(Context context) 282 { 283 for (int j = 0; j < height; ++j) 284 { 285 for (int i = 0; i < width; ++i) 286 { 287 const idx = i + j * width; 288 const p = pixels[idx]; 289 if (p != 0) 290 { 291 context.putString(i, j, "%s".format(braille[p])); 292 } 293 } 294 } 295 } 296 } 297 298 alias Painter = void delegate(Canvas.Graphics, Context); 299 Painter painter; 300 this(Painter painter) 301 { 302 this.painter = painter; 303 } 304 305 override void render(Context context) 306 { 307 scope g = new Graphics(); 308 painter(g, context); 309 g.render(context); 310 } 311 312 override bool handlesInput() 313 { 314 return false; 315 } 316 } 317 318 class Button : Component 319 { 320 string text; 321 ButtonHandler pressed; 322 323 this(string text, ButtonHandler pressed) 324 { 325 this.text = text; 326 this.pressed = pressed; 327 } 328 329 override void render(Context c) 330 { 331 if (currentFocusedComponent == this) 332 { 333 c.putString(0, 0, "> " ~ text); 334 } 335 else 336 { 337 c.putString(0, 0, " " ~ text); 338 } 339 } 340 341 override bool handleInput(KeyInput input) 342 { 343 if (input.eventType == EventType.press && input.key == Key.normal && input.c == ' ') 344 { 345 pressed(); 346 return true; 347 } 348 return false; 349 } 350 351 override bool focusable() 352 { 353 return true; 354 } 355 356 override string toString() 357 { 358 return "Button"; 359 } 360 } 361 362 class List(T, alias stringTransform) : Component 363 { 364 T[] model; 365 T[]delegate() getData; 366 367 ScrollInfo scrollInfo; 368 mixin Signal!(T) selectionChanged; 369 bool vMirror; 370 371 struct ScrollInfo 372 { 373 int selection; 374 int offset; 375 void up() 376 { 377 if (selection > 0) 378 { 379 selection--; 380 while (selection < offset) 381 { 382 offset--; 383 } 384 } 385 } 386 387 void down(T[] model, int height) 388 { 389 if (selection + 1 < model.length) 390 { 391 selection++; 392 while (selection >= offset + height) 393 { 394 offset++; 395 } 396 } 397 } 398 } 399 400 this(T[] model, bool vMirror = false) 401 { 402 this.model = model; 403 this.scrollInfo = ScrollInfo(0, 0); 404 this.vMirror = vMirror; 405 } 406 407 this(T[]delegate() getData, bool vMirror = false) 408 { 409 this.getData = getData; 410 this.scrollInfo = ScrollInfo(0, 0); 411 this.vMirror = vMirror; 412 } 413 414 override void render(Context context) 415 { 416 if (getData) 417 { 418 model = getData(); 419 } 420 scrollInfo.offset = scrollInfo.offset.clipTo(model.length + -1); 421 if (model.length + -1 < context.height) 422 { 423 scrollInfo.offset = 0; 424 } 425 scrollInfo.selection = scrollInfo.selection.clipTo(model.length + -1); 426 for (int i = 0; i < height; ++i) 427 { 428 const index = i + scrollInfo.offset; 429 if (index >= model.length) 430 return; 431 const selected = (index == scrollInfo.selection) && (currentFocusedComponent == this); 432 static if (__traits(compiles, stringTransform(T.init, cast(size_t) 0))) 433 auto text = "%s %s".format(selected ? ">" : " ", 434 stringTransform(model[index], width - 2)); 435 else 436 auto text = "%s %s".format(selected ? ">" : " ", stringTransform(model[index])); 437 text = selected ? text.forceStyle(Style.reverse) : text; 438 context.putString(0, vMirror ? height - 1 - i : i, text); 439 } 440 } 441 442 void up() 443 { 444 vMirror ? _down : _up; 445 } 446 447 void down() 448 { 449 vMirror ? _up : _down; 450 } 451 452 void _up() 453 { 454 if (model.empty) 455 { 456 return; 457 } 458 scrollInfo.up; 459 selectionChanged.emit(model[scrollInfo.selection]); 460 } 461 462 void _down() 463 { 464 if (model.empty) 465 { 466 return; 467 } 468 scrollInfo.down(model, height); 469 selectionChanged.emit(model[scrollInfo.selection]); 470 } 471 472 void select() 473 { 474 if (model.empty) 475 { 476 return; 477 } 478 selectionChanged.emit(model[scrollInfo.selection]); 479 } 480 481 auto getSelection() 482 { 483 return model[scrollInfo.selection]; 484 } 485 486 override bool handleInput(KeyInput input) 487 { 488 if ((input.eventType == EventType.press) || (input.eventType == EventType.repeat)) 489 { 490 switch (input.key) 491 { 492 case Key.up: 493 up(); 494 return true; 495 case Key.down: 496 down(); 497 return true; 498 default: 499 return super.handleInput(input); 500 } 501 } 502 return super.handleInput(input); 503 } 504 505 override bool focusable() 506 { 507 return true; 508 } 509 510 override string toString() 511 { 512 return "List"; 513 } 514 } 515 516 class ScrollPane : Component 517 { 518 Viewport viewport; 519 this(Component child) 520 { 521 super([child]); 522 } 523 524 bool up() 525 { 526 viewport.y = max(viewport.y - 1, 0); 527 return true; 528 } 529 530 bool down() 531 { 532 viewport.y++; 533 return true; 534 } 535 536 bool left() 537 { 538 viewport.x = max(viewport.x - 1, 0); 539 return true; 540 } 541 542 bool right() 543 { 544 viewport.x++; 545 return true; 546 } 547 548 override bool handleInput(KeyInput input) 549 { 550 if ((input.eventType == EventType.press) || (input.eventType == EventType.repeat)) 551 { 552 switch (input.key) 553 { 554 case Key.up: 555 return up(); 556 case Key.down: 557 return down(); 558 case Key.left: 559 return left(); 560 case Key.right: 561 return right(); 562 case Key.normal: 563 { 564 switch (input.c) 565 { 566 case 'w', 'j': 567 return up(); 568 case 's', 'k': 569 return down(); 570 case 'a', 'h': 571 return left(); 572 case 'd', 'l': 573 return right(); 574 default: 575 break; 576 } 577 break; 578 } 579 default: 580 break; 581 } 582 } 583 return super.handleInput(input); 584 } 585 586 override bool focusable() 587 { 588 return true; 589 } 590 591 override void render(Context c) 592 { 593 auto child = children.front; 594 child.render(c.forChild(child, viewport)); 595 if (currentFocusedComponent == this) 596 { 597 c.putString(0, 0, ">"); 598 } 599 } 600 601 override void resize(int left, int top, int width, int height) 602 { 603 super.resize(left, top, width, height); 604 viewport.width = width; 605 viewport.height = height; 606 auto child = children.front; 607 child.resize(0, 0, 1000, 1000); 608 } 609 }