Compare commits

21 Commits

Author SHA1 Message Date
ee6375f553 finished scroll_view 2026-01-16 19:34:06 +05:00
cbd1cc0cf1 two separate controls - panel and scroll_view 2026-01-16 17:45:34 +05:00
f4ed55a495 draw only inside current scope 2026-01-16 16:32:19 +05:00
f650e568d6 added scroll bar to tim_scroll 2026-01-14 22:11:18 +05:00
2a685dfcd0 added note about "ssty sane" 2026-01-13 18:41:22 +05:00
f8af7480d3 fixed uninitialized buffers 2026-01-13 18:31:38 +05:00
b2c4a90bea fixed tim_reset_terminal in unix.c 2026-01-13 18:19:41 +05:00
6d0190c9c0 comment about colors 2026-01-13 12:43:13 +05:00
58276638a7 changed .vscode/launch.json 2026-01-13 11:17:14 +05:00
3fb220ff54 tim_draw_lot -> tim_fill 2026-01-12 22:57:31 +05:00
7a3bde6321 removed malloc from TimEditState_construct 2026-01-12 22:51:33 +05:00
c5328cb9ed added scroll list 2026-01-12 22:02:50 +05:00
717c049265 added mouse wheel support 2026-01-12 21:54:15 +05:00
75d894b1bd tim_button_noborder 2026-01-12 18:34:13 +05:00
1aa33128e9 tim_event_consume() 2026-01-12 15:51:07 +05:00
6f8f2a54c0 added .masked and destructor for EditState 2026-01-09 13:31:08 +05:00
d417b5bbd5 fixed unicode input on windows 2026-01-09 11:12:34 +05:00
1ce7090bd6 replaced confusing u64 joinded colors with struct TimStyle 2026-01-09 09:45:00 +05:00
161f655492 added dependency config 2026-01-09 06:11:44 +05:00
6e946200a2 TimKey return type 2026-01-09 06:06:01 +05:00
4150a609e2 finished renaming things 2026-01-09 06:03:59 +05:00
24 changed files with 791 additions and 265 deletions

41
.vscode/launch.json vendored Executable file
View File

@@ -0,0 +1,41 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) test | Build and debug",
"type": "cppdbg",
"request": "launch",
"cwd": "${workspaceFolder}/bin",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": {
"program": "${workspaceFolder}/bin/tcp-chat.exe",
"externalConsole": true
},
"preLaunchTask": "build_exec_dbg",
"stopAtEntry": false,
"externalConsole": false,
"internalConsoleOptions": "neverOpen",
"MIMode": "gdb",
"miDebuggerPath": "gdb"
},
{
"name": "(gdb) test | Just debug",
"type": "cppdbg",
"request": "launch",
"cwd": "${workspaceFolder}/bin",
"program": "${workspaceFolder}/bin/tcp-chat",
"windows": {
"program": "${workspaceFolder}/bin/tcp-chat.exe",
"externalConsole": true
},
"preLaunchTask": "build_exec_dbg",
"stopAtEntry": false,
"externalConsole": false,
"internalConsoleOptions": "neverOpen",
"MIMode": "gdb",
"miDebuggerPath": "gdb"
}
]
}

31
.vscode/tasks.json vendored Executable file
View File

@@ -0,0 +1,31 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build_exec_dbg",
"detail": "build project with debug symbols",
"type": "cppbuild",
"command": "bash",
"args": [
"-c",
"cbuild build_static_lib_dbg test"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": ["$gcc"],
"group": {
"kind": "build"
},
"presentation": {
"echo": false,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true
}
}
]
}

BIN
256colors.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -3,12 +3,6 @@
#include "tim.h"
// colors
#define CTXT 0xf // text black, white
#define CFR 0x8 // frame black, gray
#define CYES 0xa000f // yes green, black, white
#define CNO 0x9000f // no red, black white
i32 main(i32 argc, char** argv) {
if (argc < 2 || strcmp(argv[1], "-h") == 0) {
printf("syntax: %s message\n", argv[0]);
@@ -18,6 +12,11 @@ i32 main(i32 argc, char** argv) {
// get text properties
TimText msg = tim_scan_str(argv[1]);
TimStyle style_frame = { .brd = TimColor16_DarkGray };
TimStyle style_text = { .fg = TimColor16_White };
TimStyle style_yes = { .brd = TimColor16_Green, .fg = style_text.fg };
TimStyle style_no = { .brd = TimColor16_Red, .fg = style_yes.fg };
while (tim_run(0)) {
// calculate size of message box
i32 w = MAX(msg.width + 4, 28);
@@ -25,18 +24,18 @@ i32 main(i32 argc, char** argv) {
tim_scope(A, A, w, h) {
// draw frame around entire scope
tim_frame(0, 0, ~0, ~0, CFR);
tim_frame(0, 0, ~0, ~0, style_frame);
// draw message
label(argv[1], A, 1, msg.width, msg.lines, CTXT);
tim_label(argv[1], A, 1, msg.width, msg.lines, style_text);
// draw 'yes' button, return 0 when clicked
if (button("[y] Yes", 2, ~1, A, A, CYES) || tim_is_key_press('y')) {
if (tim_button("[y] Yes", 2, ~1, A, A, style_yes) || tim_is_key_press('y')) {
exit(0);
}
// draw 'no' button, return 1 when clicked
if (button("[n] No", ~2, ~1, A, A, CNO) || tim_is_key_press('n')) {
if (tim_button("[n] No", ~2, ~1, A, A, style_no) || tim_is_key_press('n')) {
exit(1);
}
@@ -46,4 +45,5 @@ i32 main(i32 argc, char** argv) {
}
}
}
return 0;
}

View File

@@ -3,13 +3,18 @@
i32 main(void) {
while (tim_run(0)) { // init state and start event loop
tim_scope(A, A, 24, 8) { // centered 24x8 scope
u64 c = 0x0a060f; // three colors
TimStyle c = { // three colors
.brd= TimColor16_Green,
.bg = TimColor16_DarkCyan,
.fg = TimColor16_White
};
tim_frame(0, 0, ~0, ~0, c); // draw frame for scope
label("Greetings!", A, 2, A, A, c); // label in top center
if (button("OK", A, ~1, 8, A, c)) // button in bottom center
tim_label("Greetings!", A, 2, A, A, c); // label in top center
if (tim_button("OK", A, ~1, 8, A, c)) // button in bottom center
return 0; // exit on button click
if (tim_is_key_press('q')) // ctrl-c is masked
return 0; // exit on 'q' press
}
} // atexit() cleanup
return 0;
}

View File

@@ -6,7 +6,6 @@
#define FG 0x10
#define BG 0xdd
#define BTN (FG << 16 | BG << 8 | FG)
#define NEW 0
#define RUN 1
@@ -81,7 +80,7 @@ static void game(void) {
}
// user input
if (tim->event.type == KEY_EVENT) {
if (tim->event.type == TimEvent_Key) {
i32 key = tim->event.key;
if ((key == TimKey_Right || key == 'd') && snek.look.x != -1) {
snek.look = (point){{1, 0}};
@@ -99,17 +98,18 @@ static void game(void) {
}
static void menu(void) {
TimStyle style_button = (TimStyle){ .brd= FG, .bg = BG, .fg = FG };
tim_scope(A, A, 20, 13) {
char* lbl = snek.state == OVER ? "GAME OVER" : "SNEK - THE GAME";
char* btn = snek.state == PAUSE ? "Resume" : "Play";
label(lbl, A, 0, A, A, BTN);
if (button(btn, A, 2, 20, 5, BTN) || tim_is_key_press(TimKey_Enter)) {
tim_label(lbl, A, 0, A, A, style_button);
if (tim_button(btn, A, 2, 20, 5, style_button) || tim_is_key_press(TimKey_Enter)) {
if (snek.state != PAUSE) {
start();
}
snek.state = RUN;
}
if (button("Exit", A, 8, 20, 5, BTN) || tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
if (tim_button("Exit", A, 8, 20, 5, style_button) || tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
exit(0);
}
}
@@ -119,13 +119,13 @@ i32 main(void) {
// draw every 10 ms
while (tim_run(60)) {
TimCell bg = tim_cell(" ", 0, BG);
tim_draw_lot(bg, 0, 0, tim->w, tim->h);
tim_fill(bg, 0, 0, tim->w, tim->h);
if (snek.state == RUN) {
game();
} else {
menu();
}
}
return 0;
}

View File

@@ -16,10 +16,6 @@
#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)
@@ -60,42 +56,45 @@ extern "C" {
#pragma region constants
#define TIM_ENABLE_DBUF 1 // double buffering
#define TIM_MAX_SCOPE 20 // max scope nesting
#define TIM_MAX_SCOPE 32 // 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
#pragma region types
// 16 ANSI colors supported by any terminal emulator.
// It's better to use xterm256 colors istead,
// because ANSI colors look different in each terminal.
// https://www.ditig.com/256-colors-cheat-sheet
enum {
TimColor16_Black = 0x00,
TimColor16_DarkRed = 0x01,
TimColor16_DarkGreen = 0x02,
TimColor16_DarkYellow = 0x03,
TimColor16_DarkBlue = 0x04,
TimColor16_DarkMagenta = 0x05,
TimColor16_DarkCyan = 0x06,
TimColor16_Gray = 0x07,
TimColor16_DarkGray = 0x08,
TimColor16_Red = 0x09,
TimColor16_Green = 0x0a,
TimColor16_Yellow = 0x0b,
TimColor16_Blue = 0x0c,
TimColor16_Magenta = 0x0d,
TimColor16_Cyan = 0x0e,
TimColor16_White = 0x0f,
};
typedef struct TimStyle {
u8 brd; // border
u8 bg; // background
u8 fg; // foreground
} TimStyle;
typedef struct TimCell {
u8 fg; // foreground color
u8 bg; // background color
@@ -124,6 +123,35 @@ typedef struct TimLine {
i32 width; // line width in glyph
} TimLine;
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_MouseScrollUp = 4,
TimKey_MouseScrollDown = 5,
TimKey_Backspace = 8,
TimKey_Tab = 9,
TimKey_Enter = 13,
TimKey_Escape = 27,
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,
};
// key code or 32-bit unicode char
typedef i32 TimKey;
typedef struct TimEvent {
TimEventType type;
TimKey key; // used by TimEvent_Key and TimEvent_Mouse
@@ -132,13 +160,41 @@ typedef struct TimEvent {
char s[32]; // string representation of key, used by TimEvent_Key
} TimEvent;
typedef struct TimEditState {
bool masked; // if true prints '*' instead of buffer content
i32 cursor; // cursor position (utf8)
i32 length; // string length (utf8)
i32 capacity; // buffer size
i32 capacity; // buffer size in bytes
char* s; // zero terminated buffer
} TimEditState;
typedef struct TimPanelItem {
// Size of item to know where to draw next item.
// Set to to A and items will be spread equally across panel
i32 w;
i32 h;
void* data; // is passed to draw()
void* focus_target; // is assigned to tim->focus
void (*draw)(void* data, TimRect place, bool is_selected);
} TimPanelItem;
typedef struct TimPanel {
TimPanelItem* items; // array
i32 len; // number of items
i32 cur; // index of current item
i32 spacing; // distance between items
bool is_horizontal;
} TimPanel;
typedef struct TimScrollView {
i32 offset;
i32 content_h;
void* data; // is passed to draw()
void (*draw)(void* data, TimRect place);
} TimScrollView;
typedef struct TimState {
i32 w; // screen width
i32 h; // screen height
@@ -195,9 +251,17 @@ i64 tim_time_usec(void);
#pragma region scope
// for some stupid reason gcc requires more than 3 levels of macros to concat _i_ with line number
#define _tim_cat2(A, B) A##B
#define _tim_scope_line(L) _tim_cat2(_i_, L)
#define _tim_scope_i _tim_scope_line(__LINE__)
// enter layout scope
#define tim_scope(x, y, w, h) \
for (i32 _i = tim_enter_scope((x), (y), (w), (h)); _i; _i = tim_exit_scope())
#define tim_scope(x, y, w, h) for (\
i32 _tim_scope_i = tim_enter_scope((x), (y), (w), (h)); \
_tim_scope_i;\
_tim_scope_i = tim_exit_scope()\
) /* here goes your { code } */
// convert relative (scoped) to absolute (screen) coordinates
TimRect tim_scope_rect_to_absolute(i32 x, i32 y, i32 w, i32 h);
@@ -208,50 +272,86 @@ i32 tim_enter_scope(i32 x, i32 y, i32 w, i32 h);
// exit scope and pop stack
i32 tim_exit_scope(void);
static inline TimRect tim_rect_fit(TimRect r){
if(r.x < 0)
r.x = 0;
else if(r.x >= tim->w)
r.w = 0;
else if(r.x + r.w > tim->w)
r.w = tim->w - r.x;
if(r.y < 0)
r.y = 0;
else if(r.y >= tim->h)
r.y = tim->h - 1;
else if(r.y + r.h > tim->h)
r.h = tim->h - r.y;
return r;
}
#pragma endregion
#pragma region widgets
// TODO: create enum TimColor and struct TimStyle
// frame
// color: background, frame
void tim_frame(i32 x, i32 y, i32 w, i32 h, u64 color);
void tim_frame(i32 x, i32 y, i32 w, i32 h, TimStyle style);
// text label
// str : text - supports multiple lines
// color: background, text
void label(cstr s, i32 x, i32 y, i32 w, i32 h, u64 color);
void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, TimStyle style);
// button - returns true on click
// button with border - returns true on click
// color: frame, background, text
bool button(cstr txt, i32 x, i32 y, i32 w, i32 h, u64 color);
bool tim_button(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style);
// button without border - returns true on click
// color: frame, background, text
bool tim_button_noborder(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style);
// check box - returns true when clicked
// txt : text label
// state: persistent state, 0 unchecked, -1 semi checked, !0: checked
// color: check, background, text
bool check(cstr txt, i32* state, i32 x, i32 y, i32 w, u64 color);
bool tim_checkbox(cstr txt, i32* state, i32 x, i32 y, i32 w, TimStyle style);
// radio button - return true when clicked
// txt : text label
// state: persistent state, selected if *state == v
// v : value
// color: radio, background, text
bool radio(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, u64 color);
bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, TimStyle style);
/// text edit - value in state
/// @param e persistent edit state, use TimEditState_init() to create new state
/// @param color frame, background, text
/// @param e persistent edit state, use TimEditState_construct() to create new state
/// @param style frame, background, text
/// @return key id or 0
i32 edit(TimEditState* e, i32 x, i32 y, i32 w, u64 color);
TimKey tim_edit(TimEditState* e, i32 x, i32 y, i32 w, TimStyle style);
void TimEditState_init(TimEditState* e, i32 capacity, cstr initial_content);
/// @param e uninitialized state
/// @param buffer an array
/// @param capacity buffer size in bytes
/// @param initial_content may be NULL
void TimEditState_construct(TimEditState* e, char* buffer, i32 capacity, cstr initial_content);
void TimEditState_insert(TimEditState* e, cstr s);
void TimEditState_delete(TimEditState* e);
void edit_insert(TimEditState* e, cstr s);
void edit_delete(TimEditState* e);
/// Panel with sequence of items. You can select an item by arrow keys or mouse click.
/// @param self persistent state
/// @param is_selected if panel is not selected, it calls items[:]->draw(is_selected=false)
/// @return current item
TimPanelItem* tim_panel(TimPanel* self, bool is_selected, i32 x, i32 y, i32 w, i32 h);
void TimPanel_selectNext(TimPanel* self);
void TimPanel_selectPrev(TimPanel* self);
///
void tim_scroll_view(TimScrollView* self, i32 x, i32 y, i32 w, i32 h, TimStyle style);
#pragma endregion
@@ -262,13 +362,21 @@ void edit_delete(TimEditState* e);
bool tim_is_event_key(TimEventType type, TimKey key);
// returns true if event was press of key
bool tim_is_key_press(TimKey key);
static inline bool tim_is_key_press(TimKey key) { return tim_is_event_key(TimEvent_Key, key); }
static inline bool tim_is_mouse_scroll_up() { return tim_is_event_key(TimEvent_Mouse, TimKey_MouseScrollUp); }
static inline bool tim_is_mouse_scroll_down() { return tim_is_event_key(TimEvent_Mouse, TimKey_MouseScrollDown); }
// returns true if mouse event was over r
bool tim_is_mouse_over(TimRect r);
// returns true if event is mouse left-down and over r
bool tim_is_mouse_click_over(TimRect r);
static inline void tim_event_consume(){
tim->event.type = TimEvent_Void;
}
#pragma endregion
@@ -289,8 +397,8 @@ void tim_draw_row(TimCell cell, i32 x, i32 y, i32 w);
// draw column of cells
void tim_draw_col(TimCell cell, i32 x, i32 y, i32 h);
// fill lot (area) of cells
void tim_draw_lot(TimCell cell, i32 x, i32 y, i32 w, i32 h);
// fill area with cells
void tim_fill(TimCell cell, i32 x, i32 y, i32 w, i32 h);
// draw string to line, tags potential wide characters
void tim_draw_str(cstr s, i32 x, i32 y, i32 w, u8 fg, u8 bg);

View File

@@ -38,17 +38,19 @@ STATIC_LIB_FILE="$PROJECT.a"
# example: "-I./include -I$DEPENDENCIES_DIR/libexample"
INCLUDE="-Iinclude -Isrc"
EXEC_FILE=""
EXEC_EXT=""
# OS-specific options
case "$OS" in
WINDOWS)
EXEC_FILE="snek.exe"
EXEC_EXT=".exe"
SHARED_LIB_FILE="$PROJECT.dll"
INCLUDE="$INCLUDE "
# example: "-lSDL2 -lSDL2_image"
LINKER_LIBS=""
;;
LINUX)
EXEC_FILE="snek"
SHARED_LIB_FILE="$PROJECT.so"
INCLUDE="$INCLUDE "
LINKER_LIBS=""
@@ -58,28 +60,76 @@ case "$OS" in
;;
esac
TEST_C_ARGS="-O0 -g3"
# TASKS
case "$TASK" in
# creates executable using profiling info if it exists
build_exec)
SRC_C="$SRC_C example/snek.c"
# -flto applies more optimizations across object files
# -flto=auto is needed to multithreaded copilation
# -fuse-linker-plugin is required to use static libs with lto
# -fprofile-use enables compiler to use profiling info files to optimize executable
# -fprofile-prefix-path sets path where profiling info about objects are be saved
# -fdata-sections -ffunction-sections -Wl,--gc-sections removes unused code
C_ARGS="-O2 -flto=auto -fuse-linker-plugin -fprofile-use -fprofile-prefix-path=$(realpath $OBJDIR)/objects -fdata-sections -ffunction-sections -Wl,--gc-sections"
# ./ask question?
ask)
EXEC_FILE="ask$EXEC_EXT"
SRC_C="example/ask.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT="@cbuild/default_tasks/strip_exec.sh"
POST_TASK_SCRIPT=""
;;
# creates executable with debug info and no optimizations
build_exec_dbg)
SRC_C="$SRC_C example/snek.c"
C_ARGS="-O0 -g3"
# hello world
hello)
EXEC_FILE="hello$EXEC_EXT"
SRC_C="example/hello.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# snake game
snek)
EXEC_FILE="snek$EXEC_EXT"
SRC_C="example/snek.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# test
test)
EXEC_FILE="test$EXEC_EXT"
SRC_C="test/test.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# color
color)
EXEC_FILE="color$EXEC_EXT"
SRC_C="test/color.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
TASK_SCRIPT="@cbuild/default_tasks/build_exec.sh"
POST_TASK_SCRIPT=""
;;
# string
string)
EXEC_FILE="string$EXEC_EXT"
SRC_C="test/string.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""

View File

@@ -5,6 +5,26 @@ Fork of https://codeberg.org/chuvok/tim.h
tim is a portable library to create simple terminal applications
Demo video: https://asciinema.org/a/zn3p0dsVCOQOzwY1S9gDfyaxQ
## build
1. Clone this repository.
```
git clone https://timerix.ddns.net/git/Timerix/tim.git
```
2. Install [cbuild](https://timerix.ddns.net/git/Timerix/cbuild/releases).
Select latest version compatible with the one in `project.config`.
Example: For `2.3.2` download latest `2.3.x`.
3. Build static library
```
cd tim
cbuild build_static_lib
```
4. Build tests and examples
```
cbuild ask hello snek test color string
```
## quick start
See [example/hello.c](./example/hello.c)
@@ -44,18 +64,11 @@ take the full available space from parent.
The layout automatically adopts to terminal window resize events.
## colors
Most elements have a uint64 color argument which holds up to eight colors.
Typically byte 0 is the text color and byte 1 is the background color.
For example 0x08040f encodes three colors. When used with a button the text
is white (0f), the background is blue (04), and the frame is gray (08).
The terminal should support xterm-256 colors. The TERM variable is ignored.
The lower 16 colors vary across different terminals, so the upper 240 colors
should be used if consistency is important.
xterm-256 color chart
https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.svg
Colors are stored as 8-bit values.
Most terminals support 16 ANSI colors. You can see them in TimColor16 enum.
It's better to use xterm256 colors istead, because ANSI colors look different in each terminal.
https://www.ditig.com/256-colors-cheat-sheet
![xterm-256 color chart](./256colors.jpg)
## events
tim_run blocks until it observes an event. Mouse and key events are always
@@ -66,20 +79,13 @@ event when focused in order to prevent other key handlers on acting on them.
The current event is stored in tim->event.
event | cause
-------------|-----------------------
DRAW_EVENT | input, timeout, resize
KEY_EVENT | key press
MOUSE_EVENT | mouse click
VOID_EVENT | consumed event
## elements
frame (x, y, w, h, color)
Draw an empty frame and fill area.
x/y/w/h see layout documentation
color background, frame
style background, frame
label (str, x, y, w, h, color)
@@ -88,7 +94,7 @@ label (str, x, y, w, h, color)
str zero terminated string
x/y/w/h see layout documentation
color background, text
style background, text
button (str, x, y, w, h, color) -> bool
@@ -97,7 +103,7 @@ button (str, x, y, w, h, color) -> bool
str zero terminated string
x/y/w/h see layout documentation
color frame, background, text
style frame, background, text
edit (state, x, y, w, color) -> int
@@ -108,7 +114,7 @@ edit (state, x, y, w, color) -> int
state pointer to persistent edit state struct
x/y/w see layout documentation
color f rame, background, text
style f rame, background, text
check (str, state, x, y, w, color) -> bool
@@ -119,7 +125,7 @@ check (str, state, x, y, w, color) -> bool
str zero terminated string
state pointer to persistent state variable
x/y/w see layout documentation
color check, background, text
style check, background, text
radio (str, state, v, x, y, w, color) -> bool
@@ -131,7 +137,7 @@ radio (str, state, v, x, y, w, color) -> bool
state pointer to persistent state variable
v unique state value
x/y/w see layout documentation
color radio, background, text
style radio, background, text
## functions
tim_run (fps) -> bool
@@ -166,6 +172,8 @@ https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
https://learn.microsoft.com/en-us/windows/console/
## bugs
- If Enter key doesn't work as expected on linux, write `stty sane` to `~/.profile`.
It will change terminal settings from insane to sane xD
- Double buffering is still new, set ENABLE_DBUF to 0 if you see glitches
- Double width characters like 彁 are not fully supported. Terminals do not
handle these consistently and there is no portable way to reliably

View File

@@ -15,31 +15,41 @@ void tim_clear_cells(void) {
}
void tim_draw_chr(TimCell cell, i32 x, i32 y) {
if (x >= 0 && x < tim->w && y >= 0 && y < tim->h) {
TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
if (x >= scope.x && x < scope.x + scope.w &&
y >= scope.y && y < scope.y + scope.h)
{
tim->cells[x + y * tim->w] = cell;
}
}
void tim_draw_row(TimCell cell, i32 x, i32 y, i32 w) {
if (y >= 0 && y < tim->h && w > 0) {
for (i32 i = MAX(x, 0); i < MIN(x + w, tim->w); i++) {
TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
if (y >= scope.y && y < scope.y + scope.h && w > 0) {
for (i32 i = MAX(x, scope.x); i < MIN(x + w, scope.x + scope.w); i++) {
tim->cells[i + y * tim->w] = cell;
}
}
}
void tim_draw_col(TimCell cell, i32 x, i32 y, i32 h) {
if (x >= 0 && x < tim->w && h > 0) {
for (i32 i = MAX(y, 0); i < MIN(y + h, tim->h); i++) {
TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
if (x >= scope.x && x < scope.x + scope.w && h > 0) {
for (i32 i = MAX(y, scope.y); i < MIN(y + h, scope.y + scope.h); i++) {
tim->cells[x + i * tim->w] = cell;
}
}
}
void tim_draw_lot(TimCell cell, i32 x, i32 y, i32 w, i32 h) {
void tim_fill(TimCell cell, i32 x, i32 y, i32 w, i32 h) {
TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
if (w > 0 && h > 0) {
for (i32 iy = MAX(y, 0); iy < MIN(y + h, tim->h); iy++) {
for (i32 ix = MAX(x, 0); ix < MIN(x + w, tim->w); ix++) {
for (i32 iy = MAX(y, scope.y); iy < MIN(y + h, scope.y + scope.h); iy++) {
for (i32 ix = MAX(x, scope.x); ix < MIN(x + w, scope.x + scope.w); ix++) {
tim->cells[ix + iy * tim->w] = cell;
}
}
@@ -47,8 +57,10 @@ void tim_draw_lot(TimCell cell, i32 x, i32 y, i32 w, i32 h) {
}
void tim_draw_str(cstr s, i32 x, i32 y, i32 w, u8 fg, u8 bg) {
if (s && y >= 0 && x < tim->w && y < tim->h ) {
i32 end = MIN(x + w, tim->w);
TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
if (s && y >= 0 && x < scope.x + scope.w && y < scope.y + scope.h ) {
i32 end = MIN(x + w, scope.x + scope.w);
bool wide = false;
for (i32 i = 0; s[i] && x < end; x++) {
TimCell c = tim_cell(&s[i], fg, bg);
@@ -63,6 +75,8 @@ void tim_draw_str(cstr s, i32 x, i32 y, i32 w, u8 fg, u8 bg) {
}
void tim_draw_box(i32 x, i32 y, i32 w, i32 h, u8 fg, u8 bg) {
if(w <= 0 || h <= 0)
return;
tim_draw_chr(tim_cell("", fg, bg), x , y);
tim_draw_chr(tim_cell("", fg, bg), x + w - 1, y);
tim_draw_chr(tim_cell("", fg, bg), x , y + h - 1);
@@ -71,12 +85,14 @@ void tim_draw_box(i32 x, i32 y, i32 w, i32 h, u8 fg, u8 bg) {
tim_draw_row(tim_cell("", fg, bg), x + 1 , y + h - 1, w - 2);
tim_draw_col(tim_cell("", fg, bg), x , y + 1 , h - 2);
tim_draw_col(tim_cell("", fg, bg), x + w - 1, y + 1 , h - 2);
tim_draw_lot(tim_cell(" ", fg, bg), x + 1 , y + 1 , w - 2, h - 2);
tim_fill(tim_cell(" ", fg, bg), x + 1 , y + 1 , w - 2, h - 2);
}
void tim_draw_invert(i32 x, i32 y, i32 w) {
if (y >= 0 && y < tim->h && w > 0) {
for (i32 i = MAX(x, 0); i < MIN(x + w, tim->w); i++) {
TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
if (y >= 0 && y < scope.y + scope.h && w > 0) {
for (i32 i = MAX(x, scope.x); i < MIN(x + w, scope.x + scope.w); i++) {
TimCell 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;

View File

@@ -1,17 +1,20 @@
#include "tim.h"
void TimEditState_init(TimEditState* e, i32 capacity, cstr initial_content){
e->length = tim_utf8_len(initial_content);
e->cursor = tim_utf8_len(initial_content);
void TimEditState_construct(TimEditState* e, char* buf, i32 capacity, cstr initial_content){
e->masked = false;
e->length = initial_content ? tim_utf8_len(initial_content) : 0;
e->cursor = e->length;
e->capacity = capacity;
e->s = (char*)malloc(capacity + 1);
i32 byte_len = strlen(initial_content);
e->s = buf;
i32 byte_len = 0;
if(e->length > 0){
byte_len = strlen(initial_content);
memcpy(e->s, initial_content, byte_len);
}
e->s[byte_len] = 0;
}
void edit_insert(TimEditState* e, cstr s) {
void TimEditState_insert(TimEditState* e, cstr s) {
i32 dst_size = tim_ztrlen(e->s);
i32 src_size = tim_ztrlen(s);
if (dst_size + src_size < e->capacity) {
@@ -25,7 +28,7 @@ void edit_insert(TimEditState* e, cstr s) {
}
}
void edit_delete(TimEditState* e) {
void TimEditState_delete(TimEditState* e) {
i32 size = tim_ztrlen(e->s);
i32 cur = tim_utf8_pos(e->s, e->cursor);
i32 len = tim_utf8_pos(e->s + cur, 1);
@@ -36,7 +39,7 @@ void edit_delete(TimEditState* e) {
}
/// @return key id or 0
static i32 edit_event(TimEditState* e, TimRect r) {
static TimKey edit_event(TimEditState* e, TimRect r) {
if (tim_is_mouse_click_over(r)) {
// take focus
tim->focus = e;
@@ -47,7 +50,8 @@ static i32 edit_event(TimEditState* e, TimRect r) {
// not focused or no key press
return 0;
}
tim->event.type = TimEvent_Void; // consume event
tim_event_consume();
switch (tim->event.key) {
case TimKey_Escape:
@@ -55,12 +59,12 @@ static i32 edit_event(TimEditState* e, TimRect r) {
tim->focus = 0; // release focus
break;
case TimKey_Delete:
edit_delete(e);
TimEditState_delete(e);
break;
case TimKey_Backspace:
if (e->cursor > 0) {
e->cursor -= 1;
edit_delete(e);
TimEditState_delete(e);
}
break;
case TimKey_Left:
@@ -77,7 +81,7 @@ static i32 edit_event(TimEditState* e, TimRect r) {
break;
default:
if (tim->event.key >= ' ') {
edit_insert(e, tim->event.s);
TimEditState_insert(e, tim->event.s);
}
break;
}
@@ -85,18 +89,36 @@ static i32 edit_event(TimEditState* e, TimRect r) {
return tim->event.key;
}
i32 edit(TimEditState* e, i32 x, i32 y, i32 w, u64 color) {
TimKey tim_edit(TimEditState* e, i32 x, i32 y, i32 w, TimStyle style) {
TimRect r = tim_scope_rect_to_absolute(x, y, w, 3);
if (tim->event.type == TimEvent_Draw) {
tim_draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8);
if (tim->focus == e) {
char* s = e->s + tim_utf8_pos(e->s, e->cursor - r.w + 4);
i32 cur = MIN(r.w - 4, e->cursor);
tim_draw_str(s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8);
tim_draw_invert(r.x + cur + 2, r.y + 1, 1);
} else {
tim_draw_str(e->s, r.x + 2, r.y + 1, r.w - 3, color, color >> 8);
tim_draw_box(r.x, r.y, r.w, r.h, style.brd, style.bg);
i32 s_x = r.x + 2, s_y = r.y + 1, s_w = r.w - 3, s_w_sub1 = s_w - 1;
char* s = e->s;
bool focused = tim->focus == e;
if (focused) {
// rewind long buffer so cursor is at the most right cell
s = e->s + tim_utf8_pos(e->s, e->cursor - s_w_sub1);
}
if(e->masked){
// draw '*' instead of buffer characters
TimCell cell_masked = tim_cell("*", style.fg, style.bg);
i32 w_mask = MIN(s_w, e->length);
tim_draw_row(cell_masked, s_x, s_y, w_mask);
}
else {
// draw part of buffer content
tim_draw_str(s, s_x, s_y, s_w, style.fg, style.bg);
}
if (focused) {
// invert color of last cell
i32 cur = MIN(s_w_sub1, e->cursor);
tim_draw_invert(s_x + cur, s_y, 1);
}
}

View File

@@ -4,10 +4,6 @@ bool tim_is_event_key(TimEventType type, TimKey key) {
return tim->event.type == type && tim->event.key == key;
}
bool tim_is_key_press(TimKey key) {
return tim_is_event_key(TimEvent_Key, key);
}
bool tim_is_mouse_over(TimRect r) {
i32 x = tim->event.x;
i32 y = tim->event.y;

View File

@@ -7,10 +7,12 @@ TimState* tim = NULL;
static void tim_init(void){
tim = (TimState*)malloc(sizeof(TimState));
memset(tim, 0, sizeof(TimState));
size_t cdb_size = (TIM_MAX_CELLS << TIM_ENABLE_DBUF);
tim->cells_double_buf = (TimCell*)malloc(cdb_size * sizeof(TimCell));
size_t cdb_size = (TIM_MAX_CELLS << TIM_ENABLE_DBUF) * sizeof(TimCell);
tim->cells_double_buf = (TimCell*)malloc(cdb_size);
memset(tim->cells_double_buf, 0, cdb_size);
tim->cells = tim->cells_double_buf;
tim->buf = (char*)malloc(TIM_MAX_BUF);
memset(tim->buf, 0, TIM_MAX_BUF);
}
static void tim_deinit(void){

86
src/panel.c Executable file
View File

@@ -0,0 +1,86 @@
#include "tim.h"
void TimPanel_selectNext(TimPanel* l){
if(l->cur + 1 < l->len)
l->cur++;
}
void TimPanel_selectPrev(TimPanel* l){
if(l->cur - 1 >= 0)
l->cur--;
}
TimPanelItem* tim_panel(TimPanel* self, bool is_panel_selected, i32 x, i32 y, i32 w, i32 h){
// select item with keyboard
if(tim_is_key_press(self->is_horizontal ? TimKey_Left : TimKey_Up))
{
TimPanel_selectPrev(self);
}
else if(tim_is_key_press(self->is_horizontal ? TimKey_Right : TimKey_Down))
{
TimPanel_selectNext(self);
}
// set focus on current item
if(self->cur < self->len)
tim->focus = self->items[self->cur].focus_target;
tim_scope(x, y, w, h)
{
TimRect content_scope = tim->scopes[tim->scope];
// TODO: draw current item and as much previous items as possible in scope
TimRect item_place = { 0 };
for(i32 i = 0; i < self->len; i++){
TimPanelItem* item = &self->items[i];
item_place.w = item->w;
if(item_place.w == A){
if(self->is_horizontal){
item_place.w = content_scope.w / self->len ;
// add remaining width to the last item
if(i == self->len - 1)
item_place.w += content_scope.w % self->len;
else item_place.w -= self->spacing;
}
else {
item_place.w = content_scope.w;
}
}
item_place.h = item->h;
if(item_place.h == A){
if(self->is_horizontal){
item_place.h = content_scope.h;
}
else {
item_place.h = content_scope.h / self->len - self->spacing;
// add remaining height to the last item
if(i == self->len - 1)
item_place.h += content_scope.h % self->len;
else item_place.h -= self->spacing;
}
}
// select item with mouse click
if(tim_is_mouse_click_over(tim_scope_rect_to_absolute(item_place.x, item_place.y, item_place.w, item_place.h))){
self->cur = i;
tim->focus = item->focus_target;
}
bool is_item_selected = false;
if(is_panel_selected)
is_item_selected = i == self->cur;
item->draw(item->data, item_place, is_item_selected);
// adjust place for next item
if(self->is_horizontal){
item_place.x += item_place.w + self->spacing;
}
else {
item_place.y += item_place.h + self->spacing;
}
}
}
return &self->items[self->cur];
}

View File

@@ -35,9 +35,9 @@ TimRect tim_scope_rect_to_absolute(i32 x, i32 y, i32 w, i32 h) {
}
i32 tim_enter_scope(i32 x, i32 y, i32 w, i32 h) {
if (tim->scope + 1 >= TIM_MAX_SCOPE) {
if (tim->scope + 1 >= TIM_MAX_SCOPE)
return 0;
}
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
tim->scope += 1;
tim->scopes[tim->scope] = r;

81
src/scroll_view.c Normal file
View File

@@ -0,0 +1,81 @@
#include "tim.h"
#include <math.h>
void tim_scroll_view(TimScrollView* self, i32 x, i32 y, i32 w, i32 h, TimStyle style){
tim_scope(x, y, w, h)
{
TimRect content_scope = tim->scopes[tim->scope];
// shrink content_scope to put scrollbar
content_scope.w -= 1;
i32 max_offset = MAX(self->content_h - content_scope.h, 0);
// draw scroll bar and
TimRect arrow_up_r = {
.x = content_scope.x + content_scope.w,
.y = content_scope.y,
.w = 1,
.h = 1
};
TimRect scrollbar_r = {
.x = arrow_up_r.x,
.y = arrow_up_r.y + 1,
.w = 1,
.h = content_scope.h - 2
};
TimRect arrow_down_r = {
.x = scrollbar_r.x,
.y = scrollbar_r.y + scrollbar_r.h,
.w = 1,
.h = 1
};
if (tim->event.type == TimEvent_Draw) {
f32 scroll_ratio = 0;
f32 slider_h = 0;
if(max_offset != 0){
slider_h = ceilf((f32)scrollbar_r.h / max_offset);
scroll_ratio = (f32)self->offset / max_offset + 0.001f;
}
i32 slider_y = scrollbar_r.y + (scrollbar_r.h - slider_h) * scroll_ratio;
tim_draw_chr(tim_cell("", style.brd, style.bg), arrow_up_r.x, arrow_up_r.y);
tim_draw_col(tim_cell("", style.brd, style.bg), scrollbar_r.x, scrollbar_r.y, scrollbar_r.h);
tim_draw_col(tim_cell("", style.brd, style.bg), scrollbar_r.x, slider_y, slider_h);
tim_draw_chr(tim_cell("", style.brd, style.bg), arrow_down_r.x, arrow_down_r.y);
}
if(tim_is_mouse_click_over(arrow_up_r) || tim_is_mouse_scroll_up()){
self->offset--;
}
else if(tim_is_mouse_click_over(arrow_down_r) || tim_is_mouse_scroll_down()){
self->offset++;
}
else if(tim_is_key_press(TimKey_PageUp)){
self->offset -= content_scope.h;
}
else if(tim_is_key_press(TimKey_PageDown)){
self->offset += content_scope.h;
}
if(self->offset > max_offset)
self->offset = max_offset;
else if(self->offset < 0)
self->offset = 0;
if(tim_is_mouse_click_over(scrollbar_r)){
i32 click_y_rel = tim->event.y - scrollbar_r.y;
if(scrollbar_r.h != 0){
f32 slider_h = ceilf((f32)scrollbar_r.h / max_offset);
f32 scroll_ratio = (f32)click_y_rel / (scrollbar_r.h - slider_h) + 0.001f;
self->offset = max_offset * scroll_ratio;
}
}
// update current scope
tim->scopes[tim->scope] = content_scope;
// draw content
TimRect content_place = { .x = 0, .y = -self->offset - 1, .w = content_scope.w, .h = content_scope.h };
self->draw(self->data, content_place);
}
}

View File

@@ -1,3 +1,12 @@
// Enable POSIX 2004 definitions.
// Required to use clock_gettime, localtime_r, gmtime_r in ISO C.
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 600
#endif
// Enable cfmakeraw()
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#include "tim.h"
#ifdef TIM_UNIX
@@ -56,13 +65,14 @@ void tim_reset_terminal(void) {
tcsetattr(STDOUT_FILENO, TCSADRAIN, &tim->attr); // restore attributes
tim_write_str(S("\33[?1000l")); // disable mouse
tim_write_str(S("\33[?1002l")); // disable mouse
tim_write_str(S("\33[?1006l")); // disable mouse
tim_write_str(S("\33[m")); // reset colors
tim_write_str(S("\33[?25h")); // show cursor
tim_write_str(S("\33[?1049l")); // exit alternate buffer
}
// parse input stored in e->s
bool parse_input(event* restrict e, i32 n) {
bool parse_input(TimEvent* restrict e, i32 n) {
char* s = e->s;
if (n == 1 || s[0] != 27) {
@@ -76,14 +86,23 @@ bool parse_input(event* restrict e, i32 n) {
// sgr mouse sequence
e->type = TimEvent_Mouse;
i32 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;
}
e->x = strtol(s + 1, &s, 10);
e->y = strtol(s + 1, &s, 10);
// coordinates start from 1
if(e->x > 0) e->x--;
if(e->y > 0) e->y--;
// invalid sequence end
if (s[0] != 'M')
return false;
switch(btn){
case 0: e->key = TimKey_MouseButtonLeft; break;
case 64: e->key = TimKey_MouseScrollUp; break;
case 65: e->key = TimKey_MouseScrollDown; break;
default:
return false;
}
return true;
}
struct {char s[4]; i32 k;} key_table[] = {
@@ -120,7 +139,7 @@ bool parse_input(event* restrict e, i32 n) {
}
void tim_read_event(i32 timeout_ms) {
event* e = &tim->event;
TimEvent* e = &tim->event;
struct pollfd pfd[2] = {
{.fd = tim->signal_pipe[0], .events = POLLIN},

View File

@@ -1,20 +1,20 @@
#include "tim.h"
void tim_frame(i32 x, i32 y, i32 w, i32 h, u64 color) {
void tim_frame(i32 x, i32 y, i32 w, i32 h, TimStyle style) {
if (tim->event.type == TimEvent_Draw) {
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
tim_draw_box(r.x, r.y, r.w, r.h, color, color >> 8);
tim_draw_box(r.x, r.y, r.w, r.h, style.brd, style.bg);
}
}
void label(cstr s, i32 x, i32 y, i32 w, i32 h, u64 color) {
void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
if (tim->event.type == TimEvent_Draw) {
TimText t = tim_scan_str(s);
w = (w == A) ? t.width : w;
h = (h == A) ? t.lines : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
TimCell c = tim_cell(" ", color, color >> 8);
tim_draw_lot(c, r.x, r.y, r.w, r.h);
TimCell c = tim_cell(" ", style.fg, style.bg);
tim_fill(c, r.x, r.y, r.w, r.h);
TimLine l = {.s = s, .line = ""};
for (i32 i = 0; tim_next_line(&l); i++) {
tim_draw_str(l.line, r.x, r.y + i, l.width, c.fg, c.bg);
@@ -22,28 +22,40 @@ void label(cstr s, i32 x, i32 y, i32 w, i32 h, u64 color) {
}
}
bool button(cstr txt, i32 x, i32 y, i32 w, i32 h, u64 color) {
i32 tw = tim_utf8_len(txt);
w = (w == A) ? (tw + 4) : w;
bool tim_button(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
i32 txt_w = tim_utf8_len(txt);
w = (w == A) ? (txt_w + 4) : w;
h = (h == A) ? 3 : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
if (tim->event.type == TimEvent_Draw) {
tim_draw_box(r.x, r.y, r.w, r.h, color >> 16, color >> 8);
tim_draw_str(txt, r.x + (w - tw) / 2, r.y + h / 2, w, color, color >> 8);
tim_draw_box(r.x, r.y, r.w, r.h, style.brd, style.bg);
tim_draw_str(txt, r.x + (w - txt_w) / 2, r.y + h / 2, w, style.fg, style.bg);
}
return tim_is_mouse_click_over(r);
}
bool check(cstr txt, i32* state, i32 x, i32 y, i32 w, u64 color) {
bool tim_button_noborder(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
i32 txt_w = tim_utf8_len(txt);
w = (w == A) ? txt_w : w;
h = (h == A) ? 1 : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
if (tim->event.type == TimEvent_Draw) {
tim_draw_str(txt, r.x + (w - txt_w) / 2, r.y + h / 2, w, style.fg, style.bg);
}
return tim_is_mouse_click_over(r);
}
bool tim_checkbox(cstr txt, i32* state, i32 x, i32 y, i32 w, TimStyle style) {
w = (w == A) ? tim_utf8_len(txt) + 4 : w;
TimRect r = tim_scope_rect_to_absolute(x, y, w, 1);
if (tim->event.type == TimEvent_Draw) {
cstr st = *state == -1 ? "-" : *state ? "x" : " ";
tim_draw_str("[ ] ", r.x, r.y, 4, color, color >> 8);
tim_draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8);
tim_draw_str("[ ] ", r.x, r.y, 4, style.fg, style.bg);
tim_draw_str(st, r.x + 1, r.y, 1, style.brd, style.bg);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, style.fg, style.bg);
}
bool click = tim_is_mouse_click_over(r);
@@ -52,20 +64,18 @@ bool check(cstr txt, i32* state, i32 x, i32 y, i32 w, u64 color) {
}
bool radio(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, u64 color) {
bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, TimStyle style) {
w = (w == A) ? tim_utf8_len(txt) + 4 : w;
TimRect r = tim_scope_rect_to_absolute(x, y, w, 1);
if (tim->event.type == TimEvent_Draw) {
cstr st = *state == v ? "o" : " ";
tim_draw_str("( ) ", r.x, r.y, 4, color, color >> 8);
tim_draw_str(st, r.x + 1, r.y, 1, color >> 16, color >> 8);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, color, color >> 8);
tim_draw_str("( ) ", r.x, r.y, 4, style.fg, style.bg);
tim_draw_str(st, r.x + 1, r.y, 1, style.brd, style.bg);
tim_draw_str(txt, r.x + 4, r.y, r.w - 4, style.fg, style.bg);
}
bool click = tim_is_mouse_click_over(r);
*state = click ? v : *state;
return click;
}

View File

@@ -113,7 +113,7 @@ void tim_read_event(i32 timeout_ms) {
// received input
INPUT_RECORD rec = {0};
DWORD n = 0;
ReadConsoleInput(h, &rec, 1, &n);
ReadConsoleInputW(h, &rec, 1, &n);
switch (rec.EventType) {
case KEY_EVENT: {
@@ -140,9 +140,16 @@ void tim_read_event(i32 timeout_ms) {
}
case MOUSE_EVENT: {
bool wheel = rec.Event.MouseEvent.dwEventFlags & MOUSE_WHEELED;
if(wheel){
i16 scroll_value = HIWORD(rec.Event.MouseEvent.dwButtonState);
e->type = TimEvent_Mouse;
e->key = scroll_value > 0 ? TimKey_MouseScrollUp : TimKey_MouseScrollDown;
return;
}
bool move = rec.Event.MouseEvent.dwEventFlags & ~DOUBLE_CLICK;
bool left = rec.Event.MouseEvent.dwButtonState &
FROM_LEFT_1ST_BUTTON_PRESSED;
bool left = rec.Event.MouseEvent.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED;
if (move || !left) {
// ignore move events and buttons other than left
continue;

View File

@@ -26,4 +26,5 @@ i32 main(void) {
exit(1);
}
}
return 0;
}

View File

@@ -67,4 +67,5 @@ i32 main(void) {
TEST(tim_utf8_is_wide_perhaps(U("")) == false);
TEST(tim_utf8_is_wide_perhaps(U("")) == true);
TEST(tim_utf8_is_wide_perhaps(U("𐍈")) == true);
return 0;
}

View File

@@ -1,120 +1,142 @@
#include "tim.h"
static inline void test_screen(TimEvent* e) {
static TimEvent me;
static TimEvent ke;
static i32 render_us;
char buf[64];
static TimEditState ed1;
static TimEditState ed2;
ke = (e->type == KEY_EVENT) ? *e : ke;
me = (e->type == MOUSE_EVENT) ? *e : me;
static inline void test_screen(TimEvent* e) {
static TimEvent me = {0};
static TimEvent ke = {0};
static i32 render_us = 0;
char buf[64] = {0};
if(e->type == TimEvent_Key)
ke = *e;
if(e->type == TimEvent_Mouse)
me = *e;
// positioning
label("+", 0, 0, A, A, 0xf);
label("+", ~0, 0, A, A, 0xf);
label("+", 0, ~0, A, A, 0xf);
label("+", ~0, ~0, A, A, 0xf);
label("+", A, A, A, A, 0xf);
label("-", 0, A, A, A, 0xf);
label("-", ~0, A, A, A, 0xf);
label("|", A, 0, A, A, 0xf);
label("|", A, ~0, A, A, 0xf);
static TimStyle style_default = { .fg = TimColor16_White };
tim_label("+", 0, 0, A, A, style_default);
tim_label("+", ~0, 0, A, A, style_default);
tim_label("+", 0, ~0, A, A, style_default);
tim_label("+", ~0, ~0, A, A, style_default);
tim_label("+", A, A, A, A, style_default);
tim_label("-", 0, A, A, A, style_default);
tim_label("-", ~0, A, A, A, style_default);
tim_label("|", A, 0, A, A, style_default);
tim_label("|", A, ~0, A, A, style_default);
// some information
sprintf(buf, "screen: %dx%d", tim->w, tim->h);
label(buf, 2, 0, A, A, 0xf);
tim_label(buf, 2, 0, A, A, style_default);
sprintf(buf, "frame : [%c] %d", ": "[tim->frame & 1], tim->frame);
label(buf, 2, 1, A, A, 0xf);
tim_label(buf, 2, 1, A, A, style_default);
sprintf(buf, "key : [%d] %s", ke.key, ke.s + (ke.key < 32));
label(buf, 2, 2, A, A, 0xf);
tim_label(buf, 2, 2, A, A, style_default);
sprintf(buf, "mouse : [%d] %d:%d", me.key, me.x, me.y);
label(buf, 2, 3, A, A, 0xf);
tim_label(buf, 2, 3, A, A, style_default);
sprintf(buf, "input : %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx",
e->s[0], e->s[1], e->s[2], e->s[3], e->s[4], e->s[5], e->s[6], e->s[7]);
label(buf, 2, 4, A, A, 0xf);
tim_label(buf, 2, 4, A, A, style_default);
// replace unprintable characters with space
i32 s_len = tim_utf8_len(e->s);
for(i32 i = 0; i < s_len; i++){
i32 pos = tim_utf8_pos(e->s, i);
u8 uc = e->s[pos];
if(uc < (u8)' '){
e->s[pos] = ' ';
}
}
tim_label(e->s, 2, 5, A, A, style_default);
// lower right
render_us += tim->render_us;
sprintf(buf, "%d µs (Ø %d µs)", tim->render_us, render_us / MAX(tim->frame, 1));
label(buf, ~2, ~2, A, A, 0xf);
tim_label(buf, ~2, ~2, A, A, style_default);
sprintf(buf, "%d cells (%.0f %%)", tim->w * tim->h, 100.0 * tim->w * tim->h / TIM_MAX_CELLS);
label(buf, ~2, ~1, A, A, 0xf);
tim_label(buf, ~2, ~1, A, A, style_default);
sprintf(buf, "%d bytes (%.0f %%)", tim->buf_size, 100.0 * tim->buf_size / TIM_MAX_BUF);
label(buf, ~2, ~0, A, A, 0xf);
tim_label(buf, ~2, ~0, A, A, style_default);
// multi line label
label("multi\nliñe\nlabël", 24, 1, A, A, 0xf);
tim_label("multi\nliñe\nlabël", 24, 1, A, A, style_default);
// colors
tim_scope(1, 5, 16, 5) {
tim_frame(0, 0, ~0, ~0, 0xf);
label(" Red ", 1, 1, 7, A, 0x0900);
label(" ", 8, 1, 7, A, 0xc400);
label(" Green ", 1, 2, 7, A, 0x0a00);
label(" ", 8, 2, 7, A, 0x2e00);
label(" Blue ", 1, 3, 7, A, 0x0c00);
label(" ", 8, 3, 7, A, 0x1500);
tim_scope(1, 6, 16, 5) {
tim_frame(0, 0, ~0, ~0, style_default);
tim_label(" Red ", 1, 1, 7, A, (TimStyle){ .bg = 0x09 });
tim_label(" ", 8, 1, 7, A, (TimStyle){ .bg = 0xc4 });
tim_label(" Green ", 1, 2, 7, A, (TimStyle){ .bg = 0x0a });
tim_label(" ", 8, 2, 7, A, (TimStyle){ .bg = 0x2e });
tim_label(" Blue ", 1, 3, 7, A, (TimStyle){ .bg = 0x0c });
tim_label(" ", 8, 3, 7, A, (TimStyle){ .bg = 0x15 });
}
// button
static u64 bc = 0x100;
if (button("Click Me", 17, 5, 16, 5, bc)) {
bc = (bc + 0x100) & 0xff00;
static TimStyle style_button = { .bg = 0x01 };
if (tim_button("Click Me", 17, 6, 16, 5, style_button)) {
style_button.bg = (style_button.bg + 1) & 0xff;
}
// edit
static TimEditState ed1;
static TimEditState ed2;
TimEditState_init(&ed1, 32, "Edit 1");
TimEditState_init(&ed2, 32, "");
edit(&ed1, 1, 10, 32, 0xff00ff);
static TimStyle style_edit = { .brd = 0xff, .fg = 0xff };
tim_edit(&ed1, 1, 11, 32, style_edit);
sprintf(buf, "cursor: %d length: %d", ed1.cursor, ed1.length);
label(buf, 2, 13, A, A, 0xf);
edit(&ed2, 1, 14, 32, 0xff00ff);
label(ed2.s, 2, 17, A, A, 0xf);
tim_label(buf, 2, 12, A, A, style_default);
tim_edit(&ed2, 1, 15, 32, style_edit);
tim_label(ed2.s, 2, 18, A, A, style_default);
// checkbox
static TimStyle style_checkbox = { .brd = TimColor16_Cyan, .fg = TimColor16_White };
static i32 chk[2] = {-1, 1};
check("Check 1", &chk[0], 1, 18, A, 0xa000f);
check("Check 2", &chk[1], 14, 18, A, 0xa000f);
tim_checkbox("Check 1", &chk[0], 1, 19, A, style_checkbox);
tim_checkbox("Check 2", &chk[1], 14, 19, A, style_checkbox);
// radiobox
static i32 rad = 0;
radio("Radio 1", &rad, 1, 1, 19, A, 0xa000f);
radio("Radio 2", &rad, 2, 14, 19, A, 0xa000f);
radio("Radio 3", &rad, 3, 1, 20, A, 0xa000f);
radio("Radio 4", &rad, 4, 14, 20, A, 0xa000f);
tim_radiobutton("Radio 1", &rad, 1, 1, 20, A, style_checkbox);
tim_radiobutton("Radio 2", &rad, 2, 14, 20, A, style_checkbox);
tim_radiobutton("Radio 3", &rad, 3, 1, 21, A, style_checkbox);
tim_radiobutton("Radio 4", &rad, 4, 14, 21, A, style_checkbox);
// scope nesting
tim_scope(~1, 1, 20, 10) {
tim_scope(0, 0, 10, 5) {
tim_frame(0, 0, ~0, ~0, 0x9);
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Red });
}
tim_scope(~0, 0, 10, 5) {
tim_frame(0, 0, ~0, ~0, 0xa);
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Green });
}
tim_scope(~0, ~0, 10, 5) {
tim_frame(0, 0, ~0, ~0, 0xb);
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Yellow });
}
tim_scope(0, ~0, 10, 5) {
tim_frame(0, 0, ~0, ~0, 0xc);
tim_frame(0, 0, ~0, ~0, (TimStyle){ .brd = TimColor16_Blue });
}
}
// funny characters
static TimStyle style_funny = { .bg = TimColor16_White, .fg = TimColor16_Magenta };
tim_scope(~1, ~3, 11, 5) {
tim_frame(0, 0, ~0, ~0, 0xf);
label("123456789", 1, 1, 9, A, 0x0f05);
label("$£ह€𐍈6789", 1, 2, A, A, 0x0f05);
label("圍棋56789", 1, 3, A, A, 0x0f05);
tim_frame(0, 0, ~0, ~0, style_default);
tim_label("123456789", 1, 1, 9, A, style_funny);
tim_label("$£ह€𐍈6789", 1, 2, A, A, style_funny);
tim_label("圍棋56789", 1, 3, A, A, style_funny);
}
}
i32 main(void) {
char ed1_buf[32];
char ed2_buf[32];
TimEditState_construct(&ed1, ed1_buf, ARRAY_SIZE(ed1_buf), "Edit 1");
TimEditState_construct(&ed2, ed2_buf, ARRAY_SIZE(ed2_buf), NULL);
while (tim_run(1.5)) {
test_screen(&tim->event);
if (tim_is_key_press('q') || tim_is_key_press(TimKey_Escape)) {
break;
}
}
return 0;
}

View File

@@ -66,4 +66,5 @@ i32 main(i32 argc, char** argv) {
tim_reset_terminal();
fclose(f);
return 0;
}

19
tim.config Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# This is a dependency config.
# You can copy it to another cbuild project to add this lib as dependency.
DEP_WORKING_DIR="$DEPENDENCIES_DIR/tim"
if [[ "$TASK" = *_dbg ]]; then
dep_build_target="build_static_lib_dbg"
else
dep_build_target="build_static_lib"
fi
DEP_PRE_BUILD_COMMAND=""
DEP_BUILD_COMMAND="cbuild $dep_build_target"
DEP_POST_BUILD_COMMAND=""
DEP_CLEAN_COMMAND="cbuild clean"
DEP_DYNAMIC_OUT_FILES=""
DEP_STATIC_OUT_FILES="bin/tim.a"
DEP_OTHER_OUT_FILES=""
PRESERVE_OUT_DIRECTORY_STRUCTURE=false