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 <ae@cy.md>
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 /// `Shell` implementation using SDL2.
32 final class SDL2Shell : Shell
33 {
34 	Application application; ///
35 	SDL2CommonVideo sdlVideo; ///
36 
37 	this(Application application)
38 	{
39 		this.application = application;
40 
41 		//!!SharedLibLoader.disableAutoUnload(); // SDL MM timers may crash on exit
42 		DerelictSDL2.load();
43 		auto components = SDL_INIT_VIDEO | SDL_INIT_TIMER;
44 		if (application.needSound())
45 			components |= SDL_INIT_AUDIO;
46 		if (application.needJoystick())
47 			components |= SDL_INIT_JOYSTICK;
48 		sdlEnforce(SDL_Init(components)==0);
49 
50 		//!!SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
51 		//!!SDL_EnableUNICODE(1);
52 
53 		if (application.needJoystick() && SDL_NumJoysticks())
54 		{
55 			SDL_JoystickEventState(SDL_ENABLE);
56 			SDL_JoystickOpen(0);
57 		}
58 	} ///
59 
60 	/// A version of SDL_WaitEvent that sleeps less than 10ms at a time.
61 	private int waitEvent()
62 	{
63 		while (true)
64 		{
65 			synchronized(this)
66 				if (mainThreadQueue.length)
67 					return 1;
68 
69 			SDL_PumpEvents();
70 			switch (SDL_PeepEvents(null, 1, SDL_GETEVENT, 0, uint.max))
71 			{
72 				case -1: return 0;
73 				case  0: SDL_Delay(1); break;
74 				default: return 1;
75 			}
76 		}
77 	}
78 
79 	override void run()
80 	{
81 		assert(video, "Video object not set");
82 		sdlVideo = cast(SDL2CommonVideo)video;
83 		assert(sdlVideo, "Video is non-SDL");
84 
85 		if (audio)
86 			audio.start(application);
87 
88 		video.errorCallback = AppCallback(&quit);
89 		quitting = false;
90 
91 		// video (re-)initialization loop
92 		while (!quitting)
93 		{
94 			reinitPending = false;
95 
96 			// start renderer
97 			video.start(application);
98 
99 			// The main purpose of this call is to allow the application
100 			// to react to window size changes.
101 			application.handleInit();
102 
103 			// pump events
104 			while (!reinitPending && !quitting)
105 			{
106 				sdlEnforce(waitEvent());
107 
108 				synchronized(application)
109 				{
110 					if (mainThreadQueue.length)
111 					{
112 						foreach (fn; mainThreadQueue)
113 							if (fn)
114 								fn();
115 						mainThreadQueue = null;
116 					}
117 
118 					SDL_Event event = void;
119 					while (SDL_PollEvent(&event))
120 						handleEvent(&event);
121 				}
122 			}
123 
124 			// wait for renderer to stop
125 			video.stop();
126 		}
127 
128 		if (audio)
129 			audio.stop();
130 	} ///
131 
132 	~this() @nogc
133 	{
134 		SDL_Quit();
135 	}
136 
137 	private void delegate()[] mainThreadQueue;
138 
139 	private void runInMainThread(void delegate() fn)
140 	{
141 		synchronized(this)
142 			mainThreadQueue ~= fn;
143 	}
144 
145 	override void prod()
146 	{
147 		runInMainThread(null);
148 	} ///
149 
150 	override void setCaption(string caption)
151 	{
152 		runInMainThread({
153 			static string oldCaption;
154 			if (caption != oldCaption)
155 			{
156 				oldCaption = caption;
157 				SDL_SetWindowTitle(sdlVideo.window, toStringz(caption));
158 			}
159 		});
160 	} ///
161 
162 	/// Translate an SDL button index to a `MouseButton`.
163 	MouseButton translateMouseButton(ubyte sdlButton)
164 	{
165 		switch (sdlButton)
166 		{
167 		case SDL_BUTTON_LEFT:
168 			return MouseButton.Left;
169 		case SDL_BUTTON_MIDDLE:
170 		default:
171 			return MouseButton.Middle;
172 		case SDL_BUTTON_RIGHT:
173 			return MouseButton.Right;
174 		}
175 	}
176 
177 	/// Highest SDL_BUTTON constant.
178 	enum SDL_BUTTON_LAST = SDL_BUTTON_X2;
179 
180 	/// Translate an SDL buttons mask to a `MouseButtons`.
181 	MouseButtons translateMouseButtons(uint sdlButtons)
182 	{
183 		MouseButtons result;
184 		for (ubyte i=SDL_BUTTON_LEFT; i<=SDL_BUTTON_LAST; i++)
185 			if (sdlButtons & SDL_BUTTON(i))
186 				result |= 1<<translateMouseButton(i);
187 		return result;
188 	}
189 
190 	/// Handle a single `SDL_Event`.
191 	void handleEvent(SDL_Event* event)
192 	{
193 		switch (event.type)
194 		{
195 		case SDL_KEYDOWN:
196 			/+if ( event.key.keysym.scancode == SDL_SCANCODE_RETURN && (keypressed[SDL_SCANCODE_RALT] || keypressed[SDL_SCANCODE_LALT]))
197 			{
198 				if (application.toggleFullScreen())
199 				{
200 					video.stop();
201 					video.initialize();
202 					video.start();
203 					return false;
204 				}
205 			}+/
206 			return application.handleKeyDown(sdlKeys[event.key.keysym.scancode], /*event.key.keysym.unicode*/event.key.keysym.sym); // TODO: Use SDL_TextInputEvent
207 		case SDL_KEYUP:
208 			return application.handleKeyUp(sdlKeys[event.key.keysym.scancode]);
209 
210 		case SDL_MOUSEBUTTONDOWN:
211 			return application.handleMouseDown(event.button.x, event.button.y, translateMouseButton(event.button.button));
212 		case SDL_MOUSEBUTTONUP:
213 			return application.handleMouseUp(event.button.x, event.button.y, translateMouseButton(event.button.button));
214 		case SDL_MOUSEMOTION:
215 			return application.handleMouseMove(event.motion.x, event.motion.y, translateMouseButtons(event.motion.state));
216 
217 		case SDL_JOYAXISMOTION:
218 			return application.handleJoyAxisMotion(event.jaxis.axis, cast(short)event.jaxis.value);
219 		case SDL_JOYHATMOTION:
220 			return application.handleJoyHatMotion (event.jhat.hat, cast(JoystickHatState)event.jhat.value);
221 		case SDL_JOYBUTTONDOWN:
222 			return application.handleJoyButtonDown(event.jbutton.button);
223 		case SDL_JOYBUTTONUP:
224 			return application.handleJoyButtonUp  (event.jbutton.button);
225 
226 		case SDL_WINDOWEVENT:
227 			switch (event.window.event)
228 			{
229 				case SDL_WINDOWEVENT_MOVED:
230 					auto settings = application.getShellSettings();
231 					settings.windowPosX = event.window.data1;
232 					settings.windowPosY = event.window.data2;
233 					application.setShellSettings(settings);
234 					break;
235 				case SDL_WINDOWEVENT_SIZE_CHANGED:
236 					auto settings = application.getShellSettings();
237 					settings.windowSizeX = event.window.data1;
238 					settings.windowSizeY = event.window.data2;
239 					application.setShellSettings(settings);
240 					reinitPending = true;
241 					break;
242 				case SDL_WINDOWEVENT_CLOSE:
243 					event.type = SDL_QUIT;
244 					SDL_PushEvent(event);
245 					break;
246 				default:
247 					break;
248 			}
249 			break;
250 		case SDL_QUIT:
251 			application.handleQuit();
252 			break;
253 		default:
254 			break;
255 		}
256 	}
257 
258 	protected bool reinitPending;
259 }
260 
261 /// Wraps SDL library errors.
262 class SdlException : Exception
263 {
264 	this(string message) { super(message); } ///
265 }
266 
267 /// ditto
268 T sdlEnforce(T)(T result, string message = null)
269 {
270 	if (!result)
271 		throw new SdlException("SDL error: " ~ (message ? message ~ ": " : "") ~ to!string(SDL_GetError()));
272 	return result;
273 }
274 
275 /// Translation table from SDL key indices to `Key`.
276 Key[SDL_NUM_SCANCODES] sdlKeys;
277 
278 shared static this()
279 {
280 	sdlKeys[SDL_SCANCODE_UP      ] = Key.up      ;
281 	sdlKeys[SDL_SCANCODE_DOWN    ] = Key.down    ;
282 	sdlKeys[SDL_SCANCODE_LEFT    ] = Key.left    ;
283 	sdlKeys[SDL_SCANCODE_RIGHT   ] = Key.right   ;
284 	sdlKeys[SDL_SCANCODE_PAGEUP  ] = Key.pageUp  ;
285 	sdlKeys[SDL_SCANCODE_PAGEDOWN] = Key.pageDown;
286 	sdlKeys[SDL_SCANCODE_HOME    ] = Key.home    ;
287 	sdlKeys[SDL_SCANCODE_END     ] = Key.end     ;
288 	sdlKeys[SDL_SCANCODE_SPACE   ] = Key.space   ;
289 	sdlKeys[SDL_SCANCODE_ESCAPE  ] = Key.esc     ;
290 }