Compare commits

11 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
14 changed files with 295 additions and 109 deletions

19
.vscode/launch.json vendored
View File

@@ -5,30 +5,37 @@
"name": "(gdb) test | Build and debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/test",
"windows": { "program": "${workspaceFolder}/bin/test.exe" },
"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",
"miDebuggerPath": "gdb"
},
{
"name": "(gdb) test | Just debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/bin/test",
"windows": { "program": "${workspaceFolder}/bin/test.exe" },
"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",
"miDebuggerPath": "gdb"
}
]
}

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,
@@ -167,20 +170,30 @@ typedef struct TimEditState {
} TimEditState;
typedef struct TimScrollItem {
void* data;
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
i32 h; // height of the item to know where to draw next item
void (*draw)(bool is_selected, TimRect place, void* data);
} TimScrollItem;
void (*draw)(void* data, TimRect place, bool is_selected);
} TimPanelItem;
typedef struct TimScrollState {
TimScrollItem* items;
u32 len;
u32 cur;
bool use_mouse_wheel;
} TimScrollState;
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
@@ -259,6 +272,24 @@ 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
@@ -294,6 +325,7 @@ 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_construct() to create new state
/// @param style frame, background, text
@@ -301,24 +333,25 @@ bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, TimStyle
TimKey tim_edit(TimEditState* e, i32 x, i32 y, i32 w, TimStyle style);
/// @param e uninitialized state
/// @param capacity in bytes
/// @param buffer an array
/// @param capacity buffer size in bytes
/// @param initial_content may be NULL
void TimEditState_construct(TimEditState* e, i32 capacity, cstr initial_content);
static inline void TimEditState_destroy(TimEditState* e) {
if(!e) return;
free(e->s);
}
void TimEditState_construct(TimEditState* e, char* buffer, i32 capacity, cstr initial_content);
void TimEditState_insert(TimEditState* e, cstr s);
void TimEditState_delete(TimEditState* e);
/// @param l list of rows to display
/// @param style frame, background, text
TimScrollItem* tim_scroll(TimScrollState* l, i32 x, i32 y, i32 w, i32 h, TimStyle style);
void TimScrollState_selectNext(TimScrollState* l);
void TimScrollState_selectPrev(TimScrollState* l);
/// 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
@@ -364,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

@@ -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_construct(TimEditState* e, i32 capacity, cstr 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;
}

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;

View File

@@ -1,45 +0,0 @@
#include "tim.h"
void TimScrollState_selectNext(TimScrollState* l){
l->cur = (l->cur + 1) % l->len;
}
void TimScrollState_selectPrev(TimScrollState* l){
l->cur = (l->len + l->cur - 1) % l->len;
}
TimScrollItem* tim_scroll(TimScrollState* l, i32 x, i32 y, i32 w, i32 h, TimStyle style){
// select with buttons and mouse wheel
if(tim_is_key_press(TimKey_Down)
|| (l->use_mouse_wheel && tim_is_mouse_scroll_down()))
{
TimScrollState_selectNext(l);
}
if(tim_is_key_press(TimKey_Up)
|| (l->use_mouse_wheel && tim_is_mouse_scroll_up()))
{
TimScrollState_selectPrev(l);
}
tim->focus = l->items[l->cur].focus_target;
tim_frame(x, y, w, h, style);
TimRect absolute = tim_scope_rect_to_absolute(x, y, w, h);
TimRect place = { .x = x + 1, .y = y + 1, .w = absolute.w - 2, .h = absolute.h - 2 };
for(u32 i = 0; i < l->len; i++){
TimScrollItem* item = &l->items[i];
place.h = item->h;
// select with mouse click
if(tim_is_mouse_click_over(tim_scope_rect_to_absolute(place.x, place.y, place.w, place.h))){
l->cur = i;
tim->focus = item->focus_target;
}
item->draw(i == l->cur, place, item->data);
place.y += place.h;
}
return &l->items[l->cur];
}

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

@@ -65,6 +65,7 @@ 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

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);

View File

@@ -126,8 +126,10 @@ static inline void test_screen(TimEvent* e) {
}
i32 main(void) {
TimEditState_construct(&ed1, 32, "Edit 1");
TimEditState_construct(&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);
@@ -136,8 +138,5 @@ i32 main(void) {
}
}
TimEditState_destroy(&ed1);
TimEditState_destroy(&ed2);
return 0;
}