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 }