summary refs log tree commit diff
path: root/src/Window_X11.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Window_X11.c
initial commit
Diffstat (limited to 'src/Window_X11.c')
-rw-r--r--src/Window_X11.c1459
1 files changed, 1459 insertions, 0 deletions
diff --git a/src/Window_X11.c b/src/Window_X11.c
new file mode 100644
index 0000000..d38b4af
--- /dev/null
+++ b/src/Window_X11.c
@@ -0,0 +1,1459 @@
+#include "Core.h"
+#if CC_WIN_BACKEND == CC_WIN_BACKEND_X11
+#include "_WindowBase.h"
+#include "String.h"
+#include "Funcs.h"
+#include "Bitmap.h"
+#include "Options.h"
+#include "Errors.h"
+#include "Utils.h"
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/XKBlib.h>
+#include <X11/XF86keysym.h>
+#ifdef CC_BUILD_XINPUT2
+#include <X11/extensions/XInput2.h>
+#endif
+#include <stdio.h>
+
+#ifdef X_HAVE_UTF8_STRING
+#define CC_BUILD_XIM
+/* XIM support based off details described in */
+/* https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/ */
+#endif
+
+#define _NET_WM_STATE_REMOVE 0
+#define _NET_WM_STATE_ADD    1
+#define _NET_WM_STATE_TOGGLE 2
+
+static Display* win_display;
+static Window win_rootWin, win_handle;
+static XVisualInfo win_visual;
+#ifdef CC_BUILD_XIM
+static XIM win_xim;
+static XIC win_xic;
+#endif
+ 
+static Atom wm_destroy, net_wm_state, net_wm_ping;
+static Atom net_wm_state_minimized;
+static Atom net_wm_state_fullscreen;
+
+static Atom xa_clipboard, xa_targets, xa_utf8_string, xa_data_sel;
+static Atom xa_atom = 4;
+static cc_bool grabCursor;
+static long win_eventMask = StructureNotifyMask | /* SubstructureNotifyMask | */ 
+	ExposureMask      | KeyReleaseMask  | KeyPressMask    | KeymapStateMask   | 
+	PointerMotionMask | FocusChangeMask | ButtonPressMask | ButtonReleaseMask | 
+	EnterWindowMask   | LeaveWindowMask | PropertyChangeMask;
+
+static int MapNativeKey(KeySym key, unsigned int state) {
+	if (key >= XK_0 && key <= XK_9) { return '0' + (key - XK_0); }
+	if (key >= XK_A && key <= XK_Z) { return 'A' + (key - XK_A); }
+	if (key >= XK_a && key <= XK_z) { return 'A' + (key - XK_a); }
+
+	if (key >= XK_F1 && key <= XK_F24)    { return CCKEY_F1  + (key - XK_F1); }
+	if (key >= XK_KP_0 && key <= XK_KP_9) { return CCKEY_KP0 + (key - XK_KP_0); }
+
+	/* Same Num Lock behaviour as Windows and text editors */
+	if (key >= XK_KP_Home && key <= XK_KP_Delete && !(state & Mod2Mask)) {
+		if (key == XK_KP_Home) return CCKEY_HOME;
+		if (key == XK_KP_Up)   return CCKEY_UP;
+		if (key == XK_KP_Page_Up) return CCKEY_PAGEUP;
+
+		if (key == XK_KP_Left)   return CCKEY_LEFT;
+		if (key == XK_KP_Insert) return CCKEY_INSERT;
+		if (key == XK_KP_Right)  return CCKEY_RIGHT;
+
+		if (key == XK_KP_End)  return CCKEY_END;
+		if (key == XK_KP_Down) return CCKEY_DOWN;
+		if (key == XK_KP_Page_Down) return CCKEY_PAGEDOWN;
+	}
+	
+	switch (key) {
+		case XF86XK_AudioLowerVolume: return CCKEY_VOLUME_DOWN;
+		case XF86XK_AudioMute:        return CCKEY_VOLUME_MUTE;
+		case XF86XK_AudioRaiseVolume: return CCKEY_VOLUME_UP;
+		
+		case XF86XK_AudioPlay: return CCKEY_MEDIA_PLAY;
+		case XF86XK_AudioStop: return CCKEY_MEDIA_STOP;
+		case XF86XK_AudioPrev: return CCKEY_MEDIA_PREV;
+		case XF86XK_AudioNext: return CCKEY_MEDIA_NEXT;
+		
+		case XF86XK_HomePage:   return CCKEY_BROWSER_HOME;
+		case XF86XK_Mail:       return CCKEY_LAUNCH_MAIL;
+		case XF86XK_Search:     return CCKEY_BROWSER_SEARCH;
+		case XF86XK_Calculator: return CCKEY_LAUNCH_CALC;
+		
+		case XF86XK_Back:       return CCKEY_BROWSER_PREV;
+		case XF86XK_Forward:    return CCKEY_BROWSER_NEXT;
+		case XF86XK_Stop:       return CCKEY_BROWSER_STOP;
+		case XF86XK_Refresh:    return CCKEY_BROWSER_REFRESH;
+		case XF86XK_Sleep:      return CCKEY_SLEEP;
+		case XF86XK_Favorites:  return CCKEY_BROWSER_FAVORITES;
+		case XF86XK_AudioMedia: return CCKEY_LAUNCH_MEDIA;
+		case XF86XK_MyComputer: return CCKEY_LAUNCH_APP1;
+	}
+
+	/* A chromebook user reported issues with pressing some keys: */
+	/*  tilde - "Unknown key press: (8000060, 800007E) */
+	/*  quote - "Unknown key press: (8000027, 8000022) */
+	/* Note if 8000 is stripped, you get '0060' (XK_grave) and 0027 (XK_apostrophe) */
+	/*   ChromeOS seems to also mask to 0xFFFF, so I also do so here */
+	/* https://chromium.googlesource.com/chromium/src/+/lkgr/ui/events/keycodes/keyboard_code_conversion_x.cc */
+	key &= 0xFFFF;
+
+	switch (key) {
+		case XK_Escape: return CCKEY_ESCAPE;
+		case XK_Return: return CCKEY_ENTER;
+		case XK_space: return CCKEY_SPACE;
+		case XK_BackSpace: return CCKEY_BACKSPACE;
+
+		case XK_Shift_L: return CCKEY_LSHIFT;
+		case XK_Shift_R: return CCKEY_RSHIFT;
+		case XK_Alt_L: return CCKEY_LALT;
+		case XK_Alt_R: return CCKEY_RALT;
+		case XK_Control_L: return CCKEY_LCTRL;
+		case XK_Control_R: return CCKEY_RCTRL;
+		case XK_Super_L: return CCKEY_LWIN;
+		case XK_Super_R: return CCKEY_RWIN;
+		case XK_Meta_L: return CCKEY_LWIN;
+		case XK_Meta_R: return CCKEY_RWIN;
+
+		case XK_Menu:  return CCKEY_MENU;
+		case XK_Tab:   return CCKEY_TAB;
+		case XK_minus: return CCKEY_MINUS;
+		case XK_plus:  return CCKEY_EQUALS;
+		case XK_equal: return CCKEY_EQUALS;
+
+		case XK_Caps_Lock: return CCKEY_CAPSLOCK;
+		case XK_Num_Lock:  return CCKEY_NUMLOCK;
+
+		case XK_Pause: return CCKEY_PAUSE;
+		case XK_Break: return CCKEY_PAUSE;
+		case XK_Scroll_Lock: return CCKEY_SCROLLLOCK;
+		case XK_Insert:  return CCKEY_INSERT;
+		case XK_Print:   return CCKEY_PRINTSCREEN;
+		case XK_Sys_Req: return CCKEY_PRINTSCREEN;
+
+		case XK_backslash: return CCKEY_BACKSLASH;
+		case XK_bar:       return CCKEY_BACKSLASH;
+		case XK_braceleft:    return CCKEY_LBRACKET;
+		case XK_bracketleft:  return CCKEY_LBRACKET;
+		case XK_braceright:   return CCKEY_RBRACKET;
+		case XK_bracketright: return CCKEY_RBRACKET;
+		case XK_colon:      return CCKEY_SEMICOLON;
+		case XK_semicolon:  return CCKEY_SEMICOLON;
+		case XK_quoteright: return CCKEY_QUOTE;
+		case XK_quotedbl:   return CCKEY_QUOTE;
+		case XK_quoteleft:  return CCKEY_TILDE;
+		case XK_asciitilde: return CCKEY_TILDE;
+
+		case XK_comma: return CCKEY_COMMA;
+		case XK_less:  return CCKEY_COMMA;
+		case XK_period:  return CCKEY_PERIOD;
+		case XK_greater: return CCKEY_PERIOD;
+		case XK_slash:    return CCKEY_SLASH;
+		case XK_question: return CCKEY_SLASH;
+
+		case XK_Left:  return CCKEY_LEFT;
+		case XK_Down:  return CCKEY_DOWN;
+		case XK_Right: return CCKEY_RIGHT;
+		case XK_Up:    return CCKEY_UP;
+
+		case XK_Delete: return CCKEY_DELETE;
+		case XK_Home:   return CCKEY_HOME;
+		case XK_End:    return CCKEY_END;
+		case XK_Page_Up:   return CCKEY_PAGEUP;
+		case XK_Page_Down: return CCKEY_PAGEDOWN;
+
+		case XK_KP_Add: return CCKEY_KP_PLUS;
+		case XK_KP_Subtract: return CCKEY_KP_MINUS;
+		case XK_KP_Multiply: return CCKEY_KP_MULTIPLY;
+		case XK_KP_Divide:  return CCKEY_KP_DIVIDE;
+		case XK_KP_Decimal: return CCKEY_KP_DECIMAL;
+		case XK_KP_Insert: return CCKEY_KP0;
+		case XK_KP_End:  return CCKEY_KP1;
+		case XK_KP_Down: return CCKEY_KP2;
+		case XK_KP_Page_Down: return CCKEY_KP3;
+		case XK_KP_Left:  return CCKEY_KP4;
+		case XK_KP_Begin: return CCKEY_KP5;
+		case XK_KP_Right: return CCKEY_KP6;
+		case XK_KP_Home: return CCKEY_KP7;
+		case XK_KP_Up:   return CCKEY_KP8;
+		case XK_KP_Page_Up: return CCKEY_KP9;
+		case XK_KP_Delete:  return CCKEY_KP_DECIMAL;
+		case XK_KP_Enter:   return CCKEY_KP_ENTER;
+		
+		case XK_ISO_Level3_Shift: return CCKEY_RALT; /* AltGr mode switch on some European keyboard layouts */
+	}
+	return INPUT_NONE;
+}
+
+/* NOTE: This may not be entirely accurate, because user can configure keycode mappings */
+static const cc_uint8 keycodeMap[136] = {
+/* 00 */ 0, 0, 0, 0, 0, 0, 0, 0, 
+/* 08 */ 0, CCKEY_ESCAPE, '1', '2', '3', '4', '5', '6',
+/* 10 */ '7', '8', '9', '0', CCKEY_MINUS, CCKEY_EQUALS, CCKEY_BACKSPACE, CCKEY_TAB, 
+/* 18 */ 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I',
+/* 20 */ 'O',  'P', CCKEY_LBRACKET, CCKEY_RBRACKET, CCKEY_ENTER, CCKEY_LCTRL, 'A', 'S', 
+/* 28 */ 'D', 'F', 'G', 'H',  'J', 'K', 'L', CCKEY_SEMICOLON,
+/* 30 */ CCKEY_QUOTE, CCKEY_TILDE, CCKEY_LSHIFT, CCKEY_BACKSLASH, 'Z', 'X', 'C', 'V', 
+/* 38 */ 'B', 'N', 'M', CCKEY_PERIOD, CCKEY_COMMA, CCKEY_SLASH, CCKEY_RSHIFT, CCKEY_KP_MULTIPLY,
+/* 40 */ CCKEY_LALT, CCKEY_SPACE, CCKEY_CAPSLOCK, CCKEY_F1, CCKEY_F2, CCKEY_F3, CCKEY_F4, CCKEY_F5, 
+/* 48 */ CCKEY_F6, CCKEY_F7, CCKEY_F8, CCKEY_F9, CCKEY_F10, CCKEY_NUMLOCK, CCKEY_SCROLLLOCK, CCKEY_KP7,
+/* 50 */ CCKEY_KP8, CCKEY_KP9, CCKEY_KP_MINUS, CCKEY_KP4, CCKEY_KP5, CCKEY_KP6, CCKEY_KP_PLUS, CCKEY_KP1, 
+/* 58 */ CCKEY_KP2, CCKEY_KP3, CCKEY_KP0, CCKEY_KP_DECIMAL, 0, 0, 0, CCKEY_F11,
+/* 60 */ CCKEY_F12, 0, 0, 0, 0, 0, 0, 0, 
+/* 68 */ 0, 0, 0, 0, CCKEY_RALT, CCKEY_RCTRL, CCKEY_HOME, CCKEY_UP, 
+/* 70 */ CCKEY_PAGEUP, CCKEY_LEFT, CCKEY_RIGHT, CCKEY_END, CCKEY_DOWN, CCKEY_PAGEDOWN, CCKEY_INSERT, CCKEY_DELETE, 
+/* 78 */ 0, 0, 0, 0, 0, 0, 0, CCKEY_PAUSE, 
+/* 80 */ 0, 0, 0, 0, 0, CCKEY_LWIN, 0, CCKEY_RWIN
+};
+
+static int MapNativeKeycode(unsigned int keycode) {
+	return keycode < Array_Elems(keycodeMap) ? keycodeMap[keycode] : 0;
+}
+
+static void RegisterAtoms(void) {
+	Display* display = win_display;
+	wm_destroy = XInternAtom(display, "WM_DELETE_WINDOW", true);
+	net_wm_state = XInternAtom(display, "_NET_WM_STATE", false);
+	net_wm_ping  = XInternAtom(display, "_NET_WM_PING",  false);
+	net_wm_state_minimized  = XInternAtom(display, "_NET_WM_STATE_MINIMIZED",  false);
+	net_wm_state_fullscreen = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", false);
+
+	xa_clipboard   = XInternAtom(display, "CLIPBOARD",   false);
+	xa_targets     = XInternAtom(display, "TARGETS",     false);
+	xa_utf8_string = XInternAtom(display, "UTF8_STRING", false);
+	xa_data_sel    = XInternAtom(display, "CC_SEL_DATA", false);
+}
+
+static void RefreshWindowBounds(int width, int height) {
+	if (width != Window_Main.Width || height != Window_Main.Height) {
+		Window_Main.Width  = width;
+		Window_Main.Height = height;
+		Event_RaiseVoid(&WindowEvents.Resized);
+	}
+}
+
+typedef int (*X11_ErrorHandler)(Display* dpy, XErrorEvent* ev);
+typedef int (*X11_IOErrorHandler)(Display* dpy);
+static X11_ErrorHandler   realXErrorHandler;
+static X11_IOErrorHandler realXIOErrorHandler;
+
+static void LogXErrorCore(const char* msg) {
+	char traceBuffer[2048];
+	cc_string trace;
+	Platform_LogConst(msg);
+
+	String_InitArray(trace, traceBuffer);
+	Logger_Backtrace(&trace, NULL);
+	Platform_Log(traceBuffer, trace.length);
+}
+
+static int LogXError(Display* dpy, XErrorEvent* ev) {
+	LogXErrorCore("== unhandled X11 error ==");
+	return realXErrorHandler(dpy, ev);
+}
+
+static int LogXIOError(Display* dpy) {
+	LogXErrorCore("== unhandled XIO error ==");
+	return realXIOErrorHandler(dpy);
+}
+
+static void HookXErrors(void) {
+	realXErrorHandler   = XSetErrorHandler(LogXError);
+	realXIOErrorHandler = XSetIOErrorHandler(LogXIOError);
+}
+
+
+/*########################################################################################################################*
+*--------------------------------------------------Public implementation--------------------------------------------------*
+*#########################################################################################################################*/
+#if defined CC_BUILD_EGL || !(CC_GFX_BACKEND & CC_GFX_BACKEND_GL_MASK)
+static XVisualInfo GLContext_SelectVisual(void) {
+	XVisualInfo info;
+	cc_result res;
+	int screen = DefaultScreen(win_display);
+
+	res = XMatchVisualInfo(win_display, screen, 24, TrueColor, &info) ||
+		  XMatchVisualInfo(win_display, screen, 32, TrueColor, &info);
+
+	if (!res) Logger_Abort("Selecting visual");
+	return info;
+}
+#else
+static XVisualInfo GLContext_SelectVisual(void);
+#endif
+
+void Window_PreInit(void) { }
+void Window_Init(void) {
+	Display* display = XOpenDisplay(NULL);
+	int screen;
+
+	if (!display) Logger_Abort("Failed to open the X11 display. No X server running?");
+	screen = DefaultScreen(display);
+	HookXErrors();
+
+	win_display = display;
+	win_rootWin = RootWindow(display, screen);
+	Input.Sources = INPUT_SOURCE_NORMAL;
+
+	/* TODO: Use Xinerama and XRandR for querying these */
+	DisplayInfo.Width  = DisplayWidth(display,  screen);
+	DisplayInfo.Height = DisplayHeight(display, screen);
+	DisplayInfo.Depth  = DefaultDepth(display,  screen);
+	DisplayInfo.ScaleX = 1;
+	DisplayInfo.ScaleY = 1;
+}
+
+void Window_Free(void) { }
+
+#ifdef CC_BUILD_ICON
+/* See misc/linux/linux_icon_gen.cs for how to generate this file */
+#include "../misc/linux/CCIcon_X11.h"
+
+static void ApplyIcon(void) {
+	Atom net_wm_icon = XInternAtom(win_display, "_NET_WM_ICON", false);
+	Atom xa_cardinal = XInternAtom(win_display, "CARDINAL", false);
+	
+	XChangeProperty(win_display, win_handle, net_wm_icon, xa_cardinal, 32, PropModeReplace, 
+					(unsigned char*)CCIcon_Data, CCIcon_Size);
+}
+#else
+static void ApplyIcon(void) { }
+#endif
+
+static void DoCreateWindow(int width, int height) {
+	XSetWindowAttributes attributes = { 0 };
+	XSizeHints hints = { 0 };
+	Atom protocols[2];
+	int supported, x, y;
+	Window focus;
+	int focusRevert;
+
+	x = Display_CentreX(width);
+	y = Display_CentreY(height);
+	RegisterAtoms();
+	win_visual = GLContext_SelectVisual();
+
+	Platform_Log1("Created window (visual id: %h)", &win_visual.visualid);
+	attributes.colormap   = XCreateColormap(win_display, win_rootWin, win_visual.visual, AllocNone);
+	attributes.event_mask = win_eventMask;
+
+	win_handle = XCreateWindow(win_display, win_rootWin, x, y, width, height,
+		0, win_visual.depth /* CopyFromParent*/, InputOutput, win_visual.visual, 
+		CWColormap | CWEventMask, &attributes);
+	if (!win_handle) Logger_Abort("XCreateWindow failed");
+
+#ifdef CC_BUILD_XIM
+	win_xim = XOpenIM(win_display, NULL, NULL, NULL);
+	win_xic = XCreateIC(win_xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+						XNClientWindow, win_handle, NULL);
+#endif
+
+	/* Set hints to try to force WM to create window at requested x,y */
+	/* Without this, some WMs will instead place the window whereever */
+	hints.base_width  = width;
+	hints.base_height = height;
+	hints.flags = PSize | PPosition;
+	XSetWMNormalHints(win_display, win_handle, &hints);
+
+	/* Register for window destroy notification */
+	protocols[0] = wm_destroy;
+	protocols[1] = net_wm_ping;
+	XSetWMProtocols(win_display, win_handle, protocols, 2);
+
+	/* Request that auto-repeat is only set on devices that support it physically.
+	   This typically means that it's turned off for keyboards (which is what we want).
+	   We prefer this method over XAutoRepeatOff/On, because the latter needs to
+	   be reset before the program exits. */
+	XkbSetDetectableAutoRepeat(win_display, true, &supported);
+
+	RefreshWindowBounds(width, height);
+	Window_Main.Exists = true;
+	Window_Main.Handle = (void*)win_handle;
+	grabCursor = Options_GetBool(OPT_GRAB_CURSOR, false);
+	
+	/* So right name appears in e.g. Ubuntu Unity launchbar */
+	XClassHint hint = { 0 };
+	#ifdef CC_BUILD_FLATPAK
+          hint.res_name   = "net.classicube.flatpak.client";
+          hint.res_class  = "net.classicube.flatpak.client";
+        #else
+	  hint.res_name   = GAME_APP_TITLE;
+          hint.res_class  = GAME_APP_TITLE;
+        #endif
+	XSetClassHint(win_display, win_handle, &hint);
+	ApplyIcon();
+
+	/* Check for focus initially, in case WM doesn't send a FocusIn event */
+	XGetInputFocus(win_display, &focus, &focusRevert);
+	if (focus == win_handle) Window_Main.Focused = true;
+}
+void Window_Create2D(int width, int height) { DoCreateWindow(width, height); }
+void Window_Create3D(int width, int height) { DoCreateWindow(width, height); }
+
+void Window_SetTitle(const cc_string* title) {
+	char str[NATIVE_STR_LEN];
+	String_EncodeUtf8(str, title);
+	XStoreName(win_display, win_handle, str);
+}
+
+static char clipboard_copy_buffer[256];
+static char clipboard_paste_buffer[256];
+static cc_string clipboard_copy_text  = String_FromArray(clipboard_copy_buffer);
+static cc_string clipboard_paste_text = String_FromArray(clipboard_paste_buffer);
+static cc_bool clipboard_paste_received;
+
+void Clipboard_GetText(cc_string* value) {
+	Window owner = XGetSelectionOwner(win_display, xa_clipboard);
+	int i;
+	if (!owner) return; /* no window owner */
+
+	XConvertSelection(win_display, xa_clipboard, xa_utf8_string, xa_data_sel, win_handle, 0);
+	clipboard_paste_received    = false;
+	clipboard_paste_text.length = 0;
+
+	/* wait up to 1 second for SelectionNotify event to arrive */
+	for (i = 0; i < 100; i++) {
+		Window_ProcessEvents(0.0);
+		if (clipboard_paste_received) {
+			String_AppendString(value, &clipboard_paste_text);
+			return;
+		} else {
+			Thread_Sleep(10);
+		}
+	}
+}
+
+void Clipboard_SetText(const cc_string* value) {
+	String_Copy(&clipboard_copy_text, value);
+	XSetSelectionOwner(win_display, xa_clipboard, win_handle, 0);
+}
+
+int Window_GetWindowState(void) {
+	cc_bool fullscreen = false, minimised = false;
+	Atom prop_type;
+	unsigned long items, after;
+	unsigned char* data = NULL;
+	int i, prop_format;
+	Atom* list;
+
+	XGetWindowProperty(win_display, win_handle,
+		net_wm_state, 0, 256, false, xa_atom, &prop_type,
+		&prop_format, &items, &after, &data);
+
+	if (!data) return WINDOW_STATE_NORMAL;
+	list = (Atom*)data;
+		
+	for (i = 0; i < items; i++) {
+		Atom atom = list[i];
+
+		if (atom == net_wm_state_minimized) {
+			minimised  = true;
+		} else if (atom == net_wm_state_fullscreen) {
+			fullscreen = true;
+		}
+	}
+	XFree(data);
+
+	if (fullscreen) return WINDOW_STATE_FULLSCREEN;
+	if (minimised)  return WINDOW_STATE_MINIMISED;
+	return WINDOW_STATE_NORMAL;
+}
+
+static void ToggleFullscreen(long op) {
+	XEvent ev = { 0 };
+	ev.xclient.type   = ClientMessage;
+	ev.xclient.window = win_handle;
+	ev.xclient.message_type = net_wm_state;
+	ev.xclient.format = 32;
+	ev.xclient.data.l[0] = op;
+	ev.xclient.data.l[1] = net_wm_state_fullscreen;
+
+	XSendEvent(win_display, win_rootWin, false,
+		SubstructureRedirectMask | SubstructureNotifyMask, &ev);
+	XSync(win_display, false);
+	XRaiseWindow(win_display, win_handle);
+	Window_ProcessEvents(0.0);
+}
+
+cc_result Window_EnterFullscreen(void) {
+	ToggleFullscreen(_NET_WM_STATE_ADD); return 0;
+}
+cc_result Window_ExitFullscreen(void) {
+	ToggleFullscreen(_NET_WM_STATE_REMOVE); return 0;
+}
+
+int Window_IsObscured(void) { return 0; }
+
+void Window_Show(void) { XMapWindow(win_display, win_handle); }
+
+void Window_SetSize(int width, int height) {
+	XResizeWindow(win_display, win_handle, width, height);
+	Window_ProcessEvents(0.0);
+}
+
+void Window_RequestClose(void) {
+	XEvent ev = { 0 };
+	ev.type = ClientMessage;
+	ev.xclient.format  = 32;
+	ev.xclient.display = win_display;
+	ev.xclient.window  = win_handle;
+	ev.xclient.data.l[0] = wm_destroy;
+	
+	XSendEvent(win_display, win_handle, false, 0, &ev);
+	XFlush(win_display);
+}
+
+static int MapNativeMouse(int button) {
+	if (button == 1) return CCMOUSE_L;
+	if (button == 2) return CCMOUSE_M;
+	if (button == 3) return CCMOUSE_R;
+
+	if (button ==  8) return CCMOUSE_X1;
+	if (button ==  9) return CCMOUSE_X2;
+	if (button == 10) return CCMOUSE_X3;
+	if (button == 11) return CCMOUSE_X4;
+	if (button == 12) return CCMOUSE_X5;
+	if (button == 13) return CCMOUSE_X6;
+
+	/* Mouse horizontal and vertical scroll */
+	if (button >= 4 && button <= 7) return 0;
+	Platform_Log1("Unknown mouse button: %i", &button);
+	return 0;
+}
+
+static int TryGetKey(XKeyEvent* ev) {
+	KeySym keysym1 = XLookupKeysym(ev, 0);
+	KeySym keysym2 = XLookupKeysym(ev, 1);
+
+	int key = MapNativeKey(keysym1, ev->state);
+	if (!key) key = MapNativeKey(keysym2, ev->state);
+	if (key) return key;
+
+	Platform_Log3("Unknown key %i (%x, %x)", &ev->keycode, &keysym1, &keysym2);
+	/* The user may be using a keyboard layout such as cryllic - */
+	/*   fallback to trying to conver the raw scancodes instead */
+	return MapNativeKeycode(ev->keycode);
+}
+
+static Atom Window_GetSelectionProperty(XEvent* e) {
+	Atom prop = e->xselectionrequest.property;
+	if (prop) return prop;
+
+	/* For obsolete clients. See ICCCM spec, selections chapter for reasoning. */
+	return e->xselectionrequest.target;
+}
+
+static Bool FilterEvent(Display* d, XEvent* e, XPointer w) { 
+	return
+		e->xany.window == (Window)w ||
+		!e->xany.window || /* KeymapNotify events don't have a window */
+		e->type == GenericEvent; /* For XInput events */
+}
+
+static void HandleWMDestroy(void) {
+	Platform_LogConst("Exit message received.");
+	Event_RaiseVoid(&WindowEvents.Closing);
+
+	/* sync and discard all events queued */
+	XSync(win_display, true);
+	XDestroyWindow(win_display, win_handle);
+	Window_Main.Exists = false;
+}
+
+static void HandleWMPing(XEvent* e) {
+	e->xany.window = win_rootWin;
+	XSendEvent(win_display, win_rootWin, false,
+		SubstructureRedirectMask | SubstructureNotifyMask, e);
+}
+static void HandleGenericEvent(XEvent* e);
+
+void Window_ProcessEvents(float delta) {
+	XEvent e;
+	Window focus;
+	int focusRevert;
+	int i, btn, key, status;
+
+	while (Window_Main.Exists) {
+		if (!XCheckIfEvent(win_display, &e, FilterEvent, (XPointer)win_handle)) break;
+		if (XFilterEvent(&e, None) == True) continue;
+
+		switch (e.type) {
+		case GenericEvent:
+			HandleGenericEvent(&e); break;
+		case ClientMessage:
+			if (e.xclient.data.l[0] == wm_destroy) {
+				HandleWMDestroy();
+			} else if (e.xclient.data.l[0] == net_wm_ping) {
+				HandleWMPing(&e);
+			}
+			break;
+
+		case DestroyNotify:
+			Platform_LogConst("Window destroyed");
+			Window_Main.Exists = false;
+			break;
+
+		case ConfigureNotify:
+			RefreshWindowBounds(e.xconfigure.width, e.xconfigure.height);
+			break;
+
+		case Expose:
+			if (e.xexpose.count == 0) Event_RaiseVoid(&WindowEvents.RedrawNeeded);
+			break;
+
+		case LeaveNotify:
+			XGetInputFocus(win_display, &focus, &focusRevert);
+			if (focus == PointerRoot) {
+				Window_Main.Focused = false; Event_RaiseVoid(&WindowEvents.FocusChanged);
+			}
+			break;
+
+		case EnterNotify:
+			XGetInputFocus(win_display, &focus, &focusRevert);
+			if (focus == PointerRoot) {
+				Window_Main.Focused = true; Event_RaiseVoid(&WindowEvents.FocusChanged);
+			}
+			break;
+
+		case KeyPress:
+		{
+			char data[64];
+			key = TryGetKey(&e.xkey);
+			if (key) Input_SetPressed(key);
+			
+#ifdef CC_BUILD_XIM
+			cc_codepoint cp;
+			char* chars = data;
+			Status status_type;
+
+			status = Xutf8LookupString(win_xic, &e.xkey, data, Array_Elems(data), NULL, &status_type);
+			while (status > 0) {
+				i = Convert_Utf8ToCodepoint(&cp, chars, status);
+				if (!i) break;
+
+				Event_RaiseInt(&InputEvents.Press, cp);
+				status -= i; chars += i;
+			}
+#else
+			/* Treat the 8 bit characters as first 256 unicode codepoints */
+			/* This only really works for latin keys (e.g. so some finnish keys still work) */
+			status = XLookupString(&e.xkey, data, Array_Elems(data), NULL, NULL);
+			for (i = 0; i < status; i++) {
+				Event_RaiseInt(&InputEvents.Press, (cc_uint8)data[i]);
+			}
+#endif
+		} break;
+
+		case KeyRelease:
+			key = TryGetKey(&e.xkey);
+			if (key) Input_SetReleased(key);
+			break;
+
+		case ButtonPress:
+			btn = MapNativeMouse(e.xbutton.button);
+			if (btn) Input_SetPressed(btn);
+			else if (e.xbutton.button == 4) Mouse_ScrollVWheel( +1);
+			else if (e.xbutton.button == 5) Mouse_ScrollVWheel( -1);
+			else if (e.xbutton.button == 6) Mouse_ScrollHWheel(+1);
+			else if (e.xbutton.button == 7) Mouse_ScrollHWheel(-1);
+			break;
+
+		case ButtonRelease:
+			btn = MapNativeMouse(e.xbutton.button);
+			if (btn) Input_SetReleased(btn);
+			break;
+
+		case MotionNotify:
+			Pointer_SetPosition(0, e.xmotion.x, e.xmotion.y);
+			break;
+
+		case FocusIn:
+		case FocusOut:
+			/* Don't lose focus when another app grabs key or mouse */
+			if (e.xfocus.mode == NotifyGrab || e.xfocus.mode == NotifyUngrab) break;
+
+			Window_Main.Focused = e.type == FocusIn;
+			Event_RaiseVoid(&WindowEvents.FocusChanged);
+			/* TODO: Keep track of keyboard when focus is lost */
+			if (!Window_Main.Focused) Input_Clear();
+			break;
+
+		case MappingNotify:
+			if (e.xmapping.request == MappingModifier || e.xmapping.request == MappingKeyboard) {
+				Platform_LogConst("keyboard mapping refreshed");
+				XRefreshKeyboardMapping(&e.xmapping);
+			}
+			break;
+
+		case PropertyNotify:
+			if (e.xproperty.atom == net_wm_state) {
+				Event_RaiseVoid(&WindowEvents.StateChanged);
+			}
+			break;
+
+		case SelectionNotify:
+			if (e.xselection.selection == xa_clipboard && e.xselection.target == xa_utf8_string && e.xselection.property == xa_data_sel) {
+				Atom prop_type;
+				int prop_format;
+				unsigned long items, after;
+				cc_uint8* data = NULL;
+
+				XGetWindowProperty(win_display, win_handle, xa_data_sel, 0, 1024, false, 0,
+					&prop_type, &prop_format, &items, &after, &data);
+				XDeleteProperty(win_display, win_handle, xa_data_sel);
+
+				if (data && items && prop_type == xa_utf8_string) {
+					clipboard_paste_received    = true;
+					clipboard_paste_text.length = 0;
+					String_AppendUtf8(&clipboard_paste_text, data, items);
+				}
+				if (data) XFree(data);
+			}
+			break;
+
+		case SelectionRequest:
+		{
+			XEvent reply = { 0 };
+			reply.xselection.type = SelectionNotify;
+			reply.xselection.send_event = true;
+			reply.xselection.display = win_display;
+			reply.xselection.requestor = e.xselectionrequest.requestor;
+			reply.xselection.selection = e.xselectionrequest.selection;
+			reply.xselection.target = e.xselectionrequest.target;
+			reply.xselection.property = 0;
+			reply.xselection.time = e.xselectionrequest.time;
+
+			if (e.xselectionrequest.selection == xa_clipboard && e.xselectionrequest.target == xa_utf8_string && clipboard_copy_text.length) {
+				reply.xselection.property = Window_GetSelectionProperty(&e);
+				char str[800];
+				int len = String_EncodeUtf8(str, &clipboard_copy_text);
+
+				XChangeProperty(win_display, reply.xselection.requestor, reply.xselection.property, xa_utf8_string, 8,
+					PropModeReplace, (unsigned char*)str, len);
+			} else if (e.xselectionrequest.selection == xa_clipboard && e.xselectionrequest.target == xa_targets) {
+				reply.xselection.property = Window_GetSelectionProperty(&e);
+
+				Atom data[2] = { xa_utf8_string, xa_targets };
+				XChangeProperty(win_display, reply.xselection.requestor, reply.xselection.property, xa_atom, 32,
+					PropModeReplace, (unsigned char*)data, 2);
+			}
+			XSendEvent(win_display, e.xselectionrequest.requestor, true, 0, &reply);
+		} break;
+		}
+	}
+}
+
+void Window_ProcessGamepads(float delta) { }
+
+static void Cursor_GetRawPos(int* x, int* y) {
+	Window rootW, childW;
+	int childX, childY;
+	unsigned int mask;
+	XQueryPointer(win_display, win_rootWin, &rootW, &childW, x, y, &childX, &childY, &mask);
+}
+
+void Cursor_SetPosition(int x, int y) {
+	XWarpPointer(win_display, None, win_handle, 0, 0, 0, 0, x, y);
+	XFlush(win_display); /* TODO: not sure if XFlush call is necessary */
+}
+
+static Cursor blankCursor;
+static void Cursor_DoSetVisible(cc_bool visible) {
+	if (visible) {
+		XUndefineCursor(win_display, win_handle);
+	} else {
+		if (!blankCursor) {
+			char data  = 0;
+			XColor col = { 0 };
+			Pixmap pixmap = XCreateBitmapFromData(win_display, win_handle, &data, 1, 1);
+			blankCursor   = XCreatePixmapCursor(win_display, pixmap, pixmap, &col, &col, 0, 0);
+			XFreePixmap(win_display, pixmap);
+		}
+		XDefineCursor(win_display, win_handle, blankCursor);
+	}
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------X11 message box-----------------------------------------------------*
+*#########################################################################################################################*/
+struct X11MessageBox {
+	Window win;
+	Display* dpy;
+	GC gc;
+	unsigned long white, black, background;
+	unsigned long btnBorder, highlight, shadow;
+};
+
+static unsigned long X11_Col(struct X11MessageBox* m, cc_uint8 r, cc_uint8 g, cc_uint8 b) {
+	Colormap cmap = XDefaultColormap(m->dpy, DefaultScreen(m->dpy));
+	XColor col = { 0 };
+	col.red   = r << 8;
+	col.green = g << 8;
+	col.blue  = b << 8;
+	col.flags = DoRed | DoGreen | DoBlue;
+
+	XAllocColor(m->dpy, cmap, &col);
+	return col.pixel;
+}
+
+static void X11MessageBox_Init(struct X11MessageBox* m) {
+	m->black = BlackPixel(m->dpy, DefaultScreen(m->dpy));
+	m->white = WhitePixel(m->dpy, DefaultScreen(m->dpy));
+	m->background = X11_Col(m, 206, 206, 206);
+
+	m->btnBorder = X11_Col(m, 60,  60,  60);
+	m->highlight = X11_Col(m, 144, 144, 144);
+	m->shadow    = X11_Col(m, 49,  49,  49);
+
+	m->win = XCreateSimpleWindow(m->dpy, DefaultRootWindow(m->dpy), 0, 0, 100, 100,
+								0, m->black, m->background);
+	XSelectInput(m->dpy, m->win, ExposureMask   | StructureNotifyMask |
+							   KeyReleaseMask   | PointerMotionMask |
+								ButtonPressMask | ButtonReleaseMask );
+
+	m->gc = XCreateGC(m->dpy, m->win, 0, NULL);
+	XSetForeground(m->dpy, m->gc, m->black);
+	XSetBackground(m->dpy, m->gc, m->background);
+}
+
+static void X11MessageBox_Free(struct X11MessageBox* m) {
+	XFreeGC(m->dpy, m->gc);
+	XDestroyWindow(m->dpy, m->win);
+}
+
+struct X11Textbox {
+	int x, y, width, height;
+	int lineHeight, descent;
+	const char* text;
+};
+
+static void X11Textbox_Measure(struct X11Textbox* t, XFontStruct* font) {
+	cc_string str = String_FromReadonly(t->text), line;
+	XCharStruct overall;
+	int direction, ascent, descent, lines = 0;
+
+	for (; str.length; lines++) {
+		String_UNSAFE_SplitBy(&str, '\n', &line);
+		XTextExtents(font, line.buffer, line.length, &direction, &ascent, &descent, &overall);
+		t->width = max(overall.width, t->width);
+	}
+
+	t->lineHeight = ascent + descent;
+	t->descent    = descent;
+	t->height     = t->lineHeight * lines;
+}
+
+static void X11Textbox_Draw(struct X11Textbox* t, struct X11MessageBox* m) {
+	cc_string str = String_FromReadonly(t->text), line;
+	int y = t->y + t->lineHeight - t->descent; /* TODO: is -descent even right? */
+
+	for (; str.length; y += t->lineHeight) {
+		String_UNSAFE_SplitBy(&str, '\n', &line);
+		XDrawString(m->dpy, m->win, m->gc, t->x, y, line.buffer, line.length);		
+	}
+}
+
+struct X11Button {
+	int x, y, width, height;
+	cc_bool clicked;
+	struct X11Textbox text;
+};
+
+static void X11Button_Draw(struct X11Button* b, struct X11MessageBox* m) {
+	struct X11Textbox* t;
+	int begX, endX, begY, endY;
+
+	XSetForeground(m->dpy, m->gc,  m->btnBorder);
+	XDrawRectangle(m->dpy, m->win, m->gc, b->x, b->y,
+					b->width, b->height);
+
+	t = &b->text;
+	begX = b->x + 1; endX = b->x + b->width  - 1;
+	begY = b->y + 1; endY = b->y + b->height - 1;
+
+	if (b->clicked) {
+		XSetForeground(m->dpy, m->gc,  m->highlight);
+		XDrawRectangle(m->dpy, m->win, m->gc, begX, begY,
+						endX - begX, endY - begY);
+	} else {
+		XSetForeground(m->dpy, m->gc, m->white);
+		XDrawLine(m->dpy, m->win, m->gc, begX, begY,
+					endX - 1, begY);
+		XDrawLine(m->dpy, m->win, m->gc, begX, begY,
+					begX, endY - 1);
+
+		XSetForeground(m->dpy, m->gc, m->highlight);
+		XDrawLine(m->dpy, m->win, m->gc, begX + 1, endY - 1,
+					endX - 1, endY - 1);
+		XDrawLine(m->dpy, m->win, m->gc, endX - 1, begY + 1,
+					endX - 1, endY - 1);
+
+		XSetForeground(m->dpy, m->gc, m->shadow);
+		XDrawLine(m->dpy, m->win, m->gc, begX, endY, endX, endY);
+		XDrawLine(m->dpy, m->win, m->gc, endX, begY, endX, endY);
+	}
+
+	XSetForeground(m->dpy, m->gc, m->black);
+	t->x = b->x + b->clicked + (b->width  - t->width)  / 2;
+	t->y = b->y + b->clicked + (b->height - t->height) / 2;
+	X11Textbox_Draw(t, m);
+}
+
+static int X11Button_Contains(struct X11Button* b, int x, int y) {
+	return x >= b->x && x < (b->x + b->width) &&
+		   y >= b->y && y < (b->y + b->height);
+}
+
+static Bool X11_FilterEvent(Display* d, XEvent* e, XPointer w) { return e->xany.window == (Window)w; }
+static void X11_MessageBox(const char* title, const char* text, struct X11MessageBox* m) {
+	struct X11Button ok    = { 0 };
+	struct X11Textbox body = { 0 };
+
+	Atom protocols[2];
+	XFontStruct* font;
+	int x, y, width, height;
+	XSizeHints hints = { 0 };
+	int mouseX = -1, mouseY = -1, over;
+	XEvent e;
+
+	X11MessageBox_Init(m);
+	XMapWindow(m->dpy, m->win);
+	XStoreName(m->dpy, m->win, title);
+
+	protocols[0] = XInternAtom(m->dpy, "WM_DELETE_WINDOW", false);
+	protocols[1] = XInternAtom(m->dpy, "_NET_WM_PING",     false);
+	XSetWMProtocols(m->dpy, m->win, protocols, 2);
+
+	font = XQueryFont(m->dpy, XGContextFromGC(m->gc));
+	if (!font) return;
+
+	/* Compute size of widgets */
+	body.text = text;
+	X11Textbox_Measure(&body, font);
+	ok.text.text = "OK";
+	X11Textbox_Measure(&ok.text, font);
+	ok.width  = ok.text.width  + 70;
+	ok.height = ok.text.height + 10;
+
+	/* Compute size and position of window */
+	width  = body.width                   + 20;
+	height = body.height + 20 + ok.height + 20;
+	x = DisplayWidth (m->dpy, DefaultScreen(m->dpy))/2 -  width/2;
+	y = DisplayHeight(m->dpy, DefaultScreen(m->dpy))/2 - height/2;
+	XMoveResizeWindow(m->dpy, m->win, x, y, width, height);
+
+	/* Adjust bounds of widgets */
+	body.x = 10; body.y = 10;
+	ok.x = width/2 - ok.width/2;
+	ok.y = height  - ok.height - 10;
+
+	/* This marks the window as popup window of the main window */
+	/* http://tronche.com/gui/x/icccm/sec-4.html#WM_TRANSIENT_FOR */
+	/* Depending on WM, removes minimise and doesn't show in taskbar */
+	if (win_handle) XSetTransientForHint(m->dpy, m->win, win_handle);
+
+	XFreeFontInfo(NULL, font, 1);
+	XUnmapWindow(m->dpy, m->win); /* Make window non resizeable */
+
+	hints.flags      = PSize | PMinSize | PMaxSize;
+	hints.min_width  = hints.max_width  = hints.base_width  = width;
+	hints.min_height = hints.max_height = hints.base_height = height;
+
+	XSetWMNormalHints(m->dpy, m->win, &hints);
+	XMapRaised(m->dpy, m->win);
+	XFlush(m->dpy);
+
+    for (;;) {
+		/* The naive solution is to use XNextEvent(m->dpy, &e) here. */
+		/* However this causes issues as that removes events that */
+		/* should have been delivered to the main game window. */
+		/* (e.g. breaks initial window resize with i3 WM) */
+		XIfEvent(m->dpy, &e, X11_FilterEvent, (XPointer)m->win);
+
+		switch (e.type)
+		{
+		case ButtonPress:
+		case ButtonRelease:
+			if (e.xbutton.button != Button1) break;
+			over = X11Button_Contains(&ok, mouseX, mouseY);
+
+			if (ok.clicked && e.type == ButtonRelease) {
+				if (over) return;
+			}
+			ok.clicked = e.type == ButtonPress && over;
+			/* fallthrough to redraw window */
+
+		case Expose:
+		case MapNotify:
+			XClearWindow(m->dpy, m->win);
+			X11Textbox_Draw(&body, m);
+			X11Button_Draw(&ok, m);
+			XFlush(m->dpy);
+			break;
+
+		case KeyRelease:
+			if (XLookupKeysym(&e.xkey, 0) == XK_Escape) return;
+			break;
+
+		case ClientMessage:
+			/* { WM_DELETE_WINDOW, _NET_WM_PING } */
+			if (e.xclient.data.l[0] == protocols[0]) return;
+			if (e.xclient.data.l[0] == protocols[1]) HandleWMPing(&e);
+			break;
+
+		case MotionNotify:
+			mouseX = e.xmotion.x; mouseY = e.xmotion.y;
+			break;
+		}
+	}
+}
+
+static void ShowDialogCore(const char* title, const char* msg) {
+	struct X11MessageBox m = { 0 };
+	m.dpy = win_display;
+	
+	/* Failing to create a display means can't display a message box. */
+	/* However the user might have launched the game through terminal, */
+	/* so fallback to console instead of just dying from a segfault */
+	if (!m.dpy) {
+		Platform_LogConst("### MESSAGE ###");
+		Platform_LogConst(title);
+		Platform_LogConst(msg);
+		return;
+	}
+	
+	X11_MessageBox(title, msg, &m);
+	X11MessageBox_Free(&m);
+	XFlush(m.dpy); /* flush so window disappears immediately */
+}
+
+static cc_result OpenSaveFileDialog(const char* args, FileDialogCallback callback, const char* defaultExt) {
+	cc_string path; char pathBuffer[1024];
+	char result[4096] = { 0 };
+	int len, i;
+	/* TODO this doesn't detect when Zenity doesn't exist */
+	FILE* fp = popen(args, "r");
+	if (!fp) return 0;
+
+	/* result from zenity is normally just one string */
+	while (fgets(result, sizeof(result), fp)) { }
+	pclose(fp);
+
+	len = String_Length(result);
+	if (!len) return 0;
+
+	String_InitArray(path, pathBuffer);
+	String_AppendUtf8(&path, result, len);
+
+	/* Add default file extension if necessary */
+	if (defaultExt) {
+		cc_string file = path;
+		Utils_UNSAFE_GetFilename(&file);
+		if (String_IndexOf(&file, '.') == -1) String_AppendConst(&path, defaultExt);
+	}
+	callback(&path);
+	return 0;
+}
+
+cc_result Window_OpenFileDialog(const struct OpenFileDialogArgs* args) {
+	const char* const* filters = args->filters;
+	cc_string path; char pathBuffer[1024];
+	int i;
+
+	String_InitArray_NT(path, pathBuffer);
+	String_Format1(&path, "zenity --file-selection --file-filter='%c (", args->description);
+
+	for (i = 0; filters[i]; i++)
+	{
+		if (i) String_Append(&path, ',');
+		String_Format1(&path, "*%c", filters[i]);
+	}
+	String_AppendConst(&path, ") |");
+
+	for (i = 0; filters[i]; i++)
+	{
+		String_Format1(&path, " *%c", filters[i]);
+	}
+	String_AppendConst(&path, "'");
+
+	path.buffer[path.length] = '\0';
+	return OpenSaveFileDialog(path.buffer, args->Callback, NULL);
+}
+
+cc_result Window_SaveFileDialog(const struct SaveFileDialogArgs* args) {
+	const char* const* titles   = args->titles;
+	const char* const* fileExts = args->filters;
+	cc_string path; char pathBuffer[1024];
+	int i;
+
+	String_InitArray_NT(path, pathBuffer);
+	String_AppendConst(&path, "zenity --file-selection");
+	for (i = 0; fileExts[i]; i++)
+	{
+		String_Format3(&path, " --file-filter='%c (*%c) | *%c'", titles[i], fileExts[i], fileExts[i]);
+	}
+	String_AppendConst(&path, " --save --confirm-overwrite");
+
+	/* TODO: Utf8 encode filename */
+	if (args->defaultName.length) {
+		String_Format1(&path, " --filename='%s'", &args->defaultName);
+	}
+
+	path.buffer[path.length] = '\0';
+	return OpenSaveFileDialog(path.buffer, args->Callback, fileExts[0]);
+}
+
+static GC fb_gc;
+static XImage* fb_image;
+static void* fb_data;
+static int fb_fast;
+
+void Window_AllocFramebuffer(struct Bitmap* bmp, int width, int height) {
+	if (!fb_gc) fb_gc = XCreateGC(win_display, win_handle, 0, NULL);
+
+	bmp->scan0  = (BitmapCol*)Mem_Alloc(width * height, 4, "window pixels");
+	bmp->width  = width;
+	bmp->height = height;
+
+	/* X11 requires that the image to draw has same depth as window */
+	/* Easy for 24/32 bit case, but much trickier with other depths */
+	/*  (have to do a manual and slow second blit for other depths) */
+	fb_fast = win_visual.depth == 24 || win_visual.depth == 32;
+	fb_data = fb_fast ? bmp->scan0 : Mem_Alloc(width * height, 4, "window blit");
+
+	fb_image = XCreateImage(win_display, win_visual.visual,
+		win_visual.depth, ZPixmap, 0, fb_data,
+		width, height, 32, 0);
+}
+
+static void BlitFramebuffer(int x1, int y1, int width, int height, struct Bitmap* bmp) {
+	unsigned char* dst;
+	BitmapCol* row;
+	BitmapCol src;
+	cc_uint32 pixel;
+	int R, G, B, A;
+	int x, y;
+
+	for (y = y1; y < y1 + height; y++) {
+		row = Bitmap_GetRow(bmp, y);
+		dst = ((unsigned char*)fb_image->data) + y * fb_image->bytes_per_line;
+
+		for (x = x1; x < x1 + width; x++) {
+			src = row[x];
+			R = BitmapCol_R(src);
+			G = BitmapCol_G(src);
+			B = BitmapCol_B(src);
+			A = BitmapCol_A(src);
+
+			switch (win_visual.depth)
+			{
+			case 30: /* R10 G10 B10 A2 */
+				pixel = (R << 2) | ((G << 2) << 10) | ((B << 2) << 20) | ((A >> 6) << 30);
+				((cc_uint32*)dst)[x] = pixel;
+				break;
+			case 16: /* B5 G6 R5 */
+				pixel = (B >> 3) | ((G >> 2) <<  5) | ((R >> 3) << 11);
+				((cc_uint16*)dst)[x] = pixel;
+				break;
+			case 15: /* B5 G5 R5 */
+				pixel = (B >> 3) | ((G >> 3) <<  5) | ((R >> 3) << 10);
+				((cc_uint16*)dst)[x] = pixel;
+				break;
+			case 8:  /* B2 G3 R3 */
+				pixel = (B >> 6) | ((G >> 5) <<  2) | ((R >> 5) <<  5);
+				((cc_uint8*) dst)[x] = pixel;
+				break;
+			}
+		}
+	}
+}
+
+void Window_DrawFramebuffer(Rect2D r, struct Bitmap* bmp) {
+	/* Convert 32 bit depth to window depth when required */
+	if (!fb_fast) BlitFramebuffer(r.x, r.y, r.width, r.height, bmp);
+
+	XPutImage(win_display, win_handle, fb_gc, fb_image,
+		r.x, r.y, r.x, r.y, r.width, r.height);
+}
+
+void Window_FreeFramebuffer(struct Bitmap* bmp) {
+	XFree(fb_image);
+	Mem_Free(bmp->scan0);
+	if (bmp->scan0 != fb_data) Mem_Free(fb_data);
+}
+
+void OnscreenKeyboard_Open(struct OpenKeyboardArgs* args) { }
+void OnscreenKeyboard_SetText(const cc_string* text) { }
+void OnscreenKeyboard_Draw2D(Rect2D* r, struct Bitmap* bmp) { }
+void OnscreenKeyboard_Draw3D(void) { }
+void OnscreenKeyboard_Close(void) { }
+
+static cc_bool rawMouseInited, rawMouseSupported;
+static int xiOpcode;
+
+#ifdef CC_BUILD_XINPUT2
+static void CheckMovementDelta(double dx, double dy) {
+	/* Despite the assumption that XI_RawMotion is relative,     */
+	/*  unfortunately there's a few buggy corner cases out there */
+	/*  where absolute coordinates are provided instead.         */
+	/* The ugly code belows tries to detect these corner cases,  */
+	/*  and disables XInput2 when that happens                   */
+	static int valid, fails;
+
+	if (valid) return;
+	/* The default window resolution is 854 x 480, so if there's */
+	/*  a delta less than half of that, then it's almost certain */
+	/*  that the provided coordinates are relative.*/
+	if (dx < 300 || dy < 200) { valid = true; return; }
+
+	if (fails++ < 20) return;
+	/* Checked over 20 times now, but no relative coordinates,   */
+	/*  so give up trying to use XInput2 anymore.                */
+	Platform_LogConst("Buggy XInput2 detected, disabling it.."); 
+	rawMouseSupported = false;
+}
+
+static void HandleGenericEvent(XEvent* e) {
+	const double* values;
+	XIRawEvent* ev;
+	double dx, dy;
+
+	if (!rawMouseSupported || e->xcookie.extension != xiOpcode) return;
+	if (!XGetEventData(win_display, &e->xcookie)) return;
+
+	if (e->xcookie.evtype == XI_RawMotion && Input.RawMode) {
+		ev     = (XIRawEvent*)e->xcookie.data;
+		values = ev->raw_values;
+
+		/* Raw motion events may not always have values for both axes */
+		dx = XIMaskIsSet(ev->valuators.mask, 0) ? *values++ : 0;
+		dy = XIMaskIsSet(ev->valuators.mask, 1) ? *values++ : 0;
+
+		CheckMovementDelta(dx, dy);
+		/* Using 0.5f here makes the sensitivity about same as normal cursor movement */
+		Event_RaiseRawMove(&PointerEvents.RawMoved, dx * 0.5f, dy * 0.5f);
+	}
+	XFreeEventData(win_display, &e->xcookie);
+}
+
+static void InitRawMouse(void) {
+	XIEventMask evmask;
+	unsigned char masks[XIMaskLen(XI_LASTEVENT)] = { 0 };
+	int ev, err, major, minor;
+
+	if (!XQueryExtension(win_display, "XInputExtension", &xiOpcode, &ev, &err)) {
+		Platform_LogConst("XInput unsupported");
+		return;
+	}
+
+	/* Only XInput 2.0 is actually required. However, 2.0 has the annoying */
+	/* behaviour where raw input is NOT delivered while pointer is grabbed. */
+	/* (i.e. if you press mouse button, no more raw mouse movement events) */
+	/* http://wine.1045685.n8.nabble.com/PATCH-0-9-Implement-DInput8-mouse-using-RawInput-and-XInput2-RawEvents-only-td6016923.html */
+	/* Thankfully XInput >= 2.1 corrects this behaviour */
+	/* http://who-t.blogspot.com/2011/09/whats-new-in-xi-21-raw-events.html */
+	major = 2; minor = 2;
+	if (XIQueryVersion(win_display, &major, &minor) != Success) {
+		Platform_Log2("Only XInput %i.%i supported", &major, &minor);
+		return;
+	}
+
+	/* Sometimes XIQueryVersion will report Success, even though the */
+	/* supported version is only 2.0! So make sure to handle that. */
+	if (major < 2 || minor < 2) {
+		Platform_Log2("Only XInput %i.%i supported", &major, &minor);
+		return;
+	}
+
+	XISetMask(masks, XI_RawMotion);
+	evmask.deviceid = XIAllMasterDevices;
+	evmask.mask_len = sizeof(masks);
+	evmask.mask     = masks;
+
+	XISelectEvents(win_display, win_rootWin, &evmask, 1);
+	rawMouseSupported = true;
+}
+#else
+static void HandleGenericEvent(XEvent* e) { }
+static void InitRawMouse(void) { }
+#endif
+
+void Window_EnableRawMouse(void) {
+	DefaultEnableRawMouse();
+	if (!rawMouseInited) InitRawMouse();
+	rawMouseInited = true;
+
+	if (!grabCursor) return;
+	XGrabPointer(win_display, win_handle, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
+		GrabModeAsync, GrabModeAsync, win_handle, blankCursor, CurrentTime);
+}
+
+void Window_UpdateRawMouse(void) {
+	if (rawMouseSupported) {
+		/* Handled by XI_RawMotion generic event */
+		CentreMousePosition();
+	} else {
+		DefaultUpdateRawMouse();
+	}
+}
+
+void Window_DisableRawMouse(void) {
+	DefaultDisableRawMouse();
+	if (!grabCursor) return;
+	XUngrabPointer(win_display, CurrentTime);
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------glX OpenGL--------------------------------------------------------*
+*#########################################################################################################################*/
+#if (CC_GFX_BACKEND & CC_GFX_BACKEND_GL_MASK) && !defined CC_BUILD_EGL
+#include <GL/glx.h>
+static GLXContext ctx_handle;
+typedef int  (*FP_SWAPINTERVAL)(int interval);
+typedef Bool (*FP_QUERYRENDERER)(int attribute, unsigned int* value);
+static FP_SWAPINTERVAL swapIntervalMESA, swapIntervalSGI;
+static FP_QUERYRENDERER queryRendererMESA;
+
+void GLContext_Create(void) {
+	static const cc_string vsync_mesa = String_FromConst("GLX_MESA_swap_control");
+	static const cc_string vsync_sgi  = String_FromConst("GLX_SGI_swap_control");
+	static const cc_string info_mesa  = String_FromConst("GLX_MESA_query_renderer");
+
+	const char* raw_exts;
+	cc_string exts;
+	ctx_handle = glXCreateContext(win_display, &win_visual, NULL, true);
+
+	if (!ctx_handle) {
+		Platform_LogConst("Context create failed. Trying indirect...");
+		ctx_handle = glXCreateContext(win_display, &win_visual, NULL, false);
+	}
+	if (!ctx_handle) Logger_Abort("Failed to create OpenGL context");
+
+	if (!glXIsDirect(win_display, ctx_handle)) {
+		Platform_LogConst("== WARNING: Context is not direct ==");
+	}
+	if (!glXMakeCurrent(win_display, win_handle, ctx_handle)) {
+		Logger_Abort("Failed to make OpenGL context current.");
+	}
+
+	/* GLX may return non-null function pointers that don't actually work */
+	/* So we need to manually check the extensions string for support */
+	raw_exts = glXQueryExtensionsString(win_display, DefaultScreen(win_display));
+	exts = String_FromReadonly(raw_exts);
+
+	if (String_CaselessContains(&exts, &vsync_mesa)) {
+		swapIntervalMESA  = (FP_SWAPINTERVAL)GLContext_GetAddress("glXSwapIntervalMESA");
+	}
+	if (String_CaselessContains(&exts, &vsync_sgi)) {
+		swapIntervalSGI   = (FP_SWAPINTERVAL)GLContext_GetAddress("glXSwapIntervalSGI");
+	}
+	if (String_CaselessContains(&exts, &info_mesa)) {
+		queryRendererMESA = (FP_QUERYRENDERER)GLContext_GetAddress("glXQueryCurrentRendererIntegerMESA");
+	}
+}
+
+void GLContext_Update(void) { }
+cc_bool GLContext_TryRestore(void) { return true; }
+void GLContext_Free(void) {
+	if (!ctx_handle) return;
+	glXMakeCurrent(win_display, None, NULL);
+	glXDestroyContext(win_display, ctx_handle);
+	ctx_handle = NULL;
+}
+
+void* GLContext_GetAddress(const char* function) {
+	return (void*)glXGetProcAddress((const GLubyte*)function);
+}
+
+cc_bool GLContext_SwapBuffers(void) {
+	glXSwapBuffers(win_display, win_handle);
+	return true;
+}
+
+void GLContext_SetFpsLimit(cc_bool vsync, float minFrameMs) {
+	int res = 0;
+	if (swapIntervalMESA) {
+		res = swapIntervalMESA(vsync);
+	} else if (swapIntervalSGI) {
+		res = swapIntervalSGI(vsync);
+	}
+	if (res) Platform_Log1("Set VSync failed, error: %i", &res);
+}
+
+void GLContext_GetApiInfo(cc_string* info) {
+	unsigned int vram, acc;
+	if (!queryRendererMESA) return;
+
+	queryRendererMESA(0x8186, &acc);  /* GLX_RENDERER_ACCELERATED_MESA */
+	queryRendererMESA(0x8187, &vram); /* GLX_RENDERER_VIDEO_MEMORY_MESA */
+	String_Format2(info, "VRAM: %i MB, %c", &vram,
+		acc ? "HW accelerated" : "no HW acceleration");
+}
+
+static void GetAttribs(struct GraphicsMode* mode, int* attribs, int depth) {
+	int i = 0;
+	/* See http://www-01.ibm.com/support/knowledgecenter/ssw_aix_61/com.ibm.aix.opengl/doc/openglrf/glXChooseFBConfig.htm%23glxchoosefbconfig */
+	/* See http://www-01.ibm.com/support/knowledgecenter/ssw_aix_71/com.ibm.aix.opengl/doc/openglrf/glXChooseVisual.htm%23b5c84be452rree */
+	/* for the attribute declarations. Note that the attributes are different than those used in glxChooseVisual */
+
+	/* TODO always use RGBA? need to test 8bpp displays */
+	if (DisplayInfo.Depth >= 15) { attribs[i++] = GLX_RGBA; }
+	attribs[i++] = GLX_RED_SIZE;   attribs[i++] = mode->R;
+	attribs[i++] = GLX_GREEN_SIZE; attribs[i++] = mode->G;
+	attribs[i++] = GLX_BLUE_SIZE;  attribs[i++] = mode->B;
+	attribs[i++] = GLX_ALPHA_SIZE; attribs[i++] = mode->A;
+	attribs[i++] = GLX_DEPTH_SIZE; attribs[i++] = depth;
+
+	attribs[i++] = GLX_DOUBLEBUFFER;
+	attribs[i++] = 0;
+}
+
+static XVisualInfo GLContext_SelectVisual(void) {
+	int attribs[20];
+	int major, minor;
+	XVisualInfo* visual = NULL;
+
+	int fbcount, screen;
+	GLXFBConfig* fbconfigs;
+	XVisualInfo info;
+	struct GraphicsMode mode;
+
+	InitGraphicsMode(&mode);
+	GetAttribs(&mode, attribs, GLCONTEXT_DEFAULT_DEPTH);
+	screen = DefaultScreen(win_display);
+
+	if (!glXQueryVersion(win_display, &major, &minor)) {
+		Platform_LogConst("glXQueryVersion failed");
+	} else if (major >= 1 && minor >= 3) {
+		/* ChooseFBConfig returns an array of GLXFBConfig opaque structures */
+		fbconfigs = glXChooseFBConfig(win_display, screen, attribs, &fbcount);
+		if (fbconfigs && fbcount) {
+			/* Use the first GLXFBConfig from the fbconfigs array (best match) */
+			visual = glXGetVisualFromFBConfig(win_display, *fbconfigs);
+			XFree(fbconfigs);
+		}
+	}
+
+	if (!visual) {
+		Platform_LogConst("Falling back to glXChooseVisual.");
+		visual = glXChooseVisual(win_display, screen, attribs);
+	}
+	/* Some really old devices will only supply 16 bit depths */
+	if (!visual) {
+		GetAttribs(&mode, attribs, 16);
+		visual = glXChooseVisual(win_display, screen, attribs);
+	}
+	if (!visual) Logger_Abort("Requested GraphicsMode not available.");
+
+	info = *visual;
+	XFree(visual);
+	return info;
+}
+#endif
+#endif