#pragma once #pragma region include #include #include #include #include #include #include #include #ifdef _WIN32 // windows #define TIM_WINDOWS #define _CRT_SECURE_NO_WARNINGS #define WIN32_LEAN_AND_MEAN #include // fix windows.h name clash, coincidentally they have the same values #undef TimEvent_Key // 0x0001 #undef TimEvent_Mouse // 0x0002 #ifdef _MSC_VER // disable integer conversion warnings #pragma warning(disable:4244) #endif #else // unix #define TIM_UNIX #include #include #include #include #endif typedef int8_t i8; typedef int16_t i16; typedef int32_t i32; typedef int64_t i64; typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef float f32; typedef double f64; typedef const char* cstr; #if !defined(__cplusplus) && !defined(bool) # define bool u8 # define false 0 # define true 1 #endif #ifdef __cplusplus extern "C" { #endif #pragma endregion include #pragma region constants #define TIM_ENABLE_DBUF 1 // double buffering #define TIM_MAX_SCOPE 20 // max scope nesting #define TIM_MAX_CELLS 0x20000 // size of screen buffer #define TIM_MAX_BUF (TIM_MAX_CELLS * 4) // size of output buffer #define A INT_MAX // auto center / width / height typedef enum TimEventType { TimEvent_Void, // an event was consumed TimEvent_Draw, // draw screen TimEvent_Key, // a key was pressed TimEvent_Mouse, // mouse button, scroll or move } TimEventType; enum { TimKey_MouseButtonLeft = 1, TimKey_Backspace = 8, TimKey_Tab = 9, TimKey_Enter = 13, TimKey_Escape = 27, /* printable utf8 characters */ TimKey_Insert = -1, TimKey_Delete = -2, TimKey_Home = -3, TimKey_End = -4, TimKey_PageUp = -5, TimKey_PageDown = -6, TimKey_Up = -7, TimKey_Down = -8, TimKey_Left = -9, TimKey_Right = -10, }; typedef i32 TimKey; #pragma endregion constants #pragma region types typedef struct TimCell_t { u8 fg; // foreground color u8 bg; // background color u8 wide; // wide or following wide character u8 n; // number of bytes in buf u8 buf[4]; // utf8 code point } TimCell_t; typedef struct TimRect_t { int x; // x coordinate (left = 0) int y; // y coordinate (top = 0) int w; // width int h; // height } TimRect_t; typedef struct TimText_t { int size; // size in bytes without terminator int width; // widest line int lines; // number of lines } TimText_t; typedef struct TimLine_t { const char* s; // input and parse state const char* line; // line strings, not terminated int size; // line size in bytes int width; // line width in glyph } TimLine_t; typedef struct TimEvent_t { TimEventType type; TimKey key; // used by TimEvent_Key and TimEvent_Mouse i32 x; // used by TimEvent_Mouse i32 y; // used by TimEvent_Mouse char s[32]; // string representation of key, used by TimEvent_Key } TimEvent_t; typedef struct TimEdit_t { int cursor; // cursor position (utf8) int length; // string length (utf8) int capacity; // buffer size char* s; // zero terminated buffer } TimEdit_t; typedef struct TimState_t { int w; // screen width int h; // screen height int frame; // frame counter TimEvent_t event; // current event void* focus; // focused element int loop_stage; // loop stage bool resized; // screen was resized int scope; // current scope TimRect_t scopes[TIM_MAX_SCOPE]; // scope stack TimCell_t* cells; // screen buffer char* buf; // final output buffer int buf_size; // position in write buffer i64 start_us; // render start time int render_us; // elapsed render time #ifdef TIM_UNIX // struct termios attr; // initial attributes int signal_pipe[2]; // signal fifo pipe #endif // #ifdef TIM_WINDOWS // SMALL_RECT window; // screen buffer window size DWORD mode_in; // initial input mode DWORD mode_out; // initial output mode UINT cp_in; // initial input code page UINT cp_out; // initial output code page #endif } TimState_t; #pragma endregion types #pragma region macros #define MAX(a, b) ((a) > (b) ? (a) : (b)) // #define MIN(a, b) ((a) < (b) ? (a) : (b)) // #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) // number of items in array #define S(s) ("" s), (sizeof(s) - 1) // expand to s, sizeof(s) - 1 #pragma endregion macros // TODO: remove global variables // These buffers were part of tim struct but caused the linker to produce very // large binaries. static TimCell_t tim_cells[TIM_MAX_CELLS << TIM_ENABLE_DBUF]; // screen buffer static char tim_buf[TIM_MAX_BUF]; // output buffer TimState_t tim = { .cells = tim_cells, .buf = tim_buf, }; #pragma region string // like strlen, returns 0 on NULL or int overflow static inline int ztrlen(const char* s) { if(s == NULL) return 0; int n = strlen(s); if(n < 0) n = 0; return n; } // bit scan reverse, count leading zeros static inline int bsr8(u8 x) { #if defined __GNUC__ || defined __clang__ unsigned int b = x; b <<= sizeof(b) * CHAR_BIT - 8; b |= 1 << (sizeof(b) * CHAR_BIT - 9); return __builtin_clz(b); #elif defined _MSC_VER unsigned long n = 0; unsigned long b = x; b <<= sizeof(b) * CHAR_BIT - 8; b |= 1 << (sizeof(b) * CHAR_BIT - 9); _BitScanReverse(&n, b); return n; #else int n = 0; for (; n < 8 && !(x & 128); n++, x <<= 1) {} return n; #endif } // decode one utf8 code point static i32 utfchr(const char* s) { s = s ? s : ""; // use bit magic to mask out leading utf8 1s u32 c = s[0] & ((1 << (8 - bsr8(~s[0]))) - 1); for (int i = 1; s[0] && s[i] && i < 4; i++) { c = (c << 6) | (s[i] & 63); } return (i32)c; } // number of utf8 code points static int utflen(const char* s) { int n = 0; for (int i = 0; s && s[i]; i++) { n += (s[i] & 192) != 128; } return n; } // index of utf8 code point at pos static int utfpos(const char* s, int pos) { int i = 0; for (int n = 0; pos >= 0 && s && s[i]; i++) { n += (s[i] & 192) != 128; if (n == pos + 1) { return i; } } return i; } // scan string for width and lines static TimText_t scan_str(const char* s) { if(s == NULL) s = ""; TimText_t t = { .width = 0, .lines = (s[0] != 0), }; int width = 0; for (t.size = 0; s[t.size]; t.size++) { char ch = s[t.size]; int newline = (ch == '\n'); width = newline ? 0 : width; width += (ch & 192) != 128 && (u8)ch > 31; t.lines += newline; t.width = MAX(t.width, width); } return t; } // iterate through lines, false when end is reached static bool next_line(TimLine_t* l) { if (!l->s || !l->s[0]) { return false; } l->line = l->s; l->size = 0; l->width = 0; for (const char* s = l->s; s[0] && s[0] != '\n'; s++) { l->size += 1; l->width += (s[0] & 192) != 128 && (u8)s[0] > 31; } l->s += l->size + !!l->s[l->size]; return true; } // true if utf8 code point could be wide static bool is_wide_perhaps(const u8* s, int n) { // Character width depends on character, terminal and font. There is no // reliable method, however most frequently used characters are narrow. // Zero with characters are ignored, and hope that user input is benign. if (n < 3 || s[0] < 225) { // u+0000 - u+1000, basic latin - tibetan return false; } else if (s[0] == 226 && s[1] >= 148 && s[1] < 152) { // u+2500 - u+2600 box drawing, block elements, geometric shapes return false; } return true; } #pragma endregion string #pragma region unix // Unix-like terminal IO. Osx is missing ppoll and __unix__. Come on, fix it! #ifdef TIM_UNIX static void write_str(const char* s, int size) { ssize_t _ = write(STDOUT_FILENO, s, size); (void)_; // remove unused-result warning } static void signal_handler(int signal) { // signals are written into a fifo pipe and read by event loop ssize_t _ = write(tim.signal_pipe[1], &signal, sizeof(signal)); (void)_; // remove unused-result warning } static void update_screen_size(void) { struct winsize ws = {0}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != 0) { printf("ERROR: can't get console buffer size\n"); exit(1); } int w = ws.ws_col; int h = ws.ws_row; tim.resized = (unsigned)(w * h) <= TIM_MAX_CELLS && (w != tim.w || h != tim.h); if (tim.resized) { tim.w = tim.scopes[0].w = w; tim.h = tim.scopes[0].h = h; } } static void init_terminal(void) { tcgetattr(STDOUT_FILENO, &tim.attr); // store attributes struct termios attr = tim.attr; // cfmakeraw(&attr); // configure raw mode tcsetattr(STDOUT_FILENO, TCSADRAIN, &attr); // set new attributes write_str(S("\33[?2004l")); // reset bracketed paste mode write_str(S("\33[?1049h")); // use alternate buffer write_str(S("\33[?25l")); // hide cursor write_str(S("\33[?1000h")); // enable mouse write_str(S("\33[?1002h")); // enable button events write_str(S("\33[?1006h")); // use mouse sgr protocol update_screen_size(); // get terminal size int err = pipe(tim.signal_pipe); // create signal pipe if (!err) { // signal(SIGWINCH, signal_handler); // terminal size changed } } static void reset_terminal(void) { tcsetattr(STDOUT_FILENO, TCSADRAIN, &tim.attr); // restore attributes write_str(S("\33[?1000l")); // disable mouse write_str(S("\33[?1002l")); // disable mouse write_str(S("\33[m")); // reset colors write_str(S("\33[?25h")); // show cursor write_str(S("\33[?1049l")); // exit alternate buffer } // parse input stored in e->s static bool parse_input(event* restrict e, int n) { char* s = e->s; if (n == 1 || s[0] != 27) { // regular key press e->type = TimEvent_Key; e->key = s[0] == 127 ? TimKey_Backspace : utfchr(s); return true; } if (n >= 9 && !memcmp(s, S("\33[<"))) { // sgr mouse sequence e->type = TimEvent_Mouse; int btn = strtol(s + 3, &s, 10); e->x = strtol(s + 1, &s, 10) - 1; e->y = strtol(s + 1, &s, 10) - 1; if (btn == 0 && s[0] == 'M') { // left button pressed e->key = TimKey_MouseButtonLeft; return true; } return false; } static struct {char s[4]; int k;} key_table[] = { {"[A" , TimKey_Up}, // {"[B" , TimKey_Down}, // {"[C" , TimKey_Right}, // {"[D" , TimKey_Left}, // {"[2~", TimKey_Insert}, // {"[4h", TimKey_Insert}, // st {"[3~", TimKey_Delete}, // {"[P" , TimKey_Delete}, // st {"[H" , TimKey_Home}, // {"[1~", TimKey_Home}, // rxvt, lxterm, putty, tmux, screen {"[7~", TimKey_Home}, // rxvt {"[F" , TimKey_End}, // {"[4~", TimKey_End}, // rxvt, lxterm, putty, tmux, screen, st {"[8~", TimKey_End}, // rxvt {"[5~", TimKey_PageUp}, // {"[6~", TimKey_PageDown}, // }; if ((n == 3 || n == 4) && s[0] == 27) { // key sequence for (int i = 0; i < (int)ARRAY_SIZE(key_table); i++) { if (!memcmp(s + 1, key_table[i].s, n - 1)) { e->type = TimEvent_Key; e->key = key_table[i].k; return true; } } } return false; } static void read_event(int timeout_ms) { event* e = &tim.event; struct pollfd pfd[2] = { {.fd = tim.signal_pipe[0], .events = POLLIN}, {.fd = STDIN_FILENO, .events = POLLIN}, }; while (true) { memset(e, 0, sizeof(*e)); int r = poll(pfd, 2, timeout_ms > 0 ? timeout_ms : -1); if (r < 0) { // poll error, EINTR or EAGAIN continue; } else if (r == 0) { // poll timeout e->type = TimEvent_Draw; return; } if (pfd[0].revents & POLLIN) { // received signal int sig = 0; int n = read(tim.signal_pipe[0], &sig, sizeof(sig)); if (n > 0 && sig == SIGWINCH) { // screen size changed e->type = TimEvent_Draw; update_screen_size(); return; } } if (pfd[1].revents & POLLIN) { // received input int n = read(STDIN_FILENO, e->s, sizeof(e->s) - 1); if (parse_input(e, n)) { return; } } } // while } static inline i64 time_us(void) { struct timespec ts = {0}; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000000 + ts.tv_nsec / 1000; } #endif // TIM_UNIX #pragma endregion unix #pragma region windows // Windows terminal IO. Win32 is actually not that horrible as many say. Quirky // but well documented. #ifdef TIM_WINDOWS static void write_str(const char* s, int size) { HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); WriteFile(h, s, size, NULL, NULL); FlushFileBuffers(h); } static void update_screen_size(void) { HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csbi = {0}; if (GetConsoleScreenBufferInfo(hout, &csbi) == 0) { printf("ERROR: can't get console buffer size\n"); exit(1); } int w = csbi.srWindow.Right - csbi.srWindow.Left + 1; int h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; tim.resized = (unsigned)(w * h) <= TIM_MAX_CELLS && (w != tim.w || h != tim.h); if (tim.resized) { tim.w = tim.scopes[0].w = w; tim.h = tim.scopes[0].h = h; tim.window = csbi.srWindow; } } static void init_terminal(void) { DWORD mode = 0; HANDLE hin = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(hin, &tim.mode_in); // get current input mode mode = tim.mode_in; // mode &= ~ENABLE_ECHO_INPUT; // disable echo mode &= ~ENABLE_LINE_INPUT; // disable line buffer // TODO: enable ctrl-c again mode &= ~ENABLE_PROCESSED_INPUT; // disable ctrl-c mode |= ENABLE_WINDOW_INPUT; // enable resize event mode |= ENABLE_MOUSE_INPUT; // enable mouse event mode |= ENABLE_EXTENDED_FLAGS; // for ENABLE_QUICK_EDIT mode &= ~ENABLE_QUICK_EDIT_MODE; // disable select mode SetConsoleMode(hin, mode); // set input mode // HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); // GetConsoleMode(hout, &tim.mode_out); // get current output mode mode = tim.mode_out; // mode |= ENABLE_PROCESSED_OUTPUT; // enable ascii sequences mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; // enable vt sequences SetConsoleMode(hout, mode); // set output mode // tim.cp_in = GetConsoleCP(); // get current code page tim.cp_out = GetConsoleOutputCP(); // SetConsoleCP(CP_UTF8); // set utf8 in/out code page SetConsoleOutputCP(CP_UTF8); // write_str(S("\33[?1049h")); // use alternate buffer update_screen_size(); // } static void reset_terminal(void) { write_str(S("\33[m")); // reset colors write_str(S("\33[?25h")); // show cursor write_str(S("\33[?1049l")); // exit alternate buffer HANDLE hin = GetStdHandle(STD_INPUT_HANDLE); // HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE); // SetConsoleMode(hin, tim.mode_in); // set original mode SetConsoleMode(hout, tim.mode_out); // SetConsoleCP(tim.cp_in); // set original code page SetConsoleOutputCP(tim.cp_out); // } static void read_event(int timeout_ms) { TimEvent_t* e = &tim.event; HANDLE h = GetStdHandle(STD_INPUT_HANDLE); static const i8 key_table[256] = { [VK_BACK] = TimKey_Backspace, [VK_TAB] = TimKey_Tab, [VK_RETURN] = TimKey_Enter, [VK_ESCAPE] = TimKey_Escape, [VK_PRIOR] = TimKey_PageUp, [VK_NEXT] = TimKey_PageDown, [VK_END] = TimKey_End, [VK_HOME] = TimKey_Home, [VK_LEFT] = TimKey_Left, [VK_UP] = TimKey_Up, [VK_RIGHT] = TimKey_Right, [VK_DOWN] = TimKey_Down, [VK_INSERT] = TimKey_Insert, [VK_DELETE] = TimKey_Delete, }; while (true) { memset(e, 0, sizeof(*e)); // In cmd.exe the cursor somtimes reappears. This reliably hides it. write_str(S("\33[?25l")); DWORD r = WaitForSingleObject(h, timeout_ms); if (r == WAIT_TIMEOUT) { e->type = TimEvent_Draw; update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT return; } else if (r != WAIT_OBJECT_0) { continue; } // received input INPUT_RECORD rec = {0}; DWORD n = 0; ReadConsoleInput(h, &rec, 1, &n); switch (rec.EventType) { case KEY_EVENT: { if (!rec.Event.KeyEvent.bKeyDown) { // only interested in key press continue; } int key = key_table[(u8)rec.Event.KeyEvent.wVirtualKeyCode]; WCHAR chr = rec.Event.KeyEvent.uChar.UnicodeChar; if (!key && chr < ' ') { // non printable key continue; } update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT e->type = TimEvent_Key; if (key) { e->key = key; return; } e->key = chr; WideCharToMultiByte(CP_UTF8, 0, &chr, 1, e->s, sizeof(e->s), NULL, NULL); return; } case MOUSE_EVENT: { bool move = rec.Event.MouseEvent.dwEventFlags & ~DOUBLE_CLICK; bool left = rec.Event.MouseEvent.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED; if (move || !left) { // ignore move events and buttons other than left continue; } update_screen_size(); // workaround, see WINDOW_BUFFER_SIZE_EVENT e->type = TimEvent_Mouse; e->key = TimKey_MouseButtonLeft; e->x = rec.Event.MouseEvent.dwMousePosition.X - tim.window.Left; e->y = rec.Event.MouseEvent.dwMousePosition.Y - tim.window.Top; return; } case WINDOW_BUFFER_SIZE_EVENT: e->type = TimEvent_Draw; // cmd.exe screen buffer and window size are separate, making this // event a bit unreliable. Effectively it is only emitted when the // terminal width changes and not for the height. As a workaround // the screen size is updated every time an event is emitted. update_screen_size(); return; } } // while } static inline i64 time_us(void) { LARGE_INTEGER ticks = {0}; LARGE_INTEGER freq = {0}; QueryPerformanceCounter(&ticks); QueryPerformanceFrequency(&freq); return 1000000 * ticks.QuadPart / freq.QuadPart; } #endif // TIM_WINDOWS #pragma endregion windows #pragma region events // returns true if event was of type and key static inline bool is_event_key(TimEventType type, TimKey key) { return tim.event.type == type && tim.event.key == key; } // returns true if event was press of key static inline bool is_key_press(TimKey key) { return is_event_key(TimEvent_Key, key); } // returns true if mouse event was over r static inline bool is_mouse_over(TimRect_t r) { int x = tim.event.x; int y = tim.event.y; return x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h; } // returns true if event is mouse left-down and over r static inline bool is_click_over(TimRect_t r) { return is_event_key(TimEvent_Mouse, TimKey_MouseButtonLeft) && is_mouse_over(r); } #pragma endregion events #pragma region drawing // create cell from utf8 code point with fg and bg colors static inline TimCell_t cell(const char* s, u8 fg, u8 bg) { TimCell_t c = {.fg = fg, .bg = bg, .n = 1, .buf = {s[0]}}; while ((s[c.n] & 192) == 128 && c.n < sizeof(c.buf)) { c.buf[c.n] = s[c.n]; c.n += 1; } return c; } // clear cell buffer static void clear_cells(void) { size_t size = sizeof(tim.cells[0]) * tim.w * tim.h; memset(tim.cells, 0, size); } // draw cell at position static void draw_chr(TimCell_t cell, int x, int y) { if (x >= 0 && x < tim.w && y >= 0 && y < tim.h) { tim.cells[x + y * tim.w] = cell; } } // draw row of cells static void draw_row(TimCell_t cell, int x, int y, int w) { if (y >= 0 && y < tim.h && w > 0) { for (int i = MAX(x, 0); i < MIN(x + w, tim.w); i++) { tim.cells[i + y * tim.w] = cell; } } } // draw column of cells static void draw_col(TimCell_t cell, int x, int y, int h) { if (x >= 0 && x < tim.w && h > 0) { for (int i = MAX(y, 0); i < MIN(y + h, tim.h); i++) { tim.cells[x + i * tim.w] = cell; } } } // fill lot (area) of cells static void draw_lot(TimCell_t cell, int x, int y, int w, int h) { if (w > 0 && h > 0) { for (int iy = MAX(y, 0); iy < MIN(y + h, tim.h); iy++) { for (int ix = MAX(x, 0); ix < MIN(x + w, tim.w); ix++) { tim.cells[ix + iy * tim.w] = cell; } } } } // draw string to line, tags potential wide characters static void draw_str(const char* s, int x, int y, int w, u8 fg, u8 bg) { if (s && y >= 0 && x < tim.w && y < tim.h ) { int end = MIN(x + w, tim.w); bool wide = false; for (int i = 0; s[i] && x < end; x++) { TimCell_t c = cell(&s[i], fg, bg); wide = wide || is_wide_perhaps(c.buf, c.n); if (x >= 0) { c.wide = wide; tim.cells[x + y * tim.w] = c; } i += c.n; } } } // draw box of ascii cell characters static void draw_box(int x, int y, int w, int h, u8 fg, u8 bg) { draw_chr(cell("┌", fg, bg), x , y); draw_chr(cell("┐", fg, bg), x + w - 1, y); draw_chr(cell("└", fg, bg), x , y + h - 1); draw_chr(cell("┘", fg, bg), x + w - 1, y + h - 1); draw_row(cell("─", fg, bg), x + 1 , y , w - 2); draw_row(cell("─", fg, bg), x + 1 , y + h - 1, w - 2); draw_col(cell("│", fg, bg), x , y + 1 , h - 2); draw_col(cell("│", fg, bg), x + w - 1, y + 1 , h - 2); draw_lot(cell(" ", fg, bg), x + 1 , y + 1 , w - 2, h - 2); } // invert fg and bg colors of line of cells static void draw_invert(int x, int y, int w) { if (y >= 0 && y < tim.h && w > 0) { for (int i = MAX(x, 0); i < MIN(x + w, tim.w); i++) { TimCell_t c = tim.cells[i + y * tim.w]; tim.cells[i + y * tim.w].fg = c.bg; tim.cells[i + y * tim.w].bg = c.fg; } } } #pragma endregion drawing #pragma region scope // enter layout scope #define scope(x, y, w, h) \ for (int _i = enter_scope((x), (y), (w), (h)); _i; _i = exit_scope()) // convert relative (scoped) to absolute (screen) coordinates static TimRect_t abs_xywh(int x, int y, int w, int h) { TimRect_t p = tim.scopes[tim.scope]; // parent scope x = (x == A && w == A) ? 0 : x; // special cases y = (y == A && h == A) ? 0 : y; // w = (w == A) ? ~0 : w; // h = (h == A) ? ~0 : h; // // if (w < 0) { // w += p.w - x + 1; // get w from parent } // if (h < 0) { // h += p.h - y + 1; // get h from parent } // if (x == A) { // x = p.x + (p.w - w) / 2; // center x on parent } else { // if (x < 0) { // x += p.w - w + 1; // anchor x to right } // x += p.x; // anchor x to left } // if (y == A) { // y = p.y + (p.h - h) / 2; // center y on parent } else { // if (y < 0) { // y += p.h - h + 1; // anchor y to bottom } // y += p.y; // anchor y to top } return (TimRect_t){x, y, w, h}; } // enter scope and push coordinates on stack static inline int enter_scope(int x, int y, int w, int h) { if (tim.scope + 1 >= TIM_MAX_SCOPE) { return 0; } TimRect_t r = abs_xywh(x, y, w, h); tim.scope += 1; tim.scopes[tim.scope] = r; return 1; } // exit scope and pop stack static inline int exit_scope(void) { tim.scope -= (tim.scope > 0); return 0; } #pragma endregion scope #pragma region frame // frame // color: background, frame static inline void frame(int x, int y, int w, int h, u64 color) { if (tim.event.type == TimEvent_Draw) { TimRect_t r = abs_xywh(x, y, w, h); draw_box(r.x, r.y, r.w, r.h, color, color >> 8); } } #pragma endregion frame #pragma region label // text label // str : text - supports multiple lines // color: background, text static inline void label(const char* s, int x, int y, int w, int h, u64 color) { if (tim.event.type == TimEvent_Draw) { TimText_t t = scan_str(s); w = (w == A) ? t.width : w; h = (h == A) ? t.lines : h; TimRect_t r = abs_xywh(x, y, w, h); TimCell_t c = cell(" ", color, color >> 8); draw_lot(c, r.x, r.y, r.w, r.h); TimLine_t l = {.s = s, .line = ""}; for (int i = 0; next_line(&l); i++) { draw_str(l.line, r.x, r.y + i, l.width, c.fg, c.bg); } } } #pragma endregion label #pragma region button // button - returns true on click // color: frame, background, text static inline bool button(const char* txt, int x, int y, int w, int h, u64 color) { int tw = utflen(txt); w = (w == A) ? (tw + 4) : w; h = (h == A) ? 3 : h; TimRect_t r = abs_xywh(x, y, w, h); if (tim.event.type == TimEvent_Draw) { draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8); draw_str(txt, r.x + (w - tw) / 2, r.y + h / 2, w, color, color >> 8); } return is_click_over(r); } #pragma endregion button #pragma region edit static inline void edit_init(TimEdit_t* e, int capacity, const char* initial_content){ e->length = utflen(initial_content); e->cursor = utflen(initial_content); e->capacity = capacity; e->s = (char*)malloc(capacity + 1); int byte_len = strlen(initial_content); memcpy(e->s, initial_content, byte_len); e->s[byte_len] = 0; } static void edit_insert(TimEdit_t* e, const char* s) { int dst_size = ztrlen(e->s); int src_size = ztrlen(s); if (dst_size + src_size < e->capacity) { int len = utflen(s); // usually 1, except when smashing keys int cur = utfpos(e->s, e->cursor); memmove(e->s + cur + src_size, e->s + cur, dst_size - cur); memmove(e->s + cur, s, src_size); e->s[dst_size + src_size] = 0; e->length += len; e->cursor += len; } } static void edit_delete(TimEdit_t* e) { int size = ztrlen(e->s); int cur = utfpos(e->s, e->cursor); int len = utfpos(e->s + cur, 1); if (size - cur > 0) { memmove(e->s + cur, e->s + cur + len, size - cur); e->length -= 1; } } /// @return key id or 0 static int edit_event(TimEdit_t* e, TimRect_t r) { if (is_click_over(r)) { // take focus tim.focus = e; return 0; } if (tim.focus != e || tim.event.type != TimEvent_Key) { // not focused or no key press return 0; } tim.event.type = TimEvent_Void; // consume event switch (tim.event.key) { case TimKey_Escape: case TimKey_Enter: tim.focus = 0; // release focus break; case TimKey_Delete: edit_delete(e); break; case TimKey_Backspace: if (e->cursor > 0) { e->cursor -= 1; edit_delete(e); } break; case TimKey_Left: e->cursor -= (e->cursor > 0); break; case TimKey_Right: e->cursor += (e->cursor < e->length); break; case TimKey_Home: e->cursor = 0; break; case TimKey_End: e->cursor = e->length; break; default: if (tim.event.key >= ' ') { edit_insert(e, tim.event.s); } break; } return tim.event.key; } /// text edit - value in state /// @param e persistent edit state, use edit_init() to create new state /// @param color frame, background, text /// @return key id or 0 static inline int edit(TimEdit_t* e, int x, int y, int w, u64 color) { TimRect_t r = abs_xywh(x, y, w, 3); if (tim.event.type == TimEvent_Draw) { draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8); if (tim.focus == e) { char* s = e->s + utfpos(e->s, e->cursor - r.w + 4); int cur = MIN(r.w - 4, e->cursor); draw_str(s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8); draw_invert(r.x + cur + 2, r.y + 1, 1); } else { draw_str(e->s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8); } } return edit_event(e, r); } #pragma endregion #pragma region check // check box - returns true when clicked // txt : text label // state: persistent state, 0 unchecked, -1 semi checked, !0: checked // color: check, background, text static inline bool check(const char* txt, int* state, int x, int y, int w, u64 color) { w = (w == A) ? utflen(txt) + 4 : w; TimRect_t r = abs_xywh(x, y, w, 1); if (tim.event.type == TimEvent_Draw) { const char* st = *state == -1 ? "-" : *state ? "x" : " "; draw_str("[ ] ", r.x, r.y, 4, color, color >> 8); draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8); draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8); } bool click = is_click_over(r); *state = click ? !*state : *state; return click; } #pragma endregion #pragma region radio // radio button - return true when clicked // txt : text label // state: persistent state, selected if *state == v // v : value // color: radio, background, text static inline bool radio(const char* txt, int* state, int v, int x, int y, int w, u64 color) { w = (w == A) ? utflen(txt) + 4 : w; TimRect_t r = abs_xywh(x, y, w, 1); if (tim.event.type == TimEvent_Draw) { const char* st = *state == v ? "o" : " "; draw_str("( ) ", r.x, r.y, 4, color, color >> 8); draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8); draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8); } bool click = is_click_over(r); *state = click ? v : *state; return click; } #pragma endregion #pragma region rendering // write character to output buffer static inline void put_chr(char c) { if (tim.buf_size + 1 < TIM_MAX_BUF) { tim.buf[tim.buf_size] = c; tim.buf_size += 1; } } // write string to output buffer static inline void put_str(const char* s, int size) { if (size > 0 && tim.buf_size + size < TIM_MAX_BUF) { memmove(&tim.buf[tim.buf_size], s, size); tim.buf_size += size; } } // write integer as decimal string to output buffer static inline void put_int(int i) { // optimized for small positive values, reduces load by a third char* buf = &tim.buf[tim.buf_size]; if (tim.buf_size + 11 >= TIM_MAX_BUF) { // not enough space for 32 bit integer } else if ((unsigned)i < 10) { buf[0] = '0' + i; tim.buf_size += 1; } else if ((unsigned)i < 100) { buf[0] = '0' + i / 10; buf[1] = '0' + i % 10; tim.buf_size += 2; } else if ((unsigned)i < 1000) { buf[0] = '0' + i / 100; buf[1] = '0' + (i / 10) % 10; buf[2] = '0' + i % 10; tim.buf_size += 3; } else { tim.buf_size += sprintf(buf, "%d", i); } } static void render(void) { int fg = -1; int bg = -1; bool wide = false; bool skip = false; // screen buffers TimCell_t* new_cells = tim_cells; TimCell_t* old_cells = tim_cells; #if TIM_ENABLE_DBUF new_cells += (tim.frame & 1) ? TIM_MAX_CELLS : 0; old_cells += (tim.frame & 1) ? 0 : TIM_MAX_CELLS; #endif tim.buf_size = 0; for (int i = 0; i < tim.w * tim.h; i++) { TimCell_t c = new_cells[i]; #if TIM_ENABLE_DBUF // do nothing if cells in look-ahead are identical const int la = 8; // look-ahead if (!tim.resized && !(i % la) && (i + la < TIM_MAX_CELLS) && !memcmp(new_cells + i, old_cells + i, la * sizeof(c))) { skip = true; i = i + la - 1; continue; } #endif // Set cursor position after a new line, after a string containing wide // characters or after skipping identical cells. bool new_line = i % tim.w == 0; bool wide_spill = wide && (c.n == 0 || c.buf[0] == ' '); bool wide_flank = wide && !wide_spill && !c.wide; if (new_line || wide_flank || skip) { put_str(S("\33[")); put_int((i / tim.w) + 1); put_chr(';'); put_int((i % tim.w) + 1); put_chr('H'); } wide = c.wide || wide_spill; skip = false; // change foreground color if (c.fg != fg) { fg = c.fg; put_str(S("\33[38;5;")); put_int(fg); put_chr('m'); } // change background color if (c.bg != bg) { bg = c.bg; put_str(S("\33[48;5;")); put_int(bg); put_chr('m'); } // write character if (c.n) { put_str((char*)c.buf, c.n); } else { put_chr(' '); } } // duration depends on connection and terminal rendering speed write_str(tim.buf, tim.buf_size); tim.resized = false; tim.frame += 1; // frame counter tim.cells = old_cells; // swap buffer } #pragma endregion #pragma region event loop static bool tim_run(f32 fps) { int timeout = (fps > 0) ? (int)(1000 / fps) : 0; while (true) { switch (tim.loop_stage) { case 0: // runs only once init_terminal(); atexit(reset_terminal); // fallthru case 1: // process input event tim.start_us = time_us(); if (tim.event.type != TimEvent_Draw) { // reset focus on mouse click if (is_event_key(TimEvent_Mouse, TimKey_MouseButtonLeft)) { tim.focus = 0; } tim.loop_stage = 2; return true; } // fallthru case 2: // process draw event clear_cells(); tim.event.type = TimEvent_Draw; tim.loop_stage = 3; return true; case 3: // render screen and wait for next event render(); tim.render_us = time_us() - tim.start_us; read_event(timeout); // blocks // fallthru default: tim.loop_stage = 1; break; } } // while } #pragma endregion #ifdef __cplusplus } #endif