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