From 709879ee56d31fe543a0ad882713bd4e3d17d2d2 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 17 Aug 2023 20:28:47 +0100 Subject: Added kscurses and testing program. --- kscurses/canvas.jai | 172 +++++++++++++++++++++++++++ kscurses/events.jai | 256 +++++++++++++++++++++++++++++++++++++++++ kscurses/history_stack.jai | 69 +++++++++++ kscurses/init.jai | 125 ++++++++++++++++++++ kscurses/io.jai | 81 +++++++++++++ kscurses/lambdas.jai | 255 ++++++++++++++++++++++++++++++++++++++++ kscurses/modes.jai | 179 ++++++++++++++++++++++++++++ kscurses/module.jai | 91 +++++++++++++++ kscurses/print.jai | 148 ++++++++++++++++++++++++ kscurses/queue.jai | 57 +++++++++ kscurses/readme.md | 43 +++++++ kscurses/ui/button.jai | 25 ++++ kscurses/ui/element.jai | 156 +++++++++++++++++++++++++ kscurses/ui/group.jai | 44 +++++++ kscurses/ui/line_input.jai | 122 ++++++++++++++++++++ kscurses/ui/links.jai | 94 +++++++++++++++ kscurses/ui/master.jai | 105 +++++++++++++++++ kscurses/ui/parent.jai | 33 ++++++ kscurses/ui/popup_manager.jai | 65 +++++++++++ kscurses/ui/progress_bar.jai | 37 ++++++ kscurses/ui/scalable_group.jai | 88 ++++++++++++++ kscurses/ui/scene_manager.jai | 48 ++++++++ kscurses/ui/select_list.jai | 79 +++++++++++++ kscurses/ui/style.jai | 118 +++++++++++++++++++ kscurses/ui/table.jai | 87 ++++++++++++++ kscurses/ui/text_buf.jai | 15 +++ kscurses/ui/tilemap.jai | 20 ++++ kscurses/utils.jai | 53 +++++++++ kscurses/vectors.jai | 210 +++++++++++++++++++++++++++++++++ 29 files changed, 2875 insertions(+) create mode 100644 kscurses/canvas.jai create mode 100644 kscurses/events.jai create mode 100644 kscurses/history_stack.jai create mode 100644 kscurses/init.jai create mode 100644 kscurses/io.jai create mode 100644 kscurses/lambdas.jai create mode 100644 kscurses/modes.jai create mode 100644 kscurses/module.jai create mode 100644 kscurses/print.jai create mode 100644 kscurses/queue.jai create mode 100644 kscurses/readme.md create mode 100644 kscurses/ui/button.jai create mode 100644 kscurses/ui/element.jai create mode 100644 kscurses/ui/group.jai create mode 100644 kscurses/ui/line_input.jai create mode 100644 kscurses/ui/links.jai create mode 100644 kscurses/ui/master.jai create mode 100644 kscurses/ui/parent.jai create mode 100644 kscurses/ui/popup_manager.jai create mode 100644 kscurses/ui/progress_bar.jai create mode 100644 kscurses/ui/scalable_group.jai create mode 100644 kscurses/ui/scene_manager.jai create mode 100644 kscurses/ui/select_list.jai create mode 100644 kscurses/ui/style.jai create mode 100644 kscurses/ui/table.jai create mode 100644 kscurses/ui/text_buf.jai create mode 100644 kscurses/ui/tilemap.jai create mode 100644 kscurses/utils.jai create mode 100644 kscurses/vectors.jai (limited to 'kscurses') 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 { + < 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 { + // < 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 { + < 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 := < *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); + < 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); + < 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 < 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 <= 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); -- cgit v1.2.3