aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordam <dam@gudinoff>2023-08-17 20:28:47 +0100
committerdam <dam@gudinoff>2023-08-17 20:28:47 +0100
commit709879ee56d31fe543a0ad882713bd4e3d17d2d2 (patch)
tree12a35282bdd0f1f8a2159ade147944c89254db24
parentfa1b8ea54646f1a0f3eadef33e3a660b875cc1ff (diff)
downloadtask-time-tracker-709879ee56d31fe543a0ad882713bd4e3d17d2d2.tar.zst
task-time-tracker-709879ee56d31fe543a0ad882713bd4e3d17d2d2.zip
Added kscurses and testing program.
-rw-r--r--kscurses/canvas.jai172
-rw-r--r--kscurses/events.jai256
-rw-r--r--kscurses/history_stack.jai69
-rw-r--r--kscurses/init.jai125
-rw-r--r--kscurses/io.jai81
-rw-r--r--kscurses/lambdas.jai255
-rw-r--r--kscurses/modes.jai179
-rw-r--r--kscurses/module.jai91
-rw-r--r--kscurses/print.jai148
-rw-r--r--kscurses/queue.jai57
-rw-r--r--kscurses/readme.md43
-rw-r--r--kscurses/ui/button.jai25
-rw-r--r--kscurses/ui/element.jai156
-rw-r--r--kscurses/ui/group.jai44
-rw-r--r--kscurses/ui/line_input.jai122
-rw-r--r--kscurses/ui/links.jai94
-rw-r--r--kscurses/ui/master.jai105
-rw-r--r--kscurses/ui/parent.jai33
-rw-r--r--kscurses/ui/popup_manager.jai65
-rw-r--r--kscurses/ui/progress_bar.jai37
-rw-r--r--kscurses/ui/scalable_group.jai88
-rw-r--r--kscurses/ui/scene_manager.jai48
-rw-r--r--kscurses/ui/select_list.jai79
-rw-r--r--kscurses/ui/style.jai118
-rw-r--r--kscurses/ui/table.jai87
-rw-r--r--kscurses/ui/text_buf.jai15
-rw-r--r--kscurses/ui/tilemap.jai20
-rw-r--r--kscurses/utils.jai53
-rw-r--r--kscurses/vectors.jai210
-rw-r--r--test.jai45
30 files changed, 2920 insertions, 0 deletions
diff --git a/kscurses/canvas.jai b/kscurses/canvas.jai
new file mode 100644
index 0000000..df0b021
--- /dev/null
+++ b/kscurses/canvas.jai
@@ -0,0 +1,172 @@
+Canvas :: struct {
+ zone : Ibox2;
+
+ count : int;
+ pixels_last_draw : []Char;
+ pixels_buf : []Char;
+ links : []Link;
+
+ diff_count := 0;
+ force_full_refresh := true;
+
+ Link :: struct { prev, next : s32; }
+}
+
+deinit :: (using canvas : *Canvas) {
+ array_free(pixels_last_draw);
+ array_free(pixels_buf); pixels_buf = .[];
+ array_free(links);
+}
+resize_clear :: (using canvas : *Canvas, new_zone : Ibox2, filler := Char.{}) {
+ if new_zone != zone {
+ array_free(pixels_last_draw);
+ array_free(pixels_buf);
+ array_free(links);
+
+ zone = new_zone;
+ count = zone.width * zone.height;
+
+ pixels_last_draw = NewArray(count, Char);
+ pixels_buf = NewArray(count, Char);
+ links = NewArray(count + 1, Link);
+ links[count] = .{xx count, xx count};
+
+ if filler != .{} then {
+ for * pixels_buf {
+ <<it = filler;
+ }
+ }
+ links[count] = .{-1, -1};
+
+ force_full_refresh = true;
+ } else {
+ for y : 0..zone.height-1 {
+ for x : 0..zone.width-1 {
+ c_putchar(canvas, filler, .{xx x, xx y});
+ }
+ }
+ }
+}
+// add *void to fill_function
+resize_fill :: (using canvas : *Canvas, new_zone : Ibox2, fill_function : (coord : ivec2, zone : Ibox2) -> Char) {
+ if zone != new_zone {
+ array_free(pixels_last_draw);
+ array_free(pixels_buf);
+ array_free(links);
+
+ zone = new_zone;
+ count = zone.width * zone.height;
+
+ pixels_last_draw = NewArray(count, Char);
+ pixels_buf = NewArray(count, Char);
+ links = NewArray(count + 1, Link);
+ links[count] = .{xx count, xx count};
+ force_full_refresh = true;
+ }
+
+ c_fill(canvas, fill_function);
+ // i := 0;
+ // for y : 0..zone.height-1 {
+ // for x : 0..zone.width-1 {
+ // pixels_buf[i] = fill_function(.{x, y}, zone);
+ // i += 1;
+ // diff_count += 1;
+ // }
+ // }
+
+ // for * links {
+ // <<it = .{-1, -1};
+ // }
+}
+b_draw_canvas :: (builder : *String_Builder, using canvas : *Canvas) {
+ if force_full_refresh || diff_count * 4 > zone.width * zone.height {
+ i := 0;
+ for y : 0..zone.height-1 {
+ b_move_cursor(builder, zone.corner + ivec2.{0, y});
+ for x : 0..zone.width-1 {
+ b_putchar(builder, pixels_buf[i]);
+ pixels_last_draw[i] = pixels_buf[i];
+ links[i] = .{-1, -1};
+ i += 1;
+ }
+ }
+ } else {
+ last_pos := ivec2.{-1, -1};
+ current := links[count].next;
+ I := 0;
+
+ while current != count {
+ assert(I < count);
+ assert(current >= 0);
+
+ pos := ivec2.{xx(current % zone.width), xx(current / zone.width)} + zone.corner;
+ if pos.x != last_pos.x + 1 || pos.y != last_pos.y {
+ b_move_cursor(builder, pos);
+ }
+ b_putchar(builder, pixels_buf[current]);
+ pixels_last_draw[current] = pixels_buf[current];
+
+ next := links[current].next;
+ links[current] = .{-1, -1};
+ current = next;
+ last_pos = pos;
+ I += 1;
+ }
+ }
+
+ diff_count = 0;
+ force_full_refresh = false;
+ links[count] = .{xx count, xx count};
+}
+c_putchar :: (using canvas : *Canvas, pixel : Char, pos_local : ivec2) {
+ // pos_local := pos - zone.corner;
+ if !point_inside(pos_local, .{size = zone.size}) return;
+ current := pos_local.x + pos_local.y * zone.width;
+
+ c_last_draw := pixels_last_draw[current];
+ pixels_buf[current] = pixel;
+
+ if links[current].prev == -1 {
+ assert(links[current].next == -1);
+ if c_last_draw != pixel {
+ links[current] = .{links[count].prev, xx count};
+ links[links[count].prev].next = current;
+ links[count].prev = current;
+ diff_count += 1;
+ }
+ } else {
+ if c_last_draw == pixel {
+ nbs := links[current];
+ assert(links[nbs.prev].next == current && links[nbs.next].prev == current);
+ links[nbs.prev].next = nbs.next;
+ links[nbs.next].prev = nbs.prev;
+ links[current] = .{-1, -1};
+ diff_count -= 1;
+ }
+ }
+}
+
+c_fill :: (using canvas : *Canvas, fill_function : (coord : ivec2, zone : Ibox2) -> Char) {
+ for y : 0..zone.height-1 {
+ for x : 0..zone.width-1 {
+ char := fill_function(.{x, y}, zone);
+ c_putchar(canvas, char, .{x, y});
+ }
+ }
+ // i := 0;
+ // for y : 0..zone.height-1 {
+ // for x : 0..zone.width-1 {
+ // pixels_buf[i] = fill_function(.{x, y}, zone);
+ // i += 1;
+ // diff_count += 1;
+ // }
+ // }
+ // refresh_all = true;
+}
+ks_draw_canvas :: (canvas : *Canvas) {
+ builder := String_Builder.{allocator = temp};
+ b_draw_canvas(*builder, canvas);
+ ks_write(builder_to_string(*builder, allocator = temp));
+}
+
+
diff --git a/kscurses/events.jai b/kscurses/events.jai
new file mode 100644
index 0000000..603958e
--- /dev/null
+++ b/kscurses/events.jai
@@ -0,0 +1,256 @@
+Event :: struct {
+ type : enum u8 {
+ NONE :: 0;
+ KEY :: 1;
+ WINCH :: 2;
+ TICK :: 3;
+ INIT :: 4;
+ USER :: 5;
+ };
+ union {
+ key : Key;
+ data : *void;
+ }
+}
+
+__event_handler : struct {
+ proc := (e : Event, __data : *void) {
+ assert(__data != null);
+ if e.type == {
+ case .NONE;
+ ks_write("empty_event!\n\r");
+ case .KEY;
+ ks_write(tprint("key = %\n\r", e.key));
+ case .WINCH;
+ ks_write(tprint("winch, size = %\n\r", terminal_state.size));
+ case .TICK;
+ ks_write(tprint("tick\n\r"));
+ }
+ if e.type == .KEY && e.key == .ESCAPE {
+ <<cast(*bool)__data = true; // stop_main = true
+ }
+ };
+ data : *void;
+}
+
+use_events :: (tick_duration_ms :s32= 1000, $use_clock := true, init_event := true) #expand {
+ init_mutexes();
+ #if use_clock clock_thread := start_clock_cycle(tick_duration_ms);
+ input_thread := start_input_cycle();
+ winlooker_thread := start_winlooker_cycle();
+ set_handler();
+ if init_event push_event(.{type = .INIT});
+ `defer {
+ restore_handler();
+ stop_winlooker_cycle(winlooker_thread);
+ stop_input_cycle(input_thread);
+ #if use_clock stop_clock_cycle(clock_thread);
+ deinit_mutexes();
+ reset_globals();
+ }
+}
+wait_and_process_events :: () -> bool {
+ wait_for(*event_wait_sem);
+ lock(*event_queue_mtx); defer unlock(*event_queue_mtx);
+ processed := false;
+ while 1 {
+ e, ok := pop(*event_queue);
+ if !ok break;
+ processed = true;
+ assert(e.type != .NONE);
+ __event_handler.proc(e, __event_handler.data);
+ }
+ return processed;
+}
+pop_events :: () -> bool {
+}
+push_event :: (e : Event) {
+ lock(*event_queue_mtx);
+ push(*event_queue, e);
+ unlock(*event_queue_mtx);
+ signal(*event_wait_sem);
+}
+
+restart_clock_cycle :: () {
+ if clock_state != .DISABLED {
+ lock(*mtx_clock_state);
+ if clock_state != .STOP {
+ clock_state = .RESTART;
+ }
+ signal(*sem_clock_breaker);
+ unlock(*mtx_clock_state);
+ }
+}
+
+#scope_file
+// SIGWINCH --sigaction--> handler ---sem--> winlooker --push_event--|--> event_queue --pop_events--> master thread
+// stdin --read--> input --push_event--|
+// clock --push_event--|
+
+// event_queue
+event_queue_mtx : Mutex;
+event_queue : Queue(Event);
+event_wait_sem : Semaphore;
+
+// handler
+handler :: (sig : s32) #c_call {
+ new_context : Context;
+ push_context new_context {
+ if sig == SIGWINCH {
+ signal(*winch_wait_sem);
+ } else {
+ log("signal = %\n\r", sig);
+ }
+ }
+}
+set_handler :: () {
+ sa : sigaction_t;
+ sa.sa_handler = handler;
+ sigemptyset(*(sa.sa_mask));
+ sa.sa_flags = SA_RESTART;
+ sigaction(SIGWINCH, *sa, null);
+}
+restore_handler :: () {
+ sa : sigaction_t;
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGWINCH, null, *sa);
+}
+
+// winlooker
+winch_wait_sem : Semaphore;
+stop_winlooker := false;
+winlooker_cycle :: (thread : *Thread) -> s64 {
+ while !stop_winlooker {
+ wait_for(*winch_wait_sem);
+ if stop_winlooker break;
+ update_terminal_size();
+ push_event(.{type = .WINCH});
+ }
+ return 0;
+}
+start_winlooker_cycle :: () -> *Thread #expand {
+ init(*winch_wait_sem);
+ winlooker_thread : Thread;
+ thread_init(*winlooker_thread, winlooker_cycle);
+ thread_start(*winlooker_thread);
+ return *winlooker_thread;
+}
+stop_winlooker_cycle :: (winlooker_thread : *Thread) {
+ stop_winlooker = true;
+ signal(*winch_wait_sem);
+ thread_deinit(winlooker_thread);
+ destroy(*winch_wait_sem);
+}
+
+// input
+stop_input := false;
+input_cycle :: (thread : *Thread) -> s64 {
+ tcflush(STDIN_FILENO, TCIFLUSH);
+ while !stop_input {
+ key := ks_getch();
+ if stop_input break;
+ push_event(.{type = .KEY, key = key});
+ }
+ return 0;
+}
+start_input_cycle :: () -> *Thread #expand {
+ input_thread : Thread;
+ thread_init(*input_thread, input_cycle);
+ log("input = %\n\r", formatInt(input_thread.thread_handle, base = 16));
+ thread_start(*input_thread);
+ return *input_thread;
+}
+stop_input_cycle :: (input_thread : *Thread) {
+ stop_input = true;
+ pthread_cancel(input_thread.thread_handle);
+ thread_deinit(input_thread);
+}
+
+// clock
+clock_state : enum u8 {
+ NORMAL :: 0;
+ STOP :: 1;
+ RESTART :: 2;
+ DISABLED:: 3;
+} = .DISABLED;
+sem_clock_breaker : Semaphore;
+mtx_clock_state : Mutex;
+
+clock_cycle :: (thread : *Thread) -> s64 {
+ tick_duration_ms := <<cast(*s32)thread.data;
+
+ while clock_state != .STOP {
+ lock(*mtx_clock_state);
+ destroy(*sem_clock_breaker);
+ init(*sem_clock_breaker);
+ clock_state = .NORMAL;
+ unlock(*mtx_clock_state);
+
+ while 1 {
+ r := wait_for(*sem_clock_breaker, tick_duration_ms);
+
+ lock(*mtx_clock_state); defer unlock(*mtx_clock_state);
+ if clock_state != .NORMAL {
+ assert(r == .SUCCESS);
+ break;
+ } else {
+ push_event(.{type = .TICK});
+ }
+ }
+ }
+ return 0;
+}
+start_clock_cycle :: (tick_duration_ms : s32) -> *Thread #expand {
+ clock_thread : Thread;
+ init(*sem_clock_breaker);
+ init(*mtx_clock_state);
+ clock_state = .NORMAL;
+
+ thread_init(*clock_thread, clock_cycle);
+
+ tick_duration_ms_ptr := New(s32);
+ <<tick_duration_ms_ptr = tick_duration_ms;
+
+ clock_thread.data = tick_duration_ms_ptr;
+ log("clock = %\n\r", formatInt(clock_thread.thread_handle, base = 16));
+ thread_start(*clock_thread);
+ return *clock_thread;
+}
+stop_clock_cycle :: (clock_thread : *Thread) {
+ lock(*mtx_clock_state);
+ clock_state = .STOP;
+ signal(*sem_clock_breaker);
+ unlock(*mtx_clock_state);
+
+ thread_deinit(clock_thread);
+ free(clock_thread.data);
+ clock_state = .DISABLED;
+ destroy(*sem_clock_breaker);
+ destroy(*mtx_clock_state);
+}
+
+// utils
+reset_globals :: () {
+ stop_input = false;
+ clock_state = .NORMAL;
+ // stop_clock = false;
+ stop_winlooker = false;
+ // fd_winch = .[0, 0];
+ deinit(*event_queue);
+}
+init_mutexes :: () {
+ init(*event_wait_sem);
+ init(*event_queue_mtx);
+}
+deinit_mutexes :: () {
+ destroy(*event_queue_mtx);
+ destroy(*event_wait_sem);
+}
+
+
+TCIFLUSH :: 0;
+
+libc :: #system_library "libc";
+tcflush :: (fd : s32, queue_selector : s32) -> s32 #foreign libc;
+
+#import "Thread"; \ No newline at end of file
diff --git a/kscurses/history_stack.jai b/kscurses/history_stack.jai
new file mode 100644
index 0000000..3f73df4
--- /dev/null
+++ b/kscurses/history_stack.jai
@@ -0,0 +1,69 @@
+History_Stack :: struct(
+ Action_Type : Type,
+ destructor : (obj : Action_Type) = default_destructor
+){
+ BUF_SIZE :: 100;
+ buffer : [BUF_SIZE]Action_Type;
+ offset, current, saved : int;
+}
+
+deinit :: (using history_stack : *History_Stack) {
+ guard(history);
+
+ for i : 0..saved-1 {
+ j := (i + offset) % BUF_SIZE;
+ destructor(buffer[offset]);
+ }
+ offset, current, saved = 0;
+}
+
+write :: (using history : *History_Stack, action : Edit_Action) {
+ guard(history);
+
+ for i : current..saved-1 {
+ j := (i + offset) % BUF_SIZE;
+ destructor(buffer[j]);
+ }
+
+ i0 := (offset + current) % BUF_SIZE;
+ if current == BUF_SIZE {
+ destructor(buffer[offset]);
+ saved = current;
+ offset = (offset + 1) % BUF_SIZE;
+ } else {
+ current += 1;
+ saved = current;
+ }
+ i1 := (offset + current) % BUF_SIZE;
+ assert(i0 == i1);
+ buffer[i0] = action;
+}
+
+
+undo :: (using history : *History) -> bool, Action_Type {
+ guard(history);
+ action : Action_Type = ---;
+ if current == 0 return false, action;
+ current -= 1;
+ action = buffer[(current + offset) % BUF_SIZE];
+ return true, action;
+}
+redo :: (using history : *History) -> bool, Action_Type {
+ guard(history);
+ action : Action_Type = ---;
+ if current == saved return false, action;
+ action = buffer[(current + offset) % BUF_SIZE];
+ current += 1;
+ return true, action;
+}
+
+#scope_file
+default_destructor :: (obj : $T) { }
+
+guard :: (using history : *History) #expand {
+ check :: (using history : *History) {
+ assert(0 <= current && current <= saved && saved <= BUF_SIZE);
+ assert(0 <= offset && offset <= BUF_SIZE);
+ }
+ check(history); defer check(history);
+} \ No newline at end of file
diff --git a/kscurses/init.jai b/kscurses/init.jai
new file mode 100644
index 0000000..b67c52f
--- /dev/null
+++ b/kscurses/init.jai
@@ -0,0 +1,125 @@
+terminal_state : struct {
+ size : ivec2;
+ cursor := ivec2.{-1, -1}; // {-1, -1} if cursor hidden
+ last_mode : Graphics_Mode;
+}
+update_terminal_size :: () {
+ TIOCGWINSZ :: 0x5413;
+ winsize : struct {
+ ws_row, ws_col, ws_xpixel, ws_ypixel : u16;
+ }
+ ioctl(0, TIOCGWINSZ, *winsize);
+ terminal_state.size = .{xx winsize.ws_col, xx winsize.ws_row};
+}
+
+ks_init :: () {
+ #if OS == .LINUX {
+ __old_logger = context.logger;
+ context.logger = file_logger;
+
+ ks_write("\e[?25l"); // hide cursor
+ ks_write("\e7"); // save cursor position
+ ks_write("\e[?1047h"); // switch screen
+ ks_write("\e[?30l"); // hide scrollbar
+
+ ks_write("\e[H"); // move to top left corner
+ ks_write("\e[0m"); // set default mode
+ ks_write("\e[2J"); // clear screen
+ {
+ tcgetattr(STDIN_FILENO, *__term);
+ term_new := __term;
+
+ term_new.c_iflag &= 0xFFFFFA14;// ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
+ term_new.c_oflag &= 0xFFFFFFFE;// ~OPOST;
+ term_new.c_lflag &= 0xFFFF7FB4;// ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+ term_new.c_cflag &= 0xFFFFFECF;// ~(CSIZE | PARENB);
+ term_new.c_cflag |= 0x00000030;
+
+ tcsetattr(STDIN_FILENO, 0, *term_new);
+ }
+ update_terminal_size();
+ } else {
+ assert(false, "procedure call on unsupported OS\n");
+ }
+}
+ks_terminate :: () {
+ #if OS == .LINUX {
+ tcsetattr(STDIN_FILENO, 0, *__term); // return echo
+
+ ks_write("\e[?47l"); // restore screen
+ ks_write("\e8"); // restore cursor
+ ks_write("\e[?25h"); // show cursor
+ ks_write("\e[?30h"); // show scrollbar
+ terminal_state = .{};
+
+ context.logger = __old_logger;
+ } else {
+ assert(false, "procedure call on unsupported OS\n");
+ }
+}
+use_ks_curses :: () #expand {
+ ks_init();
+ `defer {
+ ks_terminate();
+ }
+}
+use_default_winch_handler :: () #expand {
+ init_default_winch_handler();
+ `defer deinit_default_winch_handler();
+}
+init_default_winch_handler :: () {
+ #if OS == .LINUX {
+ act := sigaction_t.{
+ sa_handler = (sig : s32) #c_call {
+ new_context: Context;
+ push_context new_context {
+ update_terminal_size();
+ }
+ },
+ sa_mask = sigset_t.{__val[0] = SIGWINCH}
+ };
+ sigaction(SIGWINCH, *act, null);
+ } else {
+ assert(false, "procedure call on unsupported OS\n");
+ }
+}
+deinit_default_winch_handler :: () {
+ #if OS == .LINUX {
+ sa : sigaction_t;
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGWINCH, null, *sa);
+ } else {
+
+ }
+}
+
+#scope_file
+__old_logger : type_of(context.logger);
+
+#if OS == .LINUX {
+ __term : My_Termios;
+
+ My_Termios :: struct {
+ c_iflag : u32;
+ c_oflag : u32;
+ c_cflag : u32;
+ c_lflag : u32;
+ unknown_pad : u8;
+ c_cc : [32]u8;
+ c_ispeed : u32;
+ c_ospeed : u32;
+ }
+
+ libc :: #system_library "libc";
+ tcsetattr :: (fd : s32, optional_actions : s32, termios_p : *My_Termios) -> s32 #foreign libc;
+ tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc;
+}
+
+#import "File";
+file_logger :: (message: string, data: *void, info: Log_Info) {
+ file, ok := file_open("log.txt",for_writing=true, keep_existing_content=true);
+ if !ok return;
+
+ file_write(*file, tprint("[%] %", calendar_to_string(to_calendar(current_time_consensus())), message));
+ file_close(*file);
+}
diff --git a/kscurses/io.jai b/kscurses/io.jai
new file mode 100644
index 0000000..b2c2df9
--- /dev/null
+++ b/kscurses/io.jai
@@ -0,0 +1,81 @@
+Key :: enum u64 {
+ READ_ERROR :: 0xffffffff_ffffffff;
+
+ UP :: 0x41_5b1b;
+ DOWN :: 0x42_5b1b;
+ RIGHT :: 0x43_5b1b;
+ LEFT :: 0x44_5b1b;
+
+ CTRL_UP :: 0x4135_3b315b1b;
+ CTRL_DOWN :: 0x4235_3b315b1b;
+ CTRL_RIGHT :: 0x4335_3b315b1b;
+ CTRL_LEFT :: 0x4435_3b315b1b;
+
+ SHIFT_UP :: 0x4132_3b315b1b;
+ SHIFT_DOWN :: 0x4232_3b315b1b;
+ SHIFT_RIGHT :: 0x4332_3b315b1b;
+ SHIFT_LEFT :: 0x4432_3b315b1b;
+
+ CTRL_SHIFT_UP :: 0x4136_3b315b1b;
+ CTRL_SHIFT_DOWN :: 0x4236_3b315b1b;
+ CTRL_SHIFT_RIGHT:: 0x4336_3b315b1b;
+ CTRL_SHIFT_LEFT :: 0x4436_3b315b1b;
+
+ ALT_UP :: 0x4133_3b315b1b;
+ ALT_DOWN :: 0x4233_3b315b1b;
+ ALT_RIGHT :: 0x4333_3b315b1b;
+ ALT_LEFT :: 0x4433_3b315b1b;
+
+ CTRL_C :: 0x03;
+ CTRL_V :: 0x16;
+ CTRL_X :: 0x18;
+ CTRL_Y :: 0x19;
+ CTRL_Z :: 0x1A;
+ CTRL_BACKSLASH :: 0x1C;
+
+ // ALT_SHIFT_UP :: 0x4133_3b315b1b;
+ // ALT_SHIFT_DOWN :: 0x4233_3b315b1b;
+ // ALT_SHIFT_RIGHT :: 0x4333_3b315b1b;
+ // ALT_SHIFT_LEFT :: 0x4433_3b315b1b;
+
+ ENTER :: 0x0D;
+ ESCAPE :: 0x1B;
+ BACKSPACE :: 0x7F;
+ DELETE :: 0x7E335B1B;
+}
+ks_getch :: (block := true) -> Key {
+ #if OS == .LINUX {
+ buf : Key = xx 0;
+ l := read(STDIN_FILENO, (cast(*u8)*buf), 8); //!!!
+ check_signal :: inline (key : Key) {
+ #if ENABLE_SIGINT if key == .CTRL_C raise(SIGINT); //!!!
+ #if ENABLE_SIGQUIT if key == .CTRL_BACKSLASH raise(SIGQUIT);
+ }
+ check_signal(buf);
+ return ifx l <= 0 then Key.READ_ERROR else buf;
+ } else {
+ return .READ_ERROR;
+ }
+}
+ks_write :: (str : string) {
+ #if OS == .LINUX {
+ printed := 0;
+ // while 1 {
+ r := write(STDIN_FILENO, str.data + printed, xx (str.count - printed));
+ // if printed + r == str.count {
+ // break;
+ // } else if r >= 0 {
+ // printed += r;
+ // }
+ // }
+ } else {
+
+ }
+ __write_counter += str.count;
+}
+__write_counter := 0;
+write_counter_delta :: () -> int {
+ result := __write_counter;
+ __write_counter = 0;
+ return result;
+} \ No newline at end of file
diff --git a/kscurses/lambdas.jai b/kscurses/lambdas.jai
new file mode 100644
index 0000000..fd5ffa4
--- /dev/null
+++ b/kscurses/lambdas.jai
@@ -0,0 +1,255 @@
+decl_lambda :: ($code_src : Code) -> Code #expand {
+ node_src := compiler_get_nodes(code_src);
+ proc_node, data_node, names, names_field, node_header, node_block := split_src(false, node_src);
+ pair_names, pair_srcs := get_lambda_pairs(proc_node, data_node, names, names_field, node_header, node_block);
+ return compiler_get_code(*Code_Compound_Declaration.{
+ kind = .COMPOUND_DECLARATION,
+ comma_separated_assignment = xx pair_names,
+ declaration_properties = *Code_Declaration.{
+ kind = .DECLARATION,
+ expression = pair_srcs
+ }
+ });
+}
+assign_lambda :: ($code_src : Code, parent_scope := #caller_code) #expand {
+ #insert,scope(parent_scope) #run _assign_lambda(code_src);
+}
+struct_lambda :: ($code_src : Code, parent_scope := #caller_code) #expand {
+ #insert,scope(parent_scope) #run _struct_lambda(code_src);
+}
+#scope_file
+#import "Compiler";
+#import "Basic";
+#import "Program_Print";
+debug_print_code :: (root : *Code_Node) {
+ builder := String_Builder.{allocator = temp};
+ print_expression(*builder, root);
+ print("%\n", builder_to_string(*builder, allocator = temp));
+}
+_assign_lambda :: ($code_src : Code) -> Code {
+ node_src := compiler_get_nodes(code_src);
+ proc_node, data_node, names, names_field, node_header, node_block := split_src(false, node_src);
+ pair_names, pair_srcs := get_lambda_pairs(proc_node, data_node, names, names_field, node_header, node_block);
+ node_assign := Code_Binary_Operator.{
+ kind = .BINARY_OPERATOR,
+ operator_type = #char"=",
+ left = pair_names,
+ right = pair_srcs
+ };
+ return compiler_get_code(*node_assign);
+}
+_struct_lambda :: ($code_src : Code) -> Code {
+ node_src := compiler_get_nodes(code_src);
+ proc_node, data_node, names, names_field, node_header, node_block := split_src(true, node_src);
+ pair_names, pair_srcs := get_lambda_pairs(proc_node, data_node, names, names_field, node_header, node_block);
+ node_assign := Code_Binary_Operator.{
+ kind = .BINARY_OPERATOR,
+ operator_type = #char"=",
+ left = pair_names,
+ right = pair_srcs
+ };
+ debug_print_code(*node_assign);
+ return compiler_get_code(*node_assign);
+}
+//TODO add support for non-"proc/data" names
+split_src :: (gen_pair : bool, node_src : *Code_Node) -> proc_node:*Code_Node, data_node:*Code_Node, names:[]string, names_field:[]string, node_header:*Code_Procedure_Header, node_block:*Code_Block #expand {
+ offset := ifx gen_pair then 1 else 2;
+ assert(node_src.kind == .BLOCK);
+ node_src_block := cast(*Code_Block) node_src;
+ node_src_statements := node_src_block.statements;
+ captured_count := node_src_statements.count - 2 - offset;
+ assert(captured_count >= 0);
+ names := NewArray(captured_count, string);
+ names_field := NewArray(captured_count, string);
+ for i : offset..captured_count-1+offset {
+ assert(node_src_statements[i].kind == .IDENT);
+ }
+ for i : 0..captured_count-1 {
+ name := (cast(*Code_Ident)node_src_statements[i + offset]).name;
+ names[i], names_field[i] = name, sprint("_%", name);
+ }
+ assert(node_src_statements[captured_count + offset].kind == .PROCEDURE_HEADER);
+ node_header := cast(*Code_Procedure_Header) node_src_statements[captured_count + offset];
+ assert(node_src_statements[captured_count + offset + 1].kind == .BLOCK);
+ node_block := cast(*Code_Block) node_src_statements[captured_count + offset + 1];
+ assert(node_block.block_type == .IMPERATIVE);
+ proc_node, data_node : *Code_Node;
+ if gen_pair {
+ proc_node = *Code_Binary_Operator.{
+ kind = .BINARY_OPERATOR,
+ operator_type = #char".",
+ left = node_src_statements[0],
+ right = *Code_Ident.{
+ kind = .IDENT,
+ name = "proc"
+ }
+ };
+ data_node = *Code_Binary_Operator.{
+ kind = .BINARY_OPERATOR,
+ operator_type = #char".",
+ left = node_src_statements[0],
+ right = *Code_Ident.{
+ kind = .IDENT,
+ name = "data"
+ }
+ };
+ } else {
+ proc_node, data_node = node_src_statements[0], node_src_statements[1];
+ }
+ return proc_node, data_node, names, names_field, node_header, node_block;
+}
+gen_struct_type_node :: (names : []string, names_field : []string) -> *Code_Node #expand {
+ captured_count := names.count;
+ nodes_ident := NewArray(captured_count, Code_Ident);
+ nodes_ptr := NewArray(captured_count, Code_Unary_Operator);
+ nodes_typeof := NewArray(captured_count, Code_Size_Or_Type_Info);
+ nodes_type := NewArray(captured_count, Code_Type_Instantiation);
+ nodes_declaration := NewArray(captured_count, Code_Declaration);
+ nodes_declaration_ptr := NewArray(captured_count, *Code_Node);
+ for i : 0..captured_count-1 {
+ nodes_ident[i] = .{
+ kind = .IDENT,
+ name = names[i]
+ };
+ nodes_ptr[i] = .{
+ kind = .UNARY_OPERATOR,
+ operator_type = #char"*",
+ subexpression = *(nodes_ident[i])
+ };
+ nodes_typeof[i] = .{
+ kind = .SIZE_OR_TYPE_INFO,
+ query_kind = .TYPE_OF,
+ type_of_expression = *(nodes_ptr[i])
+ };
+ nodes_type[i] = .{
+ kind = .TYPE_INSTANTIATION,
+ type_valued_expression = *(nodes_typeof[i])
+ };
+ nodes_declaration[i] = .{
+ kind = .DECLARATION,
+ name = names_field[i],
+ type_inst = *(nodes_type[i])
+ };
+ nodes_declaration_ptr[i] = *(nodes_declaration[i]);
+ }
+ node_struct := Code_Struct.{
+ kind = .STRUCT,
+ block = *Code_Block.{
+ kind = .BLOCK,
+ block_type = .DATA_DECLARATIONS,
+ statements = nodes_declaration_ptr
+ }
+ };
+ return *node_struct;
+}
+//TODO better assertions
+get_lambda_pairs :: (proc_node:*Code_Node, data_node:*Code_Node, names:[]string, names_field:[]string, node_header:*Code_Procedure_Header, node_block:*Code_Block) -> pair_names:*Code_Node, pair_srcs:*Code_Node #expand {
+ node_0 := gen_struct_type_node(names, names_field);
+ stat_count := node_block.statements.count;
+ nodes_new_statements := NewArray(stat_count + 1, *Code_Node);
+ for i : 0..stat_count-1 {
+ nodes_new_statements[i + 1] = node_block.statements[i];
+ }
+ nodes_new_statements[0] = *Code_Using.{
+ kind = .USING,
+ expression = *Code_Cast.{
+ kind = .CAST,
+ target_type = *Code_Type_Instantiation.{
+ kind = .TYPE_INSTANTIATION,
+ type_valued_expression = *Code_Unary_Operator.{
+ kind = .UNARY_OPERATOR,
+ subexpression = node_0,
+ operator_type = #char"*"
+ }
+ },
+ expression = *Code_Ident.{
+ kind = .IDENT,
+ name = "__data"
+ }
+ }
+ };
+ arg_count := node_header.arguments.count;
+ node_new_args := NewArray(arg_count + 1, *Code_Declaration);
+ for i : 0..arg_count-1 {
+ node_new_args[i] = node_header.arguments[i];
+ }
+ node_new_args[arg_count] = *Code_Declaration.{
+ kind = .DECLARATION,
+ name = "__data",
+ type_inst = *Code_Type_Instantiation.{
+ kind = .TYPE_INSTANTIATION,
+ type_valued_expression = *Code_Unary_Operator.{
+ kind = .UNARY_OPERATOR,
+ operator_type = #char"*",
+ subexpression = *Code_Ident.{
+ kind = .IDENT,
+ name = "void"
+ }
+ }
+ }
+ };
+ nodes_13 := NewArray(names.count, Code_Ident);
+ nodes_14 := NewArray(names.count, Code_Unary_Operator);
+ nodes_14_ptr := NewArray(names.count, *Code_Node);
+ for i : 0..names.count-1 {
+ nodes_13[i] = Code_Ident.{
+ kind = .IDENT,
+ name = names[i]
+ };
+ nodes_14[i] = Code_Unary_Operator.{
+ kind = .UNARY_OPERATOR,
+ subexpression = *(nodes_13[i]),
+ operator_type = #char"*"
+ };
+ nodes_14_ptr[i] = *(nodes_14[i]);
+ }
+ exprs : [2]*Code_Node;
+ exprs[0] = *Code_Procedure_Header.{
+ kind = .PROCEDURE_HEADER,
+ arguments = node_new_args,
+ returns = node_header.returns,
+ body_or_null = *Code_Procedure_Body.{
+ kind = .PROCEDURE_BODY,
+ block = *Code_Block.{
+ kind = .BLOCK,
+ statements = nodes_new_statements
+ }
+ }
+ };
+ exprs[1] = *Code_Unary_Operator.{
+ kind = .UNARY_OPERATOR,
+ operator_type = #char"*",
+ subexpression = *Code_Literal.{
+ kind = .LITERAL,
+ value_type = .STRUCT,
+ struct_literal_info = *Code_Struct_Literal_Info.{
+ type_expression = *Code_Type_Instantiation.{
+ kind = .TYPE_INSTANTIATION,
+ type_valued_expression = node_0
+ },
+ arguments = nodes_14_ptr
+ }
+ }
+ };
+ _exprs : [2]Code_Comma_Separated_Argument = .[
+ .{exprs[0], .NONE},
+ .{exprs[1], .NONE},
+ ];
+ node_19 := Code_Comma_Separated_Arguments.{
+ kind = .COMMA_SEPARATED_ARGUMENTS,
+ arguments = _exprs
+ };
+ exprs2 : [2]*Code_Node;
+ exprs2[0] = proc_node;
+ exprs2[1] = data_node;
+ _exprs2 : [2]Code_Comma_Separated_Argument = .[
+ .{exprs2[0], .NONE},
+ .{exprs2[1], .NONE},
+ ];
+
+ node_22 := Code_Comma_Separated_Arguments.{
+ kind = .COMMA_SEPARATED_ARGUMENTS,
+ arg = _exprs2
+ };
+ return *node_22, *node_19;
+}
diff --git a/kscurses/modes.jai b/kscurses/modes.jai
new file mode 100644
index 0000000..8c167e0
--- /dev/null
+++ b/kscurses/modes.jai
@@ -0,0 +1,179 @@
+MAX_ATTRS :: 7;
+Graphics_Mode :: struct {
+ foreground : Color;
+ background : Color;
+ attr_flags : enum_flags u8 {
+ F_BOLD :: 0x1;
+ F_DIM :: 0x2;
+ F_ITALIC :: 0x4;
+ F_UNDERLINE :: 0x8;
+ F_BLINKING :: 0x10;
+ F_INVERSE :: 0x20;
+ F_STRIKETHROUGH :: 0x40;
+ }
+ // attrs : [MAX_ATTRS]bool;
+ // 0 - bold (on/off/keep)
+ // 1 - dim/faint
+ // 2 - italic
+ // 3 - underline
+ // 4 - blinking
+ // 5 - inverse
+ // 6 - strikethrough
+ fcol256 : u8;
+ bcol256 : u8;
+}
+Color :: enum u8 {
+ RESET :: 0;
+ DEFAULT :: 39;
+ COLOR256 :: 38;
+
+ BLACK :: 30;
+ RED :: 31;
+ GREEN :: 32;
+ YELLOW :: 33;
+ BLUE :: 34;
+ MAGENTA :: 35;
+ CYAN :: 36;
+ WHITE :: 37;
+
+ BRIGHT_BLACK :: 90;
+ BRIGHT_RED :: 91;
+ BRIGHT_GREEN :: 92;
+ BRIGHT_YELLOW :: 93;
+ BRIGHT_BLUE :: 94;
+ BRIGHT_MAGENTA :: 95;
+ BRIGHT_CYAN :: 96;
+ BRIGHT_WHITE :: 97;
+}
+Attr :: enum u8 {
+ BOLD :: 1;
+ DIM :: 2;
+ ITALIC :: 3;
+ UNDERLINE :: 4;
+ BLINKING :: 5;
+ INVERSE :: 7;
+ STRIKETHROUGH :: 9;
+
+ BOLD_AND_DIM_OFF :: 22;
+ ITALIC_OFF :: 23;
+ UNDERLINE_OFF :: 24;
+ BLINKING_OFF :: 25;
+ INVERSE_OFF :: 27;
+ STRIKETHROUGH_OFF :: 29;
+}
+
+make_graphics_mode :: (foreground := Color.DEFAULT, background := Color.DEFAULT, bold := false, dim := false, italic := false, underline := false, blinking := false, inverse := false, strikethrough := false, fcol256 :u8= 0, bcol256 :u8= 0) -> Graphics_Mode {
+ result : Graphics_Mode;
+
+ result.foreground = foreground;
+ result.background = xx (background + 10);
+
+ if bold result.attr_flags |= .F_BOLD;
+ if dim result.attr_flags |= .F_DIM;
+ if italic result.attr_flags |= .F_ITALIC;
+ if underline result.attr_flags |= .F_UNDERLINE;
+ if blinking result.attr_flags |= .F_BLINKING;
+ if inverse result.attr_flags |= .F_INVERSE;
+ if strikethrough result.attr_flags |= .F_STRIKETHROUGH;
+ //========
+
+ // result.attrs[0] = bold;
+ // result.attrs[1] = dim;
+ // result.attrs[2] = italic;
+ // result.attrs[3] = underline;
+ // result.attrs[4] = blinking;
+ // result.attrs[5] = inverse;
+ // result.attrs[6] = strikethrough;
+ result.fcol256 = fcol256;
+ result.bcol256 = bcol256;
+
+ return result;
+}
+
+Char :: struct {
+ code : u32;
+ #place code;
+ codes : [4]u8 = ---;
+ mode : Graphics_Mode;
+}
+make_char :: (code : u32, foreground := Color.DEFAULT, background := Color.DEFAULT, bold := false, dim := false, italic := false, underline := false, blinking := false, inverse := false, strikethrough := false, fcol256 :u8= 0, bcol256 :u8= 0) -> Char {
+ return .{code = code, mode = make_graphics_mode(foreground, background, bold, dim, italic, underline, blinking, inverse, strikethrough, fcol256, bcol256)};
+}
+length :: inline (c : Char) -> u8 {
+ return length_code(c.code);
+}
+operator== :: (c1 : Char, c2 : Char) -> bool {
+ return c1.code == c2.code && c1.mode == c2.mode;
+}
+operator== :: (m1 : Graphics_Mode, m2 : Graphics_Mode) -> bool {
+ if m1.foreground != m2.foreground return false;
+ if m1.background != m2.background return false;
+ if m1.fcol256 != m2.fcol256 return false;
+ if m1.bcol256 != m2.bcol256 return false;
+ if m1.attr_flags != m2.attr_flags return false;
+ return true;
+}
+
+find_best_char :: (p : Vector3, use_mix : bool) -> Char {
+ cast_255 :: (x : float) -> s32 {
+ return clamp(cast(s32)(x * 255 + .5), 0, 255);
+ }
+ return find_best_char(ivec3.{cast_255(p.x), cast_255(p.y), cast_255(p.z)}, use_mix);
+}
+find_best_char :: (p : u8vec3, use_mix : bool) -> Char {
+ return find_best_char(cast_vec(s32, p), use_mix);
+}
+find_best_char :: (_p : ivec3, use_mix : bool) -> Char {
+ p := _p;
+ to_6level :: (x : s32) -> u8, s32 {
+ a := u8.[
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 4, 4, 4, 4, 4, 4, 4,
+ 4, 4, 4, 5, 5, 5, 5, 5,
+ ][clamp(x, 0, 255) >> 2];
+ return a, s32.[0, 87, 135, 175, 215, 255][a];
+ }
+
+ to_6level :: (v : ivec3) -> u8vec3, ivec3, dist:int {
+ i : u8vec3;
+ h : ivec3;
+ i.x, h.x = to_6level(v.x);
+ i.y, h.y = to_6level(v.y);
+ i.z, h.z = to_6level(v.z);
+ w := v - h;
+ return i, h, w.x * w.x + w.y * w.y + w.z * w.z;
+ }
+ col_code :: (v : u8vec3) -> u8 { return 16 + 36 * v.x + 6 * v.y + v.z; }
+
+ b_6, b_256 := to_6level(p);
+ code_00, code_25, code_50, code_75 := #run utf8(" "), #run utf8("░"), #run utf8("▒"), #run utf8("▓");
+
+ result := make_char(
+ code_00,
+ foreground = .COLOR256,
+ background = .COLOR256,
+ bcol256 = col_code(b_6)
+ );
+
+ if use_mix {
+ f_6_25, f_256_25, dist_25 := to_6level(p * 4 - b_256 * 3);
+ f_6_50, f_256_50, dist_50 := to_6level(p * 2 - b_256);
+ f_6_75, f_256_75, dist_75 := to_6level((p * 4 - b_256) / 3);
+
+
+ if dist_25 > dist_50 then f_6_25, code_25, dist_25 = f_6_50, code_50, dist_50;
+ if dist_25 > dist_75 then f_6_25, code_25, dist_25 = f_6_75, code_75, dist_75;
+
+ result.code = code_25;
+ result.mode.fcol256 = col_code(f_6_25);
+ }
+ return result;
+}
+
+#scope_file
+#import "Math"; \ No newline at end of file
diff --git a/kscurses/module.jai b/kscurses/module.jai
new file mode 100644
index 0000000..23c0105
--- /dev/null
+++ b/kscurses/module.jai
@@ -0,0 +1,91 @@
+#module_parameters(
+ ENABLE_SIGINT := true,
+ ENABLE_SIGQUIT := true,
+ TICK_DURATION_MS := 1000,
+ ENABLE_UI_BELL := true,
+ ENABLE_UI_BLINKING := true
+);
+
+#if OS == .LINUX {
+ #import "POSIX";
+}
+
+// ivec2 :: Generic_Vector(s32, 2);
+// ivec3 :: Generic_Vector(s32, 3);
+// u8vec3 :: Generic_Vector(u8, 3);
+// Ibox2 :: Generic_Box(s32, 2);
+
+// #load "../Extra_Containers/module.jai";
+// #import "Extra_Containers";
+
+
+#import "Basic"();
+#import "Process";
+#import "String";
+
+#load "io.jai";
+#load "init.jai";
+#load "events.jai";
+
+#load "vectors.jai";
+#load "modes.jai";
+#load "print.jai";
+#load "utils.jai";
+#load "canvas.jai";
+
+#load "ui/style.jai";
+#load "ui/element.jai";
+#load "ui/master.jai";
+#load "ui/links.jai";
+
+#load "ui/button.jai";
+#load "ui/group.jai";
+#load "ui/text_buf.jai";
+#load "ui/select_list.jai";
+#load "ui/parent.jai";
+#load "ui/scene_manager.jai";
+#load "ui/popup_manager.jai";
+#load "ui/line_input.jai";
+#load "ui/table.jai";
+#load "ui/scalable_group.jai";
+#load "ui/progress_bar.jai";
+
+#load "lambdas.jai";
+#load "queue.jai";
+
+// TODO:
+// remake canvas:
+// -force full refresh only after resize / init
+// maybe_resize()
+// fill_by_proc()
+// clear()
+
+
+// in resize & fill: check if size same
+// add c_print_ascii_line, c_print_ascii_line_bounded
+
+// restart_clock && multiple clocks
+
+// in set_main_scene check if root is predecessor of active element
+// stop_clock -> clock_state
+
+// module overlap check / set minimum size of terminal
+
+// make generic_vec2/3
+// make better how_to
+// add meaningfull assertion text
+// UI_Scene <- UI_Popup
+// autogrow in line_input
+// detection of the element on which rendering breaks
+// extra debug lines on top
+// group view
+// + grid / group (maybe)
+// make text u8/u32 modes in text_buf, line_input & other
+// add block_input
+// (maybe) unset_active_recursive without arguments
+// plotter
+// tree/directory view
+// push graphics mode
+
+// add windows(os) support xd
+// (August 13, 2023). Fuck it, I don't want to deal with this shit anymore.
diff --git a/kscurses/print.jai b/kscurses/print.jai
new file mode 100644
index 0000000..a023fea
--- /dev/null
+++ b/kscurses/print.jai
@@ -0,0 +1,148 @@
+ks_bell :: () {
+ bell_str : string;
+ bell := 7;
+ bell_str.data, bell_str.count = xx *bell, 1;
+ ks_write(bell_str);
+}
+
+ui_bell :: inline () {
+ #if ENABLE_UI_BELL ks_bell();
+}
+
+
+b_move_cursor :: inline (builder : *String_Builder, coord : ivec2) {
+ print_to_builder(builder, "\e[%;%H", coord.y + 1, coord.x + 1);
+}
+t_move_cursor :: inline (coord : ivec2) -> string {
+ return tprint("\e[%;%H", coord.y, coord.x);
+}
+ks_move_cursor :: inline (coord : ivec2) {
+ ks_write(tprint("\e[%;%H", coord.y + 1, coord.x + 1));
+}
+b_cursor_set_visibility :: inline (builder : *String_Builder, $$visible : bool) {
+ append(builder, ifx visible "\e[?25h" else "\e[?25l");
+}
+
+b_print :: (builder : *String_Builder, coord : ivec2, mode : Graphics_Mode, fmt : string, args : ..Any) {
+ if coord != .{-1, -1} b_move_cursor(builder, coord);
+ b_mode_set(builder, mode);
+ print_to_builder(builder, fmt, ..args);
+}
+t_print :: (coord : ivec2, mode : Graphics_Mode, fmt : string, args : ..Any) -> string {
+ builder := String_Builder.{allocator = temp};
+ b_print(*builder, coord, mode, fmt, ..args);
+ return builder_to_string(*builder, temp);
+}
+ks_print :: inline (coord : ivec2, mode : Graphics_Mode, fmt : string, args : ..Any) {
+ ks_write(t_print(coord, mode, fmt, ..args));
+}
+
+
+b_clear_screen :: (builder : *String_Builder) {
+ append(builder, "\e[2J");
+}
+t_clear_screen :: inline () -> string {
+ return "\e[2J";
+}
+ks_clear_screen :: inline () {
+ ks_write("\e[2J");
+}
+
+
+b_esc_m :: inline (builder : *String_Builder, $$code : int) {
+ print_to_builder(builder, "\e[%m", code);
+}
+t_esc_m :: inline ($$code : int) -> string {
+ return tprint("\e[%m", code);
+}
+ks_esc_m :: inline ($$code : int) {
+ ks_write(t_esc_m(code));
+}
+
+
+#scope_file
+b_mode_difference :: (builder : *String_Builder, prev : Graphics_Mode, using current : Graphics_Mode) {
+ printed_first := false;
+ add_code :: (code : u8) #expand {
+ // assert(code != xx Attr.BOLD);
+ // assert(code != xx Attr.DIM);
+ if printed_first {
+ print_to_builder(builder, ";%", code);
+ } else {
+ print_to_builder(builder, "\e[%", code);
+ printed_first = true;
+ }
+ }
+ add_code_on_off :: (prev : bool, current : bool, on_code : Attr, off_code : Attr) #expand {
+ if prev != current add_code(xx(ifx current on_code else off_code));
+ }
+ if foreground != prev.foreground || fcol256 != prev.fcol256 {
+ if foreground != Color.COLOR256 {
+ add_code(xx foreground);
+ } else {
+ add_code(38); add_code(5); add_code(xx fcol256);
+ }
+ }
+ if background != prev.background || bcol256 != prev.bcol256 {
+ if background != (Color.COLOR256 + 10) {
+ add_code(xx background);
+ } else {
+ add_code(48); add_code(5); add_code(xx bcol256);
+ }
+ }
+ if (prev.attr_flags & 1) != (attr_flags & 1) || (prev.attr_flags & 2) != (attr_flags & 2) {
+ off0 := (prev.attr_flags & 1) && !(attr_flags & 1);
+ off1 := (prev.attr_flags & 2) && !(attr_flags & 2);
+ if off0 || off1 {
+ add_code(xx Attr.BOLD_AND_DIM_OFF);
+ if (attr_flags & 1) add_code(xx Attr.BOLD);
+ if (attr_flags & 2) add_code(xx Attr.DIM);
+ } else {
+ if (attr_flags & 1) && !(prev.attr_flags & 1) add_code(xx Attr.BOLD);
+ if (attr_flags & 2) && !(prev.attr_flags & 2) add_code(xx Attr.DIM);
+ }
+ }
+ add_code_on_off(xx prev.attr_flags & 4, xx attr_flags & 4, .ITALIC, .ITALIC_OFF);
+ add_code_on_off(xx prev.attr_flags & 8, xx attr_flags & 8, .UNDERLINE, .UNDERLINE_OFF);
+ add_code_on_off(xx prev.attr_flags & 16, xx attr_flags & 16, .BLINKING, .BLINKING_OFF);
+ add_code_on_off(xx prev.attr_flags & 32, xx attr_flags & 32, .INVERSE, .INVERSE_OFF);
+ add_code_on_off(xx prev.attr_flags & 64, xx attr_flags & 64, .STRIKETHROUGH, .STRIKETHROUGH_OFF);
+ if printed_first append(builder, "m");
+}
+#scope_export
+
+//TODO update_state := true
+b_mode_set :: (builder : *String_Builder, using mode : Graphics_Mode, update_state := true) {
+ b_mode_difference(builder, terminal_state.last_mode, mode);
+ if update_state terminal_state.last_mode = mode;
+}
+t_mode_set :: (mode : Graphics_Mode, update_state := true) -> string {
+ builder := String_Builder.{allocator = temp};
+ b_mode_set(*builder, mode, update_state);
+ return builder_to_string(*builder, temp);
+}
+
+ks_mode_reset :: (update_state := true) {
+ ks_write("\e[0m");
+ if update_state terminal_state.last_mode = .{};
+}
+t_mode_reset :: inline (update_state := true) -> string {
+ if update_state terminal_state.last_mode = .{};
+ return "\e[0m";
+}
+b_mode_reset :: inline (builder : *String_Builder, update_state := true) {
+ if update_state terminal_state.last_mode = .{};
+ append(builder, "\e[0m");
+}
+
+b_putchar :: (builder : *String_Builder, c : Char) {
+ b_mode_set(builder, c.mode);
+ b_putchar(builder, c.code);
+}
+
+b_putchar :: inline (builder : *String_Builder, code : u64) {
+ char_str : string;
+ char_str.data = xx *(code);
+ char_str.count = length_code(code);
+ append(builder, char_str);
+}
diff --git a/kscurses/queue.jai b/kscurses/queue.jai
new file mode 100644
index 0000000..86f5dc8
--- /dev/null
+++ b/kscurses/queue.jai
@@ -0,0 +1,57 @@
+Queue :: struct(Element_Type : Type) {
+ buffer : []Element_Type;
+ begin, count := 0;
+}
+pop :: (using queue : *Queue($Element_Type)) -> Element_Type, bool {
+ elem : Element_Type;
+ ok := true;
+ if count {
+ elem = buffer[begin];
+ begin += 1;
+ count -= 1;
+ } else {
+ ok = false;
+ }
+ return elem, ok;
+}
+get_space :: (using queue : *Queue($Element_Type), new_elements_count : int) {
+ if begin + count + new_elements_count <= buffer.count return;
+ if count <= begin && count + new_elements_count <= buffer.count {
+ memcpy(buffer.data, buffer.data + begin, size_of(Element_Type) * count);
+ begin = 0;
+ } else {
+ new_buf := NewArray(count + max(new_elements_count, count), Element_Type);
+ memcpy(new_buf.data, buffer.data + begin, count * size_of(Element_Type));
+ begin = 0;
+ array_free(buffer);
+ buffer = new_buf;
+ }
+}
+push :: (using queue : *Queue($Element_Type), element : Element_Type) {
+ get_space(queue, 1);
+ assert(begin + count + 1 <= buffer.count);
+ buffer[begin + count] = element;
+ count += 1;
+}
+deinit :: (using queue : *Queue($Element_Type)) {
+ array_free(buffer);
+ <<queue = .{};
+}
+for_expansion :: (queue : Queue, body : Code, flags : For_Flags) #expand {
+ for i : 0..queue.count-1 {
+ `it_index := i;
+ `it := queue.buffer[begin + i];
+
+ #insert body;
+ }
+}
+first :: (queue : Queue($Element_Type)) -> Element_Type, bool {
+ result : Element_Type;
+ if queue.count > 0 then result = queue.buffer[queue.begin];
+ return result, queue.count > 0;
+}
+last :: (using queue : Queue($Element_Type)) -> Element_Type, bool {
+ result : Element_Type;
+ if queue.count > 0 then result = queue.buffer[queue.begin + queue.count - 1];
+ return result, queue.count > 0;
+}
diff --git a/kscurses/readme.md b/kscurses/readme.md
new file mode 100644
index 0000000..7a2f6fc
--- /dev/null
+++ b/kscurses/readme.md
@@ -0,0 +1,43 @@
+# kscurses
+## _Curses replacement on jai for my needs. Use at your own risk._
+
+`tested on version 0.1.073`
+
+# setup & build
+1. download extra-containers module https://github.com/CyanMARgh/extra-containers
+2. move it to your extra modules folder
+3. specify this foder on top of demos/first.jai
+4. compile first.jai
+
+Currently works only on linux (tested on gnome terminal).
+
+# features list
+- character input
+- window resize handle
+- text modifiers (bold, italic, underline, blinking, inverse, strikethrough), color256 support
+- text/background color.
+- arrows, escape key and some
+- saving and restoring the terminal
+- exit with and crtl+C (optional).
+- ui elements (empty, button, text block, selection list)
+- multiple scenes and popups support
+- events (ticks, input, window resize and user-defined events)
+- ui can work both in single-thread mode and multi-thread mode
+- 4 print modes:
+- - ks_**method** : method prints directly terminal
+- - t_**method** : method returns temporary string or string from constant data section
+- - b_**method** : method prints to builder
+- - c_**method**: method prints to canvas
+
+# demos list
+- basic print methods, canvas and graphic modes usage
+- video (now uses events and color256 approximation with semi-transparent characters)
+- shorter canvas usage
+- ui : progress bars and extra events handler
+- ui : text buffer, buttons, selection list, groups, scenes
+- ui : popup
+- ui : line input
+- events processing without default ui
+- snake minigame
+- ui : table
+- ui : scalable group and anchors
diff --git a/kscurses/ui/button.jai b/kscurses/ui/button.jai
new file mode 100644
index 0000000..3866e03
--- /dev/null
+++ b/kscurses/ui/button.jai
@@ -0,0 +1,25 @@
+UI_Button :: struct {
+ #as using base : UI_Elem = .{type = .BUTTON};
+ text := "";
+ on_click : struct {
+ proc := (data : *void){};
+ data : *void;
+ };
+}
+
+handle_key_button :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ using cast(*UI_Button) ui_elem;
+ assert(cursor_state == .ON);
+ assert(!links.inner);
+ if key == .ENTER {
+ on_click.proc(on_click.data);
+ return true;
+ }
+ return false;
+}
+c_draw_button :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Button) ui_elem;
+ mode := ifx cursor_state == .ON && box_type == .NONE then style.text.cursor else style.text.default;
+ c_draw_line_ascii(canvas, text, zone, .{(zone.width - xx text.count) / 2, xx ((zone.height - 1) / 2)}, mode);
+ return true;
+}
diff --git a/kscurses/ui/element.jai b/kscurses/ui/element.jai
new file mode 100644
index 0000000..3db8fcb
--- /dev/null
+++ b/kscurses/ui/element.jai
@@ -0,0 +1,156 @@
+UI_Elem :: struct {
+ type : enum u8 {
+ NONE :: 0;
+ BUTTON :: 1;
+ TEXT_BUF :: 2;
+ GROUP :: 3;
+ SELECT_LIST :: 4;
+ SCENE_MANAGER :: 5;
+ POPUP_MANAGER :: 6;
+ LINE_INPUT :: 7;
+ SCALABLE_GROUP :: 8;
+ TABLE :: 9;
+ PROGRESS_BAR :: 10;
+ } = .NONE;
+
+ using visual_data : struct {
+ cursor_state : enum u8 { OUTSIDE :: 0; ON :: 1; IN :: 2; } = .OUTSIDE;
+ box_type : enum u8 { NONE :: 0; BORDER :: 1; NO_BORDER :: 2; } = .BORDER;
+ description_pos : enum u8 { TOP_LEFT :: 0; TOP_CENTER :: 1; TOP_RIGHT :: 2; BOTTOM_LEFT :: 4; BOTTOM_CENTER :: 5; BOTTOM_RIGHT :: 6; } = .TOP_LEFT;
+ description := "";
+ }
+
+ using links : struct {
+ left, right, bottom, top, inner, outer : *UI_Elem;
+ parent : *UI_Parent;
+ }
+
+ extra_handler : struct {
+ proc : (key : Key, data : *void) -> bool = null;
+ data : *void;
+ }
+}
+
+
+vtable_c_draw : []#type (*Canvas, *UI_Elem, Ibox2, *UI_Style) -> (bool) = .[
+ c_draw_default,
+ c_draw_button,
+ c_draw_textbuf,
+ c_draw_group,
+ c_draw_select_list,
+ c_draw_scene_manager,
+ c_draw_popup_manager,
+ c_draw_line_input,
+ c_draw_scalable_group,
+ c_draw_table,
+ c_draw_progress_bar
+];
+c_draw :: (canvas : *Canvas, using ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ ok := box_type == .NONE || c_box(canvas, zone, ifx cursor_state == .ON && __ui_master.blink_stage != 1 then style.box.cursor else style.box.default, box_type == .BORDER);
+ if !ok return false;
+ ok = c_draw_description(canvas, ui_elem, zone, style);
+ if !ok return false;
+
+ content_zone := ifx box_type == .BORDER then cut_border(zone, 1) else zone;
+
+ c_draw_proc := vtable_c_draw[xx ui_elem.type];
+ assert(xx c_draw_proc);
+ return c_draw_proc(canvas, ui_elem, content_zone, style);
+}
+c_draw_default :: (canvas : *Canvas, using ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool { return true; };
+
+intersection_line :: (box : Ibox2, _line : string, _start : ivec2) -> line:string, start:ivec2 {
+ line, start := _line, _start;
+ if start.x < box.left {
+ line.count += start.x - box.left;
+ start.x = box.left;
+ }
+ if start.x + line.count > box.left + box.width {
+ line.count = box.left + box.width - start.x;
+ }
+ return line, start;
+}
+
+c_draw_line_ascii_raw :: (canvas : *Canvas, line : string, start : ivec2, mode : Graphics_Mode) {
+ for x : 0..line.count-1 c_putchar(canvas, .{code = xx line[x], mode = mode}, start + ivec2.{xx x, 0});
+}
+
+c_draw_line_ascii :: (canvas : *Canvas, _line : string, _start : ivec2, mode : Graphics_Mode) {
+ line, start := intersection_line(canvas.zone, _line, _start);
+ c_draw_line_ascii_raw(canvas, line, start, mode);
+}
+c_draw_line_ascii :: (canvas : *Canvas, _line : string, bounds : Ibox2, offset : ivec2, mode : Graphics_Mode) {
+ assert(inside(bounds, canvas.zone));
+ line, start := intersection_line(bounds, _line, bounds.corner + offset);
+ c_draw_line_ascii_raw(canvas, line, start, mode);
+}
+c_draw_description :: (canvas : *Canvas, using ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ if !description return true;
+ if description.count > zone.width - 2 return false;
+
+ top_or_bottom := !(description_pos & 4);
+ y := ifx top_or_bottom then zone.top else zone.top + zone.height - 1;
+
+ mode := style.text.default;
+ if description_pos & 3 == {
+ case 0; c_draw_line_ascii(canvas, description, .{zone.left + 1, y}, mode);
+ case 1; c_draw_line_ascii(canvas, description, .{xx (zone.left + (zone.width - description.count) / 2), y}, mode);
+ case 2; c_draw_line_ascii(canvas, description, .{xx (zone.left + zone.width - description.count - 1), y}, mode);
+ case; assert(false);
+ }
+ return true;
+}
+
+vtable_handle_key : []#type (*UI_Elem, Key) -> (bool) = .[
+ handle_key_none,
+ handle_key_button,
+ handle_key_none,
+ handle_key_group,
+ handle_key_select_list,
+ handle_key_scene_manager,
+ handle_key_popup_manager,
+ handle_key_line_input,
+ handle_key_scalable_group,
+ handle_key_table,
+ handle_key_none
+];
+handle_key :: (using ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ handle_proc := vtable_handle_key[ui_elem.type];
+ assert(xx handle_proc);
+
+ handled := handle_proc(ui_elem, key);
+ // assert(cursor_state == .ON || cursor_state == .IN);
+ if !handled && cursor_state == .ON {
+ handled = handle_key_move(ui_elem, key);
+ }
+
+ if !handled && extra_handler.proc {
+ handled = extra_handler.proc(key, extra_handler.data);
+ }
+ return handled;
+}
+handle_key_none :: (using ui_elem : *UI_Elem, key : Key) -> handled:bool { return false; }
+handle_key_move :: (using ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ __ui_master.blink_stage = 0;
+ restart_clock_cycle();
+
+ active_elem := ui_elem;
+ assert(active_elem.cursor_state == .ON);
+ try_move :: (new_elem : *UI_Elem) #expand {
+ if new_elem {
+ unset_active_recursive(`active_elem);
+ set_active_recursive(new_elem);
+ `active_elem = new_elem;
+ }
+ }
+ if key == {
+ case .LEFT; try_move(links.left);
+ case .RIGHT; try_move(links.right);
+ case .UP; try_move(links.top);
+ case .DOWN; try_move(links.bottom);
+ case .ESCAPE; try_move(links.outer);
+ case .ENTER; try_move(links.inner);;
+ }
+ assert(active_elem.cursor_state == .ON);
+ return ui_elem != active_elem;
+} \ No newline at end of file
diff --git a/kscurses/ui/group.jai b/kscurses/ui/group.jai
new file mode 100644
index 0000000..366c513
--- /dev/null
+++ b/kscurses/ui/group.jai
@@ -0,0 +1,44 @@
+UI_Group :: struct {
+ #as using base_parent : UI_Parent = .{type = .GROUP};
+
+ Element :: struct {
+ ptr : *UI_Elem;
+ zone : Ibox2;
+ }
+
+ elements : []Element;
+}
+set_sub_elements :: (group : *UI_Group, elements : ..UI_Group.Element) {
+ group.elements = elements;
+ for e : elements {
+ e.ptr.parent = group;
+ }
+}
+c_draw_group :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Group) ui_elem;
+ for e : elements {
+ e_zone := e.zone;
+ if !inside(e_zone, zone.size) return false;
+ e_zone.corner += zone.corner;
+ if !c_draw(canvas, e.ptr, e_zone, style) return false;
+ }
+ return true;
+}
+handle_key_group :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ using cast(*UI_Group) ui_elem;
+ assert(cursor_state == .ON || cursor_state == .OUTSIDE);
+
+ handled := false;
+ if cursor_state == .OUTSIDE {
+ assert(xx active_element);
+ {
+ ok := false;
+ for e : elements if e.ptr == active_element ok = true;
+ assert(ok);
+ }
+ handled = handle_key(active_element, key);
+ } else {
+ assert(xx !active_element);
+ }
+ return handled;
+} \ No newline at end of file
diff --git a/kscurses/ui/line_input.jai b/kscurses/ui/line_input.jai
new file mode 100644
index 0000000..17823db
--- /dev/null
+++ b/kscurses/ui/line_input.jai
@@ -0,0 +1,122 @@
+Char_Type :: u8;
+UI_Line_Input :: struct {
+ #as using base : UI_Elem = .{type = .LINE_INPUT};
+ buffer : []Char_Type;
+
+ // resizeable := false;
+ ptr_left, ptr_right : int;
+ offset : int;
+}
+
+handle_key_line_input :: (ui_elem : *UI_Elem, key : Key) -> bool {
+ using ui_line_input := cast(*UI_Line_Input) ui_elem;
+
+ handle_inner :: (using ui_line_input : *UI_Line_Input, key : Key) -> bool {
+ if is_printable(key) {
+ return add_char(ui_line_input, xx key);
+ } else if key == {
+ case .LEFT; #through;
+ case .RIGHT;
+ return move_ptr(ui_line_input, key);
+ case .BACKSPACE;
+ return remove_char_left(ui_line_input);
+ case .ESCAPE;
+ cursor_state = .ON;
+ return true;
+ }
+ return false;
+ }
+
+ if cursor_state == .IN {
+ return handle_inner(ui_line_input, key);
+ } else if cursor_state == .ON && key == .ENTER {
+ cursor_state = .IN;
+ return true;
+ }
+
+ return false;
+}
+c_draw_line_input :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Line_Input) ui_elem;
+
+ fix_offset :: () #expand {
+ if ptr_left - offset < 0 {
+ offset = ptr_left;
+ } else if ptr_left - offset >= zone.width {
+ offset = ptr_left - zone.width + 1;
+ }
+ }
+ fix_offset();
+
+ left_part, right_part : string;
+ left_part.data, left_part.count = buffer.data, ptr_left;
+
+ right_part.data, right_part.count = buffer.data + ptr_right + 1, (buffer.count - ptr_right - 1);
+
+ c_draw_line_ascii(canvas, left_part, zone, .{xx -offset, 0}, style.text.default);
+ c_draw_line_ascii(canvas, right_part, zone, .{xx (ptr_left - offset), 0}, style.text.default);
+
+ terminal_state.cursor = ifx cursor_state == .IN then zone.corner + ivec2.{xx(ptr_left - offset), 0} else .{-1, -1};
+
+ return true;
+}
+
+init :: (using ui_line_input : *UI_Line_Input, max_length := 100) {
+ buffer = NewArray(max_length, Char_Type);
+ ptr_left, ptr_right = 0, max_length - 1;
+}
+add_char :: (using ui_line_input : *UI_Line_Input, c : Char_Type) -> bool {
+ if ptr_left > ptr_right return false;
+ buffer[ptr_left] = c;
+ ptr_left += 1;
+ return true;
+}
+remove_char_left :: (using ui_line_input : *UI_Line_Input) -> bool {
+ if ptr_left == 0 return false;
+ ptr_left -= 1;
+ return true;
+}
+move_ptr :: (using ui_line_input : *UI_Line_Input, key : Key) -> bool {
+ if key == {
+ case .LEFT;
+ if ptr_left > 0 {
+ ptr_left -= 1;
+ buffer[ptr_right] = buffer[ptr_left];
+ ptr_right -= 1;
+ return true;
+ }
+ case .RIGHT;
+ if ptr_right < buffer.count-1 {
+ ptr_right += 1;
+ buffer[ptr_left] = buffer[ptr_right];
+ ptr_left += 1;
+ return true;
+ }
+ case;
+ assert(false);
+ }
+ return false;
+}
+deinit :: (using ui_line_input : *UI_Line_Input) {
+ array_free(buffer);
+}
+
+is_printable :: (key : Key) -> bool {
+ code := cast(u64) key;
+ return (code >= #char" " && code <= #char"~");
+}
+
+get_string :: (using ui_line_input : *UI_Line_Input, allocator := context.allocator) -> string {
+ result : string;
+ size := ptr_left + (buffer.count - ptr_right - 1);
+ result.count = size;
+ result.data = alloc(size, allocator);
+
+ memcpy(result.data, buffer.data, ptr_left);
+ memcpy(result.data + ptr_left, buffer.data + ptr_right + 1, buffer.count - ptr_right - 1);
+ return result;
+}
+reset :: (using ui_line_input : *UI_Line_Input) {
+ ptr_left = 0;
+ ptr_right = buffer.count - 1;
+} \ No newline at end of file
diff --git a/kscurses/ui/links.jai b/kscurses/ui/links.jai
new file mode 100644
index 0000000..dc8c667
--- /dev/null
+++ b/kscurses/ui/links.jai
@@ -0,0 +1,94 @@
+link_lr :: (el : *UI_Elem, er : *UI_Elem) {
+ el.right = er;
+ er.left = el;
+}
+link_tb :: (et : *UI_Elem, eb : *UI_Elem) {
+ et.bottom = eb;
+ eb.top = et;
+}
+link_oi :: (eo : *UI_Elem, ei : *UI_Elem) {
+ eo.inner = ei;
+ ei.outer = eo;
+}
+
+link_grid :: (size : ivec2, elements : ..*UI_Elem) {
+ assert(size.x * size.y == elements.count);
+ for y : 0..size.y-1 {
+ for x : 0..size.x-2 {
+ i := x + size.x * y;
+ link_lr(elements[i], elements[i + 1]);
+ }
+ }
+ for y : 0..size.y-2 {
+ for x : 0..size.x-1 {
+ i := x + size.x * y;
+ link_tb(elements[i], elements[i + size.x]);
+ }
+ }
+}
+link_grid :: (size : ivec2, elements : []UI_Elem) {
+ assert(size.x * size.y == elements.count);
+ for y : 0..size.y-1 {
+ for x : 0..size.x-2 {
+ i := x + size.x * y;
+ link_lr(*elements[i], *elements[i + 1]);
+ }
+ }
+ for y : 0..size.y-2 {
+ for x : 0..size.x-1 {
+ i := x + size.x * y;
+ link_tb(*elements[i], *elements[i + size.x]);
+ }
+ }
+}
+link_row :: (elements : ..*UI_Elem) {
+ for i : 0..elements.count-2 {
+ link_lr(elements[i], elements[i + 1]);
+ }
+}
+link_column :: (elements : ..*UI_Elem) {
+ for i : 0..elements.count-2 {
+ link_tb(elements[i], elements[i + 1]);
+ }
+}
+link_to_outer :: (eo : *UI_Elem, ei : ..*UI_Elem) {
+ if ei.count > 0 {
+ for ei {
+ it.outer = eo;
+ }
+ eo.inner = ei[0];
+ }
+
+}
+link_to_bottom :: (eb : *UI_Elem, et : ..*UI_Elem) {
+ if et.count > 0 {
+ for et {
+ it.bottom = eb;
+ }
+ eb.top = et[0];
+ }
+}
+link_to_top :: (et : *UI_Elem, eb : ..*UI_Elem) {
+ if eb.count > 0 {
+ for eb {
+ it.top = et;
+ }
+ et.bottom = eb[0];
+ }
+}
+link_to_right :: (er : *UI_Elem, el : ..*UI_Elem) {
+ if el.count > 0 {
+ for el {
+ it.right = er;
+ }
+ er.left = el[0];
+ }
+}
+link_to_left :: (el : *UI_Elem, er : ..*UI_Elem) {
+ if er.count > 0 {
+ for er {
+ it.left = el;
+ }
+ el.right = er[0];
+ }
+} \ No newline at end of file
diff --git a/kscurses/ui/master.jai b/kscurses/ui/master.jai
new file mode 100644
index 0000000..9cb0702
--- /dev/null
+++ b/kscurses/ui/master.jai
@@ -0,0 +1,105 @@
+UI_Master :: struct {
+ style : UI_Style;
+ canvas : Canvas;
+ root : *UI_Elem;
+ should_exit := false;
+
+ blink_stage := 0;
+
+ before_draw : struct {
+ proc := (data : *void) { };
+ data : *void;
+ };
+ extra_handle : struct {
+ proc := (e : Event, data : *void) { };
+ data : *void;
+ };
+}
+__ui_master : UI_Master;
+
+
+set_main_scene :: (scene : UI_Scene) {
+ __ui_master.root = scene.root;
+ set_active_recursive(scene.entry);
+}
+run_singlethread_ui :: () {
+ use_default_winch_handler();
+ using __ui_master;
+ while 1 {
+ before_draw.proc(before_draw.data);
+ if should_exit break;
+ draw(*__ui_master);
+ reset_temporary_storage();
+ handle_key(*__ui_master, ks_getch(block = false));
+ if should_exit break;
+ }
+ deinit(*__ui_master);
+}
+run_multithread_ui :: () {
+ use_events(tick_duration_ms = 530);
+ __event_handler = .{
+ proc = (e : Event, __data : *void) {
+ using __ui_master;
+ if e.type == {
+ case .KEY;
+ handle_key(*__ui_master, e.key);
+ case .TICK;
+ #if ENABLE_UI_BLINKING __ui_master.blink_stage = xx !__ui_master.blink_stage;
+ }
+ extra_handle.proc(e, extra_handle.data);
+ },
+ data = null
+ }; defer __event_handler = .{};
+
+ using __ui_master;
+
+ while 1 {
+ before_draw.proc(before_draw.data);
+ if should_exit break;
+ processed := wait_and_process_events();
+ if should_exit break;
+ if processed {
+ draw(*__ui_master);
+ reset_temporary_storage();
+ }
+ }
+ deinit(*__ui_master);
+}
+
+#scope_file
+draw :: (using ui_master : *UI_Master) {
+ new_zone := Ibox2.{size = terminal_state.size};
+ builder : String_Builder;
+
+ if new_zone != canvas.zone {
+ resize_clear(*canvas, new_zone);
+ }
+ ok := c_draw(*canvas, root, canvas.zone, *style);
+ if ok {
+ ks_draw_canvas(*canvas);
+ } else {
+ b_mode_set(*builder, style.text.default);
+ b_clear_screen(*builder);
+ b_print(*builder, .{0, 0}, style.text.debug, "screen to small: %x%", terminal_state.size.x, terminal_state.size.y);
+ }
+ b_cursor_set_visibility(*builder, terminal_state.cursor != ivec2.{-1, -1});
+ if terminal_state.cursor != ivec2.{-1, -1} then b_move_cursor(*builder, terminal_state.cursor);
+
+ ks_write(builder_to_string(*builder, allocator = temp));
+}
+handle_key :: (using ui_master : *UI_Master, key : Key) {
+ handled := handle_key(root, key);
+ if handled return;
+
+ if key == {
+ case .ESCAPE; {
+ should_exit = true;
+ unset_active_recursive(__last_set);
+ }
+ case; if key != .READ_ERROR ui_bell();
+ }
+}
+deinit :: (using ui_master : *UI_Master) {
+ deinit(*canvas);
+ __ui_master = .{};
+} \ No newline at end of file
diff --git a/kscurses/ui/parent.jai b/kscurses/ui/parent.jai
new file mode 100644
index 0000000..af459de
--- /dev/null
+++ b/kscurses/ui/parent.jai
@@ -0,0 +1,33 @@
+UI_Parent :: struct {
+ #as using base : UI_Elem;
+ active_element : *UI_Elem;
+}
+
+__last_set : *UI_Elem;
+
+set_active_recursive :: (ui_elem : *UI_Elem) {
+ assert(!__last_set); __last_set = ui_elem;
+ assert(ui_elem.cursor_state == .OUTSIDE); ui_elem.cursor_state = .ON;
+
+ current := ui_elem;
+ while 1 {
+ parent := current.parent;
+ if !parent break;
+ assert(!parent.active_element);
+ parent.active_element = current;
+ current = xx parent;
+ }
+}
+unset_active_recursive :: (ui_elem : *UI_Elem) {
+ assert(__last_set == ui_elem); __last_set = null;
+ assert(ui_elem.cursor_state == .ON); ui_elem.cursor_state = .OUTSIDE;
+
+ current := ui_elem;
+ while 1 {
+ parent := current.parent;
+ if !parent break;
+ assert(parent.active_element == current);
+ parent.active_element = null;
+ current = xx parent;
+ }
+} \ No newline at end of file
diff --git a/kscurses/ui/popup_manager.jai b/kscurses/ui/popup_manager.jai
new file mode 100644
index 0000000..704ad49
--- /dev/null
+++ b/kscurses/ui/popup_manager.jai
@@ -0,0 +1,65 @@
+MAX_POPUP_LEVES :: 10;
+
+UI_Popup_Manager :: struct {
+ #as using base_parent : UI_Parent = .{type = .POPUP_MANAGER, box_type = .NONE};
+
+ layers : [MAX_POPUP_LEVES]UI_Popup;
+
+ layers_count := 0;
+}
+
+set_background :: (using ui_popup_manager : *UI_Popup_Manager, scene : UI_Scene) {
+ assert(layers_count == 0);
+ layers[0] = .{root = scene.root, entry = scene.entry};
+ scene.root.parent = xx ui_popup_manager;
+ layers_count = 1;
+}
+
+handle_key_popup_manager :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ using ui_popup_manager := cast(*UI_Popup_Manager) ui_elem;
+ assert(layers_count > 0, "0 layers in popup manager");
+ if !handle_key(active_element, key) {
+ if key == .ESCAPE && layers_count > 1 {
+ pop(ui_popup_manager);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return true;
+ }
+}
+
+c_draw_popup_manager :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using ui_popup_manager := cast(*UI_Popup_Manager) ui_elem;
+ assert(layers_count > 0, "0 layers in popup manager");
+ for i : 0..layers_count-1 {
+ popup_zone := zone;
+ ok : bool;
+ if layers[i].size != .{-1, -1} then {
+ popup_zone, ok = fit_in_center(zone, layers[i].size);
+ if !ok return false;
+ }
+ if !c_draw(canvas, layers[i].root, popup_zone, style) return false;
+ }
+ return true;
+}
+
+pop :: (using ui_popup_manager : *UI_Popup_Manager) {
+ assert(layers_count > 1, "can't pop background");
+ layers_count -= 1;
+ unset_active_recursive(__last_set);
+ layers[layers_count].root.parent = null;
+ set_active_recursive(layers[layers_count - 1].entry);
+
+}
+push :: (using ui_popup_manager : *UI_Popup_Manager, scene : UI_Popup) {
+ assert(layers_count < MAX_POPUP_LEVES, "too much popup layers");
+ unset_active_recursive(__last_set);
+
+ layers[layers_count] = scene;
+ scene.root.parent = xx ui_popup_manager;
+ set_active_recursive(layers[layers_count].entry);
+
+ layers_count += 1;
+} \ No newline at end of file
diff --git a/kscurses/ui/progress_bar.jai b/kscurses/ui/progress_bar.jai
new file mode 100644
index 0000000..23d7dc9
--- /dev/null
+++ b/kscurses/ui/progress_bar.jai
@@ -0,0 +1,37 @@
+UI_Progress_Bar :: struct {
+ #as using base : UI_Elem = .{type = .PROGRESS_BAR};
+
+ value : float;
+ value_ptr : *float;
+
+ draw_proc := (percent : float, pix_coord : float) -> Vector3 { return ifx pix_coord < percent then Vector3.{0, 1, 0} else .{0, 0, 0}; }
+ show_percent := true;
+}
+
+set_value :: (progress_bar : *UI_Progress_Bar, value : float) {
+ progress_bar.value = value;
+ progress_bar.value_ptr = null;
+}
+set_value_ptr :: (progress_bar : *UI_Progress_Bar, value_ptr : *float) {
+ progress_bar.value_ptr = value_ptr;
+}
+
+c_draw_progress_bar :: (canvas : *Canvas, ui_elem : *UI_Elem, _zone : Ibox2, style : *UI_Style) -> bool {
+ using progress_bar := cast(*UI_Progress_Bar) ui_elem;
+ value_current := ifx value_ptr then <<value_ptr else value;
+ zone := _zone;
+ if zone.width < 6 return false;
+ if show_percent {
+ percent_str := tprint("%1%%", formatFloat(value_current * 100, width = 4, trailing_width = 1, zero_removal = .NO));
+ c_draw_line_ascii(canvas, percent_str, zone, .{zone.width - 5, zone.height / 2}, style.text.default);
+ zone.width -= 5;
+ }
+ for x : 0..zone.width-1 {
+ pix_coord := (x + .5) / zone.width;
+ char := find_best_char(draw_proc(value_current, pix_coord), true);
+ for y : 0..zone.height-1 {
+ c_putchar(canvas, char, zone.corner + ivec2.{x, y});
+ }
+ }
+ return true;
+}
diff --git a/kscurses/ui/scalable_group.jai b/kscurses/ui/scalable_group.jai
new file mode 100644
index 0000000..6c12bcc
--- /dev/null
+++ b/kscurses/ui/scalable_group.jai
@@ -0,0 +1,88 @@
+UI_Scalable_Group :: struct {
+ #as using base_parent : UI_Parent = .{type = .SCALABLE_GROUP};
+
+ Scale_Params :: struct {
+ // using metrics : struct {
+ x1, y1, x2, y2 : s32;
+ #place x1; v1 : ivec2;
+ #place x2; v2 : ivec2;
+ // };
+ scale_mode : enum u8 {
+ ANCHOR_TL :: 0; // v1 - size, v2 - offset from corner (both positive)
+ ANCHOR_TR :: 1;
+ ANCHOR_BL :: 2;
+ ANCHOR_BR :: 3;
+
+ STRETCH_T :: 4; // x1/2 - left/right offset, y1 - top offset, y2 - height
+ STRETCH_B :: 5; // x1/2 - left/right offset, y1 - bottom offset, y2 - height
+ STRETCH_L :: 6; // y1/2 - top/bottom offset, x1 - left offset, x2 - width
+ STRETCH_R :: 7; // y1/2 - top/bottom offset, x1 - right offset, x2 - width
+
+ STRETCH_C :: 8; // v1 - top-left offset, v2 - bottom-right offset
+ CENTERIZE :: 9; // v1 - size, v2 - offset from center(signed)
+
+ FIT_EXACT :: 10; //TODO
+ FIT_ROUGH :: 11;
+ } = .ANCHOR_TL;
+ }
+
+ Element :: struct {
+ ptr : *UI_Elem;
+ scale_params : Scale_Params;
+ }
+
+ elements : []Element;
+}
+
+get_zone :: (using zone : Ibox2, using scale_params : UI_Scalable_Group.Scale_Params) -> Ibox2 {
+ if scale_mode == {
+ case .ANCHOR_TL; return .{corner = corner + v2, size = v1};
+ case .ANCHOR_TR; return .{corner = .{left + width - x2 - x1, top + y2}, size = v1};
+ case .ANCHOR_BL; return .{corner = .{left + x2, top + height - y2 - y1}, size = v1};
+ case .ANCHOR_BR; return .{corner = .{left + width - x2 - x1, top + height - y2 - y1}, size = v1};
+
+ case .STRETCH_T; return .{corner = .{left + x1, top + y1}, size = .{width - x1 - x2, y2}};
+ case .STRETCH_B; return .{corner = .{left + x1, top + height - y1 - y2}, size = .{width - x1 - x2, y2}};
+ case .STRETCH_L; return .{corner = .{left + x1, top + y1}, size = .{x2, height - y1 - y2}};
+ case .STRETCH_R; return .{corner = .{left + width - x1 - x2, top + y1}, size = .{x2, height - y1 - y2}};
+
+ case .STRETCH_C; return .{corner = corner + v1, size = size - v1 - v2};
+ case .CENTERIZE; return .{corner = corner + (size - v1) / 2 + v2 , size = v1};
+ }
+ assert(false);
+ return .{};
+}
+
+set_sub_elements :: (group : *UI_Scalable_Group, elements : ..UI_Scalable_Group.Element) {
+ group.elements = elements;
+ for e : elements {
+ e.ptr.parent = group;
+ }
+}
+c_draw_scalable_group :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Scalable_Group) ui_elem;
+ for e : elements {
+ e_zone := get_zone(zone, e.scale_params);
+ if !inside(e_zone, zone) return false;
+ if !c_draw(canvas, e.ptr, e_zone, style) return false;
+ }
+ return true;
+}
+handle_key_scalable_group :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ using cast(*UI_Scalable_Group) ui_elem;
+ assert(cursor_state == .ON || cursor_state == .OUTSIDE);
+
+ handled := false;
+ if cursor_state == .OUTSIDE {
+ assert(xx active_element);
+ {
+ ok := false;
+ for e : elements if e.ptr == active_element ok = true;
+ assert(ok);
+ }
+ handled = handle_key(active_element, key);
+ } else {
+ assert(xx !active_element);
+ }
+ return handled;
+} \ No newline at end of file
diff --git a/kscurses/ui/scene_manager.jai b/kscurses/ui/scene_manager.jai
new file mode 100644
index 0000000..1caff8b
--- /dev/null
+++ b/kscurses/ui/scene_manager.jai
@@ -0,0 +1,48 @@
+UI_Scene :: struct {
+ root : *UI_Elem;
+ entry : *UI_Elem;
+}
+UI_Popup :: struct {
+ root : *UI_Elem;
+ entry : *UI_Elem;
+ size : ivec2 = .{-1, -1};
+}
+
+UI_Scene_Manager :: struct {
+ #as using base_parent : UI_Parent = .{type = .SCENE_MANAGER, box_type = .NONE};
+ scenes : []UI_Scene;
+}
+
+set_sub_elements :: (ui_scene_manager : *UI_Scene_Manager, scenes : ..UI_Scene) {
+ ui_scene_manager.scenes = scenes;
+ for ui_scene_manager.scenes it.root.parent = ui_scene_manager;
+}
+
+handle_key_scene_manager :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ using cast(*UI_Scene_Manager) ui_elem;
+ assert(cursor_state == .ON || cursor_state == .OUTSIDE);
+
+ handled := false;
+ if cursor_state == .OUTSIDE {
+ assert(xx active_element);
+ {
+ ok := false;
+ for s : scenes if s.root == active_element ok = true;
+ assert(ok);
+ }
+ handled = handle_key(active_element, key);
+ } else {
+ assert(xx !active_element);
+ }
+ return handled;
+}
+
+c_draw_scene_manager :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Scene_Manager) ui_elem;
+ return c_draw(canvas, active_element, zone, style);
+}
+
+switch_scene :: (using ui_scene_manager : *UI_Scene_Manager, id : int) {
+ unset_active_recursive(__last_set);
+ set_active_recursive(scenes[id].entry);
+}
diff --git a/kscurses/ui/select_list.jai b/kscurses/ui/select_list.jai
new file mode 100644
index 0000000..8c338c6
--- /dev/null
+++ b/kscurses/ui/select_list.jai
@@ -0,0 +1,79 @@
+UI_Select_List :: struct {
+ #as using base : UI_Elem = .{type = .SELECT_LIST};
+ only_one := true;
+ options : []string;
+ selected : []bool;
+
+ selected_id := -1;
+ cursor, offset := 0, 0;
+
+ prefix_default := "[ ]";
+ prefix_selected := "[+]";
+}
+handle_key_select_list :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ using cast(*UI_Select_List) ui_elem;
+ assert(cursor_state != .OUTSIDE);
+ if cursor_state == .ON {
+ if key == .ENTER {
+ cursor_state = .IN;
+ return true;
+ }
+ } else {
+ if key == {
+ case .DOWN;
+ if cursor < options.count - 1 then cursor += 1;
+ case .UP;
+ if cursor > 0 then cursor -= 1;
+ case .ESCAPE;
+ cursor_state = .ON;
+ case .ENTER;
+ if only_one {
+ if cursor == selected_id {
+ selected_id = -1;
+ } else {
+ selected_id = cursor;
+ }
+ } else {
+ selected[cursor] ^= true;
+ }
+ case;
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+init :: (select_list : *UI_Select_List, only_one := true) {
+ select_list.only_one = only_one;
+ if !only_one select_list.selected = NewArray(select_list.options.count, bool);
+}
+deinit :: (using select_list : *UI_Select_List) {
+ array_free(selected);
+}
+c_draw_select_list :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Select_List) ui_elem;
+ rows := min(cast(int) zone.height, options.count - offset);
+
+ fix_offset :: () #expand {
+ if cursor - offset < 0 {
+ offset = cursor;
+ } else if cursor - offset >= zone.height {
+ offset = cursor - zone.height + 1;
+ }
+ }
+ fix_offset();
+
+ for y : 0..rows-1 {
+ i := y + offset;
+ is_selected := ifx only_one then i == selected_id else selected[i];
+ prefix := ifx is_selected then prefix_selected else prefix_default;
+ mode := ifx i == cursor && cursor_state == .IN
+ ifx is_selected style.text.cursor_and_selection else style.text.cursor
+ else
+ ifx is_selected style.text.selection else style.text.default;
+
+ c_draw_line_ascii(canvas, prefix, zone, .{0, xx y}, mode);
+ c_draw_line_ascii(canvas, options[i], zone, .{xx prefix.count, xx y}, mode);
+ }
+ return true;
+} \ No newline at end of file
diff --git a/kscurses/ui/style.jai b/kscurses/ui/style.jai
new file mode 100644
index 0000000..4715860
--- /dev/null
+++ b/kscurses/ui/style.jai
@@ -0,0 +1,118 @@
+mode_black_and_white :: #run make_graphics_mode(foreground = .BRIGHT_WHITE, background = .BLACK);
+
+Box_Style :: struct {
+ c, tb, lr, tl, tr, bl, br, tbl, tbr, tlr, blr, tblr : u32;
+ mode_border, mode_space, mode_no_border := mode_black_and_white;
+}
+
+box_style_active :: Box_Style.{
+ c = #run utf8(" "),
+ tb = #run utf8("║"),
+ lr = #run utf8("═"),
+ tl = #run utf8("╝"),
+ tr = #run utf8("╚"),
+ bl = #run utf8("╗"),
+ br = #run utf8("╔"),
+ tbl = #run utf8("╣"),
+ tbr = #run utf8("╠"),
+ tlr = #run utf8("╩"),
+ blr = #run utf8("╦"),
+ tblr = #run utf8("╬"),
+ mode_no_border = #run make_graphics_mode(foreground = .BRIGHT_WHITE, background = .BRIGHT_BLACK)
+};
+box_style_passive :: Box_Style.{
+ c = #run utf8(" "),
+ tb = #run utf8("│"),
+ lr = #run utf8("─"),
+ tl = #run utf8("┘"),
+ tr = #run utf8("└"),
+ bl = #run utf8("┐"),
+ br = #run utf8("┌"),
+ tbl = #run utf8("┤"),
+ tbr = #run utf8("├"),
+ tlr = #run utf8("┴"),
+ blr = #run utf8("┬"),
+ tblr = #run utf8("┼")
+};
+
+UI_Style :: struct {
+ box : struct {
+ default := box_style_passive;
+ cursor := box_style_active;
+ }
+
+ mode_main := mode_black_and_white;
+
+ text : struct {
+ default := mode_black_and_white;
+ cursor := #run make_graphics_mode(background = .BRIGHT_BLACK);
+ selection := #run make_graphics_mode(background = .YELLOW);
+ cursor_and_selection := #run make_graphics_mode(background = .BRIGHT_YELLOW);
+
+ debug := #run make_graphics_mode(foreground = .BLACK, background = .BRIGHT_GREEN);
+ }
+}
+
+c_box :: (canvas : *Canvas, zone : Ibox2, using box_style : Box_Style, border := true) -> bool {
+ if !inside(ifx border then ivec2.{2, 2} else ivec2.{0, 0}, zone.size) return false;
+ assert(inside(zone, canvas.zone));
+ charset : [9]u32;
+ if border {
+ charset[0], charset[1], charset[2], charset[3], charset[4], charset[5], charset[6], charset[7], charset[8] = br, lr, bl, tb, c, tb, tr, lr, tl;
+ } else {
+ charset[0], charset[1], charset[2], charset[3], charset[4], charset[5], charset[6], charset[7], charset[8] = c, c, c, c, c, c, c, c, c;
+ }
+
+ for y : 0..zone.height-1 {
+ yi := ifx y == 0 then 0 else ifx y == zone.height - 1 then 2 else 1;
+ for x : 0..zone.width-1 {
+ xi := ifx x == 0 then 0 else ifx x == zone.width - 1 then 2 else 1;
+ i := yi * 3 + xi;
+ c_putchar(canvas, .{code = charset[i], mode = mode_border}, zone.corner + ivec2.{xx x, xx y});
+ }
+ }
+ return true;
+}
+
+b_box :: (builder : *String_Builder, zone : Ibox2, using charset := box_style_passive, mode := Graphics_Mode.{}, clear_center := true, border := true) {
+ //assert(inside(zone, terminal_state.size));
+
+ charset : [9]u32;
+ if border {
+ charset = .[br, lr, bl, tb, c, tb, tr, lr, tl];
+ } else {
+ charset = .[c, c, c, c, c, c, c, c, c];
+ }
+
+ b_move_cursor(builder, zone.corner);
+ b_mode_set(builder, mode);
+
+ for y : 0..zone.height-1 {
+ yi := ifx y == 0 then 0 else ifx y == zone.height - 1 then 2 else 1;
+ b_move_cursor(builder, .{zone.corner.x, zone.corner.y + y});
+
+ if !clear_center && yi == 1 {
+ b_putchar(builder, charset[3]);
+ b_move_cursor(builder, .{zone.corner.x + zone.width - 1, zone.corner.y + y});
+ b_putchar(builder, charset[5]);
+ } else {
+ for x : 0..zone.width-1 {
+ xi := ifx x == 0 then 0 else ifx x == zone.width - 1 then 2 else 1;
+ i := yi * 3 + xi;
+ b_putchar(builder, charset[i]);
+ }
+ }
+
+ }
+}
+t_box :: (zone : Ibox2, charset := box_style_passive, clear_center := false) -> string {
+ builder := String_Builder.{allocator=temp};
+ b_box(*builder, zone, charset, make_graphics_mode(), clear_center);
+ return builder_to_string(*builder, temp);
+}
+ks_box :: (zone : Ibox2, charset := box_style_passive, clear_center := false) {
+ ks_write(t_box(zone, charset, clear_center));
+}
+
+
+
diff --git a/kscurses/ui/table.jai b/kscurses/ui/table.jai
new file mode 100644
index 0000000..dadc1c3
--- /dev/null
+++ b/kscurses/ui/table.jai
@@ -0,0 +1,87 @@
+UI_Table :: struct {
+ #as using base : UI_Elem = .{type = .TABLE};
+
+ // auto_scale := false;
+ // metrics : []int;
+ columns := 0;
+ rows := 0;
+
+ // description : []string;
+ content : [..]string;
+
+ cursor, offset := 0;
+ show_cursor := false;
+}
+
+handle_key_table :: (ui_elem : *UI_Elem, key : Key) -> handled:bool {
+ return false;
+}
+
+c_draw_table_content :: (canvas : *Canvas, using ui_table : *UI_Table, zone : Ibox2, style : *UI_Style, rows_visible : int) -> bool {
+ for y : 0..zone.height-1 {
+ is_selected := y == cursor && show_cursor;
+ mode := ifx is_selected then style.text.selection else style.text.default;
+ separator_char := Char.{code = style.box.default.tb, mode = mode};
+
+ if zone.width < columns {
+ for x : 0..zone.width {
+ c_putchar(canvas, separator_char, zone.corner + ivec2.{xx x, xx y});
+ }
+ } else {
+ i := y + offset;
+ for x : 0..columns-1 {
+ l := (zone.width + 1) * x / columns;
+ r := (zone.width + 1) * (x + 1) / columns - 1;
+ if y < rows_visible {
+ field_content := content[i * columns + x];
+ field_content.count = min(field_content.count, r - l);
+ c_draw_line_ascii(canvas, field_content, zone, .{xx l, xx y}, mode);
+ }
+ if x != columns-1 c_putchar(canvas, separator_char, zone.corner + ivec2.{xx r, xx y});
+ }
+ }
+ }
+ return true;
+}
+
+c_draw_table :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using ui_table := cast(*UI_Table) ui_elem;
+ assert(rows * columns == content.count);
+
+ rows_visible := min(cast(int) zone.height, rows - offset);
+
+ fix_offset :: () #expand {
+ if cursor - offset < 0 {
+ offset = cursor;
+ } else if cursor - offset >= zone.height {
+ offset = cursor - zone.height + 1;
+ }
+ }
+ fix_offset();
+
+ ok := c_draw_table_content(canvas, ui_table, zone, style, rows_visible);
+ return ok;
+}
+init :: (table : *UI_Table, description : []string) {
+ table.columns = description.count;
+ table.description = description;
+}
+init :: (table : *UI_Table, columns : int) {
+ table.columns = columns;
+}
+
+add_line :: (using table : *UI_Table, line : ..string) {
+ assert(line.count == columns);
+ array_add(*content, ..line);
+ rows += 1;
+}
+deinit :: (using table : *UI_Table) {
+ array_free(content);
+}
+
+// TODO
+// variadic columns count, different content types
+// empty separator
+// automatic scale, as option
+// description
+// align content left/right/center \ No newline at end of file
diff --git a/kscurses/ui/text_buf.jai b/kscurses/ui/text_buf.jai
new file mode 100644
index 0000000..689bb98
--- /dev/null
+++ b/kscurses/ui/text_buf.jai
@@ -0,0 +1,15 @@
+UI_Text_Buf :: struct {
+ #as using base : UI_Elem = .{type = .TEXT_BUF};
+ lines : []string;
+ lines_dynamic : *[]string;
+}
+c_draw_textbuf :: (canvas : *Canvas, ui_elem : *UI_Elem, zone : Ibox2, style : *UI_Style) -> bool {
+ using cast(*UI_Text_Buf) ui_elem;
+
+ lines_to_draw := ifx lines_dynamic then <<lines_dynamic else lines;
+ for l, y : lines_to_draw {
+ if y >= zone.height break;
+ c_draw_line_ascii(canvas, l, zone, .{0, xx y}, style.text.default);
+ }
+ return true;
+}
diff --git a/kscurses/ui/tilemap.jai b/kscurses/ui/tilemap.jai
new file mode 100644
index 0000000..73f96e5
--- /dev/null
+++ b/kscurses/ui/tilemap.jai
@@ -0,0 +1,20 @@
+UI_Tilemap :: struct {
+ #as using base : UI_Elem = .{type = .TILEMAP};
+
+ map_size : ivec2;
+ map : []u64;
+ tileset : *Tileset;
+
+ background : Char; // if not set, then box's filler instead
+}
+Tileset :: struct {
+ tile_size : ivec2;
+ tiles_count : int;
+ data : []Char;
+ // code = 0 -> skip entire character
+ // fcol/bcol = .DEFAULT -> skip foreground/background
+}
+c_draw_tile :: (canvas : *Canvas, tileset : *Tileset, offset : ivec2, id : u64, cutoff : Ibox2) {
+
+}
+
diff --git a/kscurses/utils.jai b/kscurses/utils.jai
new file mode 100644
index 0000000..2b88e85
--- /dev/null
+++ b/kscurses/utils.jai
@@ -0,0 +1,53 @@
+// array_slice :: (array : []$T, left : int, right : int) -> []T {
+// assert(left <= right && left >= 0 && right <= array.count);
+// result : []T;
+// result.data, result.count = array.data + left, right - left;
+// return result;
+// }
+
+length_code :: inline (c : u64) -> u8 {
+ for 0..7 {
+ if !c return xx it;
+ c >>= 8;
+ }
+ return 8;
+}
+utf8 :: (str : string) -> u32 {
+ code : u32;
+ assert(str.count <= 4);
+ memcpy(*code, str.data, str.count);
+ return code;
+}
+byteswap_64 :: (c : u64) -> u64 {
+ c = ((0xFFFFFFFF00000000 & c) >> 32) | ((0x00000000FFFFFFFF & c) << 32);
+ c = ((0xFFFF0000FFFF0000 & c) >> 16) | ((0x0000FFFF0000FFFF & c) << 16);
+ c = ((0xFF00FF00FF00FF00 & c) >> 8) | ((0x00FF00FF00FF00FF & c) << 8);
+ return c;
+}
+byteswap_32 :: (c : u32) -> u32 {
+ c = ((0xFFFF0000 & c) >> 16) | ((0x0000FFFF & c) << 16);
+ c = ((0xFF00FF00 & c) >> 8) | ((0x00FF00FF & c) << 8);
+ return c;
+}
+char_bs :: (code : u32) -> u32 {
+ l := length_code(code);
+ return byteswap_32(code) >> ((8 - l) * 8);
+}
+utf8_bs :: (str : string) -> u32 {
+ return char_bs(utf8(str));
+}
+arrow_code_to_ivec2 :: (key : Key, $swap_y := true) -> ivec2 {
+ #if swap_y {
+ return ifx key == .UP ivec2.{ 0, -1}
+ else ifx key == .RIGHT ivec2.{ 1, 0}
+ else ifx key == .DOWN ivec2.{ 0, 1}
+ else ifx key == .LEFT ivec2.{-1, 0}
+ else ivec2.{};
+ } else {
+ return ifx key == .UP ivec2.{ 0, 1}
+ else ifx key == .RIGHT ivec2.{ 1, 0}
+ else ifx key == .DOWN ivec2.{ 0, -1}
+ else ifx key == .LEFT ivec2.{-1, 0}
+ else ivec2.{};
+ }
+}
diff --git a/kscurses/vectors.jai b/kscurses/vectors.jai
new file mode 100644
index 0000000..f27d572
--- /dev/null
+++ b/kscurses/vectors.jai
@@ -0,0 +1,210 @@
+Generic_Vector :: struct(type : Type, N : int) {
+ #assert N > 1;
+ #if N < 5 {
+ x, y : type;
+ #if N > 2 {
+ z : type;
+ #place x; xy : Generic_Vector(type, 2);
+ #place y; yz : Generic_Vector(type, 2);
+ #place x; r : type;
+ #place y; g : type;
+ #place z; b : type;
+ }
+ #if N > 3 {
+ w : type;
+ #place z; zw : Generic_Vector(type, 2);
+ #place x; xyz : Generic_Vector(type, 3);
+ #place y; yzw : Generic_Vector(type, 3);
+ #place w; a : type;
+ }
+ #place x;
+ }
+ values : [N]type;
+}
+generic_vector_binary_proc_vv :: (v1 : Generic_Vector($type, $N), v2 : Generic_Vector(type, N), proc : (s1 : type, s2 : type) -> (type)) -> Generic_Vector(type, N) {
+ result : Generic_Vector(type, N) = ---;
+ for i : 0..N-1 result.values[i] = proc(v1.values[i], v2.values[i]);
+ return result;
+}
+generic_vector_binary_proc_vs :: (v : Generic_Vector($type, $N), s : type, proc : (s1 : type, s2 : type) -> (type)) -> Generic_Vector(type, N) {
+ result : Generic_Vector(type, N) = ---;
+ for i : 0..N-1 result.values[i] = proc(v.values[i], s);
+ return result;
+}
+generic_vector_binary_proc_sv :: (s : type, v : Generic_Vector($type, $N), proc : (s1 : type, s2 : type) -> (type)) -> Generic_Vector(type, N) {
+ result : Generic_Vector(type, N) = ---;
+ for i : 0..N-1 result.values[i] = proc(s, v.values[i]);
+ return result;
+}
+generic_vector_unary_proc :: (v : Generic_Vector($type, $N), proc : (s : type) -> (type)) -> Generic_Vector(type, N) {
+ result : Generic_Vector(type, N) = ---;
+ for i : 0..N-1 result.values[i] = proc(v.values[i]);
+ return result;
+}
+
+operator- :: #bake_arguments generic_vector_unary_proc(proc = (s) => -s);
+
+operator+ :: #bake_arguments generic_vector_binary_proc_vv(proc = (s1, s2) => s1 + s2);
+operator- :: #bake_arguments generic_vector_binary_proc_vv(proc = (s1, s2) => s1 - s2);
+operator/ :: #bake_arguments generic_vector_binary_proc_vv(proc = (s1, s2) => s1 / s2);
+operator* :: #bake_arguments generic_vector_binary_proc_vv(proc = (s1, s2) => s1 * s2);
+
+operator+ :: #bake_arguments generic_vector_binary_proc_sv(proc = (s1, s2) => s1 + s2);
+operator- :: #bake_arguments generic_vector_binary_proc_sv(proc = (s1, s2) => s1 - s2);
+operator/ :: #bake_arguments generic_vector_binary_proc_sv(proc = (s1, s2) => s1 / s2);
+operator* :: #bake_arguments generic_vector_binary_proc_sv(proc = (s1, s2) => s1 * s2);
+
+operator+ :: #bake_arguments generic_vector_binary_proc_vs(proc = (s1, s2) => s1 + s2);
+operator- :: #bake_arguments generic_vector_binary_proc_vs(proc = (s1, s2) => s1 - s2);
+operator/ :: #bake_arguments generic_vector_binary_proc_vs(proc = (s1, s2) => s1 / s2);
+operator* :: #bake_arguments generic_vector_binary_proc_vs(proc = (s1, s2) => s1 * s2);
+
+operator== :: (v1 : Generic_Vector($type, $N), v2 : Generic_Vector(type, N)) -> bool {
+ for i : 0..N-1 {
+ if v1.values[i] != v2.values[i] return false;
+ }
+ return true;
+}
+
+// affects only on naming
+Generic_Box :: struct(type : Type, N : int, left_handed := false) {
+ #assert N > 1;
+ #if N <= 3 {
+ width, height : type;
+ #if N == 3 { length : type; }
+
+ #if left_handed { right : type; } else { left : type; }
+ top : type;
+ #if N == 3 { front : type; }
+ #place width;
+ }
+ size, corner : Generic_Vector(type, N);
+}
+operator== :: (a : Generic_Box($type, $N, $left_handed), b : Generic_Box(type, N, left_handed)) -> bool {
+ return a.size == b.size && a.corner == b.corner;
+}
+is_empty :: (box : Generic_Box) -> bool {
+ return box.size == .{0, 0};
+}
+is_invalid :: (box : Generic_Box) -> bool {
+ return box.width < 0 || box.height < 0;
+}
+point_inside :: (point : Generic_Vector($type, $N), zone : Generic_Box(type, N)) -> bool {
+ for i : 0..N-1 {
+ p, l, w := point.values[i], zone.corner.values[i], zone.size.values[i];
+ if p < l return false;
+ if p > l + w return false;
+ }
+ return true;
+}
+inside :: (inner : Generic_Box($type, $N, $left_handed), outer : Generic_Box(type, N, left_handed)) -> bool {
+ return point_inside(inner.corner, outer) && point_inside(inner.corner + inner.size, outer);
+}
+inside :: (inner : Generic_Box($type, $N, $left_handed), outer : Generic_Vector(type, N)) -> bool {
+ return inside(inner, Generic_Box(type, N, left_handed).{size = outer});
+}
+inside :: (inner : Generic_Vector($type, $N), outer : Generic_Vector(type, N)) -> bool {
+ for i : 0..N-1 {
+ if inner.values[i] > outer.values[i] return false;
+ }
+ return true;
+}
+cut_border :: (box : Generic_Box($type, $N, $left_handed), gap : type) -> Generic_Box(type, N, left_handed) {
+ result : Generic_Box(type, N, left_handed) = ---;
+ for i : 0..N-1 {
+ result.corner.values[i] = box.corner.values[i] + gap;
+ result.size.values[i] = box.size.values[i] - gap * 2;
+ }
+ return result;
+}
+fit_in_center :: (zone : Generic_Box($type, $N, $left_handed), box_size : Generic_Vector(type, N)) -> Generic_Box(type, N, left_handed), fit:bool {
+ return .{size = box_size, corner = zone.corner + (zone.size - zone.corner) / 2}, inside(box_size, zone.size);
+}
+intersection :: (box1 : Generic_Box($type, $N, $left_handed), box2 : Generic_Box(type, N, left_handed)) -> Ibox2 {
+ result : Generic_Box(type, N, left_handed) = ---;
+ for i : 0..N-1 {
+ l1, w1 := box1.corner.values[i], box1.size.values[i];
+ l2, w2 := box2.corner.values[i], box2.size.values[i];
+ result.corner.values[i] = max(l1, l2);
+ result.size.values[i] = min(l1 + w1, l2 + w2);
+ }
+ return result;
+}
+
+cast_vec :: ($type_out : Type, in : Generic_Vector($type_in, $N)) -> Generic_Vector(type_out, N) {
+ result : Generic_Vector(type_out, N) = ---;
+ for i : 0..N-1 {
+ result.values[i] = xx in.values[i];
+ }
+ return result;
+}
+
+fract :: (x : float) -> float { return x - floor(x); }
+fract :: (using v : Vector2) -> Vector2 { return .{fract(x), fract(y)}; }
+fract :: (using v : Vector3) -> Vector3 { return .{fract(x), fract(y), fract(z)}; }
+fract :: (using v : Vector4) -> Vector4 { return .{fract(x), fract(y), fract(z), fract(w)}; }
+
+floor :: (v : Vector2) -> Vector2 { return .{floor(v.x), floor(v.y)}; }
+floor :: (v : Vector3) -> Vector3 { return .{floor(v.x), floor(v.y), floor(v.z)}; }
+floor :: (v : Vector4) -> Vector4 { return .{floor(v.x), floor(v.y), floor(v.z), floor(v.w)}; }
+
+mix :: (x : float, y : float, m : float) -> float { return x + (y - x) * m; }
+mix :: (x : Vector2, y : Vector2, m : float) -> Vector2 { return .{mix(x.x, y.x, m), mix(x.y, y.y, m)}; }
+mix :: (x : Vector3, y : Vector3, m : float) -> Vector3 { return .{mix(x.x, y.x, m), mix(x.y, y.y, m), mix(x.z, y.z, m)}; }
+mix :: (x : Vector4, y : Vector4, m : float) -> Vector4 { return .{mix(x.x, y.x, m), mix(x.y, y.y, m), mix(x.z, y.z, m), mix(x.w, y.w, m)}; }
+
+mix :: (x : Vector2, y : Vector2, m : Vector2) -> Vector2 { return .{mix(x.x, y.x, m.x), mix(x.y, y.y, m.y)}; }
+mix :: (x : Vector3, y : Vector3, m : Vector3) -> Vector3 { return .{mix(x.x, y.x, m.x), mix(x.y, y.y, m.y), mix(x.z, y.z, m.z)}; }
+mix :: (x : Vector4, y : Vector4, m : Vector4) -> Vector4 { return .{mix(x.x, y.x, m.x), mix(x.y, y.y, m.y), mix(x.z, y.z, m.z), mix(x.w, y.w, m.z)}; }
+
+clamp :: (a : Vector2, mi : float, ma : float) -> Vector2 { return .{clamp(a.x, mi, ma), clamp(a.y, mi, ma)}; }
+clamp :: (a : Vector3, mi : float, ma : float) -> Vector3 { return .{clamp(a.x, mi, ma), clamp(a.y, mi, ma), clamp(a.z, mi, ma)}; }
+clamp :: (a : Vector4, mi : float, ma : float) -> Vector4 { return .{clamp(a.x, mi, ma), clamp(a.y, mi, ma), clamp(a.z, mi, ma), clamp(a.w, mi, ma)}; }
+
+
+sin :: (v : Vector2) -> Vector2 { return .{sin(v.x), sin(v.y)}; }
+sin :: (v : Vector3) -> Vector3 { return .{sin(v.x), sin(v.y), sin(v.z)}; }
+sin :: (v : Vector4) -> Vector4 { return .{sin(v.x), sin(v.y), sin(v.z), sin(v.w)}; }
+
+hash22 :: (_p : Vector2, seed := Vector2.{}) -> Vector2 {
+ p := multiply(Matrix2.{127.1, 311.7, 269.5, 183.3}, _p);
+ p = Vector2.{-1., -1.} + 2. * fract(sin(p) * 43758.545);
+ return sin(p * 6.283 + seed * Vector2.{124.1, 8123.1});
+}
+
+perlin_level :: (p : Vector2) -> float {
+ pi := floor(p);
+ pf := p - pi;
+ w := pf * pf * (Vector2.{3., 3.} - Vector2.{2., 2.} * pf);
+
+ f00 := dot(hash22(pi + Vector2.{0., 0.}), pf - Vector2.{0., 0.});
+ f01 := dot(hash22(pi + Vector2.{0., 1.}), pf - Vector2.{0., 1.});
+ f10 := dot(hash22(pi + Vector2.{1., 0.}), pf - Vector2.{1., 0.});
+ f11 := dot(hash22(pi + Vector2.{1., 1.}), pf - Vector2.{1., 1.});
+
+ return mix(mix(f00, f10, w.x), mix(f01, f11, w.x), w.y);
+}
+perlin :: (_p : Vector2) -> float {
+ p := _p;
+ M1 :: 4;
+ a, r, s := 1., 0., 0.;
+ for i : 0..M1 - 1 {
+ r += a * perlin_level(p);
+ s += a;
+ p *= 2.;
+ a *= .5;
+ }
+ return r / s;
+}
+
+hsv2rgb :: (c : Vector3) -> Vector3 {
+ K := Vector4.{1, 2. / 3, 1. / 3, 3};
+ p := abs(fract(Vector3.{c.x, c.x, c.x} + K.xyz) * 6.0 - Vector3.{K.w, K.w, K.w});
+ return c.z * mix(Vector3.{K.x, K.x, K.x}, clamp(p - Vector3.{K.x, K.x, K.x}, 0, 1), c.y);
+}
+
+
+ivec2 :: Generic_Vector(s32, 2);
+ivec3 :: Generic_Vector(s32, 3);
+u8vec3 :: Generic_Vector(u8, 3);
+Ibox2 :: Generic_Box(s32, 2, false);
diff --git a/test.jai b/test.jai
new file mode 100644
index 0000000..7d03193
--- /dev/null
+++ b/test.jai
@@ -0,0 +1,45 @@
+#import "Basic";
+#import "kscurses";
+
+main :: () {
+
+ // use_ks_curses();
+ // run_singlethread_ui();
+ pos: ivec2 = .{};
+
+ ks_init();
+ key: Key = 0;
+ mode := make_graphics_mode();
+ while(key != .CTRL_C && key != #char "q") {
+ // ks_print :: inline (coord : ivec2, mode : Graphics_Mode, fmt : string, args : ..Any) {
+ mode.attr_flags |= .F_BOLD;
+ ks_print(pos, mode, " wow % wow", 333);
+ mode.attr_flags &= ~.F_BOLD;
+ new_pos := pos;
+ new_pos.x += 6;
+ ks_print(new_pos, mode, "∞");
+ // print("BAZINGA\n");
+ key = ks_getch();
+ if key == {
+ case .UP; pos.y -= 1;
+ case .DOWN; pos.y += 1;
+ case .LEFT; pos.x -= 1;
+ case .RIGHT; pos.x += 1;
+ }
+ if pos.y < 0 then pos.y = 0;
+ if pos.x < 0 then pos.x = 0;
+ ks_move_cursor(pos);
+ ks_clear_screen();
+
+ _x := 80;
+ _y := 24;
+ box: Ibox2 = .{};
+ box.width = xx _x;
+ box.height = xx _y;
+ box.size = .{x = xx _x, y = xx _y};
+ box.corner = .{x=0, y=0};
+ ks_box(box);
+ }
+ ks_terminate();
+ if key == .CTRL_C then print("WOWOWOWOWOWO\n"); // TODO CTRL_C exits without passing here.
+}