dlib/platform.d
2025-11-30 16:37:44 +11:00

2074 lines
40 KiB
D

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)) {}
}
void
Unlock(TicketMut* mut)
{
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,
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"),
];
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*)&copy),
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);
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);
}
else
{
Window owner = XGetSelectionOwner(w.display, sel.xmode);
if(owner == None)
{
Free(sel.data);
sel.data = [];
Unlock(&w.cb_mut);
u64 ticket = w.cb_mut.next_ticket;
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, &notify);
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);
}
}