1 module sily.tui.node; 2 3 public import sily.color: col; 4 public import sily.vector: ivec2; 5 6 import std.algorithm.comparison: min, max; 7 import std.algorithm.searching: canFind, countUntil; 8 import std.algorithm.mutation: remove; 9 import std.algorithm.sorting: sort; 10 import std.conv: to; 11 import std.traits: isSomeString; 12 13 import sily.terminal: terminalWidth, terminalHeight; 14 import sily.string: splitStringWidth; 15 16 import sily.tui.render; 17 18 // alias Node = Element*; 19 20 private Node _root; 21 22 static this() { 23 _root = Node(new Element( 24 Node(), 25 [], 26 "root", 27 [], 28 Size.full, 29 ivec2(0), 30 col(0, 0, 0, 0), 31 col(0, 0, 0, 0), 32 ""d, 33 TextHAlign.center, 34 TextVAlign.middle, 35 false, 36 Border.none, 37 false, 38 col(0, 0, 0, 0), 39 0, 40 true 41 )); 42 } 43 44 Node root() { 45 return _root; 46 } 47 48 private const string _upperBlock = "\u2580"; 49 private const string _lowerBlock = "\u2584"; 50 51 /* 52 To class or not to class 53 If to make element a class then I'd be able to easily define everything 54 by inheriting main class 55 But structs are kind of more memory efficient 56 57 Needed elements (bare minimum): 58 - Panel (box) 59 - Label (text/paragraph/header) 60 - Button (input?) 61 - Canvas (hard rendering) 62 63 Logically they all can be a single element type which you can 64 Node n = get!("#myelement").add(); 65 n.addEventListener(Evt.mousePress, function() {}); 66 n.drawCurve(vec2()...) 67 68 Struct it is 69 */ 70 71 private struct Element { 72 Node _parent; 73 Node[] _children = []; 74 string _id = ""; 75 string[] _classes = []; 76 /// Style size, 0-inf - normal size, -1 - Fill, -2 - Auto 77 ivec2 _size = Size.content; 78 ivec2 _pos = ivec2(0, 0); 79 col _backgroundColor = col(0.2f); 80 col _textColor = col(1.0f); 81 dstring _text = ""; 82 TextHAlign _halign = TextHAlign.center; 83 TextVAlign _valign = TextVAlign.middle; 84 bool _isEmpty = false; 85 dchar[8] _borderChars = Border.normal; 86 bool _drawBorderBackground = true; 87 col _borderColor = col(0.8); 88 // TODO: text decorations 89 // TODO: padding? 90 long _priority = 0; 91 92 // Should be always true at start or forceRender at start 93 bool _renderNeeded = true; 94 95 96 /** 97 Creates "tree" representation in format: 98 --- 99 Name: [Child, Child2: [Child3, Child 4]] 100 --- 101 */ 102 string toTreeString() { 103 if (_children.length == 0) return (_id == "" ? "__NO_ID__" : _id); 104 string _out = (_id == "" ? "__NO_ID__" : _id) ~ ": ["; 105 for (int i = 0; i < _children.length; ++i) { 106 Element child = *(_children[i]); 107 _out ~= child.toTreeString(); 108 if (i + 1 != _children.length) _out ~= ", "; 109 } 110 _out ~= ']'; 111 return _out; 112 } 113 114 void render() { 115 if (_id == "root") { 116 screenClearOnly(); 117 } 118 119 ivec2 pos = getPosition(); 120 // do render of itself 121 ivec2 size = getSize(); 122 bool isTopEdge = pos.y % 2 == 1; 123 bool isUnevenSize = size.y % 2 == 1; 124 bool isBottomEdge = isTopEdge != isUnevenSize; // wierd XOR 125 col pcolor = getParentColor(); 126 int height = size.y / 2; 127 if (isTopEdge || isBottomEdge) height += 1; 128 129 // TODO: calculate other children color overlap 130 // Kinda fixed by border but still a thing 131 // (A is not child of B but sill overlays with wrong color) 132 133 // draw background 134 if (_backgroundColor.a > 0.1) { 135 for (int y = 0; y < height; ++y) { 136 cursorMoveTo(pos.x, pos.y / 2 + y); 137 bool isEdge = (isTopEdge && y == 0) || (isBottomEdge && y + 1 == height); 138 if (isEdge) { 139 if (isEdge && pcolor.a > 0.1) write(pcolor.escape(true)); 140 write(_backgroundColor.escape(false)); 141 } else { 142 write(_backgroundColor.escape(true)); 143 } 144 for (int x = 0; x < size.x; ++x) { 145 if (isTopEdge && y == 0) { 146 write(_lowerBlock); 147 } else 148 if (isBottomEdge && y + 1 == height) { 149 write(_upperBlock); 150 } else { 151 write(" "); 152 } 153 } 154 write("\033[m"); 155 } 156 } 157 158 bool borderExists = false; 159 foreach (c; _borderChars) { 160 if (c != ' ') { 161 borderExists = true; 162 break; 163 } 164 } 165 166 if (_textColor.a > 0.1 && _text.length > 0) { 167 dstring[] t = splitStringWidth(_text, size.x - (borderExists ? 2 : 0)); 168 int yred = borderExists ? 2 : 0; 169 if (isTopEdge) yred += 1; 170 if (isBottomEdge) yred += 1; 171 int borderRed = (borderExists ? 1 : 0); 172 int maxY = max(min(t.length, height - yred), 0); 173 for (int y = isTopEdge ? 1 : 0; y < maxY + isTopEdge ? 1 : 0; ++y) { 174 ivec2 tp = ivec2(pos.x, pos.y / 2 + y); 175 dstring line = t[y - (isTopEdge ? 1 : 0)]; 176 int len = cast(int) line.length; 177 int lct = cast(int) t.length; 178 if (_halign == TextHAlign.center) { 179 // offset it by (width - line width) / 2 180 tp.x = tp.x + (size.x - len) / 2 - borderRed; 181 } else 182 if (_halign == TextHAlign.right){ // right 183 // offset it by widht - line width 184 tp.x = tp.x + size.x - len - borderRed; 185 } 186 if (_valign == TextVAlign.middle) { 187 tp.y = tp.y + (height - lct) / 2 - borderRed; 188 } else 189 if (_valign == TextVAlign.bottom) { 190 tp.y = tp.y + height - lct - (isTopEdge ? 1 : 0) - borderRed; 191 } 192 193 cursorMoveTo(tp.x, tp.y); 194 if (_backgroundColor.a > 0.1) { 195 write(_backgroundColor.escape(true)); 196 write(_textColor.escape(false)); 197 write(line); 198 199 } else { 200 write(_textColor.escape(false)); 201 for (int k = 0; k < line.length; ++k) { 202 dchar ch = line[k]; 203 col c = getBackColorAt(tp + ivec2(k, 0)); 204 write(c.escape(true)); 205 write(ch); 206 } 207 } 208 write("\033[m"); 209 } 210 } 211 212 if (borderExists && _borderColor.a > 0.1) { 213 // top 214 for (int x = 0; x < size.x; ++x) { 215 cursorMoveTo(pos.x + x, pos.y / 2); 216 if (_drawBorderBackground && _backgroundColor.a > 0.1) { 217 write(_backgroundColor.escape(true)); 218 } else { 219 col c = getBackColorAt(pos + ivec2(x, 0)); 220 write(c.escape(true)); 221 } 222 write(_borderColor.escape(false)); 223 if (x == 0) { 224 write(_borderChars[0]); 225 } else 226 if (x + 1 == size.x) { 227 write(_borderChars[2]); 228 } else { 229 write(_borderChars[1]); 230 } 231 } 232 // side left 233 for (int y = 1; y + 1 < height; ++y) { 234 cursorMoveTo(pos.x, pos.y / 2 + y); 235 if (_drawBorderBackground && _backgroundColor.a > 0.1) { 236 write(_backgroundColor.escape(true)); 237 } else { 238 col c = getBackColorAt(pos + ivec2(0, y)); 239 write(c.escape(true)); 240 } 241 242 write(_borderColor.escape(false)); 243 write(_borderChars[3]); 244 } 245 // side right 246 for (int y = 1; y + 1 < height; ++y) { 247 cursorMoveTo(pos.x + size.x - 1, pos.y / 2 + y); 248 if (_drawBorderBackground && _backgroundColor.a > 0.1) { 249 write(_backgroundColor.escape(true)); 250 } else { 251 col c = getBackColorAt(pos + ivec2(size.x - 1, y)); 252 write(c.escape(true)); 253 } 254 255 write(_borderColor.escape(false)); 256 write(_borderChars[4]); 257 } 258 // bottom 259 for (int x = 0; x < size.x; ++x) { 260 cursorMoveTo(pos.x + x, pos.y / 2 + height - 1); 261 if (_drawBorderBackground && _backgroundColor.a > 0.1) { 262 write(_backgroundColor.escape(true)); 263 } else { 264 col c = getBackColorAt(pos + ivec2(x, size.y - 1)); 265 write(c.escape(true)); 266 } 267 268 write(_borderColor.escape(false)); 269 if (x == 0) { 270 write(_borderChars[5]); 271 } else 272 if (x + 1 == size.x) { 273 write(_borderChars[7]); 274 } else { 275 write(_borderChars[6]); 276 } 277 } 278 279 } 280 281 foreach (Node child; _children) { 282 (*child).render(); 283 } 284 285 // write("\033[mA"); 286 287 _renderNeeded = false; 288 } 289 290 void forceRender() { 291 render(); 292 } 293 294 void requestRender() { 295 if (_renderNeeded) { 296 render(); 297 } else { 298 foreach (Node child; _children) { 299 (*child).requestRender(); 300 } 301 } 302 } 303 304 void addChild(Node child) { 305 _children ~= child; 306 (*child)._parent = &this; 307 } 308 309 ivec2 getSize() { 310 int tw = terminalWidth(); 311 int th = terminalHeight() * 2; 312 if (_parent.isNull) return ivec2(tw, th); 313 ivec2 s = _size; 314 // -1 - full fill 315 s.x = s.x == -1 ? tw : s.x; 316 s.y = s.y == -1 ? th : s.y; 317 // -2 - fill from content 318 s.x = s.x == -2 ? tw : s.x; 319 s.y = s.y == -2 ? th : s.y; 320 321 ivec2 _parentSize = (*_parent).getSize(); 322 ivec2 _maxSize = _parentSize - _pos; 323 s = s.min(_maxSize); 324 325 // TODO: fix auto 326 // TODO: limit to parent size 327 return s; 328 } 329 330 ivec2 getPosition() { 331 if (_parent.isNull) return _pos; 332 ivec2 ppos = (*_parent).getPosition(); 333 return _pos + ppos; 334 } 335 336 col getParentColor() { 337 if (_parent.isNull) return col(0.0f, 0.0f); 338 return (*_parent)._backgroundColor; 339 } 340 341 col getBackColorAt(ivec2 pos) { 342 if (_parent.isNull) return col(0.0f, 0.0f); 343 Node[] el = (*_parent)._children; 344 345 col _col = _parent.bgcol; 346 long _pri = long.min; 347 348 foreach (c; el) { 349 if (c.ptr == &this) { continue; } 350 if (isColliding(pos / ivec2(1, 2), c.getPosition / ivec2(1, 2), c.size / ivec2(1, 2))) { 351 if (c.priority > _pri && c.bgcol.a > 0.1) { 352 _pri = c.priority; 353 _col = c.bgcol; 354 } 355 // _col = col(1, 0, 0); 356 } 357 } 358 359 return _col; 360 } 361 362 void sortChildren() { 363 _children.sort!((a, b) => (*a)._priority < (*b)._priority); 364 } 365 } 366 367 public bool isColliding(ivec2 pos, ivec2 tl, ivec2 wh) { 368 return pos.x >= tl.x && 369 pos.y >= tl.y && 370 pos.x <= tl.x + wh.x && 371 pos.y <= tl.y + wh.y; 372 } 373 374 void forceRender() { 375 (*root).forceRender(); 376 } 377 378 void requestRender() { 379 (*root).requestRender(); 380 } 381 382 /* 383 Style sizes 384 none - hidden 385 content - auto size 386 full - fill parent 387 wide - fill parent width, auto height 388 tall - fill parent height, auto width 389 */ 390 enum Size: ivec2 { 391 none = ivec2(0, 0), 392 content = ivec2(-2, -2), 393 full = ivec2(-1, -1), 394 wide = ivec2(-1, -2), 395 tall = ivec2(-2, -1) 396 } 397 398 enum TextVAlign { 399 middle, 400 top, 401 bottom 402 } 403 404 enum TextHAlign { 405 center, 406 left, 407 right 408 } 409 410 enum Border: dchar[8] { 411 none = " ", 412 normal = "┌─┐││└─┘", 413 heavy = "┏━┓┃┃┗━┛", 414 solid = "████████", 415 thick = "▛▀▜▌▐▙▄▟", 416 thickrev = "▗▄▖▐▌▝▀▘", 417 dotted = "⡏⠉⢹⡇⢸⣇⣀⣸", 418 doubled = "╔═╗║║╚═╝", 419 dbVertical = "╒═╕║║╘═╛", 420 dbHorizontal = "╓─╖║║╙─╜" 421 } 422 423 Node createNode() { 424 return Node(new Element()); 425 } 426 427 Node createNullNode() { 428 return Node(null); 429 } 430 431 struct Node { 432 Element* ptr = null; 433 alias ptr this; 434 435 private this(Element* p) { 436 this.ptr = p; 437 } 438 439 bool isNull() { 440 return ptr == null; 441 } 442 443 Node append(Node child) { 444 (*ptr).addChild(child); 445 (*ptr).sortChildren(); 446 return this; 447 } 448 449 Node bgcol(col bg) { 450 (*ptr)._backgroundColor = bg; 451 setRenderNeeded(); 452 return this; 453 } 454 455 col bgcol() { 456 return (*ptr)._backgroundColor; 457 } 458 459 Node fgcol(col fg) { 460 (*ptr)._textColor = fg; 461 setRenderNeeded(); 462 return this; 463 } 464 465 col fgcol() { 466 return (*ptr)._textColor; 467 } 468 469 Node size(ivec2 size) { 470 (*ptr)._size = size; 471 setRenderNeeded(); 472 return this; 473 } 474 475 ivec2 size() { 476 return (*ptr)._size; 477 } 478 479 Node pos(ivec2 _pos) { 480 (*ptr)._pos = _pos; 481 setRenderNeeded(); 482 return this; 483 } 484 485 ivec2 pos() { 486 return (*ptr)._pos; 487 } 488 489 Node text(T)(T text) if (isSomeString!T) { 490 static if (is(typeof(T) == dstring)) { 491 (*ptr)._text = text; 492 } else { 493 (*ptr)._text = text.to!dstring; 494 } 495 setRenderNeeded(); 496 return this; 497 } 498 499 dstring text() { 500 return (*ptr)._text; 501 } 502 503 Node halign(TextHAlign al) { 504 (*ptr)._halign = al; 505 setRenderNeeded(); 506 return this; 507 } 508 509 TextHAlign halign() { 510 return (*ptr)._halign; 511 } 512 513 Node valign(TextVAlign al) { 514 (*ptr)._valign = al; 515 setRenderNeeded(); 516 return this; 517 } 518 519 TextVAlign valign() { 520 return (*ptr)._valign; 521 } 522 523 /** 524 Set/get border characters in format [LeftUp, Up, RightUp, Left, Right, LeftDown, Down, RightDown]. 525 */ 526 Node border(dchar[8] borderChars, bool drawBorderBackground = true) { 527 (*ptr)._borderChars = borderChars; 528 (*ptr)._drawBorderBackground = drawBorderBackground; 529 setRenderNeeded(); 530 return this; 531 } 532 533 dchar[8] border() { 534 return (*ptr)._borderChars; 535 } 536 537 Node borderColor(col c) { 538 (*ptr)._borderColor = c; 539 setRenderNeeded(); 540 return this; 541 } 542 543 col borderColor() { 544 return (*ptr)._borderColor; 545 } 546 547 Node addClass(string _class) { 548 (*ptr)._classes ~= _class; 549 return this; 550 } 551 552 Node removeClass(string _class) { 553 if (!hasClass(_class)) return this; 554 size_t _pos = (*ptr)._classes.countUntil(_class); 555 (*ptr)._classes = (*ptr)._classes.remove(_pos); 556 return this; 557 } 558 559 bool hasClass(string _class) { 560 return (*ptr)._classes.canFind(_class); 561 } 562 563 string[] classes() { 564 return (*ptr)._classes; 565 } 566 567 Node id(string _id) { 568 assert(_id != "root", "ID \"root\" is not permitted since it's used for app root."); 569 (*ptr)._id = _id; 570 return this; 571 } 572 573 string id() { 574 return (*ptr)._id; 575 } 576 577 Node priority(long pr) { 578 (*ptr)._priority = pr; 579 (*ptr).sortChildren(); 580 setRenderNeeded(); 581 return this; 582 } 583 584 long priority() { 585 return (*ptr)._priority; 586 } 587 588 private void setRenderNeeded() { 589 (*ptr)._renderNeeded = true; 590 if ((*ptr)._parent == null) return; 591 (*((*ptr)._parent))._renderNeeded = true; 592 } 593 594 string toTreeString() { 595 return (*ptr).toTreeString; 596 } 597 } 598 599 // TODO: advanced query 600 // LINK: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector 601 602 /** 603 Returns node based on selector, similar to CSS selectors. 604 Example: 605 Node bigPanel = query!"#nodeid"; 606 Node[] labels = query!".label"; 607 Node[] allNodes = query!"*"; 608 */ 609 Node query(string selector)(Node from = root) if (selector.length > 0 && selector[0] == '#') { 610 if ((*from.ptr)._id == selector[1..$]) return from; 611 Node[] children = (*from.ptr)._children; 612 foreach (Node child; children) { 613 Node n = child.query!selector; 614 if (!n.isNull) { 615 return n; 616 } 617 } 618 return createNullNode(); 619 } 620 /// Ditto 621 Node[] query(string selector)(Node from = root) if (selector.length > 0 && selector[0] != '#') { 622 Node[] ret; 623 if (selector[0] == '.' && (*from.ptr)._classes.canFind(selector[1..$])) ret ~= from; 624 if (selector[0] == '*') ret ~= from; 625 Node[] children = (*from.ptr)._children; 626 foreach (Node child; children) { 627 Node[] n = child.query!selector; 628 if (n.length > 0) { 629 ret ~= n; 630 } 631 } 632 return ret; 633 } 634