1808 lines
55 KiB
C++
1808 lines
55 KiB
C++
/*
|
|
* chr - Andrew Chronister &
|
|
* inso - Alex Baines
|
|
*
|
|
* 12.19.2019
|
|
*
|
|
* Updated linux layer for 4coder
|
|
*
|
|
*/
|
|
|
|
// TOP
|
|
|
|
#define FPS 60
|
|
#define frame_useconds (1000000 / FPS)
|
|
#define frame_nseconds (UINT64_C(1000000000) / FPS)
|
|
#define SLASH '/'
|
|
#define DLL "so"
|
|
|
|
#include "4coder_base_types.h"
|
|
#include "4coder_version.h"
|
|
#include "4coder_events.h"
|
|
|
|
#include "4coder_table.h"
|
|
#include "4coder_types.h"
|
|
#include "4coder_default_colors.h"
|
|
#include "4coder_system_types.h"
|
|
#include "4ed_font_interface.h"
|
|
|
|
#define STATIC_LINK_API
|
|
#include "generated/system_api.h"
|
|
|
|
#define STATIC_LINK_API
|
|
#include "generated/graphics_api.h"
|
|
|
|
#define STATIC_LINK_API
|
|
#include "generated/font_api.h"
|
|
|
|
#include "4ed_font_set.h"
|
|
#include "4ed_render_target.h"
|
|
#include "4ed_search_list.h"
|
|
#include "4ed.h"
|
|
|
|
#include "generated/system_api.cpp"
|
|
#include "generated/graphics_api.cpp"
|
|
#include "generated/font_api.cpp"
|
|
|
|
#include "4coder_base_types.cpp"
|
|
#include "4coder_stringf.cpp"
|
|
#include "4coder_events.cpp"
|
|
#include "4coder_hash_functions.cpp"
|
|
#include "4coder_table.cpp"
|
|
#include "4coder_log.cpp"
|
|
|
|
#include "4coder_hash_functions.cpp"
|
|
#include "4coder_system_allocator.cpp"
|
|
#include "4coder_malloc_allocator.cpp"
|
|
#include "4coder_codepoint_map.cpp"
|
|
|
|
#include "4ed_mem.cpp"
|
|
#include "4ed_font_set.cpp"
|
|
#include "4ed_search_list.cpp"
|
|
#include "4ed_font_provider_freetype.h"
|
|
#include "4ed_font_provider_freetype.cpp"
|
|
|
|
#include <dirent.h>
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <locale.h>
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
|
|
#include <sys/epoll.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/timerfd.h>
|
|
#include <sys/eventfd.h>
|
|
#include <sys/syscall.h>
|
|
|
|
#define Cursor XCursor
|
|
#undef function
|
|
#undef internal
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/cursorfont.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/extensions/Xfixes.h>
|
|
#include <X11/XKBlib.h>
|
|
#include <X11/keysym.h>
|
|
#define function static
|
|
#undef Cursor
|
|
|
|
#include <fontconfig/fontconfig.h>
|
|
#define internal static
|
|
|
|
#include <GL/glx.h>
|
|
#include <GL/glext.h>
|
|
|
|
#ifdef INSO_DEBUG
|
|
#define LINUX_FN_DEBUG(fmt, ...) do { \
|
|
fprintf(stderr, "%s: " fmt "\n", __func__, ##__VA_ARGS__);\
|
|
} while (0)
|
|
|
|
// I want to see a message
|
|
#undef AssertBreak
|
|
#define AssertBreak(m) ({\
|
|
fprintf(stderr, "\n** ASSERTION FAILURE: %s:%d: %s\n\n", __FILE__, __LINE__, #m);\
|
|
*((volatile u64*)0) = 0xba771e70ad5;\
|
|
})
|
|
#else
|
|
#define LINUX_FN_DEBUG(...)
|
|
#endif
|
|
|
|
////////////////////////////
|
|
|
|
struct Linux_Input_Chunk_Transient {
|
|
Input_List event_list;
|
|
b8 mouse_l_press;
|
|
b8 mouse_l_release;
|
|
b8 mouse_r_press;
|
|
b8 mouse_r_release;
|
|
i8 mouse_wheel;
|
|
b8 trying_to_kill;
|
|
};
|
|
|
|
struct Linux_Input_Chunk_Persistent {
|
|
Vec2_i32 mouse;
|
|
Input_Modifier_Set_Fixed modifiers;
|
|
b8 mouse_l;
|
|
b8 mouse_r;
|
|
b8 mouse_out_of_window;
|
|
};
|
|
|
|
struct Linux_Input_Chunk {
|
|
Linux_Input_Chunk_Transient trans;
|
|
Linux_Input_Chunk_Persistent pers;
|
|
};
|
|
|
|
struct Linux_Vars {
|
|
Thread_Context tctx;
|
|
Arena *frame_arena;
|
|
|
|
Display* dpy;
|
|
Window win;
|
|
|
|
b32 has_xfixes;
|
|
int xfixes_selection_event;
|
|
XIM xim;
|
|
XIC xic;
|
|
FcConfig* fontconfig;
|
|
XkbDescPtr xkb;
|
|
|
|
Linux_Input_Chunk input;
|
|
int xkb_event;
|
|
int xkb_group; // active keyboard layout (0-3)
|
|
|
|
int epoll;
|
|
int step_timer_fd;
|
|
u64 last_step_time;
|
|
b32 step_pending;
|
|
|
|
XCursor xcursors[APP_MOUSE_CURSOR_COUNT];
|
|
Application_Mouse_Cursor cursor;
|
|
XCursor hidden_cursor;
|
|
i32 cursor_show;
|
|
i32 prev_cursor_show;
|
|
|
|
Node free_linux_objects;
|
|
Node timer_objects;
|
|
|
|
System_Mutex global_frame_mutex;
|
|
|
|
Arena* clipboard_arena;
|
|
String_Const_u8 clipboard_contents;
|
|
b32 received_new_clipboard;
|
|
b32 clipboard_catch_all;
|
|
|
|
Atom atom_TARGETS;
|
|
Atom atom_CLIPBOARD;
|
|
Atom atom_UTF8_STRING;
|
|
Atom atom__NET_WM_STATE;
|
|
Atom atom__NET_WM_STATE_MAXIMIZED_HORZ;
|
|
Atom atom__NET_WM_STATE_MAXIMIZED_VERT;
|
|
Atom atom__NET_WM_STATE_FULLSCREEN;
|
|
Atom atom__NET_WM_PING;
|
|
Atom atom__NET_WM_WINDOW_TYPE;
|
|
Atom atom__NET_WM_WINDOW_TYPE_NORMAL;
|
|
Atom atom__NET_WM_PID;
|
|
Atom atom_WM_DELETE_WINDOW;
|
|
};
|
|
|
|
global Linux_Vars linuxvars;
|
|
global Render_Target render_target;
|
|
|
|
////////////////////////////
|
|
|
|
// Defererencing an epoll_event's .data.ptr will always give one of these event types.
|
|
|
|
typedef i32 Epoll_Kind;
|
|
enum {
|
|
EPOLL_STEP_TIMER,
|
|
EPOLL_X11,
|
|
EPOLL_X11_INTERNAL,
|
|
EPOLL_CLI_PIPE,
|
|
EPOLL_USER_TIMER,
|
|
};
|
|
|
|
// Where per-event epoll data is not needed, .data.ptr will point to one of
|
|
// these static vars below.
|
|
// If per-event data is needed, container_of can be used on data.ptr
|
|
// to access the containing struct and all its other members.
|
|
|
|
internal Epoll_Kind epoll_tag_step_timer = EPOLL_STEP_TIMER;
|
|
internal Epoll_Kind epoll_tag_x11 = EPOLL_X11;
|
|
internal Epoll_Kind epoll_tag_x11_internal = EPOLL_X11_INTERNAL;
|
|
internal Epoll_Kind epoll_tag_cli_pipe = EPOLL_CLI_PIPE;
|
|
|
|
////////////////////////////
|
|
|
|
typedef i32 Linux_Object_Kind;
|
|
enum {
|
|
LinuxObjectKind_ERROR = 0,
|
|
LinuxObjectKind_Timer = 1,
|
|
LinuxObjectKind_Thread = 2,
|
|
LinuxObjectKind_Mutex = 3,
|
|
LinuxObjectKind_ConditionVariable = 4,
|
|
};
|
|
|
|
struct Linux_Object {
|
|
Linux_Object_Kind kind;
|
|
Node node;
|
|
union {
|
|
struct {
|
|
int fd;
|
|
Epoll_Kind epoll_tag;
|
|
} timer;
|
|
struct {
|
|
pthread_t pthread;
|
|
Thread_Function* proc;
|
|
void* ptr;
|
|
} thread;
|
|
pthread_mutex_t mutex;
|
|
pthread_cond_t condition_variable;
|
|
};
|
|
};
|
|
|
|
Linux_Object*
|
|
handle_to_object(Plat_Handle ph){
|
|
return *(Linux_Object**)&ph;
|
|
}
|
|
|
|
internal Linux_Object*
|
|
linux_alloc_object(Linux_Object_Kind kind){
|
|
Linux_Object* result = NULL;
|
|
|
|
if (linuxvars.free_linux_objects.next != &linuxvars.free_linux_objects) {
|
|
result = CastFromMember(Linux_Object, node, linuxvars.free_linux_objects.next);
|
|
}
|
|
|
|
if (result == NULL) {
|
|
i32 count = 512;
|
|
|
|
Linux_Object* objects = (Linux_Object*)system_memory_allocate(
|
|
sizeof(Linux_Object) * count,
|
|
file_name_line_number_lit_u8
|
|
);
|
|
|
|
objects[0].node.prev = &linuxvars.free_linux_objects;
|
|
linuxvars.free_linux_objects.next = &objects[0].node;
|
|
for (i32 i = 1; i < count; ++i) {
|
|
objects[i - 1].node.next = &objects[i].node;
|
|
objects[i].node.prev = &objects[i - 1].node;
|
|
}
|
|
objects[count - 1].node.next = &linuxvars.free_linux_objects;
|
|
linuxvars.free_linux_objects.prev = &objects[count - 1].node;
|
|
|
|
result = CastFromMember(Linux_Object, node, linuxvars.free_linux_objects.next);
|
|
}
|
|
|
|
Assert(result != 0);
|
|
dll_remove(&result->node);
|
|
block_zero_struct(result);
|
|
result->kind = kind;
|
|
return result;
|
|
}
|
|
|
|
internal void
|
|
linux_free_object(Linux_Object *object){
|
|
if (object->node.next != 0){
|
|
dll_remove(&object->node);
|
|
}
|
|
dll_insert(&linuxvars.free_linux_objects, &object->node);
|
|
}
|
|
|
|
////////////////////////////
|
|
|
|
internal int
|
|
linux_compare_file_infos(File_Info** a, File_Info** b) {
|
|
b32 a_hidden = (*a)->file_name.str[0] == '.';
|
|
b32 b_hidden = (*b)->file_name.str[0] == '.';
|
|
|
|
// hidden files lower in list
|
|
if(a_hidden != b_hidden) {
|
|
return a_hidden - b_hidden;
|
|
}
|
|
|
|
// push_stringf seems to null terminate
|
|
return strcoll((char*)(*a)->file_name.str, (char*)(*b)->file_name.str);
|
|
}
|
|
|
|
internal int
|
|
linux_system_get_file_list_filter(const struct dirent *dirent) {
|
|
String_Const_u8 file_name = SCu8((u8*)dirent->d_name);
|
|
if (string_match(file_name, string_u8_litexpr("."))) {
|
|
return 0;
|
|
}
|
|
else if (string_match(file_name, string_u8_litexpr(".."))) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
internal u64
|
|
linux_ns_from_timespec(const struct timespec timespec) {
|
|
return timespec.tv_nsec + UINT64_C(1000000000) * timespec.tv_sec;
|
|
}
|
|
|
|
internal File_Attribute_Flag
|
|
linux_convert_file_attribute_flags(int mode) {
|
|
File_Attribute_Flag result = {};
|
|
MovFlag(mode, S_IFDIR, result, FileAttribute_IsDirectory);
|
|
return result;
|
|
}
|
|
|
|
internal File_Attributes
|
|
linux_file_attributes_from_struct_stat(struct stat* file_stat) {
|
|
File_Attributes result = {};
|
|
result.size = file_stat->st_size;
|
|
result.last_write_time = linux_ns_from_timespec(file_stat->st_mtim);
|
|
result.flags = linux_convert_file_attribute_flags(file_stat->st_mode);
|
|
return result;
|
|
}
|
|
|
|
internal void
|
|
linux_schedule_step(){
|
|
if(!__sync_bool_compare_and_swap(&linuxvars.step_pending, 0, 1)) {
|
|
return;
|
|
}
|
|
|
|
u64 now = system_now_time();
|
|
u64 diff = (now - linuxvars.last_step_time);
|
|
|
|
struct itimerspec its = {};
|
|
|
|
if(diff >= frame_nseconds) {
|
|
its.it_value.tv_nsec = 1;
|
|
} else {
|
|
its.it_value.tv_nsec = frame_nseconds - diff;
|
|
}
|
|
|
|
timerfd_settime(linuxvars.step_timer_fd, 0, &its, NULL);
|
|
}
|
|
|
|
enum wm_state_mode {
|
|
WM_STATE_DEL = 0,
|
|
WM_STATE_ADD = 1,
|
|
WM_STATE_TOGGLE = 2,
|
|
};
|
|
|
|
internal void
|
|
linux_set_wm_state(Atom one, Atom two, enum wm_state_mode mode){
|
|
//NOTE(inso): this will only work after the window has been mapped
|
|
|
|
XEvent e = {};
|
|
e.xany.type = ClientMessage;
|
|
e.xclient.message_type = linuxvars.atom__NET_WM_STATE;
|
|
e.xclient.format = 32;
|
|
e.xclient.window = linuxvars.win;
|
|
e.xclient.data.l[0] = mode;
|
|
e.xclient.data.l[1] = one;
|
|
e.xclient.data.l[2] = two;
|
|
e.xclient.data.l[3] = 1L;
|
|
|
|
XSendEvent(linuxvars.dpy,
|
|
RootWindow(linuxvars.dpy, 0),
|
|
0, SubstructureNotifyMask | SubstructureRedirectMask, &e);
|
|
}
|
|
|
|
internal void
|
|
linux_window_maximize(enum wm_state_mode mode){
|
|
linux_set_wm_state(linuxvars.atom__NET_WM_STATE_MAXIMIZED_HORZ, linuxvars.atom__NET_WM_STATE_MAXIMIZED_VERT, mode);
|
|
}
|
|
|
|
internal void
|
|
linux_window_fullscreen(enum wm_state_mode mode) {
|
|
linux_set_wm_state(linuxvars.atom__NET_WM_STATE_FULLSCREEN, 0, mode);
|
|
}
|
|
|
|
internal int
|
|
linux_get_xsettings_dpi(Display* dpy, int screen){
|
|
struct XSettingHeader {
|
|
u8 type;
|
|
u8 pad0;
|
|
u16 name_len;
|
|
char name[0];
|
|
};
|
|
|
|
struct XSettings {
|
|
u8 byte_order;
|
|
u8 pad[3];
|
|
u32 serial;
|
|
u32 num_settings;
|
|
};
|
|
|
|
enum { XSettingsTypeInt, XSettingsTypeString, XSettingsTypeColor };
|
|
|
|
int dpi = -1;
|
|
unsigned char* prop = NULL;
|
|
char sel_buffer[64];
|
|
struct XSettings* xs;
|
|
const char* p;
|
|
|
|
snprintf(sel_buffer, sizeof(sel_buffer), "_XSETTINGS_S%d", screen);
|
|
|
|
Atom XSET_SEL = XInternAtom(dpy, sel_buffer, True);
|
|
Atom XSET_SET = XInternAtom(dpy, "_XSETTINGS_SETTINGS", True);
|
|
|
|
if (XSET_SEL == None || XSET_SET == None){
|
|
//LOG("XSETTINGS unavailable.\n");
|
|
return(dpi);
|
|
}
|
|
|
|
Window xset_win = XGetSelectionOwner(dpy, XSET_SEL);
|
|
if (xset_win == None){
|
|
// TODO(inso): listen for the ClientMessage about it becoming available?
|
|
// there's not much point atm if DPI scaling is only done at startup
|
|
goto out;
|
|
}
|
|
|
|
{
|
|
Atom type;
|
|
int fmt;
|
|
unsigned long pad, num;
|
|
|
|
if (XGetWindowProperty(dpy, xset_win, XSET_SET, 0, 1024, False, XSET_SET, &type, &fmt, &num, &pad, &prop) != Success){
|
|
//LOG("XSETTINGS: GetWindowProperty failed.\n");
|
|
goto out;
|
|
}
|
|
|
|
if (fmt != 8){
|
|
//LOG("XSETTINGS: Wrong format.\n");
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
xs = (struct XSettings*)prop;
|
|
p = (char*)(xs + 1);
|
|
|
|
if (xs->byte_order != 0){
|
|
//LOG("FIXME: XSETTINGS not host byte order?\n");
|
|
goto out;
|
|
}
|
|
|
|
for (int i = 0; i < xs->num_settings; ++i){
|
|
struct XSettingHeader* h = (struct XSettingHeader*)p;
|
|
|
|
p += sizeof(struct XSettingHeader);
|
|
p += h->name_len;
|
|
p += ((4 - (h->name_len & 3)) & 3);
|
|
p += 4; // serial
|
|
|
|
switch (h->type){
|
|
case XSettingsTypeInt: {
|
|
if (strncmp(h->name, "Xft/DPI", h->name_len) == 0){
|
|
dpi = *(i32*)p;
|
|
if (dpi != -1) dpi /= 1024;
|
|
}
|
|
p += 4;
|
|
} break;
|
|
|
|
case XSettingsTypeString: {
|
|
u32 len = *(u32*)p;
|
|
p += 4;
|
|
p += len;
|
|
p += ((4 - (len & 3)) & 3);
|
|
} break;
|
|
|
|
case XSettingsTypeColor: {
|
|
p += 8;
|
|
} break;
|
|
|
|
default: {
|
|
//LOG("XSETTINGS: Got invalid type...\n");
|
|
goto out;
|
|
} break;
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (prop){
|
|
XFree(prop);
|
|
}
|
|
|
|
return dpi;
|
|
}
|
|
|
|
internal void*
|
|
linux_thread_proc_start(void* arg) {
|
|
Linux_Object* info = (Linux_Object*)arg;
|
|
Assert(info->kind == LinuxObjectKind_Thread);
|
|
info->thread.proc(info->thread.ptr);
|
|
return NULL;
|
|
}
|
|
|
|
#include "linux_icon.h"
|
|
internal void
|
|
linux_set_icon(Display* d, Window w){
|
|
Atom WM_ICON = XInternAtom(d, "_NET_WM_ICON", False);
|
|
XChangeProperty(d, w, WM_ICON, XA_CARDINAL, 32, PropModeReplace, (unsigned char*)linux_icon, sizeof(linux_icon) / sizeof(long));
|
|
}
|
|
|
|
#include "linux_error_box.cpp"
|
|
|
|
////////////////////////////
|
|
|
|
#include "linux_4ed_functions.cpp"
|
|
|
|
////////////////////////////
|
|
|
|
#include <GL/gl.h>
|
|
#include "opengl/4ed_opengl_defines.h"
|
|
#define GL_FUNC(N,R,P) typedef R (CALL_CONVENTION N##_Function)P; N##_Function *N = 0;
|
|
#include "opengl/4ed_opengl_funcs.h"
|
|
#include "opengl/4ed_opengl_render.cpp"
|
|
|
|
internal
|
|
graphics_get_texture_sig(){
|
|
return(gl__get_texture(dim, texture_kind));
|
|
}
|
|
|
|
internal
|
|
graphics_fill_texture_sig(){
|
|
return(gl__fill_texture(texture_kind, texture, p, dim, data));
|
|
}
|
|
|
|
////////////////////////////
|
|
|
|
internal Face_Description
|
|
linux_find_font(Face_Description* desc) {
|
|
Face_Description result = *desc;
|
|
|
|
char* name = strndupa((char*)desc->font.file_name.str, desc->font.file_name.size);
|
|
|
|
double size;
|
|
const char* style;
|
|
{
|
|
Face_Load_Parameters* p = &desc->parameters;
|
|
size = p->pt_size;
|
|
|
|
if(p->bold && p->italic) {
|
|
style = "Bold Italic";
|
|
} else if(p->bold) {
|
|
style = "Bold";
|
|
} else if(p->italic) {
|
|
style = "Italic";
|
|
} else {
|
|
style = "Regular";
|
|
}
|
|
}
|
|
|
|
FcPattern *pattern = FcPatternBuild(
|
|
0,
|
|
FC_POSTSCRIPT_NAME, FcTypeString, name,
|
|
FC_SIZE, FcTypeDouble, size,
|
|
FC_FONTFORMAT, FcTypeString, "TrueType",
|
|
FC_STYLE, FcTypeString, (FcChar8*)style,
|
|
NULL);
|
|
|
|
if(!pattern) {
|
|
return result;
|
|
}
|
|
|
|
if (FcConfigSubstitute(linuxvars.fontconfig, pattern, FcMatchPattern)){
|
|
FcDefaultSubstitute(pattern);
|
|
|
|
FcResult res;
|
|
FcPattern *font = FcFontMatch(linuxvars.fontconfig, pattern, &res);
|
|
if (!font){
|
|
return result;
|
|
}
|
|
|
|
FcChar8 *filename = 0;
|
|
FcPatternGetString(font, FC_FILE, 0, &filename);
|
|
if(filename) {
|
|
LINUX_FN_DEBUG("FONTCONFIG FILENAME = %s\n", filename);
|
|
result.font.file_name = push_u8_stringf(linuxvars.frame_arena, "%s", filename);
|
|
}
|
|
|
|
FcPatternDestroy(font);
|
|
}
|
|
|
|
FcPatternDestroy(pattern);
|
|
|
|
return result;
|
|
}
|
|
|
|
internal
|
|
font_make_face_sig() {
|
|
|
|
Face* result = ft__font_make_face(arena, description, scale_factor);
|
|
|
|
// if it failed to load the font directly, try via fontconfig.
|
|
if(!result) {
|
|
Face_Description desc2 = {};
|
|
desc2.parameters = description->parameters;
|
|
desc2.font.file_name = string_front_of_path(description->font.file_name);
|
|
|
|
printf("FONT %.*s\n", string_expand(desc2.font.file_name));
|
|
desc2 = linux_find_font(&desc2);
|
|
result = ft__font_make_face(arena, &desc2, scale_factor);
|
|
}
|
|
|
|
if(!result) {
|
|
// is this fatal? 4ed.cpp:277 (caller) does not check for null.
|
|
String_Const_u8 s = description->font.file_name;
|
|
char msg[4096];
|
|
snprintf(msg, sizeof(msg), "Unable to load font: %.*s", (int)s.size, s.str);
|
|
system_error_box(msg);
|
|
}
|
|
|
|
return(result);
|
|
}
|
|
|
|
////////////////////////////
|
|
|
|
internal b32
|
|
glx_init(void) {
|
|
int glx_maj, glx_min;
|
|
|
|
if(!glXQueryVersion(linuxvars.dpy, &glx_maj, &glx_min)) {
|
|
return false;
|
|
}
|
|
|
|
return glx_maj > 1 || (glx_maj == 1 && glx_min >= 3);
|
|
}
|
|
|
|
internal b32
|
|
glx_get_config(GLXFBConfig* fb_config, XVisualInfo* vi) {
|
|
|
|
static const int attrs[] = {
|
|
GLX_X_RENDERABLE , True,
|
|
GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
|
|
GLX_RENDER_TYPE , GLX_RGBA_BIT,
|
|
GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
|
|
GLX_RED_SIZE , 8,
|
|
GLX_GREEN_SIZE , 8,
|
|
GLX_BLUE_SIZE , 8,
|
|
GLX_ALPHA_SIZE , 8,
|
|
GLX_DEPTH_SIZE , 24,
|
|
GLX_STENCIL_SIZE , 8,
|
|
GLX_DOUBLEBUFFER , True,
|
|
None
|
|
};
|
|
|
|
int conf_count = 0;
|
|
GLXFBConfig* conf_list = glXChooseFBConfig(linuxvars.dpy, DefaultScreen(linuxvars.dpy), attrs, &conf_count);
|
|
if(!conf_count || conf_count <= 0) {
|
|
return false;
|
|
}
|
|
|
|
*fb_config = *conf_list;
|
|
XFree(conf_list);
|
|
|
|
XVisualInfo* xvi = glXGetVisualFromFBConfig(linuxvars.dpy, *fb_config);
|
|
if(!xvi) {
|
|
return false;
|
|
}
|
|
|
|
*vi = *xvi;
|
|
XFree(xvi);
|
|
|
|
return true;
|
|
}
|
|
|
|
internal b32 glx_ctx_error;
|
|
|
|
internal int
|
|
glx_error_handler(Display* dpy, XErrorEvent* ev){
|
|
glx_ctx_error = true;
|
|
return 0;
|
|
}
|
|
|
|
typedef GLXContext (glXCreateContextAttribsARB_Function)(Display*, GLXFBConfig, GLXContext, Bool, const int*);
|
|
typedef void (glXSwapIntervalEXT_Function) (Display *dpy, GLXDrawable drawable, int interval);
|
|
typedef int (glXSwapIntervalMESA_Function) (unsigned int interval);
|
|
typedef int (glXGetSwapIntervalMESA_Function) (void);
|
|
typedef int (glXSwapIntervalSGI_Function) (int interval);
|
|
|
|
internal b32
|
|
glx_create_context(GLXFBConfig fb_config){
|
|
const char *glx_exts = glXQueryExtensionsString(linuxvars.dpy, DefaultScreen(linuxvars.dpy));
|
|
|
|
glXCreateContextAttribsARB_Function *glXCreateContextAttribsARB = 0;
|
|
glXSwapIntervalEXT_Function *glXSwapIntervalEXT = 0;
|
|
glXSwapIntervalMESA_Function *glXSwapIntervalMESA = 0;
|
|
glXGetSwapIntervalMESA_Function *glXGetSwapIntervalMESA = 0;
|
|
glXSwapIntervalSGI_Function *glXSwapIntervalSGI = 0;
|
|
|
|
#define GLXLOAD(f) f = (f##_Function*) glXGetProcAddressARB((const GLubyte*) #f);
|
|
GLXLOAD(glXCreateContextAttribsARB);
|
|
|
|
GLXContext ctx = NULL;
|
|
int (*old_handler)(Display*, XErrorEvent*) = XSetErrorHandler(&glx_error_handler);
|
|
|
|
if (glXCreateContextAttribsARB == NULL){
|
|
//LOG("glXCreateContextAttribsARB() not found, using old-style GLX context\n" );
|
|
ctx = glXCreateNewContext(linuxvars.dpy, fb_config, GLX_RGBA_TYPE, 0, True);
|
|
} else {
|
|
static const int context_attribs[] = {
|
|
GLX_CONTEXT_MAJOR_VERSION_ARB, 2,
|
|
GLX_CONTEXT_MINOR_VERSION_ARB, 1,
|
|
GLX_CONTEXT_PROFILE_MASK_ARB , GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
|
|
#if defined(FRED_INTERNAL)
|
|
GLX_CONTEXT_FLAGS_ARB , GLX_CONTEXT_DEBUG_BIT_ARB,
|
|
#endif
|
|
None
|
|
};
|
|
|
|
//LOG("Creating GL 2.1 context... ");
|
|
ctx = glXCreateContextAttribsARB(linuxvars.dpy, fb_config, 0, True, context_attribs);
|
|
}
|
|
|
|
XSync(linuxvars.dpy, False);
|
|
if(glx_ctx_error || !ctx) {
|
|
return false;
|
|
}
|
|
|
|
XSync(linuxvars.dpy, False);
|
|
XSetErrorHandler(old_handler);
|
|
|
|
//b32 direct = glXIsDirect(linuxvars.dpy, ctx);
|
|
|
|
//LOG("Making context current\n");
|
|
glXMakeCurrent(linuxvars.dpy, linuxvars.win, ctx);
|
|
|
|
//glx_enable_vsync();
|
|
|
|
// NOTE(allen): Load gl functions
|
|
#define GL_FUNC(f,R,P) GLXLOAD(f)
|
|
#include "opengl/4ed_opengl_funcs.h"
|
|
|
|
#undef GLXLOAD
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////
|
|
|
|
internal void
|
|
linux_x11_init(int argc, char** argv, Plat_Settings* settings) {
|
|
|
|
Display* dpy = XOpenDisplay(0);
|
|
if (!dpy){
|
|
fprintf(stderr, "FATAL: Cannot open X11 Display!\n");
|
|
exit(1);
|
|
}
|
|
|
|
linuxvars.dpy = dpy;
|
|
|
|
#define LOAD_ATOM(x) linuxvars.atom_##x = XInternAtom(linuxvars.dpy, #x, False);
|
|
|
|
LOAD_ATOM(TARGETS);
|
|
LOAD_ATOM(CLIPBOARD);
|
|
LOAD_ATOM(UTF8_STRING);
|
|
LOAD_ATOM(_NET_WM_STATE);
|
|
LOAD_ATOM(_NET_WM_STATE_MAXIMIZED_HORZ);
|
|
LOAD_ATOM(_NET_WM_STATE_MAXIMIZED_VERT);
|
|
LOAD_ATOM(_NET_WM_STATE_FULLSCREEN);
|
|
LOAD_ATOM(_NET_WM_PING);
|
|
LOAD_ATOM(_NET_WM_WINDOW_TYPE);
|
|
LOAD_ATOM(_NET_WM_WINDOW_TYPE_NORMAL);
|
|
LOAD_ATOM(_NET_WM_PID);
|
|
LOAD_ATOM(WM_DELETE_WINDOW);
|
|
|
|
#undef LOAD_ATOM
|
|
|
|
if (!glx_init()){
|
|
system_error_box("Your XServer's GLX version is too old. GLX 1.3+ is required.");
|
|
}
|
|
|
|
GLXFBConfig fb_config;
|
|
XVisualInfo vi;
|
|
if (!glx_get_config(&fb_config, &vi)){
|
|
system_error_box("Could not get a matching GLX FBConfig. Check your OpenGL drivers are installed correctly.");
|
|
}
|
|
|
|
// TODO: window size
|
|
#define WINDOW_W_DEFAULT 800
|
|
#define WINDOW_H_DEFAULT 600
|
|
int w = WINDOW_W_DEFAULT;
|
|
int h = WINDOW_H_DEFAULT;
|
|
|
|
// TEMP
|
|
render_target.width = w;
|
|
render_target.height = h;
|
|
|
|
XSetWindowAttributes swa = {};
|
|
swa.backing_store = WhenMapped;
|
|
swa.event_mask = StructureNotifyMask;
|
|
swa.bit_gravity = NorthWestGravity;
|
|
swa.colormap = XCreateColormap(dpy, RootWindow(dpy, vi.screen), vi.visual, AllocNone);
|
|
|
|
u32 CWflags = CWBackingStore|CWBitGravity|CWBackPixel|CWBorderPixel|CWColormap|CWEventMask;
|
|
linuxvars.win = XCreateWindow(dpy, RootWindow(dpy, vi.screen), 0, 0, w, h, 0, vi.depth, InputOutput, vi.visual, CWflags, &swa);
|
|
|
|
if (!linuxvars.win){
|
|
system_error_box("XCreateWindow failed. Make sure your display is set up correctly.");
|
|
}
|
|
|
|
//NOTE(inso): Set the window's type to normal
|
|
XChangeProperty(linuxvars.dpy, linuxvars.win, linuxvars.atom__NET_WM_WINDOW_TYPE, XA_ATOM, 32, PropModeReplace, (unsigned char*)&linuxvars.atom__NET_WM_WINDOW_TYPE_NORMAL, 1);
|
|
|
|
//NOTE(inso): window managers want the PID as a window property for some reason.
|
|
pid_t pid = getpid();
|
|
XChangeProperty(linuxvars.dpy, linuxvars.win, linuxvars.atom__NET_WM_PID, XA_CARDINAL, 32, PropModeReplace, (unsigned char*)&pid, 1);
|
|
|
|
//NOTE(inso): set wm properties
|
|
XStoreName(linuxvars.dpy, linuxvars.win, WINDOW_NAME);
|
|
|
|
XSizeHints *sz_hints = XAllocSizeHints();
|
|
XWMHints *wm_hints = XAllocWMHints();
|
|
XClassHint *cl_hints = XAllocClassHint();
|
|
|
|
sz_hints->flags = PMinSize | PMaxSize | PWinGravity;
|
|
|
|
sz_hints->min_width = 50;
|
|
sz_hints->min_height = 50;
|
|
|
|
sz_hints->max_width = sz_hints->max_height = (1UL << 16UL);
|
|
sz_hints->win_gravity = NorthWestGravity;
|
|
|
|
if (settings->set_window_pos){
|
|
sz_hints->flags |= USPosition;
|
|
sz_hints->x = settings->window_x;
|
|
sz_hints->y = settings->window_y;
|
|
}
|
|
|
|
wm_hints->flags |= InputHint | StateHint;
|
|
wm_hints->input = True;
|
|
wm_hints->initial_state = NormalState;
|
|
|
|
cl_hints->res_name = "4coder";
|
|
cl_hints->res_class = "4coder";
|
|
|
|
char* win_name_list[] = { WINDOW_NAME };
|
|
XTextProperty win_name;
|
|
XStringListToTextProperty(win_name_list, 1, &win_name);
|
|
|
|
XSetWMProperties(linuxvars.dpy, linuxvars.win, &win_name, NULL, argv, argc, sz_hints, wm_hints, cl_hints);
|
|
|
|
XFree(win_name.value);
|
|
XFree(sz_hints);
|
|
XFree(wm_hints);
|
|
XFree(cl_hints);
|
|
|
|
linux_set_icon(linuxvars.dpy, linuxvars.win);
|
|
|
|
// NOTE(inso): make the window visible
|
|
XMapWindow(linuxvars.dpy, linuxvars.win);
|
|
|
|
if(!glx_create_context(fb_config)) {
|
|
system_error_box("Unable to create GLX context.");
|
|
}
|
|
|
|
XRaiseWindow(linuxvars.dpy, linuxvars.win);
|
|
|
|
if (settings->set_window_pos){
|
|
XMoveWindow(linuxvars.dpy, linuxvars.win, settings->window_x, settings->window_y);
|
|
}
|
|
|
|
if (settings->maximize_window){
|
|
linux_set_wm_state(linuxvars.atom__NET_WM_STATE_MAXIMIZED_HORZ, linuxvars.atom__NET_WM_STATE_MAXIMIZED_VERT, WM_STATE_ADD);
|
|
} else if (settings->fullscreen_window){
|
|
linux_set_wm_state(linuxvars.atom__NET_WM_STATE_FULLSCREEN, 0, WM_STATE_ADD);
|
|
}
|
|
|
|
XSync(linuxvars.dpy, False);
|
|
|
|
Atom wm_protos[] = {
|
|
linuxvars.atom_WM_DELETE_WINDOW,
|
|
linuxvars.atom__NET_WM_PING
|
|
};
|
|
|
|
XSetWMProtocols(linuxvars.dpy, linuxvars.win, wm_protos, 2);
|
|
|
|
// XFixes extension for clipboard notification.
|
|
{
|
|
int xfixes_version_unused, xfixes_err_unused;
|
|
Bool has_xfixes = XQueryExtension(linuxvars.dpy, "XFIXES", &xfixes_version_unused, &linuxvars.xfixes_selection_event, &xfixes_err_unused);
|
|
linuxvars.has_xfixes = (has_xfixes == True);
|
|
|
|
// request notifications for CLIPBOARD updates.
|
|
if(has_xfixes) {
|
|
XFixesSelectSelectionInput(linuxvars.dpy, linuxvars.win, linuxvars.atom_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask);
|
|
}
|
|
}
|
|
|
|
// Input handling init
|
|
|
|
setlocale(LC_ALL, "");
|
|
XSetLocaleModifiers("");
|
|
b32 locale_supported = XSupportsLocale();
|
|
|
|
if (!locale_supported){
|
|
setlocale(LC_ALL, "C");
|
|
}
|
|
|
|
linuxvars.xim = XOpenIM(dpy, 0, 0, 0);
|
|
if (!linuxvars.xim){
|
|
// NOTE(inso): Try falling back to the internal XIM implementation that
|
|
// should in theory always exist.
|
|
XSetLocaleModifiers("@im=none");
|
|
linuxvars.xim = XOpenIM(dpy, 0, 0, 0);
|
|
}
|
|
|
|
// If it still isn't there we're screwed.
|
|
if (!linuxvars.xim){
|
|
system_error_box("Could not initialize X Input.");
|
|
}
|
|
|
|
XIMStyles *styles = NULL;
|
|
const XIMStyle style_want = (XIMPreeditNothing | XIMStatusNothing);
|
|
b32 found_style = false;
|
|
|
|
if (!XGetIMValues(linuxvars.xim, XNQueryInputStyle, &styles, NULL) && styles){
|
|
for (i32 i = 0; i < styles->count_styles; ++i){
|
|
XIMStyle style = styles->supported_styles[i];
|
|
if (style == style_want) {
|
|
found_style = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!found_style) {
|
|
system_error_box("Could not find supported X Input style.");
|
|
}
|
|
|
|
XFree(styles);
|
|
|
|
linuxvars.xic = XCreateIC(linuxvars.xim,
|
|
XNInputStyle, style_want,
|
|
XNClientWindow, linuxvars.win,
|
|
XNFocusWindow, linuxvars.win,
|
|
NULL);
|
|
|
|
if(!linuxvars.xic) {
|
|
system_error_box("Error creating X Input context.");
|
|
}
|
|
|
|
int xim_event_mask;
|
|
if (XGetICValues(linuxvars.xic, XNFilterEvents, &xim_event_mask, NULL)){
|
|
xim_event_mask = 0;
|
|
}
|
|
|
|
u32 event_mask = ExposureMask
|
|
| KeyPressMask | KeyReleaseMask
|
|
| ButtonPressMask | ButtonReleaseMask
|
|
| EnterWindowMask | LeaveWindowMask
|
|
| PointerMotionMask
|
|
| FocusChangeMask
|
|
| StructureNotifyMask
|
|
| ExposureMask | VisibilityChangeMask
|
|
| xim_event_mask;
|
|
|
|
XSelectInput(linuxvars.dpy, linuxvars.win, event_mask);
|
|
|
|
// init XKB keyboard extension
|
|
|
|
if(!XkbQueryExtension(linuxvars.dpy, 0, &linuxvars.xkb_event, 0, 0, 0)) {
|
|
system_error_box("XKB Extension not available.");
|
|
}
|
|
|
|
XkbSelectEvents(linuxvars.dpy, XkbUseCoreKbd, XkbAllEventsMask, XkbAllEventsMask);
|
|
linuxvars.xkb = XkbGetKeyboard(linuxvars.dpy, XkbAllComponentsMask, XkbUseCoreKbd);
|
|
if(!linuxvars.xkb) {
|
|
system_error_box("Error getting XKB keyboard details.");
|
|
}
|
|
|
|
// closer to windows behaviour (holding key doesn't generate release events)
|
|
XkbSetDetectableAutoRepeat(linuxvars.dpy, True, NULL);
|
|
|
|
XCursor cursors[APP_MOUSE_CURSOR_COUNT] = {
|
|
None,
|
|
None,
|
|
XCreateFontCursor(linuxvars.dpy, XC_xterm),
|
|
XCreateFontCursor(linuxvars.dpy, XC_sb_h_double_arrow),
|
|
XCreateFontCursor(linuxvars.dpy, XC_sb_v_double_arrow)
|
|
};
|
|
block_copy(linuxvars.xcursors, cursors, sizeof(cursors));
|
|
|
|
// sneaky invisible cursor
|
|
{
|
|
char data = 0;
|
|
XColor c = {};
|
|
Pixmap p = XCreateBitmapFromData(linuxvars.dpy, linuxvars.win, &data, 1, 1);
|
|
|
|
linuxvars.hidden_cursor = XCreatePixmapCursor(linuxvars.dpy, p, p, &c, &c, 0, 0);
|
|
|
|
XFreePixmap(linuxvars.dpy, p);
|
|
}
|
|
}
|
|
|
|
global Key_Code keycode_lookup_table[255];
|
|
|
|
internal void
|
|
linux_keycode_init(Display* dpy){
|
|
|
|
block_zero_array(keycode_lookup_table);
|
|
|
|
// Find these keys by physical position, and map to QWERTY KeyCodes
|
|
#define K(k) glue(KeyCode_, k)
|
|
static const u8 positional_keys[] = {
|
|
K(1), K(2), K(3), K(4), K(5), K(6), K(7), K(8), K(9), K(0), K(Minus), K(Equal),
|
|
K(Q), K(W), K(E), K(R), K(T), K(Y), K(U), K(I), K(O), K(P), K(LeftBracket), K(RightBracket),
|
|
K(A), K(S), K(D), K(F), K(G), K(H), K(J), K(K), K(L), K(Semicolon), K(Quote), /*uk hash*/0,
|
|
K(Z), K(X), K(C), K(V), K(B), K(N), K(M), K(Comma), K(Period), K(ForwardSlash), 0, 0
|
|
};
|
|
#undef K
|
|
|
|
// XKB gives the alphanumeric keys names like AE01 -> E is the row (from B-E), 01 is the column (01-12).
|
|
// to get key names in .ps file: setxkbmap -print | xkbcomp - - | xkbprint -label name - out.ps
|
|
|
|
static const int ncols = 12;
|
|
static const int nrows = 4;
|
|
|
|
for(int i = XkbMinLegalKeyCode; i <= XkbMaxLegalKeyCode; ++i) {
|
|
const char* name = linuxvars.xkb->names->keys[i].name;
|
|
|
|
if(name[0] == 'A' && name[1] >= 'B' && name[1] <= 'E') {
|
|
int row = (nrows - 1) - (name[1] - 'B');
|
|
int col = (name[2] - '0') * 10 + (name[3] - '0') - 1;
|
|
|
|
if(row >= 0 && row < nrows && col >= 0 && col < ncols) {
|
|
keycode_lookup_table[i] = positional_keys[row * ncols + col];
|
|
}
|
|
}
|
|
|
|
// a few special cases:
|
|
|
|
else if(memcmp(name, "TLDE", XkbKeyNameLength) == 0) {
|
|
keycode_lookup_table[i] = KeyCode_Tick;
|
|
} else if(memcmp(name, "BKSL", XkbKeyNameLength) == 0) {
|
|
keycode_lookup_table[i] = KeyCode_BackwardSlash;
|
|
} else if(memcmp(name, "LSGT", XkbKeyNameLength) == 0) {
|
|
// UK extra key between left shift and Z, not sure what to do with it...
|
|
// Setting to F13-F16 seems to break text input.
|
|
// it prints \ and | with shift. KeyCode_Backslash will be where UK # is.
|
|
// keycode_lookup_table[i] =
|
|
}
|
|
}
|
|
|
|
// Find the rest by their key label
|
|
struct SymCode { KeySym sym; Key_Code code; };
|
|
SymCode sym_table[100];
|
|
SymCode* p = sym_table;
|
|
|
|
*p++ = { XK_space, KeyCode_Space };
|
|
*p++ = { XK_Tab, KeyCode_Tab };
|
|
*p++ = { XK_Escape, KeyCode_Escape };
|
|
*p++ = { XK_Pause, KeyCode_Pause };
|
|
*p++ = { XK_Up, KeyCode_Up };
|
|
*p++ = { XK_Down, KeyCode_Down };
|
|
*p++ = { XK_Left, KeyCode_Left };
|
|
*p++ = { XK_Right, KeyCode_Right };
|
|
*p++ = { XK_BackSpace, KeyCode_Backspace };
|
|
*p++ = { XK_Return, KeyCode_Return };
|
|
*p++ = { XK_Delete, KeyCode_Delete };
|
|
*p++ = { XK_Insert, KeyCode_Insert };
|
|
*p++ = { XK_Home, KeyCode_Home };
|
|
*p++ = { XK_End, KeyCode_End };
|
|
*p++ = { XK_Page_Up, KeyCode_PageUp };
|
|
*p++ = { XK_Page_Down, KeyCode_PageDown };
|
|
*p++ = { XK_Caps_Lock, KeyCode_CapsLock };
|
|
*p++ = { XK_Num_Lock, KeyCode_NumLock };
|
|
*p++ = { XK_Scroll_Lock, KeyCode_ScrollLock };
|
|
*p++ = { XK_Menu, KeyCode_Menu };
|
|
*p++ = { XK_Shift_L, KeyCode_Shift };
|
|
*p++ = { XK_Shift_R, KeyCode_Shift };
|
|
*p++ = { XK_Control_L, KeyCode_Control };
|
|
*p++ = { XK_Control_R, KeyCode_Control };
|
|
*p++ = { XK_Alt_L, KeyCode_Alt };
|
|
*p++ = { XK_Alt_R, KeyCode_Alt };
|
|
*p++ = { XK_Super_L, KeyCode_Command };
|
|
*p++ = { XK_Super_R, KeyCode_Command };
|
|
|
|
for(Key_Code k = KeyCode_F1; k <= KeyCode_F16; ++k) {
|
|
*p++ = { XK_F1 + (k - KeyCode_F1), k };
|
|
}
|
|
|
|
const int table_size = p - sym_table;
|
|
Assert(table_size < ArrayCount(sym_table));
|
|
|
|
for(int i = XkbMinLegalKeyCode; i <= XkbMaxLegalKeyCode; ++i) {
|
|
KeySym sym = NoSymbol;
|
|
|
|
// lookup key in current layout with no modifiers held (0)
|
|
if(!XkbTranslateKeyCode(linuxvars.xkb, i, XkbBuildCoreState(0, linuxvars.xkb_group), NULL, &sym)) {
|
|
continue;
|
|
}
|
|
|
|
for(int j = 0; j < table_size; ++j) {
|
|
if(sym_table[j].sym == sym) {
|
|
keycode_lookup_table[i] = sym_table[j].code;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void
|
|
linux_epoll_init(void) {
|
|
struct epoll_event e = {};
|
|
e.events = EPOLLIN | EPOLLET;
|
|
|
|
linuxvars.step_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
|
|
linuxvars.epoll = epoll_create(16);
|
|
|
|
e.data.ptr = &epoll_tag_x11;
|
|
epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, ConnectionNumber(linuxvars.dpy), &e);
|
|
|
|
e.data.ptr = &epoll_tag_step_timer;
|
|
epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, linuxvars.step_timer_fd, &e);
|
|
}
|
|
|
|
internal void
|
|
linux_clipboard_send(XSelectionRequestEvent* req) {
|
|
|
|
XSelectionEvent rsp = {};
|
|
rsp.type = SelectionNotify;
|
|
rsp.requestor = req->requestor;
|
|
rsp.selection = req->selection;
|
|
rsp.target = req->target;
|
|
rsp.time = req->time;
|
|
rsp.property = None;
|
|
|
|
Atom formats[] = {
|
|
linuxvars.atom_UTF8_STRING,
|
|
XA_STRING,
|
|
};
|
|
|
|
if(linuxvars.clipboard_contents.size == 0) {
|
|
goto done;
|
|
}
|
|
|
|
if(req->selection != linuxvars.atom_CLIPBOARD || req->property == None) {
|
|
goto done;
|
|
}
|
|
|
|
if (req->target == linuxvars.atom_TARGETS){
|
|
|
|
XChangeProperty(
|
|
req->display,
|
|
req->requestor,
|
|
req->property,
|
|
XA_ATOM,
|
|
32,
|
|
PropModeReplace,
|
|
(u8*)formats,
|
|
ArrayCount(formats));
|
|
|
|
rsp.property = req->property;
|
|
|
|
} else {
|
|
|
|
int i;
|
|
for(i = 0; i < ArrayCount(formats); ++i){
|
|
if (req->target == formats[i]){
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i != ArrayCount(formats)){
|
|
XChangeProperty(
|
|
req->display,
|
|
req->requestor,
|
|
req->property,
|
|
req->target,
|
|
8,
|
|
PropModeReplace,
|
|
linuxvars.clipboard_contents.str,
|
|
linuxvars.clipboard_contents.size
|
|
);
|
|
|
|
rsp.property = req->property;
|
|
}
|
|
}
|
|
|
|
done:
|
|
XSendEvent(req->display, req->requestor, True, 0, (XEvent*)&rsp);
|
|
}
|
|
|
|
internal void
|
|
linux_clipboard_recv(XSelectionEvent* ev) {
|
|
|
|
if(ev->selection != linuxvars.atom_CLIPBOARD ||
|
|
ev->target != linuxvars.atom_UTF8_STRING ||
|
|
ev->property == None) {
|
|
return;
|
|
}
|
|
|
|
Atom type;
|
|
int fmt;
|
|
unsigned long nitems;
|
|
unsigned long bytes_left;
|
|
u8 *data;
|
|
|
|
int result = XGetWindowProperty(
|
|
linuxvars.dpy,
|
|
linuxvars.win,
|
|
linuxvars.atom_CLIPBOARD,
|
|
0L, 0x20000000L, False,
|
|
linuxvars.atom_UTF8_STRING,
|
|
&type, &fmt, &nitems,
|
|
&bytes_left, &data);
|
|
|
|
if(result == Success && fmt == 8){
|
|
linalloc_clear(linuxvars.clipboard_arena);
|
|
linuxvars.clipboard_contents = push_u8_stringf(linuxvars.clipboard_arena, "%.*s", nitems, data);
|
|
linuxvars.received_new_clipboard = true;
|
|
XFree(data);
|
|
XDeleteProperty(linuxvars.dpy, linuxvars.win, linuxvars.atom_CLIPBOARD);
|
|
linux_schedule_step();
|
|
}
|
|
}
|
|
|
|
internal String_Const_u8
|
|
linux_filter_text(Arena* arena, u8* buf, int len) {
|
|
u8* const result = push_array(arena, u8, len);
|
|
u8* outp = result;
|
|
|
|
for(int i = 0; i < len; ++i) {
|
|
u8 c = buf[i];
|
|
|
|
if(c == '\r') {
|
|
*outp++ = '\n';
|
|
} else if(c > 127 || (' ' <= c && c <= '~') || c == '\t') {
|
|
*outp++ = c;
|
|
}
|
|
}
|
|
|
|
return SCu8(result, outp - result);
|
|
}
|
|
|
|
internal void
|
|
linux_handle_x11_events() {
|
|
static XEvent prev_event = {};
|
|
b32 should_step = false;
|
|
|
|
while (XPending(linuxvars.dpy)) {
|
|
XEvent event;
|
|
XNextEvent(linuxvars.dpy, &event);
|
|
|
|
if (XFilterEvent(&event, None) == True){
|
|
continue;
|
|
}
|
|
|
|
switch(event.type) {
|
|
case KeyPress: {
|
|
should_step = true;
|
|
|
|
Input_Modifier_Set_Fixed* mods = &linuxvars.input.pers.modifiers;
|
|
|
|
int state = event.xkey.state;
|
|
set_modifier(mods, KeyCode_Shift, state & ShiftMask);
|
|
set_modifier(mods, KeyCode_Control, state & ControlMask);
|
|
set_modifier(mods, KeyCode_CapsLock, state & LockMask);
|
|
set_modifier(mods, KeyCode_Alt, state & Mod1Mask);
|
|
|
|
event.xkey.state &= ~(ControlMask);
|
|
|
|
Status status;
|
|
KeySym keysym = NoSymbol;
|
|
u8 buf[256] = {};
|
|
|
|
int len = Xutf8LookupString(linuxvars.xic, &event.xkey, (char*)buf, sizeof(buf) - 1, &keysym, &status);
|
|
|
|
if (status == XBufferOverflow){
|
|
Xutf8ResetIC(linuxvars.xic);
|
|
XSetICFocus(linuxvars.xic);
|
|
}
|
|
|
|
if (keysym == XK_ISO_Left_Tab){
|
|
add_modifier(mods, KeyCode_Shift);
|
|
}
|
|
|
|
Key_Code key = keycode_lookup_table[(u8)event.xkey.keycode];
|
|
//printf("key %d = %s\n", event.xkey.keycode, key_code_name[key]);
|
|
|
|
Input_Event* key_event = NULL;
|
|
if(key) {
|
|
add_modifier(mods, key);
|
|
key_event = push_input_event(linuxvars.frame_arena, &linuxvars.input.trans.event_list);
|
|
key_event->kind = InputEventKind_KeyStroke;
|
|
key_event->key.code = key;
|
|
key_event->key.modifiers = copy_modifier_set(linuxvars.frame_arena, mods);
|
|
}
|
|
|
|
Input_Event* text_event = NULL;
|
|
if(status == XLookupChars || status == XLookupBoth) {
|
|
String_Const_u8 str = linux_filter_text(linuxvars.frame_arena, buf, len);
|
|
if(str.size) {
|
|
text_event = push_input_event(linuxvars.frame_arena, &linuxvars.input.trans.event_list);
|
|
text_event->kind = InputEventKind_TextInsert;
|
|
text_event->text.string = str;
|
|
}
|
|
}
|
|
|
|
if(key_event && text_event) {
|
|
key_event->key.first_dependent_text = text_event;
|
|
}
|
|
|
|
} break;
|
|
|
|
case KeyRelease: {
|
|
should_step = true;
|
|
|
|
Input_Modifier_Set_Fixed* mods = &linuxvars.input.pers.modifiers;
|
|
|
|
int state = event.xkey.state;
|
|
set_modifier(mods, KeyCode_Shift, state & ShiftMask);
|
|
set_modifier(mods, KeyCode_Control, state & ControlMask);
|
|
set_modifier(mods, KeyCode_CapsLock, state & LockMask);
|
|
set_modifier(mods, KeyCode_Alt, state & Mod1Mask);
|
|
|
|
Key_Code key = keycode_lookup_table[(u8)event.xkey.keycode];
|
|
|
|
Input_Event* key_event = NULL;
|
|
if(key) {
|
|
remove_modifier(mods, key);
|
|
key_event = push_input_event(linuxvars.frame_arena, &linuxvars.input.trans.event_list);
|
|
key_event->kind = InputEventKind_KeyRelease;
|
|
key_event->key.code = key;
|
|
key_event->key.modifiers = copy_modifier_set(linuxvars.frame_arena, mods);
|
|
}
|
|
} break;
|
|
|
|
case MotionNotify: {
|
|
int x = clamp(0, event.xmotion.x, render_target.width - 1);
|
|
int y = clamp(0, event.xmotion.y, render_target.height - 1);
|
|
linuxvars.input.pers.mouse = { x, y };
|
|
should_step = true;
|
|
} break;
|
|
|
|
case ButtonPress: {
|
|
should_step = true;
|
|
switch(event.xbutton.button) {
|
|
case Button1: {
|
|
linuxvars.input.trans.mouse_l_press = true;
|
|
linuxvars.input.pers.mouse_l = true;
|
|
|
|
// NOTE(inso): improves selection dragging (especially in notepad-like mode).
|
|
// we will still get mouse events when the pointer leaves the window if it's dragging.
|
|
XGrabPointer(
|
|
linuxvars.dpy,
|
|
linuxvars.win,
|
|
True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask,
|
|
GrabModeAsync, GrabModeAsync,
|
|
None, None, CurrentTime);
|
|
|
|
} break;
|
|
|
|
case Button3: {
|
|
linuxvars.input.trans.mouse_r_press = true;
|
|
linuxvars.input.pers.mouse_r = true;
|
|
} break;
|
|
|
|
case Button4: {
|
|
linuxvars.input.trans.mouse_wheel = -100;
|
|
} break;
|
|
|
|
case Button5: {
|
|
linuxvars.input.trans.mouse_wheel = +100;
|
|
} break;
|
|
}
|
|
} break;
|
|
|
|
case ButtonRelease: {
|
|
should_step = true;
|
|
switch(event.xbutton.button) {
|
|
case Button1: {
|
|
linuxvars.input.trans.mouse_l_release = true;
|
|
linuxvars.input.pers.mouse_l = false;
|
|
|
|
XUngrabPointer(linuxvars.dpy, CurrentTime);
|
|
} break;
|
|
|
|
case Button3: {
|
|
linuxvars.input.trans.mouse_r_release = true;
|
|
linuxvars.input.pers.mouse_r = false;
|
|
} break;
|
|
}
|
|
} break;
|
|
|
|
case FocusIn:
|
|
case FocusOut: {
|
|
linuxvars.input.pers.mouse_l = false;
|
|
linuxvars.input.pers.mouse_r = false;
|
|
block_zero_struct(&linuxvars.input.pers.modifiers);
|
|
} break;
|
|
|
|
case EnterNotify: {
|
|
linuxvars.input.pers.mouse_out_of_window = 0;
|
|
} break;
|
|
|
|
case LeaveNotify: {
|
|
linuxvars.input.pers.mouse_out_of_window = 1;
|
|
} break;
|
|
|
|
case ConfigureNotify: {
|
|
i32 w = event.xconfigure.width;
|
|
i32 h = event.xconfigure.height;
|
|
|
|
if (w != render_target.width || h != render_target.height){
|
|
should_step = true;
|
|
render_target.width = w;
|
|
render_target.height = h;
|
|
}
|
|
} break;
|
|
|
|
case ClientMessage: {
|
|
Atom atom = event.xclient.data.l[0];
|
|
|
|
// Window X button clicked
|
|
if(atom == linuxvars.atom_WM_DELETE_WINDOW) {
|
|
should_step = true;
|
|
linuxvars.input.trans.trying_to_kill = true;
|
|
}
|
|
|
|
// Notify WM that we're still responding (don't grey our window out).
|
|
else if(atom == linuxvars.atom__NET_WM_PING) {
|
|
event.xclient.window = DefaultRootWindow(linuxvars.dpy);
|
|
XSendEvent(
|
|
linuxvars.dpy,
|
|
event.xclient.window,
|
|
False,
|
|
SubstructureRedirectMask | SubstructureNotifyMask,
|
|
&event);
|
|
}
|
|
} break;
|
|
|
|
case SelectionRequest: {
|
|
linux_clipboard_send((XSelectionRequestEvent*)&event);
|
|
} break;
|
|
|
|
case SelectionNotify: {
|
|
linux_clipboard_recv((XSelectionEvent*)&event);
|
|
} break;
|
|
|
|
case SelectionClear: {
|
|
if(event.xselectionclear.selection == linuxvars.atom_CLIPBOARD) {
|
|
linalloc_clear(linuxvars.clipboard_arena);
|
|
block_zero_struct(&linuxvars.clipboard_contents);
|
|
}
|
|
} break;
|
|
|
|
case Expose:
|
|
case VisibilityNotify: {
|
|
should_step = true;
|
|
} break;
|
|
|
|
default: {
|
|
// clipboard update notification - ask for the new content
|
|
if (linuxvars.clipboard_catch_all && event.type == linuxvars.xfixes_selection_event) {
|
|
XFixesSelectionNotifyEvent* sne = (XFixesSelectionNotifyEvent*)&event;
|
|
if (sne->subtype == XFixesSelectionNotify && sne->owner != linuxvars.win){
|
|
XConvertSelection(
|
|
linuxvars.dpy,
|
|
linuxvars.atom_CLIPBOARD,
|
|
linuxvars.atom_UTF8_STRING,
|
|
linuxvars.atom_CLIPBOARD,
|
|
linuxvars.win,
|
|
CurrentTime);
|
|
}
|
|
}
|
|
|
|
else if(event.type == linuxvars.xkb_event) {
|
|
XkbEvent* kb = (XkbEvent*)&event;
|
|
|
|
// Keyboard layout changed, refresh lookup table.
|
|
if(kb->any.xkb_type == XkbStateNotify && kb->state.group != linuxvars.xkb_group) {
|
|
linuxvars.xkb_group = kb->state.group;
|
|
XkbRefreshKeyboardMapping((XkbMapNotifyEvent*)kb);
|
|
linux_keycode_init(linuxvars.dpy);
|
|
}
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
|
|
if(should_step) {
|
|
linux_schedule_step();
|
|
}
|
|
}
|
|
|
|
internal b32
|
|
linux_epoll_process(struct epoll_event* events, int num_events) {
|
|
b32 do_step = false;
|
|
|
|
for (int i = 0; i < num_events; ++i){
|
|
struct epoll_event* ev = events + i;
|
|
Epoll_Kind* tag = (Epoll_Kind*)ev->data.ptr;
|
|
|
|
switch (*tag){
|
|
case EPOLL_X11: {
|
|
linux_handle_x11_events();
|
|
} break;
|
|
|
|
case EPOLL_X11_INTERNAL: {
|
|
//XProcessInternalConnection(linuxvars.dpy, fd);
|
|
} break;
|
|
|
|
case EPOLL_STEP_TIMER: {
|
|
u64 count;
|
|
int ret;
|
|
do {
|
|
ret = read(linuxvars.step_timer_fd, &count, 8);
|
|
} while (ret != -1 || errno != EAGAIN);
|
|
do_step = true;
|
|
} break;
|
|
|
|
case EPOLL_CLI_PIPE: {
|
|
linux_schedule_step();
|
|
} break;
|
|
|
|
case EPOLL_USER_TIMER: {
|
|
Linux_Object* obj = CastFromMember(Linux_Object, timer.epoll_tag, tag);
|
|
close(obj->timer.fd);
|
|
obj->timer.fd = -1;
|
|
linux_schedule_step();
|
|
} break;
|
|
}
|
|
}
|
|
|
|
return do_step;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv){
|
|
|
|
// NOTE(allen): context setup
|
|
{
|
|
Base_Allocator* alloc = get_base_allocator_system();
|
|
thread_ctx_init(&linuxvars.tctx, ThreadKind_Main, alloc, alloc);
|
|
}
|
|
|
|
API_VTable_system system_vtable = {};
|
|
system_api_fill_vtable(&system_vtable);
|
|
|
|
API_VTable_graphics graphics_vtable = {};
|
|
graphics_api_fill_vtable(&graphics_vtable);
|
|
|
|
API_VTable_font font_vtable = {};
|
|
font_api_fill_vtable(&font_vtable);
|
|
|
|
// NOTE(allen): memory
|
|
linuxvars.frame_arena = reserve_arena(&linuxvars.tctx);
|
|
linuxvars.clipboard_arena = reserve_arena(&linuxvars.tctx);
|
|
render_target.arena = make_arena_system(KB(256));
|
|
|
|
linuxvars.fontconfig = FcInitLoadConfigAndFonts();
|
|
|
|
linuxvars.cursor_show = MouseCursorShow_Always;
|
|
linuxvars.prev_cursor_show = MouseCursorShow_Always;
|
|
|
|
dll_init_sentinel(&linuxvars.free_linux_objects);
|
|
dll_init_sentinel(&linuxvars.timer_objects);
|
|
|
|
//InitializeCriticalSection(&win32vars.thread_launch_mutex);
|
|
//InitializeConditionVariable(&win32vars.thread_launch_cv);
|
|
|
|
linuxvars.clipboard_catch_all = true;
|
|
|
|
// NOTE(allen): load core
|
|
System_Library core_library = {};
|
|
App_Functions app = {};
|
|
{
|
|
App_Get_Functions *get_funcs = 0;
|
|
Scratch_Block scratch(&linuxvars.tctx, Scratch_Share);
|
|
Path_Search_List search_list = {};
|
|
search_list_add_system_path(scratch, &search_list, SystemPath_Binary);
|
|
|
|
String_Const_u8 core_path = get_full_path(scratch, &search_list, SCu8("4ed_app.so"));
|
|
if (system_load_library(scratch, core_path, &core_library)){
|
|
get_funcs = (App_Get_Functions*)system_get_proc(core_library, "app_get_functions");
|
|
if (get_funcs != 0){
|
|
app = get_funcs();
|
|
}
|
|
else{
|
|
char msg[] = "Failed to get application code from '4ed_app.so'.";
|
|
system_error_box(msg);
|
|
}
|
|
}
|
|
else{
|
|
char msg[] = "Could not load '4ed_app.so'. This file should be in the same directory as the main '4ed' executable.";
|
|
system_error_box(msg);
|
|
}
|
|
}
|
|
|
|
// NOTE(allen): send system vtable to core
|
|
app.load_vtables(&system_vtable, &font_vtable, &graphics_vtable);
|
|
// get_logger calls log_init which is needed.
|
|
app.get_logger();
|
|
//linuxvars.log_string = app.get_logger();
|
|
|
|
// NOTE(allen): init & command line parameters
|
|
Plat_Settings plat_settings = {};
|
|
void *base_ptr = 0;
|
|
{
|
|
Scratch_Block scratch(&linuxvars.tctx, Scratch_Share);
|
|
String_Const_u8 curdir = system_get_path(scratch, SystemPath_CurrentDirectory);
|
|
|
|
char **files = 0;
|
|
i32 *file_count = 0;
|
|
base_ptr = app.read_command_line(&linuxvars.tctx, curdir, &plat_settings, &files, &file_count, argc, argv);
|
|
/* TODO(inso): what is this doing?
|
|
{
|
|
i32 end = *file_count;
|
|
i32 i = 0, j = 0;
|
|
for (; i < end; ++i){
|
|
if (system_file_can_be_made(scratch, (u8*)files[i])){
|
|
files[j] = files[i];
|
|
++j;
|
|
}
|
|
}
|
|
*file_count = j;
|
|
}*/
|
|
}
|
|
|
|
// NOTE(allen): load custom layer
|
|
System_Library custom_library = {};
|
|
Custom_API custom = {};
|
|
{
|
|
char custom_not_found_msg[] = "Did not find a library for the custom layer.";
|
|
char custom_fail_version_msg[] = "Failed to load custom code due to missing version information or a version mismatch. Try rebuilding with buildsuper.";
|
|
char custom_fail_init_apis[] = "Failed to load custom code due to missing 'init_apis' symbol. Try rebuilding with buildsuper";
|
|
|
|
Scratch_Block scratch(&linuxvars.tctx, Scratch_Share);
|
|
String_Const_u8 default_file_name = string_u8_litexpr("custom_4coder.so");
|
|
Path_Search_List search_list = {};
|
|
search_list_add_system_path(scratch, &search_list, SystemPath_CurrentDirectory);
|
|
search_list_add_system_path(scratch, &search_list, SystemPath_Binary);
|
|
String_Const_u8 custom_file_names[2] = {};
|
|
i32 custom_file_count = 1;
|
|
if (plat_settings.custom_dll != 0){
|
|
custom_file_names[0] = SCu8(plat_settings.custom_dll);
|
|
if (!plat_settings.custom_dll_is_strict){
|
|
custom_file_names[1] = default_file_name;
|
|
custom_file_count += 1;
|
|
}
|
|
}
|
|
else{
|
|
custom_file_names[0] = default_file_name;
|
|
}
|
|
String_Const_u8 custom_file_name = {};
|
|
for (i32 i = 0; i < custom_file_count; i += 1){
|
|
custom_file_name = get_full_path(scratch, &search_list, custom_file_names[i]);
|
|
if (custom_file_name.size > 0){
|
|
break;
|
|
}
|
|
}
|
|
b32 has_library = false;
|
|
if (custom_file_name.size > 0){
|
|
if (system_load_library(scratch, custom_file_name, &custom_library)){
|
|
has_library = true;
|
|
}
|
|
}
|
|
|
|
if (!has_library){
|
|
system_error_box(custom_not_found_msg);
|
|
}
|
|
custom.get_version = (_Get_Version_Type*)system_get_proc(custom_library, "get_version");
|
|
if (custom.get_version == 0 || custom.get_version(MAJOR, MINOR, PATCH) == 0){
|
|
system_error_box(custom_fail_version_msg);
|
|
}
|
|
custom.init_apis = (_Init_APIs_Type*)system_get_proc(custom_library, "init_apis");
|
|
if (custom.init_apis == 0){
|
|
system_error_box(custom_fail_init_apis);
|
|
}
|
|
}
|
|
|
|
linux_x11_init(argc, argv, &plat_settings);
|
|
linux_keycode_init(linuxvars.dpy);
|
|
linux_epoll_init();
|
|
|
|
// app init
|
|
{
|
|
Scratch_Block scratch(&linuxvars.tctx, Scratch_Share);
|
|
String_Const_u8 curdir = system_get_path(scratch, SystemPath_CurrentDirectory);
|
|
app.init(&linuxvars.tctx, &render_target, base_ptr, curdir, custom);
|
|
}
|
|
|
|
linuxvars.global_frame_mutex = system_mutex_make();
|
|
system_mutex_acquire(linuxvars.global_frame_mutex);
|
|
|
|
linux_schedule_step();
|
|
b32 first_step = true;
|
|
|
|
for (;;) {
|
|
|
|
if (XEventsQueued(linuxvars.dpy, QueuedAlready)){
|
|
linux_handle_x11_events();
|
|
}
|
|
|
|
system_mutex_release(linuxvars.global_frame_mutex);
|
|
|
|
struct epoll_event events[16];
|
|
int num_events = epoll_wait(linuxvars.epoll, events, ArrayCount(events), -1);
|
|
|
|
system_mutex_acquire(linuxvars.global_frame_mutex);
|
|
|
|
if (num_events == -1){
|
|
if (errno != EINTR){
|
|
perror("epoll_wait");
|
|
//LOG("epoll_wait\n");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if(!linux_epoll_process(events, num_events)) {
|
|
continue;
|
|
}
|
|
|
|
linuxvars.last_step_time = system_now_time();
|
|
|
|
// NOTE(allen): Frame Clipboard Input
|
|
// Request clipboard contents from X11 on first step, or every step if they don't have XFixes notification ability.
|
|
if (first_step || (!linuxvars.has_xfixes && linuxvars.clipboard_catch_all)){
|
|
XConvertSelection(linuxvars.dpy, linuxvars.atom_CLIPBOARD, linuxvars.atom_UTF8_STRING, linuxvars.atom_CLIPBOARD, linuxvars.win, CurrentTime);
|
|
}
|
|
|
|
Application_Step_Input input = {};
|
|
|
|
if (linuxvars.received_new_clipboard){
|
|
input.clipboard = linuxvars.clipboard_contents;
|
|
linuxvars.received_new_clipboard = false;
|
|
}
|
|
|
|
input.first_step = first_step;
|
|
input.dt = frame_useconds/1000000.f; // variable?
|
|
input.events = linuxvars.input.trans.event_list;
|
|
input.trying_to_kill = linuxvars.input.trans.trying_to_kill;
|
|
|
|
input.mouse.out_of_window = linuxvars.input.pers.mouse_out_of_window;
|
|
input.mouse.p = linuxvars.input.pers.mouse;
|
|
input.mouse.l = linuxvars.input.pers.mouse_l;
|
|
input.mouse.r = linuxvars.input.pers.mouse_r;
|
|
input.mouse.press_l = linuxvars.input.trans.mouse_l_press;
|
|
input.mouse.release_l = linuxvars.input.trans.mouse_l_release;
|
|
input.mouse.press_r = linuxvars.input.trans.mouse_r_press;
|
|
input.mouse.release_r = linuxvars.input.trans.mouse_r_release;
|
|
input.mouse.wheel = linuxvars.input.trans.mouse_wheel;
|
|
|
|
// NOTE(allen): Application Core Update
|
|
Application_Step_Result result = {};
|
|
if (app.step != 0){
|
|
result = app.step(&linuxvars.tctx, &render_target, base_ptr, &input);
|
|
}
|
|
|
|
// NOTE(allen): Finish the Loop
|
|
if (result.perform_kill){
|
|
break;
|
|
}
|
|
|
|
// NOTE(NAME): Switch to New Title
|
|
if (result.has_new_title){
|
|
XStoreName(linuxvars.dpy, linuxvars.win, result.title_string);
|
|
}
|
|
|
|
// NOTE(allen): Switch to New Cursor
|
|
if (result.mouse_cursor_type != linuxvars.cursor && !linuxvars.input.pers.mouse_l){
|
|
XCursor c = linuxvars.xcursors[result.mouse_cursor_type];
|
|
if (linuxvars.cursor_show){
|
|
XDefineCursor(linuxvars.dpy, linuxvars.win, c);
|
|
}
|
|
linuxvars.cursor = result.mouse_cursor_type;
|
|
}
|
|
|
|
gl_render(&render_target);
|
|
glXSwapBuffers(linuxvars.dpy, linuxvars.win);
|
|
|
|
first_step = false;
|
|
|
|
linalloc_clear(linuxvars.frame_arena);
|
|
block_zero_struct(&linuxvars.input.trans);
|
|
linuxvars.step_pending = false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// NOTE(inso): to prevent me continuously messing up indentation
|
|
// vim: et:ts=4:sts=4:sw=4
|