1 /**
2  * ae.ui.shell.sdl2.shell
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.ui.shell.sdl2.shell;
15 
16 import std.conv;
17 import std.string;
18 
19 import derelict.sdl2.sdl;
20 import derelict.util.loader;
21 
22 import ae.ui.shell.shell;
23 //import ae.ui.video.video;
24 import ae.ui.video.sdl2common.video;
25 import ae.ui.app.application;
26 public import ae.ui.shell.events;
27 import ae.ui.timer.timer;
28 
29 //!!version(Posix) pragma(lib, "dl"); // for Derelict
30 
31 final class SDL2Shell : Shell
32 {
33 	Application application;
34 	SDL2CommonVideo sdlVideo;
35 
36 	this(Application application)
37 	{
38 		this.application = application;
39 
40 		//!!SharedLibLoader.disableAutoUnload(); // SDL MM timers may crash on exit
41 		DerelictSDL2.load();
42 		auto components = SDL_INIT_VIDEO | SDL_INIT_TIMER;
43 		if (application.needSound())
44 			components |= SDL_INIT_AUDIO;
45 		if (application.needJoystick())
46 			components |= SDL_INIT_JOYSTICK;
47 		sdlEnforce(SDL_Init(components)==0);
48 
49 		//!!SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
50 		//!!SDL_EnableUNICODE(1);
51 
52 		if (application.needJoystick() && SDL_NumJoysticks())
53 		{
54 			SDL_JoystickEventState(SDL_ENABLE);
55 			SDL_JoystickOpen(0);
56 		}
57 	}
58 
59 	/// A version of SDL_WaitEvent that sleeps less than 10ms at a time.
60 	private int waitEvent()
61 	{
62 		while (true)
63 		{
64 			synchronized(this)
65 				if (mainThreadQueue.length)
66 					return 1;
67 
68 			SDL_PumpEvents();
69 			switch (SDL_PeepEvents(null, 1, SDL_GETEVENT, 0, uint.max))
70 			{
71 				case -1: return 0;
72 				case  0: SDL_Delay(1); break;
73 				default: return 1;
74 			}
75 		}
76 	}
77 
78 	override void run()
79 	{
80 		assert(video, "Video object not set");
81 		sdlVideo = cast(SDL2CommonVideo)video;
82 		assert(sdlVideo, "Video is non-SDL");
83 
84 		video.errorCallback = AppCallback(&quit);
85 		quitting = false;
86 
87 		// video (re-)initialization loop
88 		while (!quitting)
89 		{
90 			reinitPending = false;
91 
92 			// start renderer
93 			video.start(application);
94 
95 			// The main purpose of this call is to allow the application
96 			// to react to window size changes.
97 			application.handleInit();
98 
99 			// pump events
100 			while (!reinitPending && !quitting)
101 			{
102 				sdlEnforce(waitEvent());
103 
104 				synchronized(application)
105 				{
106 					if (mainThreadQueue.length)
107 					{
108 						foreach (fn; mainThreadQueue)
109 							if (fn)
110 								fn();
111 						mainThreadQueue = null;
112 					}
113 
114 					SDL_Event event = void;
115 					while (SDL_PollEvent(&event))
116 						handleEvent(&event);
117 				}
118 			}
119 
120 			// wait for renderer to stop
121 			video.stop();
122 		}
123 	}
124 
125 	~this()
126 	{
127 		SDL_Quit();
128 	}
129 
130 	private void delegate()[] mainThreadQueue;
131 
132 	private void runInMainThread(void delegate() fn)
133 	{
134 		synchronized(this)
135 			mainThreadQueue ~= fn;
136 	}
137 
138 	override void prod()
139 	{
140 		runInMainThread(null);
141 	}
142 
143 	override void setCaption(string caption)
144 	{
145 		runInMainThread({ SDL_SetWindowTitle(sdlVideo.window, toStringz(caption)); });
146 	}
147 
148 	MouseButton translateMouseButton(ubyte sdlButton)
149 	{
150 		switch (sdlButton)
151 		{
152 		case SDL_BUTTON_LEFT:
153 			return MouseButton.Left;
154 		case SDL_BUTTON_MIDDLE:
155 		default:
156 			return MouseButton.Middle;
157 		case SDL_BUTTON_RIGHT:
158 			return MouseButton.Right;
159 		}
160 	}
161 
162 	enum SDL_BUTTON_LAST = SDL_BUTTON_X2;
163 
164 	MouseButtons translateMouseButtons(uint sdlButtons)
165 	{
166 		MouseButtons result;
167 		for (ubyte i=SDL_BUTTON_LEFT; i<=SDL_BUTTON_LAST; i++)
168 			if (sdlButtons & SDL_BUTTON(i))
169 				result |= 1<<translateMouseButton(i);
170 		return result;
171 	}
172 
173 	void handleEvent(SDL_Event* event)
174 	{
175 		switch (event.type)
176 		{
177 		case SDL_KEYDOWN:
178 			/+if ( event.key.keysym.scancode == SDL_SCANCODE_RETURN && (keypressed[SDL_SCANCODE_RALT] || keypressed[SDL_SCANCODE_LALT]))
179 			{
180 				if (application.toggleFullScreen())
181 				{
182 					video.stop();
183 					video.initialize();
184 					video.start();
185 					return false;
186 				}
187 			}+/
188 			return application.handleKeyDown(sdlKeys[event.key.keysym.scancode], event.key.keysym.unicode);
189 		case SDL_KEYUP:
190 			return application.handleKeyUp(sdlKeys[event.key.keysym.scancode]);
191 
192 		case SDL_MOUSEBUTTONDOWN:
193 			return application.handleMouseDown(event.button.x, event.button.y, translateMouseButton(event.button.button));
194 		case SDL_MOUSEBUTTONUP:
195 			return application.handleMouseUp(event.button.x, event.button.y, translateMouseButton(event.button.button));
196 		case SDL_MOUSEMOTION:
197 			return application.handleMouseMove(event.motion.x, event.motion.y, translateMouseButtons(event.motion.state));
198 
199 		case SDL_JOYAXISMOTION:
200 			return application.handleJoyAxisMotion(event.jaxis.axis, cast(short)event.jaxis.value);
201 		case SDL_JOYHATMOTION:
202 			return application.handleJoyHatMotion (event.jhat.hat, cast(JoystickHatState)event.jhat.value);
203 		case SDL_JOYBUTTONDOWN:
204 			return application.handleJoyButtonDown(event.jbutton.button);
205 		case SDL_JOYBUTTONUP:
206 			return application.handleJoyButtonUp  (event.jbutton.button);
207 
208 		case SDL_WINDOWEVENT:
209 			switch (event.window.event)
210 			{
211 				case SDL_WINDOWEVENT_MOVED:
212 					auto settings = application.getShellSettings();
213 					settings.windowPosX = event.window.data1;
214 					settings.windowPosY = event.window.data2;
215 					application.setShellSettings(settings);
216 					break;
217 				case SDL_WINDOWEVENT_SIZE_CHANGED:
218 					auto settings = application.getShellSettings();
219 					settings.windowSizeX = event.window.data1;
220 					settings.windowSizeY = event.window.data2;
221 					application.setShellSettings(settings);
222 					reinitPending = true;
223 					break;
224 				case SDL_WINDOWEVENT_CLOSE:
225 					event.type = SDL_QUIT;
226 					SDL_PushEvent(event);
227 					break;
228 				default:
229 					break;
230 			}
231 			break;
232 		case SDL_QUIT:
233 			application.handleQuit();
234 			break;
235 		default:
236 			break;
237 		}
238 	}
239 
240 	bool reinitPending;
241 }
242 
243 class SdlException : Exception
244 {
245 	this(string message) { super(message); }
246 }
247 
248 T sdlEnforce(T)(T result, string message = null)
249 {
250 	if (!result)
251 		throw new SdlException("SDL error: " ~ (message ? message ~ ": " : "") ~ to!string(SDL_GetError()));
252 	return result;
253 }
254 
255 Key[SDL_NUM_SCANCODES] sdlKeys;
256 
257 shared static this()
258 {
259 	sdlKeys[SDL_SCANCODE_UP    ] = Key.up   ;
260 	sdlKeys[SDL_SCANCODE_DOWN  ] = Key.down ;
261 	sdlKeys[SDL_SCANCODE_LEFT  ] = Key.left ;
262 	sdlKeys[SDL_SCANCODE_RIGHT ] = Key.right;
263 	sdlKeys[SDL_SCANCODE_SPACE ] = Key.space;
264 	sdlKeys[SDL_SCANCODE_ESCAPE] = Key.esc  ;
265 }