1 /** 
2  * Copyright: Enalye
3  * License: Zlib
4  * Authors: Enalye
5  */
6 module atelier.render.window;
7 
8 import std.stdio;
9 import std..string;
10 
11 import bindbc.sdl, bindbc.sdl.image, bindbc.sdl.mixer, bindbc.sdl.ttf;
12 
13 import atelier.core;
14 import atelier.common;
15 import atelier.render.canvas;
16 import atelier.render.quadview;
17 import atelier.render.sprite;
18 
19 static {
20     package(atelier) {
21         SDL_Window* _sdlWindow;
22         SDL_Renderer* _sdlRenderer;
23         Color _windowClearColor;
24     }
25 
26     private {
27         SDL_Surface* _icon;
28         Vec2i _windowDimensions;
29         Vec2f _windowSize, _windowCenter;
30         bool _hasAudio = true;
31         bool _hasCustomCursor = false;
32         bool _showCursor = true;
33         Sprite _customCursorSprite;
34         DisplayMode _displayMode = DisplayMode.windowed;
35     }
36 }
37 
38 /// Width of the window in pixels.
39 int getWindowWidth() {
40     return _windowDimensions.x;
41 }
42 
43 /// Height of the window in pixels.
44 int getWindowHeight() {
45     return _windowDimensions.y;
46 }
47 
48 /// Dimensions of the window in pixels.
49 Vec2i getWindowDimensions() {
50     return _windowDimensions;
51 }
52 
53 /// Size of the window in pixels.
54 Vec2f getWindowSize() {
55     return _windowSize;
56 }
57 
58 /// Half of the size of the window in pixels.
59 Vec2f getWindowCenter() {
60     return _windowCenter;
61 }
62 
63 private struct CanvasReference {
64     const(SDL_Texture)* target;
65     Vec2f position;
66     Vec2f renderSize;
67     Vec2f size;
68     Canvas canvas;
69 }
70 
71 static private CanvasReference[] _canvases;
72 
73 /// Window display mode.
74 enum DisplayMode {
75     fullscreen,
76     desktop,
77     windowed
78 }
79 
80 import std.exception;
81 
82 /// Create the application window.
83 void createWindow(const Vec2i windowSize, string title) {
84     enforce(loadSDL() >= SDLSupport.sdl2010, "SDL support <= 2.0.10");
85     enforce(loadSDLImage() >= SDLImageSupport.sdlImage204, "SDL image support <= 2.0.4");
86     enforce(loadSDLTTF() >= SDLTTFSupport.sdlTTF2014, "SDL ttf support <= 2.0.14");
87     enforce(loadSDLMixer() >= SDLMixerSupport.sdlMixer204, "SDL mixer support <= 2.0.4");
88 
89     enforce(SDL_Init(SDL_INIT_EVERYTHING) == 0,
90             "SDL initialisation failure: " ~ fromStringz(SDL_GetError()));
91 
92     enforce(TTF_Init() != -1, "SDL ttf initialisation failure");
93     enforce(Mix_OpenAudio(44_100, MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS,
94             1024) != -1, "no audio device connected");
95     enforce(Mix_AllocateChannels(16) != -1, "audio channels allocation failure");
96 
97     SDL_SetHint(SDL_HINT_RENDER_BATCHING, "1");
98 
99     enforce(SDL_CreateWindowAndRenderer(windowSize.x, windowSize.y,
100             SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC | SDL_WINDOW_RESIZABLE,
101             &_sdlWindow, &_sdlRenderer) != -1, "window initialisation failure");
102 
103     CanvasReference canvasRef;
104     canvasRef.target = null;
105     canvasRef.position = cast(Vec2f)(windowSize) / 2;
106     canvasRef.size = cast(Vec2f)(windowSize);
107     canvasRef.renderSize = cast(Vec2f)(windowSize);
108     _canvases ~= canvasRef;
109 
110     _windowDimensions = windowSize;
111     _windowSize = cast(Vec2f)(windowSize);
112     _windowCenter = _windowSize / 2f;
113 
114     setWindowTitle(title);
115 }
116 
117 /// Cleanup the application window.
118 void destroyWindow() {
119     if (_sdlWindow)
120         SDL_DestroyWindow(_sdlWindow);
121 
122     if (_sdlRenderer)
123         SDL_DestroyRenderer(_sdlRenderer);
124 
125     if (_hasAudio)
126         Mix_CloseAudio();
127 }
128 
129 /// Enable/Disable audio (Call before creating the window). \
130 /// Enabled by default.
131 void enableAudio(bool enable) {
132     _hasAudio = enable;
133 }
134 
135 /// Change the actual window title.
136 void setWindowTitle(string title) {
137     SDL_SetWindowTitle(_sdlWindow, toStringz(title));
138 }
139 
140 /// Change the base color of the base canvas.
141 void setWindowClearColor(Color color) {
142     _windowClearColor = color;
143 }
144 
145 /// Update the window size. \
146 /// If `isLogical` is set, the actual window won't be resized, only the canvas will.
147 void setWindowSize(const Vec2i windowSize, bool isLogical = false) {
148     resizeWindow(windowSize);
149 
150     if (isLogical)
151         SDL_RenderSetLogicalSize(_sdlRenderer, windowSize.x, windowSize.y);
152     else
153         SDL_SetWindowSize(_sdlWindow, windowSize.x, windowSize.y);
154 }
155 
156 /// Call this to update canvas size when window's size is changed externally.
157 package(atelier) void resizeWindow(const Vec2i windowSize) {
158     _windowDimensions = windowSize;
159     _windowSize = cast(Vec2f)(windowSize);
160     _windowCenter = _windowSize / 2f;
161 
162     if (_canvases.length) {
163         _canvases[0].position = cast(Vec2f)(windowSize) / 2;
164         _canvases[0].size = cast(Vec2f)(windowSize);
165         _canvases[0].renderSize = cast(Vec2f) windowSize;
166     }
167 }
168 
169 /// Current window size.
170 private Vec2i fetchWindowSize() {
171     Vec2i windowSize;
172     SDL_GetWindowSize(_sdlWindow, &windowSize.x, &windowSize.y);
173     return windowSize;
174 }
175 
176 /// The window cannot be resized less than this.
177 void setWindowMinSize(Vec2i size) {
178     SDL_SetWindowMinimumSize(_sdlWindow, size.x, size.y);
179 }
180 
181 /// The window cannot be resized more than this.
182 void setWindowMaxSize(Vec2i size) {
183     SDL_SetWindowMaximumSize(_sdlWindow, size.x, size.y);
184 }
185 
186 /// Change the icon displayed.
187 void setWindowIcon(string path) {
188     if (_icon) {
189         SDL_FreeSurface(_icon);
190         _icon = null;
191     }
192     _icon = IMG_Load(toStringz(path));
193 
194     SDL_SetWindowIcon(_sdlWindow, _icon);
195 }
196 
197 /// Change the cursor to a custom one and disable the default one.
198 void setWindowCursor(Sprite cursorSprite) {
199     _customCursorSprite = cursorSprite;
200     _hasCustomCursor = true;
201     SDL_ShowCursor(false);
202 }
203 
204 /// Enable/Disable the cursor. \
205 /// Enabled by default.
206 void showWindowCursor(bool show) {
207     _showCursor = show;
208     if (!_hasCustomCursor)
209         SDL_ShowCursor(show);
210 }
211 
212 /// Change the display mode between windowed, desktop fullscreen and fullscreen.
213 void setWindowDisplay(DisplayMode displayMode) {
214     import atelier.ui : handleGuiElementEvent;
215 
216     _displayMode = displayMode;
217     SDL_WindowFlags mode;
218     final switch (displayMode) with (DisplayMode) {
219     case fullscreen:
220         mode = SDL_WINDOW_FULLSCREEN;
221         break;
222     case desktop:
223         mode = SDL_WINDOW_FULLSCREEN_DESKTOP;
224         break;
225     case windowed:
226         mode = cast(SDL_WindowFlags) 0;
227         break;
228     }
229     SDL_SetWindowFullscreen(_sdlWindow, mode);
230     Vec2i newSize = fetchWindowSize();
231     resizeWindow(newSize);
232     Event event;
233     event.type = Event.Type.resize;
234     event.window.size = newSize;
235     handleGuiElementEvent(event);
236 }
237 
238 /// Current display mode.
239 DisplayMode getWindowDisplay() {
240     return _displayMode;
241 }
242 
243 /// Enable/Disable the borders.
244 void setWindowBordered(bool isBordered) {
245     SDL_SetWindowBordered(_sdlWindow, isBordered ? SDL_TRUE : SDL_FALSE);
246 }
247 
248 /// Allow the user to resize the window
249 void setWindowResizable(bool isResizable) {
250     SDL_SetWindowResizable(_sdlWindow, isResizable ? SDL_TRUE : SDL_FALSE);
251 }
252 
253 /// Show/Hide the window. \
254 /// Shown by default obviously.
255 void showWindow(bool show) {
256     if (show)
257         SDL_ShowWindow(_sdlWindow);
258     else
259         SDL_HideWindow(_sdlWindow);
260 }
261 
262 /// Render everything on screen.
263 void renderWindow() {
264     Vec2f mousePos = getMousePos();
265     if (_hasCustomCursor && _showCursor && mousePos.isBetween(Vec2f.one, _windowSize - Vec2f.one)) {
266         _customCursorSprite.color = Color.white;
267         _customCursorSprite.draw(mousePos + _customCursorSprite.size / 2f);
268     }
269     SDL_RenderPresent(_sdlRenderer);
270     setRenderColor(_windowClearColor);
271     SDL_RenderClear(_sdlRenderer);
272 }
273 
274 /// Push a render canvas on the stack.
275 /// Everything after that and before the next popCanvas will be rendered onto this.
276 /// You **must** call popCanvas after that.
277 void pushCanvas(Canvas canvas, bool clear = true) {
278     canvas._isTargetOnStack = true;
279     CanvasReference canvasRef;
280     canvasRef.target = canvas.target;
281     canvasRef.position = canvas.position;
282     canvasRef.size = canvas.size;
283     canvasRef.renderSize = cast(Vec2f) canvas.renderSize;
284     canvasRef.canvas = canvas;
285     _canvases ~= canvasRef;
286 
287     SDL_SetRenderTarget(_sdlRenderer, cast(SDL_Texture*) canvasRef.target);
288     setRenderColor(canvas.clearColor, canvas.clearAlpha);
289     if (clear)
290         SDL_RenderClear(_sdlRenderer);
291 }
292 /*
293 void pushCanvas(QuadView quadView, bool clear = true) {
294 	pushCanvas(quadView.getCurrent(), clear);
295 	if(clear)
296 		quadView.advance();
297 }*/
298 
299 /// Called after pushCanvas to remove the render canvas from the stack.
300 /// When there is no canvas on the stack, everything is displayed directly on screen.
301 void popCanvas() {
302     if (_canvases.length <= 1)
303         throw new Exception("Attempt to pop the main canvas.");
304 
305     _canvases[$ - 1].canvas._isTargetOnStack = false;
306     _canvases.length--;
307     SDL_SetRenderTarget(_sdlRenderer, cast(SDL_Texture*) _canvases[$ - 1].target);
308     setRenderColor(_windowClearColor);
309 }
310 
311 /// Change coordinate system from inside to outside the canvas.
312 Vec2f transformRenderSpace(const Vec2f pos) {
313     const CanvasReference* canvasRef = &_canvases[$ - 1];
314     return (pos - canvasRef.position) * (
315             canvasRef.renderSize / canvasRef.size) + canvasRef.renderSize * 0.5f;
316 }
317 
318 /// Change coordinate system from outside to inside the canvas.
319 Vec2f transformCanvasSpace(const Vec2f pos, const Vec2f renderPos) {
320     const CanvasReference* canvasRef = &_canvases[$ - 1];
321     return (pos - renderPos) * (canvasRef.size / canvasRef.renderSize) + canvasRef.position;
322 }
323 
324 /// Change coordinate system from outside to insside the canvas.
325 Vec2f transformCanvasSpace(const Vec2f pos) {
326     const CanvasReference* canvasRef = &_canvases[$ - 1];
327     return pos * (canvasRef.size / canvasRef.renderSize);
328 }
329 
330 /// Change the scale from outside to inside the canvas.
331 Vec2f transformScale() {
332     const CanvasReference* canvasRef = &_canvases[$ - 1];
333     return canvasRef.renderSize / canvasRef.size;
334 }
335 
336 /// Check if something is inside the actual canvas rendering area.
337 bool isVisible(const Vec2f targetPosition, const Vec2f targetSize) {
338     const CanvasReference* canvasRef = &_canvases[$ - 1];
339     return (((canvasRef.position.x - canvasRef.size.x * .5f) < (
340             targetPosition.x + targetSize.x * .5f))
341             && ((canvasRef.position.x + canvasRef.size.x * .5f) > (
342                 targetPosition.x - targetSize.x * .5f))
343             && ((canvasRef.position.y - canvasRef.size.y * .5f) < (
344                 targetPosition.y + targetSize.y * .5f))
345             && ((canvasRef.position.y + canvasRef.size.y * .5f) > (
346                 targetPosition.y - targetSize.y * .5f)));
347 }
348 
349 /// Change the draw color, used internally. Don't bother use it.
350 void setRenderColor(const Color color, float alpha = 1f) {
351     const auto sdlColor = color.toSDL();
352     SDL_SetRenderDrawColor(_sdlRenderer, sdlColor.r, sdlColor.g, sdlColor.b,
353             cast(ubyte)(clamp(alpha, 0f, 1f) * 255f));
354 }
355 
356 /// Draw a single point.
357 void drawPoint(const Vec2f position, const Color color, float alpha = 1f) {
358     if (isVisible(position, Vec2f(.0f, .0f))) {
359         const Vec2f rpos = transformRenderSpace(position);
360 
361         setRenderColor(color, alpha);
362         SDL_RenderDrawPoint(_sdlRenderer, cast(int) rpos.x, cast(int) rpos.y);
363     }
364 }
365 
366 /// Draw a line between the two positions.
367 void drawLine(const Vec2f startPosition, const Vec2f endPosition, const Color color, float alpha = 1f) {
368     const Vec2f pos1 = transformRenderSpace(startPosition);
369     const Vec2f pos2 = transformRenderSpace(endPosition);
370 
371     setRenderColor(color, alpha);
372     SDL_RenderDrawLine(_sdlRenderer, cast(int) pos1.x, cast(int) pos1.y,
373             cast(int) pos2.x, cast(int) pos2.y);
374 }
375 
376 /// Draw an arrow with its head pointing at the end position.
377 void drawArrow(const Vec2f startPosition, const Vec2f endPosition, const Color color,
378         float alpha = 1f) {
379     const Vec2f pos1 = transformRenderSpace(startPosition);
380     const Vec2f pos2 = transformRenderSpace(endPosition);
381     const Vec2f dir = (pos2 - pos1).normalized;
382     const Vec2f arrowBase = pos2 - dir * 25f;
383     const Vec2f pos3 = arrowBase + dir.normal * 20f;
384     const Vec2f pos4 = arrowBase - dir.normal * 20f;
385 
386     setRenderColor(color, alpha);
387     SDL_RenderDrawLine(_sdlRenderer, cast(int) pos1.x, cast(int) pos1.y,
388             cast(int) pos2.x, cast(int) pos2.y);
389     SDL_RenderDrawLine(_sdlRenderer, cast(int) pos2.x, cast(int) pos2.y,
390             cast(int) pos3.x, cast(int) pos3.y);
391     SDL_RenderDrawLine(_sdlRenderer, cast(int) pos2.x, cast(int) pos2.y,
392             cast(int) pos4.x, cast(int) pos4.y);
393 }
394 
395 /// Draw a vertical cross (like this: +) with the indicated size.
396 void drawCross(const Vec2f center, float length, const Color color, float alpha = 1f) {
397     const float halfLength = length / 2f;
398     drawLine(center + Vec2f(-halfLength, 0f), center + Vec2f(halfLength, 0f), color, alpha);
399     drawLine(center + Vec2f(0f, -halfLength), center + Vec2f(0f, halfLength), color, alpha);
400 }
401 
402 /// Draw a rectangle border.
403 void drawRect(const Vec2f origin, const Vec2f size, const Color color, float alpha = 1f) {
404     const Vec2f pos1 = transformRenderSpace(origin);
405     const Vec2f pos2 = size * transformScale();
406 
407     const SDL_Rect rect = {
408         cast(int) pos1.x, cast(int) pos1.y, cast(int) pos2.x, cast(int) pos2.y
409     };
410 
411     setRenderColor(color, alpha);
412     SDL_RenderDrawRect(_sdlRenderer, &rect);
413 }
414 
415 /// Draw a fully filled rectangle.
416 void drawFilledRect(const Vec2f origin, const Vec2f size, const Color color, float alpha = 1f) {
417     const Vec2f pos1 = transformRenderSpace(origin);
418     const Vec2f pos2 = size * transformScale();
419 
420     const SDL_Rect rect = {
421         cast(int) pos1.x, cast(int) pos1.y, cast(int) pos2.x, cast(int) pos2.y
422     };
423 
424     setRenderColor(color, alpha);
425     SDL_RenderFillRect(_sdlRenderer, &rect);
426 }
427 
428 /// Draw a rectangle with a size of 1.
429 void drawPixel(const Vec2f position, const Color color, float alpha = 1f) {
430     const Vec2f pos = transformRenderSpace(position);
431 
432     const SDL_Rect rect = {cast(int) pos.x, cast(int) pos.y, 1, 1};
433 
434     setRenderColor(color, alpha);
435     SDL_RenderFillRect(_sdlRenderer, &rect);
436 }