1189 lines
38 KiB
C
1189 lines
38 KiB
C
#pragma once
|
|
|
|
// libc
|
|
#include <limits.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
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 _WIN32
|
|
// windows
|
|
#define TIM_WINDOWS
|
|
#define _CRT_SECURE_NO_WARNINGS
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
|
|
// 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 <poll.h>
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* 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;
|
|
|
|
/* 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;
|
|
|
|
/* 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
|
|
|
|
/* global variables ***********************************************************/
|
|
|
|
// 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,
|
|
};
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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
|
|
|
|
/* 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
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* edit ***********************************************************************/
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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
|
|
}
|
|
|
|
/* 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
|
|
}
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|