1 module dgt.window;
2 import derelict.opengl;
3 import derelict.sdl2.sdl, derelict.sdl2.image, derelict.sdl2.mixer, derelict.sdl2.ttf;
4 import core.stdc.stdio, core.stdc.stdlib, core.stdc.time, core.thread;
5 
6 import std.ascii;
7 import std.typecons : Nullable;
8 
9 import dgt.array, dgt.camera, dgt.color, dgt.font, dgt.gamepad, dgt.geom, dgt.gl_backend, dgt.io, dgt.sound, dgt.music, dgt.particle, dgt.sprite, dgt.texture, dgt.tilemap, dgt.util;
10 
11 ///The flags used to control a window's initial behavior
12 struct WindowConfig
13 {
14     bool fullscreen, resizable, borderless, minimized, maximized, input_grabbed, vsync = true;
15 
16     @property package SDL_WindowFlags flags() const
17     {
18         return SDL_WINDOW_OPENGL |
19         (resizable ? SDL_WINDOW_RESIZABLE : cast(SDL_WindowFlags)0) |
20         (fullscreen ? SDL_WINDOW_FULLSCREEN : cast(SDL_WindowFlags)0) |
21         (borderless ? SDL_WINDOW_BORDERLESS : cast(SDL_WindowFlags)0) |
22         (minimized ? SDL_WINDOW_MINIMIZED : cast(SDL_WindowFlags)0) |
23         (maximized ? SDL_WINDOW_MAXIMIZED : cast(SDL_WindowFlags)0) |
24         (input_grabbed ? SDL_WINDOW_INPUT_GRABBED : cast(SDL_WindowFlags)0);
25     }
26 }
27 
28 private static immutable SDL_NUM_KEYS = 284;
29 
30 /**
31 The main window
32 
33 Handles drawing and input
34 */
35 struct Window
36 {
37     private:
38     SDL_Window *window;
39     GLBackend ctx;
40     bool shouldContinue = true;
41     bool[SDL_NUM_KEYS] current_keys; //The total number of SDL keys
42     bool[SDL_NUM_KEYS] previous_keys;
43     Vector mousePos = Vector(0, 0), previousMouse = Vector(0, 0);
44     bool mouseLeft = false, mouseRight = false, mouseMiddle = false,
45          mouseLeftPrevious = true, mouseRightPrevious = true, mouseMiddlePrevious = true;
46     //TODO: Add a function to wait on IO
47     Array!Particle particles;
48     int offsetX, offsetY, windowWidth, windowHeight;
49     Texture white;
50     Camera camera;
51     Array!Gamepad connectedGamepads;
52 
53     @disable this();
54     @disable this(this);
55 
56     public:
57     ///If the window is drawing in UI Mode, where drawing ignores the camera
58     bool inUIMode = false;
59     ///The number of target frames per second
60     uint fps = 60;
61     ///The target aspect ratio of the window
62     float aspectRatio;
63 
64     /**
65     Create a window
66 
67     Params:
68     title = The window's stitle
69     width = The window width in units
70     height = The window height in units
71     config = The flags that control the behavior of the window
72     scale = the number of 'units' per pixel
73     bindToGlobal = set the global window reference to this window
74     */
75     this(in string title, in int width, in int height, in WindowConfig config = WindowConfig(), in bool bindToGlobal = true)
76     {
77         version(Windows)
78         {
79             DerelictSDL2.load(SharedLibVersion(2, 0, 3));
80         }
81         //Initialize libraries
82         SDL_Init(SDL_INIT_VIDEO);
83         new Thread({
84             SDL_Init(SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER);
85             version(Windows)
86             {
87                 DerelictSDL2Image.load();
88                 DerelictSDL2ttf.load();
89                 DerelictSDL2Mixer.load();
90             }
91             IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG);
92             TTF_Init();
93             Mix_Init(MIX_INIT_FLAC | MIX_INIT_MOD | MIX_INIT_MP3 | MIX_INIT_OGG);
94         }).start();
95         windowWidth = width;
96         windowHeight = height;
97         window = SDL_CreateWindow(title.ptr,
98             SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 
99             width, height,
100             config.flags);
101         ctx = GLBackend(window, config.vsync);
102         particles = Array!Particle(128);
103         const region = Rectangle(0, 0, width, height);
104         setTransform(Camera(region, region));
105 
106         ubyte[3] white_pixel = [ 255, 255, 255 ];
107         white = Texture(white_pixel.ptr, 1, 1, PixelFormat.RGB);
108         glViewport(0, 0, width, height);
109         aspectRatio = cast(float)width / height;
110 
111         srand(cast(uint)time(null));
112 
113         if(bindToGlobal)
114             globalWindow = &this;
115 
116         thread_joinAll();
117         Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024);
118         Mix_AllocateChannels(512);
119         connectedGamepads = Array!Gamepad(16);
120         recalculateGamepads();
121     }
122 
123     void loop(Render, Update, A...)(Render render, Update update, A state)
124     {
125         new Thread({
126             while(isOpen)
127             {
128                 update(this, state);
129                 Thread.sleep(dur!"msecs"(1000 / fps));
130             }
131         }).start();
132         while(isOpen)
133             render(this, state);
134     }
135 
136     nothrow @nogc @trusted:
137 
138     private void recalculateGamepads()
139     {
140         foreach(gamepad; connectedGamepads)
141             gamepad.destroy();
142         connectedGamepads.clear();
143         int joystick_length = SDL_NumJoysticks();
144         for(int i = 0; i < joystick_length; i++)
145             if(SDL_IsGameController(i))
146                 connectedGamepads.add(Gamepad(SDL_GameControllerOpen(i)));
147     }
148 
149     ///Stop keeping the window alive
150     void close()
151     {
152         shouldContinue = false;
153     }
154 
155     ~this()
156     {
157         foreach(gamepad; gamepads)
158             gamepad.destroy();
159         SDL_DestroyWindow(window);
160         TTF_Quit();
161         Mix_Quit();
162         IMG_Quit();
163         SDL_Quit();
164     }
165 
166 
167     ///Set the camera transform
168     void setTransform(in Camera cam)
169     {
170         ctx.setTransform(cam.opengl);
171     }
172 
173     /**
174     Start a frame
175     Params:
176     bg = The color to clear with
177     */
178     void begin(in Color bg)
179     {
180         const region = Rectangle(0, 0, width, height);
181         begin(bg, Camera(region, region));
182     }
183 
184     /**
185     Start a frame
186     Params:
187     bg = The color to clear with
188     cam = the region to draw
189     */
190     void begin(in Color bg, in Camera cam)
191     {
192         inUIMode = false;
193         ctx.clear(bg);
194         previous_keys = current_keys;
195         SDL_Event e;
196         while (shouldContinue && SDL_PollEvent(&e))
197         {
198             switch (e.type)
199             {
200                 case SDL_QUIT:
201                     shouldContinue = false;
202                     break;
203                 case SDL_KEYDOWN:
204                     current_keys[e.key.keysym.scancode] = true;
205                     break;
206                 case SDL_KEYUP:
207                     current_keys[e.key.keysym.scancode] = false;
208                     break;
209                 case SDL_WINDOWEVENT:
210                     switch(e.window.event) {
211                         case SDL_WINDOWEVENT_RESIZED:
212                         case SDL_WINDOWEVENT_SIZE_CHANGED:
213                             int w, h;
214                             SDL_GL_GetDrawableSize(window, &w, &h);
215                             float windowRatio = cast(float)w / h;
216                             offsetX = offsetY = 0;
217                             if(windowRatio > aspectRatio)
218                             {
219                                 auto oldW = w;
220                                 w = cast(int)(aspectRatio * h);
221                             }
222                             else if(windowRatio < aspectRatio)
223                             {
224                                 auto oldH = h;
225                                 h = cast(int)(w / aspectRatio);
226                                 offsetY = (oldH - h) / 2;
227                             }
228                             glViewport(offsetX, offsetY, w, h);
229                             windowWidth = w;
230                             windowHeight = h;
231                             break;
232                         default:
233                             break;
234                     }
235                     break;
236                 case SDL_CONTROLLERDEVICEADDED:
237                 case SDL_CONTROLLERDEVICEREMOVED:
238                 case SDL_CONTROLLERDEVICEREMAPPED:
239                     recalculateGamepads();
240                     break;
241                 default:
242                     break;
243             }
244         }
245         int x, y;
246         int button_mask = SDL_GetMouseState(&x, &y);
247         previousMouse = mousePos;
248         mousePos = Vector(x, y);
249         mouseLeftPrevious = mouseLeft;
250         mouseRightPrevious = mouseRight;
251         mouseMiddlePrevious = mouseMiddlePrevious;
252         mouseLeft = (button_mask & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
253         mouseRight = (button_mask & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
254         mouseMiddle = (button_mask & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0;
255         setTransform(cam);
256     }
257 
258     private void filterParticles(T)(in Tilemap!T map)
259     {
260         for(size_t i = 0; i < particles.length; i++)
261         {
262             switch (particles[i].behavior)
263             {
264             case ParticleBehavior.Die:
265                 if (!map.empty(particles[i].position.x, particles[i].position.y))
266                     particles[i].lifetime = 0;
267                 break;
268             case ParticleBehavior.Bounce:
269                 if (!map.empty(particles[i].position.x + particles[i].velocity.x, particles[i].position.y))
270                     particles[i].velocity.x *= -1;
271                 if (!map.empty(particles[i].position.x, particles[i].position.y + particles[i].velocity.y))
272                     particles[i].velocity.y *= -1;
273                 break;
274             default: break;
275             }
276         }
277     }
278 
279     ///Update particles and display the drawn objects
280     void end()
281     {
282         for (size_t i = 0; i < particles.length; i++)
283         {
284             particles[i].update();
285             if (particles[i].lifetime <= 0)
286             {
287                 particles.remove(i);
288                 i--;
289             }
290             else
291                 draw(particles[i].region, particles[i].position.x, particles[i].position.y);
292         }
293         ctx.flip();
294     }
295 
296     ///Update particles, check particles against the tilemap, and display the drawn objects
297     void end(T)(in Tilemap!T map)
298     {
299         filterParticles(map);
300         end();
301     }
302 
303     ///Draw a polygon with each point following the next in a circle around the edge
304     void draw(size_t Len)(in Color color, in Vector[Len] points)
305     {
306         static immutable Indices = (Len - 2) * 3;
307         static assert ( Len >= 3 );
308         Vertex[Len] vertices;
309         GLuint[Indices] indices;
310         for (size_t i = 0; i < Len; i++)
311         {
312             auto point = points[i];
313             vertices[i].pos.x = point.x;
314             vertices[i].pos.y = point.y;
315             vertices[i].col = color;
316         }
317         uint current = 1;
318         for (size_t i = 0; i < Indices; i += 3, current += 1) {
319             indices[i] = 0;
320             indices[i + 1] = current;
321             indices[i + 2] = current + 1;
322         }
323         ctx.add(white.id, vertices, indices);
324     }
325 
326 
327     /**
328     Draw a circle with a given color
329 
330     The circle is actually draawn as a polygon, with NumPoints points. Increase or decrease it to increase or decrease the points on the circle
331     */
332     void draw(size_t NumPoints = 32)(in Color color, in Circle circle)
333     {
334         Vector[NumPoints] points; //A large array of points to simulate a circle
335         auto rotation = Transform.rotate(360 / NumPoints);
336         auto pointer = Vector(0, -circle.radius);
337         for (size_t i = 0; i < NumPoints; i++)
338         {
339             points[i] = circle.center + pointer;
340             pointer = rotation * pointer;
341         }
342         draw(color, points);
343     }
344 
345     ///Draw a rectangle with a color
346     void draw(in Color color, in Rectangle rect)
347     {
348         Vector[4] points = [ rect.topLeft, Vector(rect.x + rect.width, rect.y),
349             rect.topLeft + rect.size, Vector(rect.x, rect.y + rect.height)];
350         draw(color, points);
351     }
352 
353     ///Draw a texture at the given position with the given color
354     void draw(in Texture tex, in Vector position, in Color col = Color.white)
355     {
356         draw(tex, position.x, position.y, col);
357     }
358 
359     ///Draw a texture at the given units with the given color
360     void draw(in Texture tex, in float x, in float y, in Color col = Color.white)
361     {
362         draw(tex, x, y, tex.size.width, tex.size.height, 0, 0, 0, 1, 1, false, false, col);
363     }
364 
365     /**
366     Draw a transformed textur
367     
368     Params:
369     tex = the texture
370     area = the space to draw in
371     rot = the rotation angle from 0 to 360
372     origin = the rotational origin of the image
373     scale = the scale to draw at
374     flipHorizontal = if the texture should be flipped horizontally
375     flipVertical = if the texture should be flipped vertically
376     color = the color to blend with
377     */
378     void draw(in Texture tex, in Rectangle area, in float rotation = 0, in Vector origin = Vector(0, 0), 
379             in Vector scale = Vector(1, 1), in bool flipHorizontal = false, in bool flipVertical = false, in Color color = Color.white)
380     {
381         draw(tex, area.x, area.y, area.width, area.height, rotation, origin.x, origin.y, scale.x, scale.y, flipHorizontal, flipVertical, color);
382     }
383 
384     /**
385     Draw a transformed texture 
386 
387     Params:
388     tex = the texture
389     x = the x in units
390     y = the y in units
391     w = the width in units
392     h = the height in units
393     rot = the rotation angle from 0 to 360
394     originX = the x origin
395     originY = the y origin
396     scaleX = the x scale of the draw
397     scaleY = the y scale of the draw
398     flipHorizontal = if the texture should be flipped horizontally
399     flipVertical = if the texture should be flipped vertically
400     color = the color to blend with
401     */
402     void draw(in Texture tex, in float x, in float y, in float w, in float h,
403                         in float rot = 0, in float originX = 0, in float originY = 0,
404                         in float scaleX = 1, in float scaleY = 1,
405                         in bool flipHorizontal = false, in bool flipVertical = false,
406                         in Color color = Color.white)
407     {
408         auto trans = Transform.identity() 
409             * Transform.translate(Vector(-originX, -originY))
410             * Transform.rotate(rot)
411             * Transform.scale(Vector(scaleX, scaleY))
412             * Transform.translate(Vector(originX, originY));
413         draw(tex, trans, x, y, w, h, flipHorizontal, flipVertical, color);
414     }
415 
416 
417     /**
418     Draw a texture with a precalculated transform
419 
420     Params:
421     tex = the texture
422     x = the x in units
423     y = the y in units
424     w = the width in units
425     h = the height in units
426     flipHorizontal = if the texture should be flipped horizontally
427     flipVertical = if the texture should be flipped vertically
428     color = the color to blend with
429     */
430     void draw(in Texture tex, in Transform trans, in float x, in float y,
431                        in float w, in float h, in bool flipHorizontal = false, in bool flipVertical = false,
432                        in Color color = Color.white)
433     {
434         //Calculate the destination points with the transformation
435         auto tl = trans * Vector(0, 0);
436         auto tr = trans * Vector(w, 0);
437         auto bl = trans * Vector(0, h);
438         auto br = trans * Vector(w, h);
439 
440         //Calculate the source points normalized to [0, 1]
441         //The conversion factor for normalizing vectors
442         float conv_factor_x = 1.0f / tex.sourceWidth;
443         float conv_factor_y = 1.0f / tex.sourceHeight;
444         float norm_x = tex.size.x * conv_factor_x;
445         float norm_y = tex.size.y * conv_factor_y;
446         float norm_w = tex.size.width * conv_factor_x;
447         float norm_h = tex.size.height * conv_factor_y;
448         auto src_tl = Vector(norm_x, norm_y);
449         auto src_tr = Vector(norm_x + norm_w, norm_y);
450         auto src_br = Vector(norm_x + norm_w, norm_y + norm_h);
451         auto src_bl = Vector(norm_x, norm_y + norm_h);
452         if (flipHorizontal) {
453             auto tmp = src_tr;
454             src_tr = src_tl;
455             src_tl = tmp;
456             tmp = src_br;
457             src_br = src_bl;
458             src_bl = tmp;
459         }
460         if (flipVertical) {
461             auto tmp = src_tr;
462             src_tr = src_br;
463             src_br = tmp;
464             tmp = src_tl;
465             src_tl = src_bl;
466             src_bl = tmp;
467         }
468         //Add all of the vertices to the context
469         auto translate = Vector(x, y);
470         Vertex[4] vertices = [ Vertex(tl + translate, src_tl, color),
471             Vertex(tr + translate, src_tr, color),
472             Vertex(br + translate, src_br, color),
473             Vertex(bl + translate, src_bl, color)];
474         GLuint[6] indices = [0, 1, 2, 2, 3, 0];
475         ctx.add(tex.id, vertices, indices);
476     }
477 
478     ///Draw a sprite to the screen
479     void draw(ref scope Sprite sprite)
480     {
481         sprite.update();
482         draw(sprite.texture, sprite.x, sprite.y, sprite.width, sprite.height,
483                 sprite.rotation, sprite.originX, sprite.originY,
484                 sprite.scaleX, sprite.scaleY, sprite.flipX, sprite.flipY, sprite.color);
485     }
486 
487     ///Draw a character using a font and find the width it took
488     float draw(ref in Font font, in char c, in float x, in float y, in Color col = Color.white)
489     {
490         Texture renderChar = font.render(c);
491         draw(renderChar, x, y, col);
492         return renderChar.size.width;
493     }
494 
495     ///Draw a string using a font
496     void draw(ref in Font font, in string str, in float x, in float y, in float lineHeight = 1, in Color col = Color.white)
497     {
498         float position = 0;
499         float cursor = y;
500         //Loop from the beginning to end of the string
501         for(size_t i = 0; i < str.length; i++)
502         {
503             char c = str[i];
504             if (c == '\t')
505                 for (int j = 0; j < 4; j++)
506                     position += draw(font, ' ', position + x, cursor, col);
507             else if (c == '\n')
508             {
509                 position = 0;
510                 cursor += font.characterHeight * lineHeight;
511             }
512             else if (c != '\r')
513                 position += draw(font, c, position + x, cursor, col);
514         }
515     }
516 
517     ///Draw a wrapped string that can wrap on word or by character
518     void draw(ref in Font font, in string str, in float x, in float y,
519         in float maxWidth, in Color col = Color.white, in bool wrapOnWord = true, float lineHeight = 1)
520     {
521         size_t left = 0;
522         float cursor = y;
523         while(left < str.length)
524         {
525             size_t right = str.length;
526             while(right > left && font.getSizeOfString(str[left..right]).width > maxWidth)
527             {
528                 do
529                 {
530                     right--;
531                 } while(wrapOnWord && right > left && str[right - 1].isAlphaNum);
532             }
533             if(right == left)
534                 right = str.length;
535             draw(font, str[left..right], x, cursor, lineHeight, col);
536             cursor += font.getSizeOfString(str[left..right], lineHeight).height;
537             left = right;
538         }
539     }
540 
541     ///Create a burst of particles using the emitter
542     void addParticleBurst(in ParticleEmitter emitter)
543     {
544         int parts = randomRange(emitter.particle_min, emitter.particle_max);
545         for (int i = 0; i < parts; i++)
546             particles.add(emitter.emit());
547     }
548     
549     ///Sets the GLSL shader, see the GL backend docs
550     void setShader(in string vertexShader, 
551 		in string fragmentShader,
552 		in string transformAttributeName = "transform",
553 		in string positionAttributeName = "position",
554 		in string texPositionAttributeName = "tex_coord",
555 		in string colorAttributeName = "color",
556 		in string textureAttributeName = "tex",
557 		in string colorOutputName = "outColor")
558     {
559         ctx.setShader(vertexShader, fragmentShader, 
560             transformAttributeName, positionAttributeName,
561             texPositionAttributeName, colorAttributeName,
562             textureAttributeName, colorOutputName);
563     }
564 
565     ///Checks if a key is being held down by a key name
566     bool isKeyDown(in string name) const
567     {
568         return current_keys[SDL_GetScancodeFromName(name.ptr)];
569     }
570 
571     ///Checks if key was down previously by a key name
572     bool wasKeyDown(in string name) const
573     {
574         return previous_keys[SDL_GetScancodeFromName(name.ptr)];
575     }
576 
577     private static Window* globalWindow;
578     ///Get a global instance of the window
579     public static @nogc Window* getInstance()
580     {
581         return globalWindow;
582     }
583 
584     pure:
585     ///Get the position of the mouse
586     @property Vector mouse() const
587     {
588         return camera.unproject * mousePos;
589     }
590     @property bool mouseLeftPressed() const { return mouseLeft; }
591     @property bool mouseRightPressed() const { return mouseRight; }
592     @property bool mouseMiddlePressed() const { return mouseMiddle; }
593     @property bool mouseLeftReleased() const { return !mouseLeft && mouseLeftPrevious; }
594     @property bool mouseRightReleased() const { return !mouseRight && mouseRightPrevious; }
595     @property bool mouseMiddleReleased() const { return !mouseMiddle && mouseMiddlePrevious; }
596     @property bool isOpen() const { return shouldContinue; }
597     @property Gamepad[] gamepads() { return connectedGamepads.array; }
598     @property int width() const { return windowWidth; }
599     @property int height() const { return windowHeight; }
600 
601 }