module dlib.platform; import dlib.aliases; import dlib.alloc : Reset; import dlib.alloc; import dlib.util; import includes; import std.typecons; import std.stdio; import core.memory; import core.thread.osthread; import core.time; const WINDOW_EDGE_BUFFER = 50; enum Input { None, // Keyboard A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, NumLock, NumSlash, NumStar, NumMinus, NumPlus, NumEnter, NumPeriod, Insert, Delete, Home, End, PageUp, PageDown, PrintScreen, ScrollLock, Pause, Comma, Period, BackSlash, Backspace, ForwardSlash, Minus, Plus, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Up, Down, Left, Right, LeftCtrl, LeftAlt, LeftShift, LeftSuper, Tab, CapsLock, RightCtrl, RightAlt, RightSuper, RightShift, Enter, Space, Tilde, Esc, Semicolon, Quote, LeftBrace, RightBrace, // Mouse MouseMotion, LeftClick, MiddleClick, RightClick, }; alias KBI = Input; 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.unistd; import core.stdc.string : strlen; struct InputEvent { Input key; bool pressed; i32 x; i32 y; i32 rel_x; i32 rel_y; } struct Inputs { DLList!(InputEvent) list; Arena arena; } 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) { DNode!(InputEvent)* node = Alloc!(DNode!(InputEvent))(&inputs.arena); node.value.key = input; node.value.pressed = pressed; node.value.x = x; node.value.y = y; 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 { Display *display; xcb_connection_t *conn; xcb_screen_t *screen; xcb_window_t window; xcb_atom_t close_event; xcb_atom_t minimize_event; u16 w; u16 h; i32 mouse_prev_x; i32 mouse_prev_y; bool locked_cursor; bool close; Inputs inputs; }; struct Library { void* ptr; }; struct Function { void* ptr; }; void CheckErr(PlatformWindow *window, xcb_void_cookie_t *cookie, xcb_generic_error_t *err, string msg) { assert(err == null, msg); pureFree(err); }; PlatformWindow CreateWindow(string name, u16 width, u16 height) { PlatformWindow window = { w: width, h: height, inputs: { arena: CreateArena(MB(1)), }, }; assert(width > 0 && height > 0, "CreateWindow error: width and height must be above 0"); window.display = XOpenDisplay(null); assert(window.display != null, "XOpenDisplay failure"); 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; 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); xcb_intern_atom_cookie_t c_proto = xcb_intern_atom(window.conn, 1, 12, "WM_PROTOCOLS"); xcb_intern_atom_reply_t *r_proto = xcb_intern_atom_reply(window.conn, c_proto, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom WM_PROTOCOLS failure"); pureFree(error); xcb_intern_atom_cookie_t c_close = xcb_intern_atom(window.conn, 0, 16, "WM_DELETE_WINDOW"); xcb_intern_atom_reply_t *r_close = xcb_intern_atom_reply(window.conn, c_close, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom WM_DELETE_WINDOW failure"); pureFree(error); xcb_intern_atom_cookie_t c_minimize = xcb_intern_atom(window.conn, 0, 20, "_NET_WM_STATE_HIDDEN"); xcb_intern_atom_reply_t *r_minimize = xcb_intern_atom_reply(window.conn, c_minimize, &error); CheckErr(&window, &cookie, error, "xcb_intern_atom _NET_WM_STATE_HIDDEN failure"); pureFree(error); cookie = xcb_change_property_checked( window.conn, XCB_PROP_MODE_REPLACE, window.window, r_proto.atom, XCB_ATOM_ATOM, 32, 1, &r_close.atom ); error = xcb_request_check(window.conn, cookie); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); pureFree(error); window.close_event = r_close.atom; window.minimize_event = r_minimize.atom; pureFree(r_proto); pureFree(r_close); pureFree(r_minimize); 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); LockCursor(&window); UnlockCursor(&window); return window; }; void LockCursor(PlatformWindow* window) { if (!window.locked_cursor) { u32 counter = 0; for(;;) { xcb_generic_error_t *error; xcb_grab_pointer_cookie_t grab_cookie = xcb_grab_pointer(window.conn, true, window.window, 0, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, window.window, XCB_NONE, XCB_CURRENT_TIME); xcb_grab_pointer_reply_t* grab_reply = xcb_grab_pointer_reply(window.conn, grab_cookie, &error); scope(exit) { pureFree(error); pureFree(grab_reply); } if (grab_reply.status == XCB_GRAB_STATUS_SUCCESS) { break; } assert(counter < 5, "Unable to grab cursor"); counter += 1; Thread.sleep(dur!("msecs")(50)); } HideCursor(window); window.locked_cursor = true; } } void UnlockCursor(PlatformWindow* window) { if (window.locked_cursor) { xcb_ungrab_pointer(window.conn, XCB_CURRENT_TIME); ShowCursor(window); window.locked_cursor = false; } } // TODO: improve error handling for show/hide cursor void HideCursor(PlatformWindow* window) { xcb_void_cookie_t hide_cursor_cookie = xcb_xfixes_hide_cursor_checked(window.conn, window.window); xcb_generic_error_t *error = xcb_request_check(window.conn, hide_cursor_cookie); CheckErr(window, &hide_cursor_cookie, error, "xcb_xfixes_hide_cursor_checked failure"); pureFree(error); } void ShowCursor(PlatformWindow* window) { xcb_void_cookie_t show_cursor_cookie = xcb_xfixes_show_cursor_checked(window.conn, window.window); xcb_generic_error_t *error = xcb_request_check(window.conn, show_cursor_cookie); CheckErr(window, &show_cursor_cookie, error, "xcb_xfixes_show_cursor_checked failure"); pureFree(error); } void FlushEvents(PlatformWindow* window) { xcb_generic_event_t* e; do { e = xcb_poll_for_event(window.conn); } while (e); } void HandleEvents(PlatformWindow* window) { xcb_generic_event_t* e; bool ignore_mouse_events = false; Inputs* inputs = &window.inputs; ResetInputs(inputs); do { e = xcb_poll_for_event(window.conn); if (e) { switch (e.response_type & ~0x80) { case XCB_CLIENT_MESSAGE: { xcb_client_message_event_t* msg = cast(xcb_client_message_event_t*)e; if (msg.window != window.window) { break; } if (msg.data.data32[0] == window.close_event) { window.close = true; } } break; case XCB_KEY_RELEASE: case XCB_KEY_PRESS: { xcb_key_press_event_t* keyboard_event = cast(xcb_key_press_event_t*)e; bool pressed = e.response_type == XCB_KEY_PRESS; xcb_keycode_t code = keyboard_event.detail; KeySym key_sym = XkbKeycodeToKeysym(window.display, cast(KeyCode)code, 0, 0); KBI input = ConvertInput(key_sym); if (input != KBI.None) { Push(inputs, input, keyboard_event.event_x, keyboard_event.event_y, pressed); } } break; case XCB_BUTTON_PRESS: case XCB_BUTTON_RELEASE: { xcb_button_press_event_t* mouse_event = cast(xcb_button_press_event_t*)e; bool pressed = e.response_type == XCB_BUTTON_PRESS; Input input = Input.None; switch (mouse_event.detail) { case XCB_BUTTON_INDEX_1: input = Input.LeftClick; break; case XCB_BUTTON_INDEX_2: input = Input.MiddleClick; break; case XCB_BUTTON_INDEX_3: input = Input.RightClick; break; default: break; } if (input != Input.None) { Push(inputs, input, mouse_event.event_x, mouse_event.event_y, pressed); } } break; case XCB_MOTION_NOTIFY: { if (ignore_mouse_events) continue; xcb_motion_notify_event_t* move_event = cast(xcb_motion_notify_event_t*)e; i16 x = move_event.event_x; i16 y = move_event.event_y; static bool first = true; if (first) { window.mouse_prev_x = x; window.mouse_prev_y = y; first = false; } if (x > 0 || y > 0) { PushMotion(inputs, window.mouse_prev_x-x, window.mouse_prev_y-y, x, y); } window.mouse_prev_x = x; window.mouse_prev_y = y; if (window.locked_cursor && (x < WINDOW_EDGE_BUFFER || y < WINDOW_EDGE_BUFFER || x > window.w - WINDOW_EDGE_BUFFER || y > window.h - WINDOW_EDGE_BUFFER)) { i16 new_x = cast(i16)(window.w / 2); i16 new_y = cast(i16)(window.h / 2); xcb_warp_pointer(window.conn, window.window, window.window, 0, 0, cast(i16)window.w, cast(i16)window.h, new_x, new_y); window.mouse_prev_x = new_x; window.mouse_prev_y = new_y; ignore_mouse_events = true; } } break; case XCB_CONFIGURE_NOTIFY: { xcb_configure_notify_event_t* config_event = cast(xcb_configure_notify_event_t*)e; if (window.w != config_event.width || window.h != config_event.height) { window.w = config_event.width; window.h = config_event.height; } } break; default: break; } } } while (e); } 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; }; Input ConvertInput(u64 x_key) { switch (x_key) { case XK_BackSpace: return KBI.Backspace; case XK_Return: return KBI.Enter; case XK_Tab: return KBI.Tab; case XK_Pause: return KBI.Pause; case XK_Caps_Lock: return KBI.CapsLock; case XK_Escape: return KBI.Esc; case XK_space: return KBI.Space; case XK_Prior: return KBI.PageUp; case XK_Next: return KBI.PageDown; case XK_End: return KBI.End; case XK_Home: return KBI.Home; case XK_Left: return KBI.Left; case XK_Up: return KBI.Up; case XK_Right: return KBI.Right; case XK_Down: return KBI.Down; case XK_Print: return KBI.PrintScreen; case XK_Insert: return KBI.Insert; case XK_Delete: return KBI.Delete; case XK_Meta_L: case XK_Super_L: return KBI.LeftSuper; case XK_Meta_R: case XK_Super_R: return KBI.RightSuper; case XK_KP_0: return KBI.Num0; case XK_KP_1: return KBI.Num1; case XK_KP_2: return KBI.Num2; case XK_KP_3: return KBI.Num3; case XK_KP_4: return KBI.Num4; case XK_KP_5: return KBI.Num5; case XK_KP_6: return KBI.Num6; case XK_KP_7: return KBI.Num7; case XK_KP_8: return KBI.Num8; case XK_KP_9: return KBI.Num9; case XK_multiply: return KBI.NumStar; case XK_KP_Subtract: return KBI.NumMinus; case XK_KP_Decimal: return KBI.NumPeriod; case XK_KP_Divide: return KBI.NumSlash; case XK_KP_Add: return KBI.NumPlus; case XK_F1: return KBI.F1; case XK_F2: return KBI.F2; case XK_F3: return KBI.F3; case XK_F4: return KBI.F4; case XK_F5: return KBI.F5; case XK_F6: return KBI.F6; case XK_F7: return KBI.F7; case XK_F8: return KBI.F8; case XK_F9: return KBI.F9; case XK_F10: return KBI.F10; case XK_F11: return KBI.F11; case XK_F12: return KBI.F12; case XK_Num_Lock: return KBI.NumLock; case XK_Scroll_Lock: return KBI.ScrollLock; case XK_Shift_L: return KBI.LeftShift; case XK_Shift_R: return KBI.RightShift; case XK_Control_L: return KBI.LeftCtrl; case XK_Control_R: return KBI.RightCtrl; case XK_Alt_L: return KBI.LeftAlt; case XK_Alt_R: return KBI.RightAlt; case XK_semicolon: return KBI.Semicolon; case XK_bracketleft: return KBI.LeftBrace; case XK_bracketright: return KBI.RightBrace; case XK_plus: return KBI.Plus; case XK_comma: return KBI.Comma; case XK_minus: return KBI.Minus; case XK_backslash: return KBI.BackSlash; case XK_slash: return KBI.ForwardSlash; case XK_grave: return KBI.Tilde; case XK_0: return KBI.Zero; case XK_1: return KBI.One; case XK_2: return KBI.Two; case XK_3: return KBI.Three; case XK_4: return KBI.Four; case XK_5: return KBI.Five; case XK_6: return KBI.Six; case XK_7: return KBI.Seven; case XK_8: return KBI.Eight; case XK_9: return KBI.Nine; case XK_a: case XK_A: return KBI.A; case XK_b: case XK_B: return KBI.B; case XK_c: case XK_C: return KBI.C; case XK_d: case XK_D: return KBI.D; case XK_e: case XK_E: return KBI.E; case XK_f: case XK_F: return KBI.F; case XK_g: case XK_G: return KBI.G; case XK_h: case XK_H: return KBI.H; case XK_i: case XK_I: return KBI.I; case XK_j: case XK_J: return KBI.J; case XK_k: case XK_K: return KBI.K; case XK_l: case XK_L: return KBI.L; case XK_m: case XK_M: return KBI.M; case XK_n: case XK_N: return KBI.N; case XK_o: case XK_O: return KBI.O; case XK_p: case XK_P: return KBI.P; case XK_q: case XK_Q: return KBI.Q; case XK_r: case XK_R: return KBI.R; case XK_s: case XK_S: return KBI.S; case XK_t: case XK_T: return KBI.T; case XK_u: case XK_U: return KBI.U; case XK_v: case XK_V: return KBI.V; case XK_w: case XK_W: return KBI.W; case XK_x: case XK_X: return KBI.X; case XK_y: case XK_Y: return KBI.Y; case XK_z: case XK_Z: return KBI.Z; default: return KBI.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) { assert(munmap(ptr, size) == 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: AllocArray!(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 = AllocArray!(Moved)(&watcher.arena, (count/2)+1); i64 m_count = 0; events = AllocArray!(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) { }