diff --git a/platform.d b/platform.d index 897eb9d..db7e249 100644 --- a/platform.d +++ b/platform.d @@ -159,6 +159,16 @@ struct InputEvent i32 rel_y; } +enum ClipboardMode +{ + Clipboard, + Primary, + Selection = Primary, + Secondary, + Max +} + +alias CBM = ClipboardMode; struct Inputs { @@ -166,6 +176,24 @@ struct Inputs 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) { @@ -347,6 +375,7 @@ 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, @@ -358,10 +387,14 @@ import core.sys.posix.pthread : PThread = pthread_t, PThreadCondWait = pthread_cond_wait, PThreadMutexUnlock = pthread_mutex_unlock, PThreadCondSignal = pthread_cond_signal, - PThreadExit = pthread_exit; + 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; @@ -369,6 +402,40 @@ struct SysThread PThreadMutex mut; } +struct Selection +{ + bool owned; + u8[] data; + xcb_atom_t target; + xcb_atom_t xmode; +} + +enum Atoms +{ + Targets, + Multiple, + Timestamp, + Incr, + Clipboard, + Utf8String, + WMProtocols, + DeleteWindow, + StateHidden, + Max, +} + +const char[][] ATOM_STRS = [ + CastStr!(char)("TARGETS"), + CastStr!(char)("MULTIPLE"), + CastStr!(char)("TIMESTAMP"), + CastStr!(char)("INCR"), + CastStr!(char)("CLIPBOARD"), + CastStr!(char)("UTF8_STRING"), + CastStr!(char)("WM_PROTOCOLS"), + CastStr!(char)("WM_DELETE_WINDOW"), + CastStr!(char)("_NET_WM_STATE_HIDDEN"), +]; + alias PThreadProc = extern (C) void* function(void*); SysThread @@ -442,25 +509,28 @@ PushMotion(Inputs* inputs, i32 rel_x, i32 rel_y, i32 x, i32 y) 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; - Modifier modifier; + xcb_atom_t[Atoms.max] atoms; + Display* display; + xcb_connection_t* conn; + xcb_screen_t* screen; + xcb_window_t window; + u16 w; + u16 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; + SysThread thread; + MessageQueue msg_queue; + u32 input_idx; + Inputs[2] inputs; + TicketMut input_mutex; + + Mut cb_mut; + Selection[CBM.max] selections; + version(linux) u32 cb_transfer_size; }; struct Library @@ -496,12 +566,18 @@ CreateWindow(string name, u16 width, u16 height) h: height, input_mutex: CreateTicketMut(), msg_queue: CreateMessageQueue(), + cb_mut: CreateMut(), inputs: [ { arena: CreateArena(MB(1)) }, { arena: CreateArena(MB(1)) }, ], }; + version(linux) + { + window.cb_transfer_size = X11_CB_TRANSFER_SIZE_DEFAULT; + } + assert(width > 0 && height > 0, "CreateWindow error: width and height must be above 0"); window.display = XOpenDisplay(null); @@ -523,7 +599,8 @@ CreateWindow(string name, u16 width, u16 height) XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION | - XCB_EVENT_MASK_STRUCTURE_NOTIFY; + 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; @@ -568,42 +645,36 @@ CreateWindow(string name, u16 width, u16 height) 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); + 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"); - 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); + window.atoms[i] = reply.atom; - 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); + 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, - r_proto.atom, + window.atoms[Atoms.WMProtocols], XCB_ATOM_ATOM, 32, 1, - &r_close.atom + &window.atoms[Atoms.DeleteWindow] ); 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); @@ -699,6 +770,333 @@ FlushEvents(PlatformWindow* window) } while (e); } +bool +TransmitSelection(PlatformWindow* w, xcb_selection_request_event_t* ev) +{ + bool result; + if(ev.property == XCB_NONE) + { + ev.property = ev.target; + } + + if(ev.target == w.atoms[Atoms.Targets]) + { + xcb_atom_t[3] targets = [ + w.atoms[Atoms.Timestamp], + w.atoms[Atoms.Targets], + w.atoms[Atoms.Utf8String] + ]; + + xcb_change_property( + w.conn, + XCB_PROP_MODE_REPLACE, + ev.requestor, + ev.property, + XCB_ATOM_ATOM, + xcb_atom_t.sizeof * 8, + targets.length, + targets.ptr + ); + + result = true; + } + else if(ev.target == w.atoms[Atoms.Timestamp]) + { + xcb_timestamp_t cur = XCB_CURRENT_TIME; + xcb_change_property( + w.conn, + XCB_PROP_MODE_REPLACE, + ev.requestor, + ev.property, + XCB_ATOM_INTEGER, + cur.sizeof * 8, + 1, + &cur + ); + + result = true; + } + else if(ev.target == w.atoms[Atoms.Utf8String]) + { + Selection* sel = null; + if(TryLock(&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) + { + xcb_change_property( + w.conn, + XCB_PROP_MODE_REPLACE, + ev.requestor, + ev.property, + ev.target, + 8, + cast(u32)sel.data.length, + sel.data.ptr + ); + + result = true; + } + + Unlock(&w.cb_mut); + } + } + + return result; +} + +bool +ClipboardOwns(PlatformWindow* w, ClipboardMode mode) +{ + bool result; + + if(TryLock(&w.cb_mut)) + { + result = w.selections[mode].owned; + Unlock(&w.cb_mut); + } + + return result; +} + +u8[] +ClipboardText(PlatformWindow* w, ClipboardMode mode) +{ + u8[] buf; + + if(TryLock(&w.cb_mut)) + { + scope(exit) Unlock(&w.cb_mut); + + Selection* sel = &w.selections[mode]; + + if(sel.owned) + { + buf = GetClipboardSelection(w, sel); + } + else + { + timeval now; + timespec timeout; + int pret = 0; + + auto owner = xcb_get_selection_owner_reply(w.conn, xcb_get_selection_owner(w.conn, sel.xmode), null); + scope(exit) pureFree(owner); + + if(owner != null && owner.owner != 0) + { + FreeArray(sel.data); + sel.data = []; + + sel.target = w.atoms[Atoms.Utf8String]; + xcb_convert_selection(w.conn, w.window, sel.xmode, sel.target, sel.xmode, XCB_CURRENT_TIME); + xcb_flush(w.conn); + + gettimeofday(&now, null); + + timeout.tv_sec = now.tv_sec + (X11_TIMEOUT_DEFAULT / 1000); + timeout.tv_nsec = (now.tv_usec * 1000UL) * ((X11_TIMEOUT_DEFAULT % 1000) * 1000000UL); + if(timeout.tv_nsec >= 1000000000UL) + { + timeout.tv_sec += timeout.tv_nsec / 1000000000UL; + timeout.tv_nsec = timeout.tv_nsec % 1000000000UL; + } + + while(pret == 0 && sel.data.length == 0) + { + pret = PThreadCondTimedWait(&w.thread.cond, &w.thread.mut, &timeout); + } + + 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 && TryLock(&w.cb_mut)) + { + Selection* sel = &w.selections[mode]; + if(sel.data.length > 0) + { + FreeArray(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]; + + xcb_set_selection_owner(w.conn, w.window, sel.xmode, XCB_CURRENT_TIME); + xcb_flush(w.conn); + } + 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 = ScratchAllocCopy(sel.data); + } + + return buf; +} + +void +RetrieveSelection(PlatformWindow* w, xcb_selection_notify_event_t* ev) +{ + u8[] buf; + u64 buf_size; + u64 bytes_after = 1; + xcb_get_property_reply_t* reply; + xcb_atom_t actual_type; + u8 actual_format; + + if(ev.property == XCB_ATOM_PRIMARY || ev.property == XCB_ATOM_SECONDARY || ev.property == w.atoms[Atoms.Clipboard]) + { + while(bytes_after > 0) + { + scope(exit) pureFree(reply); + + xcb_get_property_cookie_t cookie = xcb_get_property( + w.conn, + true, + w.window, + ev.property, + XCB_ATOM_ANY, + cast(u32)(buf_size/4), + cast(u32)(w.cb_transfer_size/4) + ); + + reply = xcb_get_property_reply(w.conn, cookie, null); + + if(reply == null || (buf_size > 0 && (reply.format != actual_format || reply.type != actual_type)) || reply.format%8 != 0) + { + Errf("RetrieveSelection failure: Invalid return value from xcb_get_property_reply"); + break; + } + + if(buf_size == 0) + { + actual_type = reply.type; + actual_format = reply.format; + } + + int nitems = xcb_get_property_value_length(reply); + if(nitems > 0) + { + if(buf_size%4 != 0) + { + Errf("RetrieveSelection failure: Data size is not a multiple of 4"); + break; + } + + u64 unit_size = reply.format/8; + buf = Alloc!(u8)(unit_size * (buf_size + nitems)); + + MemCpy(buf.ptr + buf_size, xcb_get_property_value(reply), nitems * unit_size); + buf_size += nitems * unit_size; + } + + bytes_after = reply.bytes_after; + } + } + + if(buf != null && TryLock(&w.cb_mut)) + { + 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) + { + FreeArray(sel.data); + sel.data = buf; + buf = []; + } + else + { + Errf("RetrieveSelection failure: mismatched selection actual_type: %s", actual_type); + } + + Unlock(&w.cb_mut); + } + else + { + FreeArray(buf); + } +} + +void +ClearSelection(PlatformWindow* w, xcb_selection_clear_event_t* ev) +{ + if(ev.owner == w.window) + { + foreach(i; CBM.min .. CBM.max) + { + Selection* sel = &w.selections[i]; + if(sel.xmode == ev.selection && TryLock(&w.cb_mut)) + { + FreeArray(sel.data); + sel.data = []; + sel.owned = false; + sel.target = XCB_NONE; + + Unlock(&w.cb_mut); + + break; + } + } + } +} + void HandleEvents(void* window_ptr) { @@ -782,7 +1180,7 @@ HandleEvents(void* window_ptr) break; } - if(msg.data.data32[0] == w.close_event) + if(msg.data.data32[0] == w.atoms[Atoms.DeleteWindow]) { w.close = true; } @@ -878,6 +1276,29 @@ HandleEvents(void* window_ptr) w.h = config_event.height; } } break; + case XCB_SELECTION_CLEAR: + { + ClearSelection(w, cast(xcb_selection_clear_event_t*)e); + } break; + case XCB_SELECTION_NOTIFY: + { + RetrieveSelection(w, cast(xcb_selection_notify_event_t*)e); + } break; + case XCB_SELECTION_REQUEST: + { + auto req = cast(xcb_selection_request_event_t*)e; + xcb_selection_notify_event_t notify = { + response_type: XCB_SELECTION_NOTIFY, + time: XCB_CURRENT_TIME, + requestor: req.requestor, + selection: req.selection, + target: req.target, + property: TransmitSelection(w, req) ? req.property : XCB_NONE, + }; + + xcb_send_event(w.conn, false, req.requestor, XCB_EVENT_MASK_PROPERTY_CHANGE, cast(char*)¬ify); + xcb_flush(w.conn); + } break; default: break; } diff --git a/util.d b/util.d index ddd82f0..ab16713 100644 --- a/util.d +++ b/util.d @@ -6,7 +6,7 @@ import dlib.alloc; import xxhash3; import includes; -import std.stdio; +import std.stdio : write, writeln, writefln, stderr; import std.conv; import std.string; import std.traits; @@ -43,6 +43,7 @@ Logf(Args...)(string fmt, Args args) { try { + write("[INFO]: "); writefln(fmt, args); } catch (Exception e) @@ -51,6 +52,20 @@ Logf(Args...)(string fmt, Args args) } } +void +Errf(Args...)(string fmt, Args args) +{ + try + { + stderr.write("[ERROR]: "); + stderr.writef(fmt, args, '\n'); + } + catch(Exception e) + { + assert(false, "Incompatible format type"); + } +} + void Log(string str) {