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 		if (audio)
85 			audio.start(application);
86 
87 		video.errorCallback = AppCallback(&quit);
88 		quitting = false;
89 
90 		// video (re-)initialization loop
91 		while (!quitting)
92 		{
93 			reinitPending = false;
94 
95 			// start renderer
96 			video.start(application);
97 
98 			// The main purpose of this call is to allow the application
99 			// to react to window size changes.
100 			application.handleInit();
101 
102 			// pump events
103 			while (!reinitPending && !quitting)
104 			{
105 				sdlEnforce(waitEvent());
106 
107 				synchronized(application)
108 				{
109 					if (mainThreadQueue.length)
110 					{
111 						foreach (fn; mainThreadQueue)
112 							if (fn)
113 								fn();
114 						mainThreadQueue = null;
115 					}
116 
117 					SDL_Event event = void;
118 					while (SDL_PollEvent(&event))
119 						handleEvent(&event);
120 				}
121 			}
122 
123 			// wait for renderer to stop
124 			video.stop();
125 		}
126 
127 		if (audio)
128 			audio.stop();
129 	}
130 
131 	~this()
132 	{
133 		SDL_Quit();
134 	}
135 
136 	private void delegate()[] mainThreadQueue;
137 
138 	private void runInMainThread(void delegate() fn)
139 	{
140 		synchronized(this)
141 			mainThreadQueue ~= fn;
142 	}
143 
144 	override void prod()
145 	{
146 		runInMainThread(null);
147 	}
148 
149 	override void setCaption(string caption)
150 	{
151 		runInMainThread({
152 			static string oldCaption;
153 			if (caption != oldCaption)
154 			{
155 				oldCaption = caption;
156 				SDL_SetWindowTitle(sdlVideo.window, toStringz(caption));
157 			}
158 		});
159 	}
160 
161 	MouseButton translateMouseButton(ubyte sdlButton)
162 	{
163 		switch (sdlButton)
164 		{
165 		case SDL_BUTTON_LEFT:
166 			return MouseButton.Left;
167 		case SDL_BUTTON_MIDDLE:
168 		default:
169 			return MouseButton.Middle;
170 		case SDL_BUTTON_RIGHT:
171 			return MouseButton.Right;
172 		}
173 	}
174 
175 	enum SDL_BUTTON_LAST = SDL_BUTTON_X2;
176 
177 	MouseButtons translateMouseButtons(uint sdlButtons)
178 	{
179 		MouseButtons result;
180 		for (ubyte i=SDL_BUTTON_LEFT; i<=SDL_BUTTON_LAST; i++)
181 			if (sdlButtons & SDL_BUTTON(i))
182 				result |= 1<<translateMouseButton(i);
183 		return result;
184 	}
185 
186 	void handleEvent(SDL_Event* event)
187 	{
188 		switch (event.type)
189 		{
190 		case SDL_KEYDOWN:
191 			/+if ( event.key.keysym.scancode == SDL_SCANCODE_RETURN && (keypressed[SDL_SCANCODE_RALT] || keypressed[SDL_SCANCODE_LALT]))
192 			{
193 				if (application.toggleFullScreen())
194 				{
195 					video.stop();
196 					video.initialize();
197 					video.start();
198 					return false;
199 				}
200 			}+/
201 			return application.handleKeyDown(sdlKeys[event.key.keysym.scancode], /*event.key.keysym.unicode*/event.key.keysym.sym); // TODO: Use SDL_TextInputEvent
202 		case SDL_KEYUP:
203 			return application.handleKeyUp(sdlKeys[event.key.keysym.scancode]);
204 
205 		case SDL_MOUSEBUTTONDOWN:
206 			return application.handleMouseDown(event.button.x, event.button.y, translateMouseButton(event.button.button));
207 		case SDL_MOUSEBUTTONUP:
208 			return application.handleMouseUp(event.button.x, event.button.y, translateMouseButton(event.button.button));
209 		case SDL_MOUSEMOTION:
210 			return application.handleMouseMove(event.motion.x, event.motion.y, translateMouseButtons(event.motion.state));
211 
212 		case SDL_JOYAXISMOTION:
213 			return application.handleJoyAxisMotion(event.jaxis.axis, cast(short)event.jaxis.value);
214 		case SDL_JOYHATMOTION:
215 			return application.handleJoyHatMotion (event.jhat.hat, cast(JoystickHatState)event.jhat.value);
216 		case SDL_JOYBUTTONDOWN:
217 			return application.handleJoyButtonDown(event.jbutton.button);
218 		case SDL_JOYBUTTONUP:
219 			return application.handleJoyButtonUp  (event.jbutton.button);
220 
221 		case SDL_WINDOWEVENT:
222 			switch (event.window.event)
223 			{
224 				case SDL_WINDOWEVENT_MOVED:
225 					auto settings = application.getShellSettings();
226 					settings.windowPosX = event.window.data1;
227 					settings.windowPosY = event.window.data2;
228 					application.setShellSettings(settings);
229 					break;
230 				case SDL_WINDOWEVENT_SIZE_CHANGED:
231 					auto settings = application.getShellSettings();
232 					settings.windowSizeX = event.window.data1;
233 					settings.windowSizeY = event.window.data2;
234 					application.setShellSettings(settings);
235 					reinitPending = true;
236 					break;
237 				case SDL_WINDOWEVENT_CLOSE:
238 					event.type = SDL_QUIT;
239 					SDL_PushEvent(event);
240 					break;
241 				default:
242 					break;
243 			}
244 			break;
245 		case SDL_QUIT:
246 			application.handleQuit();
247 			break;
248 		default:
249 			break;
250 		}
251 	}
252 
253 	bool reinitPending;
254 }
255 
256 class SdlException : Exception
257 {
258 	this(string message) { super(message); }
259 }
260 
261 T sdlEnforce(T)(T result, string message = null)
262 {
263 	if (!result)
264 		throw new SdlException("SDL error: " ~ (message ? message ~ ": " : "") ~ to!string(SDL_GetError()));
265 	return result;
266 }
267 
268 Key[SDL_NUM_SCANCODES] sdlKeys;
269 
270 shared static this()
271 {
272 	sdlKeys[SDL_SCANCODE_UP      ] = Key.up      ;
273 	sdlKeys[SDL_SCANCODE_DOWN    ] = Key.down    ;
274 	sdlKeys[SDL_SCANCODE_LEFT    ] = Key.left    ;
275 	sdlKeys[SDL_SCANCODE_RIGHT   ] = Key.right   ;
276 	sdlKeys[SDL_SCANCODE_PAGEUP  ] = Key.pageUp  ;
277 	sdlKeys[SDL_SCANCODE_PAGEDOWN] = Key.pageDown;
278 	sdlKeys[SDL_SCANCODE_HOME    ] = Key.home    ;
279 	sdlKeys[SDL_SCANCODE_END     ] = Key.end     ;
280 	sdlKeys[SDL_SCANCODE_SPACE   ] = Key.space   ;
281 	sdlKeys[SDL_SCANCODE_ESCAPE  ] = Key.esc     ;
282 }