Compare commits

16 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
17 changed files with 500 additions and 93 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
}
}
]
}

View File

@@ -119,7 +119,7 @@ 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();

View File

@@ -66,7 +66,10 @@ extern "C" {
#pragma region types
/* first 16 colors from xterm256 supported by any terminal emulator */
// 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,
@@ -129,6 +132,8 @@ typedef enum TimEventType {
enum {
TimKey_MouseButtonLeft = 1,
TimKey_MouseScrollUp = 4,
TimKey_MouseScrollDown = 5,
TimKey_Backspace = 8,
TimKey_Tab = 9,
TimKey_Enter = 13,
@@ -155,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 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
@@ -239,13 +272,29 @@ 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, TimStyle style);
@@ -255,10 +304,14 @@ void tim_frame(i32 x, i32 y, i32 w, i32 h, TimStyle style);
// color: background, text
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 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
@@ -272,18 +325,34 @@ bool tim_checkbox(cstr txt, i32* state, i32 x, i32 y, i32 w, TimStyle style);
// color: radio, background, text
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
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);
/// 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
@@ -293,13 +362,21 @@ void TimEditState_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
@@ -320,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

@@ -60,6 +60,8 @@ case "$OS" in
;;
esac
TEST_C_ARGS="-O0 -g3"
# TASKS
case "$TASK" in
# ./ask question?
@@ -67,7 +69,7 @@ case "$TASK" in
EXEC_FILE="ask$EXEC_EXT"
SRC_C="example/ask.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
@@ -79,7 +81,7 @@ case "$TASK" in
EXEC_FILE="hello$EXEC_EXT"
SRC_C="example/hello.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
@@ -91,7 +93,7 @@ case "$TASK" in
EXEC_FILE="snek$EXEC_EXT"
SRC_C="example/snek.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
@@ -103,7 +105,7 @@ case "$TASK" in
EXEC_FILE="test$EXEC_EXT"
SRC_C="test/test.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
@@ -115,7 +117,7 @@ case "$TASK" in
EXEC_FILE="color$EXEC_EXT"
SRC_C="test/color.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""
@@ -127,7 +129,7 @@ case "$TASK" in
EXEC_FILE="string$EXEC_EXT"
SRC_C="test/string.c"
LINKER_LIBS="bin/tim.a"
C_ARGS="-O0 -g"
C_ARGS="$TEST_C_ARGS"
CPP_ARGS="$C_ARGS"
LINKER_ARGS="$CPP_ARGS $LINKER_LIBS"
PRE_TASK_SCRIPT=""

View File

@@ -65,8 +65,9 @@ The layout automatically adopts to terminal window resize events.
## colors
Colors are stored as 8-bit values.
Most terminals support 16 basic colors. You can see them in TimColor16 enum.
There is also support for xterm-256 colors.
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
@@ -171,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,13 +1,16 @@
#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);
memcpy(e->s, initial_content, byte_len);
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;
}
@@ -47,7 +50,8 @@ static TimKey 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:
@@ -90,13 +94,31 @@ TimKey tim_edit(TimEditState* e, i32 x, i32 y, i32 w, TimStyle style) {
if (tim->event.type == TimEvent_Draw) {
tim_draw_box(r.x, r.y, r.w, r.h, style.brd, style.bg);
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, style.fg, style.bg);
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, style.fg, 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) {
return 0;
}
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 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

@@ -14,7 +14,7 @@ void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
h = (h == A) ? t.lines : h;
TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
TimCell c = tim_cell(" ", style.fg, style.bg);
tim_draw_lot(c, r.x, r.y, r.w, r.h);
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);
@@ -23,14 +23,26 @@ void tim_label(cstr s, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
}
bool tim_button(cstr txt, i32 x, i32 y, i32 w, i32 h, TimStyle style) {
i32 tw = tim_utf8_len(txt);
w = (w == A) ? (tw + 4) : w;
h = (h == A) ? 3 : h;
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, style.brd, style.bg);
tim_draw_str(txt, r.x + (w - tw) / 2, r.y + h / 2, w, style.fg, 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 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);
}
@@ -67,5 +79,3 @@ bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, TimStyle
*state = click ? v : *state;
return click;
}

View File

@@ -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;
@@ -150,8 +157,8 @@ void tim_read_event(i32 timeout_ms) {
tim_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;
e->x = rec.Event.MouseEvent.dwMousePosition.X - tim->window.Left;
e->y = rec.Event.MouseEvent.dwMousePosition.Y - tim->window.Top;
return;
}

View File

@@ -38,6 +38,16 @@ static inline void test_screen(TimEvent* e) {
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]);
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;
@@ -52,7 +62,7 @@ static inline void test_screen(TimEvent* e) {
tim_label("multi\nliñe\nlabël", 24, 1, A, A, style_default);
// colors
tim_scope(1, 5, 16, 5) {
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 });
@@ -64,30 +74,30 @@ static inline void test_screen(TimEvent* e) {
// button
static TimStyle style_button = { .bg = 0x01 };
if (tim_button("Click Me", 17, 5, 16, 5, style_button)) {
if (tim_button("Click Me", 17, 6, 16, 5, style_button)) {
style_button.bg = (style_button.bg + 1) & 0xff;
}
// edit
static TimStyle style_edit = { .brd = 0xff, .fg = 0xff };
tim_edit(&ed1, 1, 10, 32, style_edit);
tim_edit(&ed1, 1, 11, 32, style_edit);
sprintf(buf, "cursor: %d length: %d", ed1.cursor, ed1.length);
tim_label(buf, 2, 13, A, A, style_default);
tim_edit(&ed2, 1, 14, 32, style_edit);
tim_label(ed2.s, 2, 17, A, A, style_default);
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};
tim_checkbox("Check 1", &chk[0], 1, 18, A, style_checkbox);
tim_checkbox("Check 2", &chk[1], 14, 18, A, style_checkbox);
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;
tim_radiobutton("Radio 1", &rad, 1, 1, 19, A, style_checkbox);
tim_radiobutton("Radio 2", &rad, 2, 14, 19, A, style_checkbox);
tim_radiobutton("Radio 3", &rad, 3, 1, 20, A, style_checkbox);
tim_radiobutton("Radio 4", &rad, 4, 14, 20, A, style_checkbox);
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) {
@@ -116,13 +126,17 @@ static inline void test_screen(TimEvent* e) {
}
i32 main(void) {
TimEditState_init(&ed1, 32, "Edit 1");
TimEditState_init(&ed2, 32, "");
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;
}