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 }