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({ SDL_SetWindowTitle(sdlVideo.window, toStringz(caption)); });
152 	}
153 
154 	MouseButton translateMouseButton(ubyte sdlButton)
155 	{
156 		switch (sdlButton)
157 		{
158 		case SDL_BUTTON_LEFT:
159 			return MouseButton.Left;
160 		case SDL_BUTTON_MIDDLE:
161 		default:
162 			return MouseButton.Middle;
163 		case SDL_BUTTON_RIGHT:
164 			return MouseButton.Right;
165 		}
166 	}
167 
168 	enum SDL_BUTTON_LAST = SDL_BUTTON_X2;
169 
170 	MouseButtons translateMouseButtons(uint sdlButtons)
171 	{
172 		MouseButtons result;
173 		for (ubyte i=SDL_BUTTON_LEFT; i<=SDL_BUTTON_LAST; i++)
174 			if (sdlButtons & SDL_BUTTON(i))
175 				result |= 1<<translateMouseButton(i);
176 		return result;
177 	}
178 
179 	void handleEvent(SDL_Event* event)
180 	{
181 		switch (event.type)
182 		{
183 		case SDL_KEYDOWN:
184 			/+if ( event.key.keysym.scancode == SDL_SCANCODE_RETURN && (keypressed[SDL_SCANCODE_RALT] || keypressed[SDL_SCANCODE_LALT]))
185 			{
186 				if (application.toggleFullScreen())
187 				{
188 					video.stop();
189 					video.initialize();
190 					video.start();
191 					return false;
192 				}
193 			}+/
194 			return application.handleKeyDown(sdlKeys[event.key.keysym.scancode], /*event.key.keysym.unicode*/event.key.keysym.sym); // TODO: Use SDL_TextInputEvent
195 		case SDL_KEYUP:
196 			return application.handleKeyUp(sdlKeys[event.key.keysym.scancode]);
197 
198 		case SDL_MOUSEBUTTONDOWN:
199 			return application.handleMouseDown(event.button.x, event.button.y, translateMouseButton(event.button.button));
200 		case SDL_MOUSEBUTTONUP:
201 			return application.handleMouseUp(event.button.x, event.button.y, translateMouseButton(event.button.button));
202 		case SDL_MOUSEMOTION:
203 			return application.handleMouseMove(event.motion.x, event.motion.y, translateMouseButtons(event.motion.state));
204 
205 		case SDL_JOYAXISMOTION:
206 			return application.handleJoyAxisMotion(event.jaxis.axis, cast(short)event.jaxis.value);
207 		case SDL_JOYHATMOTION:
208 			return application.handleJoyHatMotion (event.jhat.hat, cast(JoystickHatState)event.jhat.value);
209 		case SDL_JOYBUTTONDOWN:
210 			return application.handleJoyButtonDown(event.jbutton.button);
211 		case SDL_JOYBUTTONUP:
212 			return application.handleJoyButtonUp  (event.jbutton.button);
213 
214 		case SDL_WINDOWEVENT:
215 			switch (event.window.event)
216 			{
217 				case SDL_WINDOWEVENT_MOVED:
218 					auto settings = application.getShellSettings();
219 					settings.windowPosX = event.window.data1;
220 					settings.windowPosY = event.window.data2;
221 					application.setShellSettings(settings);
222 					break;
223 				case SDL_WINDOWEVENT_SIZE_CHANGED:
224 					auto settings = application.getShellSettings();
225 					settings.windowSizeX = event.window.data1;
226 					settings.windowSizeY = event.window.data2;
227 					application.setShellSettings(settings);
228 					reinitPending = true;
229 					break;
230 				case SDL_WINDOWEVENT_CLOSE:
231 					event.type = SDL_QUIT;
232 					SDL_PushEvent(event);
233 					break;
234 				default:
235 					break;
236 			}
237 			break;
238 		case SDL_QUIT:
239 			application.handleQuit();
240 			break;
241 		default:
242 			break;
243 		}
244 	}
245 
246 	bool reinitPending;
247 }
248 
249 class SdlException : Exception
250 {
251 	this(string message) { super(message); }
252 }
253 
254 T sdlEnforce(T)(T result, string message = null)
255 {
256 	if (!result)
257 		throw new SdlException("SDL error: " ~ (message ? message ~ ": " : "") ~ to!string(SDL_GetError()));
258 	return result;
259 }
260 
261 Key[SDL_NUM_SCANCODES] sdlKeys;
262 
263 shared static this()
264 {
265 	sdlKeys[SDL_SCANCODE_UP    ] = Key.up   ;
266 	sdlKeys[SDL_SCANCODE_DOWN  ] = Key.down ;
267 	sdlKeys[SDL_SCANCODE_LEFT  ] = Key.left ;
268 	sdlKeys[SDL_SCANCODE_RIGHT ] = Key.right;
269 	sdlKeys[SDL_SCANCODE_SPACE ] = Key.space;
270 	sdlKeys[SDL_SCANCODE_ESCAPE] = Key.esc  ;
271 }