1 /* Base UI module
2 
3    Provides base classes used in UI making. 
4 
5    It publicly imports nice.curses, so if you use it there's no need to import
6    both.
7 
8    */
9 
10 module nice.ui.base;
11 
12 public import nice.curses;
13 
14 /* A collection of UI elements. */
15 class UI
16 {
17     protected:
18         Curses curses;
19         Window window;
20         UIElement[] elements;
21         int focus;
22         Config cfg;
23 
24     public:
25         struct Config
26         {
27             WChar[] nextElemKeys = [WChar('\t'), WChar('+'), WChar(Key.npage)];
28             WChar[] prevElemKeys = [WChar('\b'), WChar('-'), WChar(Key.ppage)];
29         }
30 
31         /* ---------- creation ---------- */
32 
33         this(Curses curses, Window window, Config cfg = Config())
34         {
35             this.curses = curses;
36             this.window = window;
37             this.cfg = cfg;
38         }
39 
40         /* Use stdscr. */
41         this(Curses curses, Config cfg = Config())
42         {
43             this.curses = curses;
44             this.window = curses.stdscr;
45             this.cfg = cfg;
46         }
47 
48         /* ---------- manipulation ---------- */
49 
50         /* Processes a keystroke. Returns true if keystroke was processed by 
51            the UI or an element. */
52         bool keystroke(WChar key)
53         {
54             import std.algorithm;
55 
56             bool res = false;
57             if (cfg.nextElemKeys.canFind(key)) {
58                 changeFocus(+1);
59                 res = true;
60             } else if (cfg.prevElemKeys.canFind(key)) {
61                 changeFocus(-1);
62                 res = true;
63             } else {
64                 if (elements != [])
65                     res = elements[focus].keystroke(key);
66             }
67             draw(true);
68             return res;
69         }
70 
71         /* Draws the UI. */
72         void draw(bool erase = true)
73         {
74             if (erase) window.erase;
75             foreach (i, elem; elements)
76                 if (elem.visible) drawElement(elem, i == focus);
77             window.refresh;
78             curses.update;
79         }
80 
81         void drawElement(UIElement elem, bool active)
82         {
83             elem.draw(active);
84             elem.window.overwrite(window);
85         }
86 
87         /* Moves the whole UI to the different window. */
88         void move(Window to)
89         {
90             window = to;
91         }
92 
93         void addElement(UIElement e)
94         {
95             elements ~= e;
96         }
97 
98         void removeElement(UIElement e)
99         {
100             import std.algorithm;
101             import std.array;
102 
103             elements = elements.filter!(el => el != e).array;
104         }
105 
106         /* Changes currently active element by shifting focus by a given amount. */
107         void changeFocus(int by)
108         {
109             if (elements == []) return;
110             int oldFocus = focus;
111             elements[focus].unfocus();
112             int direction = by > 0 ? +1 : -1;
113             int len = cast(int) elements.length;
114             /* This will still focus on an unfocusable element if there're 
115                only such elements in the UI. */
116             do {
117                 focus += direction;
118                 /* Casts are safe as long as number of elements is reasonable. */
119                 if (focus < 0) focus += len;
120                 if (focus >= elements.length) focus -= len;
121                 if (focus == oldFocus) break;
122             } while (!elements[focus].focusable || !elements[focus].visible);
123             elements[focus].focus();
124         }
125 
126         /* Change currently active element to a given element. */
127         void changeFocus(UIElement newFocused)
128         {
129             elements[focus].unfocus();
130             drawElement(elements[focus], false);
131             int i;
132             foreach (elem; elements) { /* Can't do 'i, elem' since 'i' would
133                                           then be size_t. */
134                 if (newFocused is elem) {
135                     focus = i;
136                     break;
137                 }
138                 i++;
139             }
140             newFocused.focus();
141             drawElement(newFocused, true);
142         }
143 }
144 
145 /* Base class for UI elements. */
146 abstract class UIElement
147 {
148     protected:
149         Window window;
150 
151         this(UI ui, int nlines, int ncols, int y, int x)
152         {
153             this.window = ui.curses.newWindow(nlines, ncols, y, x);
154             ui.addElement(this);
155         }
156 
157     public:
158         bool visible = true;
159         bool focusable = true;
160 
161         void draw(bool active);
162         void focus() { /* No-op by default. */ }
163         void unfocus() { /* Also no-op. */ }
164         /* Should return true if a keypress has been processed. */
165         bool keystroke(WChar key) { return false; }
166 }
167 
168 /* This is used to communicate UI events from elements to the processing loop. */
169 abstract class UISignal: Throwable
170 {
171     UIElement sender;
172 
173     this(UIElement sender)
174     {
175         super("Unhandled UI signal");
176         this.sender = sender;
177     }
178 }