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 }