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 }