module dlib.platform; import dlib.aliases; import dlib.alloc : Reset; import dlib.alloc; import dlib.util; import dlib.math; import dlibincludes; import std.typecons; import std.stdio; import core.memory; import core.thread.osthread; import core.time; import core.volatile; import core.atomic; import core.stdc.stdio : FilePtr = FILE, FRead = fread; const WINDOW_EDGE_BUFFER = 50; enum Input : u32 { None, Backspace = 0x08, Tab = 0x09, Enter = 0x0A, Escape = 0x1B, Space = 0x20, Exclamation = 0x21, DoubleQuote = 0x22, Hash = 0x23, Dollar = 0x24, Percent = 0x25, Ampersand = 0x26, SingleQuote = 0x27, LeftParen = 0x28, RightParent = 0x29, Asterisk = 0x2A, Plus = 0x2B, Comma = 0x2C, Minus = 0x2D, Period = 0x2E, Slash = 0x2F, Zero = 0x30, One = 0x31, Two = 0x32, Three = 0x33, Four = 0x34, Five = 0x35, Six = 0x36, Seven = 0x37, Eight = 0x38, Nine = 0x39, Colon = 0x3A, Semicolon = 0x3B, LessThan = 0x3C, Equals = 0x3D, GreaterThan = 0x3E, Question = 0x3F, At = 0x40, A = 0x41, B = 0x42, C = 0x43, D = 0x44, E = 0x45, F = 0x46, G = 0x47, H = 0x48, I = 0x49, J = 0x4A, K = 0x4B, L = 0x4C, M = 0x4D, N = 0x4E, O = 0x4F, P = 0x50, Q = 0x51, R = 0x52, S = 0x53, T = 0x54, U = 0x55, V = 0x56, W = 0x57, X = 0x58, Y = 0x59, Z = 0x5A, LeftBracket = 0x5B, BackSlash = 0x5C, RightBracket = 0x5D, Caret = 0x5E, Underscore = 0x5F, Grave = 0x60, a = I.A+32, b = I.B+32, c = I.C+32, d = I.D+32, e = I.E+32, f = I.F+32, g = I.G+32, h = I.H+32, i = I.I+32, j = I.J+32, k = I.K+32, l = I.L+32, m = I.M+32, n = I.N+32, o = I.O+32, p = I.P+32, q = I.Q+32, r = I.R+32, s = I.S+32, t = I.T+32, u = I.U+32, v = I.V+32, w = I.W+32, x = I.X+32, y = I.Y+32, z = I.Z+32, LeftBrace = 0x7B, VerticalBar = 0x7C, RightBrace = 0x7D, Tilde = 0x7E, Delete = 0x7F, F1 = 0x150, F2 = 0x151, F3 = 0x152, F4 = 0x153, F5 = 0x154, F6 = 0x155, F7 = 0x156, F8 = 0x157, F9 = 0x158, F10 = 0x159, F11 = 0x15A, F12 = 0x15B, PrintScreen = 0x10C, ScrollLock = 0x10D, Pause = 0x10E, Insert = 0x10F, Home = 0x110, End = 0x111, PageUp = 0x112, PageDown = 0x113, NumLock = 0x114, NumEnter = 0x10A, NumAsterisk = 0x12A, NumPlus = 0x12B, NumMinus = 0x12D, NumPeriod = 0x12E, NumSlash = 0x12F, NumZero = 0x130, NumOne = 0x131, NumTwo = 0x132, NumThree = 0x133, NumFour = 0x134, NumFive = 0x135, NumSix = 0x136, NumSeven = 0x137, NumEight = 0x138, NumNine = 0x139, LeftCtrl = 0x13A, RightCtrl = 0x13B, LeftShift = 0x13C, RightShift = 0x13D, LeftSuper = 0x13E, RightSuper = 0x13F, LeftAlt = 0x140, RightAlt = 0x141, CapsLock = 0x142, Left = 0x143, Right = 0x144, Up = 0x145, Down = 0x146, // Mouse MouseMotion = 0x147, LeftClick = 0x148, MiddleClick = 0x149, RightClick = 0x14A, }; alias I = Input; enum Modifier : u32 { None = 0x00, LeftShift = 0x01, RightShift = 0x02, LeftCtrl = 0x04, RightCtrl = 0x08, LeftAlt = 0x10, RightAlt = 0x20, } alias MD = Modifier; struct InputEvent { Input key; Modifier md; bool pressed; i32 x; i32 y; i32 rel_x; i32 rel_y; } enum ClipboardMode { Clipboard, Primary, Selection = Primary, Secondary, Max } alias CBM = ClipboardMode; struct Inputs { DLList!(InputEvent) list; Arena arena; } pragma(inline) bool Shift(Modifier md) { return cast(bool)(md & (MD.LeftShift | MD.RightShift)); } pragma(inline) bool Ctrl(Modifier md) { return cast(bool)(md & (MD.LeftCtrl | MD.RightCtrl)); } pragma(inline) bool Alt(Modifier md) { return cast(bool)(md & (MD.LeftAlt | MD.RightAlt)); } Inputs* GetEvents(PlatformWindow* window) { Lock(&window.input_mutex); Inputs* inputs = &window.inputs[window.input_idx]; window.input_idx = (window.input_idx + 1) % 2; ResetInputs(&window.inputs[window.input_idx]); Unlock(&window.input_mutex); return inputs; } Inputs* GetInputs(PlatformWindow* window) { Lock(&window.input_mutex); Inputs* inputs = window.inputs.ptr + window.input_idx; return inputs; } void ReturnInputs(PlatformWindow* window) { Unlock(&window.input_mutex); } enum SysMessageType { None, Quit, LockCursor, UnlockCursor, HideCursor, ShowCursor, } alias SMT = SysMessageType; struct Mut { bool locked; } struct TicketMut { u64 ticket; u64 next_ticket; } Mut CreateMut() { return Mut(locked: false); } bool TryLock(Mut* mut) { return cas!(MemoryOrder.acq, MemoryOrder.raw)(&mut.locked, false, true); } void Unlock(Mut* mut) { atomicStore!(MemoryOrder.rel, bool)(mut.locked, false); } TicketMut CreateTicketMut() { return TicketMut(ticket: 0, next_ticket: 0); } void Lock(TicketMut* mut) { u64 ticket = atomicFetchAdd!(MemoryOrder.acq, u64)(mut.ticket, 1); while (ticket != volatileLoad(&mut.next_ticket)) {} } u64 Unlock(TicketMut* mut) { return atomicFetchAdd!(MemoryOrder.rel, u64)(mut.next_ticket, 1); } __gshared const DNode!(SysMessage) g_sys_message; __gshared DNode!(SysMessage)* g_NIL_MSG; struct SysMessage { SysMessageType type; } struct MessageQueue { DLList!(SysMessage) list; DLList!(SysMessage) free_list; TicketMut mut; Arena arena; } MessageQueue CreateMessageQueue() { if(g_NIL_MSG == null) { g_NIL_MSG = cast(DNode!(SysMessage)*)&g_sys_message; } MessageQueue queue = { list: { first: g_NIL_MSG, last: g_NIL_MSG, }, free_list: { first: g_NIL_MSG, last: g_NIL_MSG, }, mut: CreateTicketMut(), arena: CreateArena(MB(1)), }; return queue; } pragma(inline) bool Nil(DNode!(SysMessage)* msg) { return msg == null || msg == g_NIL_MSG; } DNode!(SysMessage)* Pop(MessageQueue* queue) { Lock(&queue.mut); DNode!(SysMessage)* node = DLLPop(&queue.list, g_NIL_MSG); Unlock(&queue.mut); return node; } void Push(PlatformWindow* w, SysMessageType msg) { Lock(&w.msg_queue.mut); DNode!(SysMessage)* node = g_NIL_MSG; if(Nil(w.msg_queue.free_list.first)) { node = Alloc!(DNode!(SysMessage))(&w.msg_queue.arena); } else { node = DLLPop(&w.msg_queue.free_list, g_NIL_MSG); } node.value.type = msg; DLLPush(&w.msg_queue.list, node, g_NIL_MSG); Unlock(&w.msg_queue.mut); } void PushFree(PlatformWindow* w, DNode!(SysMessage)* node) { DLLPush(&w.msg_queue.list, node, g_NIL_MSG); } version(linux) { import core.sys.posix.dlfcn; import core.sys.posix.sys.mman; import core.sys.linux.sys.inotify; import core.sys.linux.fcntl; import core.sys.posix.sys.time : timespec, timeval, gettimeofday; import core.sys.posix.unistd; import core.sys.posix.pthread : PThread = pthread_t, PThreadCond = pthread_cond_t, PThreadMutex = pthread_mutex_t, PThreadMutexInit = pthread_mutex_init, PThreadCondInit = pthread_cond_init, PThreadCreate = pthread_create, PThreadMutexLock = pthread_mutex_lock, PThreadCondWait = pthread_cond_wait, PThreadMutexUnlock = pthread_mutex_unlock, PThreadCondSignal = pthread_cond_signal, PThreadExit = pthread_exit, PThreadCondTimedWait = pthread_cond_timedwait; import core.stdc.string : strlen; const u32 X11_CB_TRANSFER_SIZE_DEFAULT = 1048576; const u32 X11_TIMEOUT_DEFAULT = 1500; struct SysThread { PThread handle; PThreadCond cond; PThreadMutex mut; } struct Selection { bool owned; u8[] data; Atom target; Atom xmode; } enum Atoms { Targets, Multiple, Timestamp, Incr, Clipboard, Utf8String, WMProtocols, DeleteWindow, StateHidden, XselData, Max, } const char[][] ATOM_STRS = [ Atoms.Targets: CastStr!(char)("TARGETS"), Atoms.Multiple: CastStr!(char)("MULTIPLE"), Atoms.Timestamp: CastStr!(char)("TIMESTAMP"), Atoms.Incr: CastStr!(char)("INCR"), Atoms.Clipboard: CastStr!(char)("CLIPBOARD"), Atoms.Utf8String: CastStr!(char)("UTF8_STRING"), Atoms.WMProtocols: CastStr!(char)("WM_PROTOCOLS"), Atoms.DeleteWindow: CastStr!(char)("WM_DELETE_WINDOW"), Atoms.StateHidden: CastStr!(char)("_NET_WM_STATE_HIDDEN"), Atoms.XselData: CastStr!(char)("XSEL_DATA"), ]; alias PThreadProc = extern (C) void* function(void*); auto ExecLocation(T)() { static char[512] buf; u64 size = readlink("/proc/self/exe", buf.ptr, buf.length); char[] str; for(u64 i = size-1; i64(i) >= 0; i -= 1) { if(buf[i] == '/') { str = buf[0 .. i]; break; } } static if(is(T: u8) || is(T: char) || is(T: i8)) { return (cast(T[])str); } else static if(is(T: string)) { string s = (cast(immutable(char)*)str.ptr)[0 .. str.length]; return s; } else static assert(false); } auto Cwd(T)() { static char[512] buf; getcwd(buf.ptr, buf.length); static if(is(T: u8) || is(T: i8) || is(T: char)) { return (cast(T*)buf.ptr)[0 .. strlen(buf.ptr)]; } else static if(is(T: string)) { return ConvToStr(buf)[0 .. strlen(buf.ptr)]; } } bool ChDir(u8[] dir) { char[512] buf; buf[0 .. dir.length] = (cast(char*)dir)[0 .. dir.length]; buf[dir.length] = '\0'; return chdir(buf.ptr) == 0; } SysThread CreateThread(void* proc, void* param) { SysThread thread; bool result; result = cast(bool)(!PThreadMutexInit(&thread.mut, null)); result &= cast(bool)(!PThreadCondInit(&thread.cond, null)); result &= cast(bool)(!PThreadCreate(&thread.handle, null, cast(PThreadProc)proc, param)); assert(result); return thread; } void Suspend(SysThread* thread) { PThreadMutexLock(&thread.mut); PThreadCondWait(&thread.cond, &thread.mut); PThreadMutexUnlock(&thread.mut); } void Wake(SysThread* thread) { PThreadCondSignal(&thread.cond); } void Yield() { version(linux) { import core.sys.posix.sched; sched_yield(); } } void Kill() { PThreadExit(null); } void ResetInputs(Inputs* inputs) { inputs.list.first = inputs.list.last = null; Reset(&inputs.arena); } void Push(Inputs* inputs, Input input, i32 x, i32 y, bool pressed, Modifier md) { DNode!(InputEvent)* node = Alloc!(DNode!(InputEvent))(&inputs.arena); node.value.key = input; node.value.pressed = pressed; node.value.x = x; node.value.y = y; node.value.md = md; DLLPushFront(&inputs.list, node, null); } void PushMotion(Inputs* inputs, i32 rel_x, i32 rel_y, i32 x, i32 y) { DNode!(InputEvent)* node = Alloc!(DNode!(InputEvent))(&inputs.arena); node.value.key = Input.MouseMotion; node.value.rel_x = rel_x; node.value.rel_y = rel_y; node.value.x = x; node.value.y = y; node.value.pressed = false; DLLPushFront(&inputs.list, node, null); } struct PlatformWindow { Atom[Atoms.max] atoms; Display* display; Window window; Window root_window; i32 screen_id; // xcb_connection_t* conn; // xcb_screen_t* screen; // xcb_window_t window; u32 w; u32 h; i32 mouse_prev_x; i32 mouse_prev_y; bool locked_cursor; bool close; Modifier modifier; SysThread thread; MessageQueue msg_queue; u32 input_idx; Inputs[2] inputs; TicketMut input_mutex; Mut cb_msg_mut; TicketMut cb_mut; Selection[CBM.max] selections; version(linux) u32 cb_transfer_size; }; struct Library { void* ptr; }; struct Function { void* ptr; }; __gshared string WINDOW_ERR_MSG = null; string WindowError() { return WINDOW_ERR_MSG; } bool CreateWindow(PlatformWindow* window, string name, u32 width, u32 height, XVisualInfo* visual_info = null) { PlatformWindow wnd = { w: width, h: height, input_mutex: CreateTicketMut(), msg_queue: CreateMessageQueue(), cb_mut: CreateTicketMut(), cb_msg_mut: CreateMut(), inputs: [ { arena: CreateArena(MB(1)) }, { arena: CreateArena(MB(1)) }, ], }; *window = wnd; window.cb_transfer_size = X11_CB_TRANSFER_SIZE_DEFAULT; window.display = XOpenDisplay(null); if(!window.display) { WINDOW_ERR_MSG = "Unable to open X11 display"; return false; } window.root_window = DefaultRootWindow(window.display); if(window.root_window == None) { WINDOW_ERR_MSG = "Unable to retrieve X11 root window"; return false; } window.screen_id = XDefaultScreen(window.display); i64 value_mask = CWBackPixmap | CWBackPixel; if(visual_info) { XSetWindowAttributes attrs = { background_pixmap: None, background_pixel: XBlackPixel(window.display, window.screen_id), colormap: XCreateColormap(window.display, window.root_window, visual_info.visual, AllocNone), }; value_mask |= CWColormap; auto copy = CopyFromParent; window.window = XCreateWindow( window.display, window.root_window, 0, 0, width, height, 0, (visual_info ? visual_info.depth : cast(i32)copy), InputOutput, (visual_info ? visual_info.visual : cast(Visual*)©), value_mask, &attrs ); XFree(visual_info); } else { window.window = XCreateSimpleWindow( window.display, window.root_window, 0, 0, width, height, 0, XBlackPixel(window.display, window.screen_id), XBlackPixel(window.display, window.screen_id) ); } if(window.window == None) { WINDOW_ERR_MSG = "Failed to create X11 window"; return false; } i64 event_mask = KeyPressMask | KeyReleaseMask | ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask | PropertyChangeMask; XSelectInput(window.display, window.window, event_mask); foreach(atom; Atoms.min .. Atoms.max) { window.atoms[atom] = XInternAtom(window.display, ATOM_STRS[atom].ptr, false); } XSetWMProtocols(window.display, window.window, window.atoms.ptr, window.atoms.length); window.selections[CBM.Clipboard].xmode = window.atoms[Atoms.Clipboard]; window.selections[CBM.Primary].xmode = XA_PRIMARY; window.selections[CBM.Secondary].xmode = XA_SECONDARY; XChangeProperty( window.display, window.window, XA_WM_NAME, XA_STRING, 8, PropModeReplace, cast(const(u8)*)name.ptr, cast(u32)name.length ); XStoreName(window.display, window.window, name.ptr); XChangeProperty( window.display, window.window, window.atoms[Atoms.WMProtocols], XA_ATOM, 32, PropModeReplace, cast(const(u8)*)&window.atoms[Atoms.DeleteWindow], 1 ); int major, minor; XFixesQueryVersion(window.display, &major, &minor); XMapWindow(window.display, window.window); XFlush(window.display); /* window.conn = XGetXCBConnection(window.display); assert(window.conn != null, "XGetXCBConnection failure"); xcb_void_cookie_t cookie; xcb_generic_error_t *error; xcb_setup_t *setup = xcb_get_setup(window.conn); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); window.screen = iter.data; i32 event_mask = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_STRUCTURE_NOTIFY| XCB_EVENT_MASK_PROPERTY_CHANGE; i32[2] val_win = [window.screen.black_pixel, event_mask]; i32 val_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; window.window = xcb_generate_id(window.conn); cookie = xcb_create_window_checked( window.conn, XCB_COPY_FROM_PARENT, window.window, window.screen.root, 0, // x pos 0, // y pos window.w, // width window.h, // height 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, window.screen.root_visual, val_mask, val_win.ptr ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_create_window failure"); pureFree(error); cookie = xcb_map_window_checked(window.conn, window.window); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_map_window_checked failure"); pureFree(error); cookie = xcb_change_property_checked( window.conn, XCB_PROP_MODE_REPLACE, window.window, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, cast(u32)name.length, name.ptr ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); pureFree(error); for(u64 i = 0; i < Atoms.max; i += 1) { xcb_intern_atom_cookie_t intern = xcb_intern_atom(window.conn, 1, cast(u16)ATOM_STRS[i].length, ATOM_STRS[i].ptr); xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(window.conn, intern, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom failure"); window.atoms[i] = reply.atom; pureFree(error); pureFree(reply); } window.selections[CBM.Clipboard].xmode = window.atoms[Atoms.Clipboard]; window.selections[CBM.Primary].xmode = XCB_ATOM_PRIMARY; window.selections[CBM.Secondary].xmode = XCB_ATOM_SECONDARY; cookie = xcb_change_property_checked( window.conn, XCB_PROP_MODE_REPLACE, window.window, window.atoms[Atoms.WMProtocols], XCB_ATOM_ATOM, 32, 1, &window.atoms[Atoms.DeleteWindow] ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); pureFree(error); xcb_map_window(window.conn, window.window); i32 stream_result = xcb_flush(window.conn); assert(stream_result > 0, "xcb_flush failure"); xcb_xfixes_query_version(window.conn, 4, 0); */ return true; }; void StartPlatformThread(PlatformWindow* window) { window.thread = CreateThread(&HandleEvents, window); } bool LockCursor(PlatformWindow* window) { bool result = window.locked_cursor; if(!window.locked_cursor) { u32 counter = 0; for(;;) { i32 grab_res = XGrabPointer(window.display, window.window, true, 0, GrabModeAsync, GrabModeAsync, window.window, None, CurrentTime); if(grab_res == None) { result = true; break; } if(counter > 5) { break; } counter += 1; Thread.sleep(dur!("msecs")(50)); } HideCursor(window); window.locked_cursor = result; } return result; } void UnlockCursor(PlatformWindow* window) { if(window.locked_cursor) { XUngrabPointer(window.display, CurrentTime); ShowCursor(window); window.locked_cursor = false; } } bool HideCursor(PlatformWindow* window) { XFixesHideCursor(window.display, window.window); return true; } bool ShowCursor(PlatformWindow* window) { XFixesShowCursor(window.display, window.window); return true; } void ClearEvents(PlatformWindow* window) { for(;;) { XEvent ev; u32 count = XPending(window.display); for(u32 i = 0; i < count; i += 1) { XNextEvent(window.display, &ev); } if(count == 0) { break; } } } bool TransmitSelection(PlatformWindow* w, XSelectionRequestEvent* ev) { bool result; if(ev.property == None) { ev.property = ev.target; } if(ev.target == w.atoms[Atoms.Targets]) { Atom[3] targets = [ w.atoms[Atoms.Timestamp], w.atoms[Atoms.Targets], w.atoms[Atoms.Utf8String] ]; XChangeProperty( w.display, ev.requestor, ev.property, XA_ATOM, Atom.sizeof * 8, PropModeReplace, cast(const(u8)*)targets.ptr, targets.length ); result = true; } else if(ev.target == w.atoms[Atoms.Timestamp]) { Time cur = CurrentTime; XChangeProperty( w.display, ev.requestor, ev.property, XA_INTEGER, cur.sizeof * 8, PropModeReplace, cast(const(u8)*)&cur, 1 ); result = true; } else if(ev.target == w.atoms[Atoms.Utf8String]) { Selection* sel = null; Lock(&w.cb_mut); foreach(i; CBM.min .. CBM.max) { if(w.selections[i].xmode == ev.selection) { sel = &w.selections[i]; break; } } if(sel != null && sel.owned && sel.data.length > 0 && sel.target == ev.target) { XChangeProperty( w.display, ev.requestor, ev.property, ev.target, 8, PropModeReplace, cast(const(u8)*)sel.data.ptr, cast(u32)sel.data.length ); result = true; } Unlock(&w.cb_mut); } return result; } bool ClipboardOwns(PlatformWindow* w, ClipboardMode mode) { bool result; Lock(&w.cb_mut); result = w.selections[mode].owned; Unlock(&w.cb_mut); return result; } u8[] ClipboardText(PlatformWindow* w, ClipboardMode mode) { u8[] buf; Lock(&w.cb_mut); Selection* sel = &w.selections[mode]; if(sel.owned) { buf = GetClipboardSelection(w, sel); Unlock(&w.cb_mut); } else { Free(sel.data); sel.data = []; u64 ticket = Unlock(&w.cb_mut)+1; sel.target = w.atoms[Atoms.Utf8String]; XConvertSelection(w.display, sel.xmode, sel.target, sel.xmode, w.window, CurrentTime); XFlush(w.display); while(ticket == w.cb_mut.next_ticket) {} buf = GetClipboardSelection(w, sel); } return buf; } u8[] ClipboardText(PlatformWindow* w) { return ClipboardText(w, CBM.Clipboard); } bool SetClipboard(PlatformWindow* w, u8[] data, ClipboardMode mode) { bool result = true; if(data.length > 0) { Lock(&w.cb_mut); scope(exit) Unlock(&w.cb_mut); Selection* sel = &w.selections[mode]; if(sel.data.length > 0) { Free(sel.data); sel.data = []; } sel.data = Alloc!(u8)(data.length+1); if(sel.data.length > 0) { MemCpy(sel.data.ptr, data.ptr, data.length); sel.data[sel.data.length-1] = '\0'; sel.owned = true; sel.target = w.atoms[Atoms.Utf8String]; XSetSelectionOwner(w.display, sel.xmode, w.window, CurrentTime); XFlush(w.display); } else { result = false; } } return result; } bool SetClipboard(PlatformWindow* w, u8[] data) { return SetClipboard(w, data, CBM.Clipboard); } u8[] GetClipboardSelection(PlatformWindow* w, Selection* sel) { u8[] buf; if(sel.data.length > 0 && sel.target == w.atoms[Atoms.Utf8String]) { buf = ScratchAlloc(sel.data); } return buf; } void RetrieveSelection(PlatformWindow* w, XSelectionEvent* ev) { Lock(&w.cb_mut); scope(exit) Unlock(&w.cb_mut); u8[] buf; u64 buf_size; u64 bytes_after = 1; Atom actual_type; i32 actual_format; u64 nitems; u8* prop_data; if(ev.property == XA_PRIMARY || ev.property == XA_SECONDARY || ev.property == w.atoms[Atoms.Clipboard]) { while(bytes_after > 0) { scope(exit) XFree(prop_data); i32 get_res = XGetWindowProperty( w.display, w.window, ev.property, cast(u32)(buf_size/4), cast(u32)(w.cb_transfer_size/4), true, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &prop_data ); if(nitems > 0) { if(buf_size%4 != 0) { Errf("RetrieveSelection failure: Data size is not a multiple of 4"); break; } u64 unit_size = actual_format/8; u64 data_len = unit_size * nitems; buf = Realloc!(u8)(buf, data_len + buf_size); MemCpy(buf.ptr + buf_size, prop_data, data_len); buf_size += data_len; } } } if(buf != null) { Selection* sel; foreach(i; CBM.min .. CBM.max) { if(w.selections[i].xmode == ev.property) { sel = &w.selections[i]; break; } } if(sel != null && sel.target == actual_type) { Free(sel.data); sel.data = buf; buf = []; } else { Errf("RetrieveSelection failure: mismatched selection actual_type: %s", actual_type); } } else { Free(buf); } } void ClearSelection(PlatformWindow* w, XSelectionClearEvent* ev) { if(ev.window == w.window) { foreach(i; CBM.min .. CBM.max) { Selection* sel = &w.selections[i]; if(sel.xmode == ev.selection) { Lock(&w.cb_mut); Free(sel.data); sel.data = []; sel.owned = false; sel.target = XCB_NONE; Unlock(&w.cb_mut); break; } } } } void HandleEvents(void* window_ptr) { PlatformWindow* w = cast(PlatformWindow*)window_ptr; DNode!(SysMessage)* sys_msg = g_NIL_MSG; XEvent e; bool ignore_mouse_events = false; u64 no_ev_count; for(;;) { if(w.close) { Kill(); } sys_msg = Pop(&w.msg_queue); if(!Nil(sys_msg)) { SysMessage msg = sys_msg.value; switch (msg.type) { case SMT.Quit: { w.close = true; } break; case SMT.LockCursor: { for(u64 i = 0; i < 5; i += 1) { if(LockCursor(w)) { break; } } } break; case SMT.UnlockCursor: { UnlockCursor(w); } break; case SMT.HideCursor: { for (u64 i = 0; i < 5; i += 1) { if(HideCursor(w)) { break; } } } break; case SMT.ShowCursor: { for(u64 i = 0; i < 5; i += 1) { if(ShowCursor(w)) { break; } } } break; default: break; } } if(XPending(w.display)) { XNextEvent(w.display, &e); Inputs* inputs = GetInputs(w); switch (e.type) { case ClientMessage: { XClientMessageEvent* msg = &e.xclient; if(msg.window != w.window) { break; } if(msg.data.l[0] == w.atoms[Atoms.DeleteWindow]) { w.close = true; } } break; case KeyRelease: case KeyPress: { XKeyEvent* kb_ev = &e.xkey; bool pressed = e.type == KeyPress; u32 code = kb_ev.keycode; KeySym key_sym = XkbKeycodeToKeysym(w.display, cast(KeyCode)code, 0, 0); Input input = ConvertInput(key_sym); enum modifier_inputs = [Input.LeftShift, Input.RightShift, Input.LeftCtrl, Input.RightCtrl, Input.LeftAlt, Input.RightAlt]; enum modifiers = [MD.LeftShift, MD.RightShift, MD.LeftCtrl, MD.RightCtrl, MD.LeftAlt, MD.RightAlt]; static foreach(i, md; modifier_inputs) { if(input == md) { w.modifier = cast(Modifier)(pressed ? (w.modifier | modifiers[i]) : (w.modifier & ~modifiers[i])); } } if(input != Input.None) { Push(inputs, input, kb_ev.x, kb_ev.y, pressed, w.modifier); } } break; case ButtonPress: case ButtonRelease: { XButtonEvent* mouse_event = &e.xbutton; bool pressed = e.type == ButtonPress; Input input = Input.None; switch (mouse_event.button) { case Button1: input = Input.LeftClick; break; case Button2: input = Input.MiddleClick; break; case Button3: input = Input.RightClick; break; default: break; } if(input != Input.None) { Push(inputs, input, mouse_event.x, mouse_event.y, pressed, w.modifier); } } break; case MotionNotify: { if(ignore_mouse_events) continue; XMotionEvent* move_event = &e.xmotion; i32 x = move_event.x; i32 y = move_event.y; static bool first = true; if(first) { w.mouse_prev_x = x; w.mouse_prev_y = y; first = false; } if(x > 0 || y > 0) { PushMotion(inputs, w.mouse_prev_x-x, w.mouse_prev_y-y, x, y); } w.mouse_prev_x = x; w.mouse_prev_y = y; if(w.locked_cursor && (x < WINDOW_EDGE_BUFFER || y < WINDOW_EDGE_BUFFER || x > w.w - WINDOW_EDGE_BUFFER || y > w.h - WINDOW_EDGE_BUFFER)) { i32 new_x = cast(i32)(w.w / 2); i32 new_y = cast(i32)(w.h / 2); XWarpPointer(w.display, w.window, w.window, 0, 0, cast(i16)w.w, cast(i16)w.h, new_x, new_y); w.mouse_prev_x = new_x; w.mouse_prev_y = new_y; ignore_mouse_events = true; } } break; case ConfigureNotify: { XConfigureEvent* config_event = &e.xconfigure; if(w.w != config_event.width || w.h != config_event.height) { w.w = config_event.width; w.h = config_event.height; } } break; case SelectionClear: { ClearSelection(w, &e.xselectionclear); } break; case SelectionNotify: { RetrieveSelection(w, &e.xselection); } break; case SelectionRequest: { auto req = &e.xselectionrequest; XEvent notify = { xselection: { type: SelectionNotify, time: CurrentTime, display: w.display, requestor: req.requestor, selection: req.selection, target: req.target, property: TransmitSelection(w, req) ? req.property : None, }, }; XSendEvent(w.display, req.requestor, false, PropertyChangeMask, ¬ify); XFlush(w.display); } break; default: break; } ReturnInputs(w); } else { no_ev_count += 1; if(no_ev_count >= 5) { no_ev_count = 0; Yield(); } } } } Library LoadLibrary(string name) { Library lib = { ptr: null, }; lib.ptr = dlopen(name.ptr, RTLD_NOW); return lib; }; Function LoadFunction(Library lib, string name) { Function fn = { ptr: null, }; fn.ptr = dlsym(lib.ptr, name.ptr); return fn; } u8 InputToChar(Input input, bool modified = false) { switch(input) { case Input.Tab: case Input.Enter: case Input.Space: { return cast(u8)(input); } case Input.LeftBracket: case Input.RightBracket: case Input.BackSlash: case Input.Minus: case Input.Equals: case Input.Grave: case Input.Semicolon: case Input.SingleQuote: case Input.Comma: case Input.Period: case Input.Slash: case Input.Zero: .. case Input.Nine: { u32 code = cast(u32)(0xFF & input); if(!modified) { return cast(u8)code; } switch(cast(Input)code) { case Input.Zero: return ')'; case Input.One: return '!'; case Input.Two: return '@'; case Input.Three: return '#'; case Input.Four: return '$'; case Input.Five: return '%'; case Input.Six: return '^'; case Input.Seven: return '&'; case Input.Eight: return '*'; case Input.Nine: return '('; case Input.LeftBracket: return '{'; case Input.RightBracket: return '}'; case Input.BackSlash: return '|'; case Input.Minus: return '_'; case Input.Equals: return '+'; case Input.Grave: return '~'; case Input.Semicolon: return ':'; case Input.SingleQuote: return '"'; case Input.Comma: return '<'; case Input.Period: return '>'; case Input.Slash: return '?'; default: return 0; } } case Input.a: .. case Input.z: { return !modified ? cast(u8)input : cast(u8)(input-32); } case Input.NumEnter: case Input.NumAsterisk: case Input.NumPlus: case Input.NumMinus: case Input.NumPeriod: case Input.NumSlash: case Input.NumZero: .. case Input.NumNine: { u32 code = cast(u32)(input & 0xFF); return cast(u8)(code); } default: { u32 code = cast(u32)input; return code >= 0x20 && code <= 0x7E ? cast(u8)code : 0; } } } Input ConvertInput(u64 x_key) { switch (x_key) { case XK_space: .. case XK_asciitilde: { return cast(Input)(x_key); } case XK_BackSpace: return Input.Backspace; case XK_Return: return Input.Enter; case XK_Tab: return Input.Tab; case XK_Pause: return Input.Pause; case XK_Caps_Lock: return Input.CapsLock; case XK_Escape: return Input.Escape; case XK_Prior: return Input.PageUp; case XK_Next: return Input.PageDown; case XK_End: return Input.End; case XK_Home: return Input.Home; case XK_Left: return Input.Left; case XK_Up: return Input.Up; case XK_Right: return Input.Right; case XK_Down: return Input.Down; case XK_Print: return Input.PrintScreen; case XK_Insert: return Input.Insert; case XK_Delete: return Input.Delete; case XK_Meta_L: case XK_Super_L: return Input.LeftSuper; case XK_Meta_R: case XK_Super_R: return Input.RightSuper; case XK_KP_0: return Input.NumZero; case XK_KP_1: return Input.NumOne; case XK_KP_2: return Input.NumTwo; case XK_KP_3: return Input.NumThree; case XK_KP_4: return Input.NumFour; case XK_KP_5: return Input.NumFive; case XK_KP_6: return Input.NumSix; case XK_KP_7: return Input.NumSeven; case XK_KP_8: return Input.NumEight; case XK_KP_9: return Input.NumNine; case XK_KP_Multiply: return Input.NumAsterisk; case XK_KP_Subtract: return Input.NumMinus; case XK_KP_Decimal: return Input.NumPeriod; case XK_KP_Divide: return Input.NumSlash; case XK_KP_Add: return Input.NumPlus; case XK_F1: return Input.F1; case XK_F2: return Input.F2; case XK_F3: return Input.F3; case XK_F4: return Input.F4; case XK_F5: return Input.F5; case XK_F6: return Input.F6; case XK_F7: return Input.F7; case XK_F8: return Input.F8; case XK_F9: return Input.F9; case XK_F10: return Input.F10; case XK_F11: return Input.F11; case XK_F12: return Input.F12; case XK_Num_Lock: return Input.NumLock; case XK_Scroll_Lock: return Input.ScrollLock; case XK_Shift_L: return Input.LeftShift; case XK_Shift_R: return Input.RightShift; case XK_Control_L: return Input.LeftCtrl; case XK_Control_R: return Input.RightCtrl; case XK_Alt_L: return Input.LeftAlt; case XK_Alt_R: return Input.RightAlt; default: return Input.None; } } void* MemAlloc(u64 size) { return mmap(null, size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0); } void MemFree(void* ptr, u64 size) { auto result = munmap(ptr, size); assert(result == 0, "MemFree failure"); } struct Watcher { Arena arena; u8[] buffer; WatcherH handle; WatchH dir_handle; u8[] watched_dir; bool blocking; } alias WatcherH = int; alias WatchH = int; enum WatchType { None, Access = IN_ACCESS, Metadata = IN_ATTRIB, Create = IN_CREATE, Delete = IN_DELETE, Modify = IN_MODIFY, Moved = IN_MOVED_FROM | IN_MOVED_TO, } alias WT = WatchType; Watcher WatchDirectory(string dir, WatchType type, bool blocking = false) { assert(dir.length > 0); Watcher watcher = { arena: CreateArena(MB(4)), buffer: Alloc!(u8)(MB(1)), blocking: blocking, watched_dir: (cast(u8*)dir.ptr)[0 .. dir.length], }; watcher.handle = inotify_init(); assert(watcher.dir_handle >= 0, "WatchDirectory failure: unable to initialize"); if(!blocking) { fcntl(watcher.handle, F_SETFL, fcntl(watcher.handle, F_GETFL) | O_NONBLOCK); } watcher.dir_handle = inotify_add_watch(watcher.handle, dir.ptr, type); return watcher; } WatchEvent[] ViewChanges(Watcher* watcher) { assert(watcher.handle >= 0 && watcher.dir_handle >= 0, "ViewChanges failure: handles are not valid"); Reset(&watcher.arena); WatchEvent[] events; i64 length = read(watcher.handle, watcher.buffer.ptr, watcher.buffer.length); if(length > 0) { i64 count = 0; i64 i = 0; while(i < length) { inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i)); count += 1; assert(event.wd == watcher.dir_handle); i += inotify_event.sizeof + event.len; } if(count > 0) { struct Moved { u32 cookie; i64 to; i64 from; } Moved[] moved = Alloc!(Moved)(&watcher.arena, (count/2)+1); i64 m_count = 0; events = Alloc!(WatchEvent)(&watcher.arena, count); count = 0; i = 0; while (i < length) { inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i)); if(event.len > 0) { u8[] file_name = (cast(u8*)event.name)[0 .. strlen(event.name.ptr)]; if(event.mask & IN_MOVED_FROM || event.mask & IN_MOVED_TO) { bool from = (event.mask & IN_MOVED_FROM) > 0; Moved* m; foreach(j; 0 .. m_count) { if(moved[j].cookie == event.cookie) { m = moved.ptr + j; } } if(m != null) { if(from && m.to >= 0) { events[m.to].names[0] = file_name; if(watcher.watched_dir == file_name) { events[m.to].type &= ~(WET.File | WET.Dir); events[m.to].type |= WET.Self; } } else if(!from && m.from >= 0) { events[m.from].names[1] = file_name; } } else { WatchEvent* ev = events.ptr + count; ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File; ev.type |= WET.Moved; moved[m_count].cookie = event.cookie; if(from) { ev.names[0] = file_name; moved[m_count].from = count; moved[m_count].to = -1; if(watcher.watched_dir == file_name) { ev.type &= ~(WET.File | WET.Dir); ev.type |= WET.Self; } } else { ev.names[1] = file_name; moved[m_count].to = count; moved[m_count].from = -1; } count += 1; m_count += 1; } } else { WatchEvent* ev = events.ptr + count; ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File; ev.names[0] = file_name; if(ev.names[0] == watcher.watched_dir) { ev.type = WET.Self; } SetEventType(ev, event.mask); count += 1; } } else { WatchEvent* ev = events.ptr + count; ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File; SetEventType(ev, event.mask); count += 1; } i += inotify_event.sizeof + event.len; } } } return events; } void SetEventType(WatchEvent* ev, u32 mask) { if(mask & IN_ACCESS) { ev.type |= WET.Accessed; } else if(mask & IN_ATTRIB) { ev.type |= WET.Metadata; } else if(mask & IN_CREATE) { ev.type |= WET.Created; } else if(mask & IN_MODIFY) { ev.type |= WET.Modified; } else if(mask & IN_DELETE) { ev.type |= WET.Deleted; } } unittest { import std.stdio; import std.file : Remove = remove; Watcher fw = WatchDirectory("./", WT.Create | WT.Delete | WT.Moved | WT.Modify | WT.Access | WT.Metadata | WT.Access | WT.Metadata); auto f = File("test_file.txt", "wb"); f.write("test"); f.sync(); f.close(); Remove("./test_file.txt"); WatchEvent[] events = ViewChanges(&fw); assert(events.length == 3); assert(events[0].type == WET.FileCreated); assert(events[0].names[0] == r"test_file.txt"); assert(events[1].type == WET.FileModified); assert(events[1].names[0] == r"test_file.txt"); assert(events[2].type == WET.FileDeleted); assert(events[2].names[0] == r"test_file.txt"); } } enum WatchEventType { None = 0x0000, File = 0x0001, Dir = 0x0002, Self = 0x0004, Created = 0x0008, Modified = 0x0010, Deleted = 0x0020, Moved = 0x0040, Metadata = 0x0080, Accessed = 0x0100, FileCreated = WET.File | WET.Created, FileModified = WET.File | WET.Modified, FileDeleted = WET.File | WET.Deleted, FileMoved = WET.File | WET.Moved, FileMetadata = WET.File | WET.Metadata, FileAccessed = WET.File | WET.Accessed, DirCreated = WET.Dir | WET.Created, DirModified = WET.Dir | WET.Modified, DirDeleted = WET.Dir | WET.Deleted, DirMoved = WET.Dir | WET.Moved, DirMetadata = WET.Dir | WET.Metadata, DirAccessed = WET.Dir | WET.Accessed, SelfCreated = WET.Self | WET.Created, SelfModified = WET.Self | WET.Modified, SelfDeleted = WET.Self | WET.Deleted, SelfMoved = WET.Self | WET.Moved, SelfMetadata = WET.Self | WET.Metadata, SelfAccessed = WET.Self | WET.Accessed, } alias WET = WatchEventType; struct WatchEvent { WatchEventType type; u8[][2] names; } version(Windows) { import core.sys.windows.windows; struct SysThread { HANDLE handle; } alias Win32ThreadProc = extern (C) u32 function(void*); SysThread CreateThread(void* proc, void* param) { SysThread thread; CreateThread(null, 0, cast(Win32ThreadProc)proc, param, 0, null); return thread; } void Suspend(SysThread* thread) { SuspendThread(thread.handle); } void Wake(SysThread* thread) { ResumeThread(thread.handle); } void Kill() { ExitThread(0); } } // TODO: replace this with proper OS stuff u8[] LoadFile(Arena* arena, string file_name) { u8[] data; const u64 buf_size = MB(8); static u8[] buf; if(!buf) { buf = Alloc!(u8)(buf_size); } File f; try { f = File(file_name, "rb"); } catch(Exception e) { } if(f.isOpen) { u64 size = f.size(); data = Alloc!(u8)(arena, size); auto fp = f.getFP(); for(u64 pos = 0; pos < size;) { u64 length = Min(size-pos, buf_size); fread(data.ptr+pos, 1, length, fp); pos -= length; } f.flush(); f.close(); } return data; } unittest { { // Keys u8 ch = InputToChar(Input.NumEight); assert(ch == '8'); ch = InputToChar(Input.b, true); assert(ch == 'B'); version(linux) { ch = InputToChar(ConvertInput(XK_asciitilde)); assert(ch == '~'); ch = InputToChar(ConvertInput(XK_grave)); assert(ch == '`'); } } { string s = ExecLocation!(string)(); u8[] b = ExecLocation!(u8)(); assert(ChDir(b)); string wd = Cwd!(string)(); assert(wd == s); } { // File Reads Arena arena = CreateArena(MB(2)); u8[] data_valid, data_test; File f = File("platform.d", "rb"); data_valid = f.rawRead(new u8[f.size()]); data_test = LoadFile(&arena, "platform.d"); assert(data_valid == data_test); } }