1 module sily.tui.app; 2 3 import std.array : popFront; 4 import std.conv : to; 5 import std.stdio : stdout; 6 import std.string : format; 7 8 import sily.bashfmt; 9 import sily.logger : fatal; 10 import sily.terminal; 11 import sily.time; 12 import sily.vector; 13 14 import sily.tui.elements; 15 import sily.tui.render; 16 17 /// Terminal UI application 18 class App { 19 private Element _rootElement = null; 20 21 private float _fpsTarget = 30.0f; 22 public float getFpsTarget() { 23 return _fpsTarget; 24 } 25 26 public void setFpsTarget(float t) { 27 _fpsTarget = t; 28 } 29 30 private bool _isRunning = false; 31 32 private float _frameTime; 33 private int _frames; 34 private int _fps; 35 36 private InputEvent[] _unprocessedInput = []; 37 38 /** 39 Public create method. Can be overriden. 40 Called when app is created, but before all elements created 41 */ 42 public void create() { 43 } 44 /** 45 Public destroy method. Can be overriden. 46 Called when app is destroyed, but after all elements destroyed 47 */ 48 public void destroy() { 49 } 50 /** 51 Public update method. Can be overriden. 52 Called each frame after all elements have been updated 53 */ 54 public void update(float delta) { 55 } 56 /** 57 Public update method. Can be overriden. 58 Called each frame if there's input available 59 after all elements have processed input 60 */ 61 public void input(InputEvent e) { 62 } 63 /** 64 Public render method. Can be overriden. 65 Called each frame after all elements have rendered 66 */ 67 public void render() { 68 } 69 70 /** 71 Starts application and goes into raw alt terminal mode 72 73 Application runs in this order: 74 --- 75 app.create(); 76 elements.create(); 77 while (isRunning) { 78 elements.input(); 79 app.input(); 80 81 elements.update(); 82 app.update(); 83 84 elements.render(); 85 app.render(); 86 } 87 elements.destroy(); 88 app.destroy(); 89 --- 90 All those methods are overridable and intended to be 91 used to create custom app logic 92 */ 93 public final void run() { 94 if (!stdout.isatty) { 95 fatal("STDOUT is not a tty"); 96 exit(ErrorCode.noperm); 97 return; 98 } 99 100 screenEnableAltBuffer(); 101 screenClearOnly(); 102 // must be false to allow stdout.flush 103 version (Have_speedy_stdio) 104 terminalModeSetRaw(true); 105 else 106 terminalModeSetRaw(false); 107 cursorMoveHome(); 108 cursorHide(); 109 110 if (_rootElement is null) { 111 Element el = new Element(); 112 el.setApp(this); 113 el.setRoot(); 114 _rootElement = el; 115 } 116 117 _isRunning = true; 118 119 create(); 120 _rootElement.propagateCreate(); 121 122 loop(); 123 124 cleanup(); 125 126 _rootElement.propagateDestroy(); 127 destroy(); 128 } 129 130 /// Requests application to be stopped 131 public final void stop() { 132 _isRunning = false; 133 } 134 135 private void loop() { 136 _frameTime = 1.0f / _fpsTarget; 137 _frames = 0; 138 _fps = 60; 139 140 double frameCounter = 0; 141 double lastTime = Time.currTime; 142 double unprocessedTime = 0; 143 144 while (_isRunning) { 145 bool doNeedRender = false; 146 double startTime = Time.currTime; 147 double passedTime = startTime - lastTime; 148 lastTime = startTime; 149 150 unprocessedTime += passedTime; 151 frameCounter += passedTime; 152 153 while (unprocessedTime > _frameTime) { 154 doNeedRender = true; 155 156 unprocessedTime -= _frameTime; 157 158 // Might be some closing logic 159 160 _input(); 161 162 // TODO: Input.update(); 163 foreach (key; _unprocessedInput) { 164 // For each input 165 _rootElement.propagateInput(key); 166 // Custom app update logic 167 if (!key.isProcessed) 168 input(key); 169 170 _unprocessedInput.popFront(); 171 } 172 173 _rootElement.propagateUpdate(_frameTime.to!float); 174 // Custom app update logic 175 update(_frameTime.to!float); 176 177 if (frameCounter >= 1.0) { 178 _fps = _frames; 179 _frames = 0; 180 frameCounter = 0; 181 } 182 } 183 184 if (doNeedRender) { 185 Render.screenClearOnly(); 186 Render.cursorMoveHome(); 187 _rootElement.propagateRender(); 188 // Custom app render logic 189 render(); 190 Render.flushBuffer(); 191 Render.clearBuffer(); 192 ++_frames; 193 sleep(1); 194 } else { 195 sleep(1); 196 } 197 198 scope (failure) { 199 cleanup(); 200 fatal("Fatal error have occured"); 201 } 202 } 203 } 204 205 private void _input() { 206 while (kbhit()) { 207 int key = getch(); 208 InputEvent e = InputEvent(InputEvent.Type.keyboard, key); 209 _unprocessedInput ~= e; 210 } 211 } 212 213 private void cleanup() { 214 terminalModeReset(); 215 screenDisableAltBuffer(); 216 cursorShow(); 217 } 218 219 /// Sets app title 220 public void setTitle(string title) { 221 222 223 224 .setTitle(title); 225 } 226 227 /// Returns app width/height 228 public uint width() { 229 return terminalWidth(); 230 } 231 /// Ditto 232 public uint height() { 233 return terminalHeight(); 234 } 235 /// Ditto 236 public uvec2 size() { 237 return uvec2(width, height); 238 } 239 240 /// Returns current FPS 241 public int fps() { 242 return _fps; 243 } 244 245 /// Returns current FPS as string 246 public string fpsString() { 247 return _fps.to!string; 248 } 249 250 /// Returns true if app is running 251 public bool isRunning() { 252 return _isRunning; 253 } 254 255 /// Returns aspect ratio (w / h) 256 public float aspectRatio() { 257 return width.to!float / height.to!float; 258 } 259 260 /// Returns root element 261 public Element rootElement() { 262 return _rootElement; 263 } 264 }