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 }