Files
tim/tim.h
2026-01-09 02:21:50 +05:00

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