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