Compare commits

8 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
9 changed files with 262 additions and 81 deletions

View File

@@ -66,7 +66,10 @@ extern "C" {
#pragma region types #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 { enum {
TimColor16_Black = 0x00, TimColor16_Black = 0x00,
TimColor16_DarkRed = 0x01, TimColor16_DarkRed = 0x01,
@@ -167,20 +170,30 @@ typedef struct TimEditState {
} TimEditState; } TimEditState;
typedef struct TimScrollItem { typedef struct TimPanelItem {
void* data; // 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* focus_target; // is assigned to tim->focus
i32 h; // height of the item to know where to draw next item void (*draw)(void* data, TimRect place, bool is_selected);
void (*draw)(bool is_selected, TimRect place, void* data); } TimPanelItem;
} TimScrollItem;
typedef struct TimScrollState { typedef struct TimPanel {
TimScrollItem* items; TimPanelItem* items; // array
u32 len; i32 len; // number of items
u32 cur; i32 cur; // index of current item
bool use_mouse_wheel; i32 spacing; // distance between items
} TimScrollState; 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 { typedef struct TimState {
i32 w; // screen width 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 // exit scope and pop stack
i32 tim_exit_scope(void); 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 endregion
@@ -294,6 +325,7 @@ bool tim_checkbox(cstr txt, i32* state, i32 x, i32 y, i32 w, TimStyle style);
// color: radio, background, text // color: radio, background, text
bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, TimStyle style); bool tim_radiobutton(cstr txt, i32* state, i32 v, i32 x, i32 y, i32 w, TimStyle style);
/// text edit - value in state /// text edit - value in state
/// @param e persistent edit state, use TimEditState_construct() to create new state /// @param e persistent edit state, use TimEditState_construct() to create new state
/// @param style frame, background, text /// @param style frame, background, text
@@ -305,16 +337,21 @@ TimKey tim_edit(TimEditState* e, i32 x, i32 y, i32 w, TimStyle style);
/// @param capacity buffer size in bytes /// @param capacity buffer size in bytes
/// @param initial_content may be NULL /// @param initial_content may be NULL
void TimEditState_construct(TimEditState* e, char* buffer, i32 capacity, cstr initial_content); void TimEditState_construct(TimEditState* e, char* buffer, i32 capacity, cstr initial_content);
void TimEditState_insert(TimEditState* e, cstr s); void TimEditState_insert(TimEditState* e, cstr s);
void TimEditState_delete(TimEditState* e); 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); /// Panel with sequence of items. You can select an item by arrow keys or mouse click.
void TimScrollState_selectPrev(TimScrollState* l); /// @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 #pragma endregion

View File

@@ -65,8 +65,9 @@ The layout automatically adopts to terminal window resize events.
## colors ## colors
Colors are stored as 8-bit values. Colors are stored as 8-bit values.
Most terminals support 16 basic colors. You can see them in TimColor16 enum. Most terminals support 16 ANSI colors. You can see them in TimColor16 enum.
There is also support for xterm-256 colors. 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) ![xterm-256 color chart](./256colors.jpg)
## events ## events
@@ -171,6 +172,8 @@ https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
https://learn.microsoft.com/en-us/windows/console/ https://learn.microsoft.com/en-us/windows/console/
## bugs ## 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 buffering is still new, set ENABLE_DBUF to 0 if you see glitches
- Double width characters like 彁 are not fully supported. Terminals do not - Double width characters like 彁 are not fully supported. Terminals do not
handle these consistently and there is no portable way to reliably 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) { 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; tim->cells[x + y * tim->w] = cell;
} }
} }
void tim_draw_row(TimCell cell, i32 x, i32 y, i32 w) { void tim_draw_row(TimCell cell, i32 x, i32 y, i32 w) {
if (y >= 0 && y < tim->h && w > 0) { TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
for (i32 i = MAX(x, 0); i < MIN(x + w, tim->w); i++) {
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; tim->cells[i + y * tim->w] = cell;
} }
} }
} }
void tim_draw_col(TimCell cell, i32 x, i32 y, i32 h) { void tim_draw_col(TimCell cell, i32 x, i32 y, i32 h) {
if (x >= 0 && x < tim->w && h > 0) { TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
for (i32 i = MAX(y, 0); i < MIN(y + h, tim->h); i++) {
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; tim->cells[x + i * tim->w] = cell;
} }
} }
} }
void tim_fill(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) { if (w > 0 && h > 0) {
for (i32 iy = MAX(y, 0); iy < MIN(y + h, tim->h); iy++) { for (i32 iy = MAX(y, scope.y); iy < MIN(y + h, scope.y + scope.h); iy++) {
for (i32 ix = MAX(x, 0); ix < MIN(x + w, tim->w); ix++) { for (i32 ix = MAX(x, scope.x); ix < MIN(x + w, scope.x + scope.w); ix++) {
tim->cells[ix + iy * tim->w] = cell; tim->cells[ix + iy * tim->w] = cell;
} }
} }
@@ -47,8 +57,10 @@ void tim_fill(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) { 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 ) { TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
i32 end = MIN(x + w, tim->w);
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; bool wide = false;
for (i32 i = 0; s[i] && x < end; x++) { for (i32 i = 0; s[i] && x < end; x++) {
TimCell c = tim_cell(&s[i], fg, bg); 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) { 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 , y);
tim_draw_chr(tim_cell("", fg, bg), x + w - 1, y); tim_draw_chr(tim_cell("", fg, bg), x + w - 1, y);
tim_draw_chr(tim_cell("", fg, bg), x , y + h - 1); tim_draw_chr(tim_cell("", fg, bg), x , y + h - 1);
@@ -75,8 +89,10 @@ void tim_draw_box(i32 x, i32 y, i32 w, i32 h, u8 fg, u8 bg) {
} }
void tim_draw_invert(i32 x, i32 y, i32 w) { void tim_draw_invert(i32 x, i32 y, i32 w) {
if (y >= 0 && y < tim->h && w > 0) { TimRect scope = tim_rect_fit(tim->scopes[tim->scope]);
for (i32 i = MAX(x, 0); i < MIN(x + w, tim->w); i++) {
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]; TimCell c = tim->cells[i + y * tim->w];
tim->cells[i + y * tim->w].fg = c.bg; tim->cells[i + y * tim->w].fg = c.bg;
tim->cells[i + y * tim->w].bg = c.fg; tim->cells[i + y * tim->w].bg = c.fg;

View File

@@ -7,10 +7,12 @@ TimState* tim = NULL;
static void tim_init(void){ static void tim_init(void){
tim = (TimState*)malloc(sizeof(TimState)); tim = (TimState*)malloc(sizeof(TimState));
memset(tim, 0, sizeof(TimState)); memset(tim, 0, sizeof(TimState));
size_t cdb_size = (TIM_MAX_CELLS << TIM_ENABLE_DBUF); size_t cdb_size = (TIM_MAX_CELLS << TIM_ENABLE_DBUF) * sizeof(TimCell);
tim->cells_double_buf = (TimCell*)malloc(cdb_size * 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->cells = tim->cells_double_buf;
tim->buf = (char*)malloc(TIM_MAX_BUF); tim->buf = (char*)malloc(TIM_MAX_BUF);
memset(tim->buf, 0, TIM_MAX_BUF);
} }
static void tim_deinit(void){ 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) { 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; return 0;
}
TimRect r = tim_scope_rect_to_absolute(x, y, w, h); TimRect r = tim_scope_rect_to_absolute(x, y, w, h);
tim->scope += 1; tim->scope += 1;
tim->scopes[tim->scope] = r; 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 tcsetattr(STDOUT_FILENO, TCSADRAIN, &tim->attr); // restore attributes
tim_write_str(S("\33[?1000l")); // disable mouse tim_write_str(S("\33[?1000l")); // disable mouse
tim_write_str(S("\33[?1002l")); // 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[m")); // reset colors
tim_write_str(S("\33[?25h")); // show cursor tim_write_str(S("\33[?25h")); // show cursor
tim_write_str(S("\33[?1049l")); // exit alternate buffer tim_write_str(S("\33[?1049l")); // exit alternate buffer