aboutsummaryrefslogtreecommitdiff
path: root/kscurses/ui
diff options
context:
space:
mode:
Diffstat (limited to 'kscurses/ui')
-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
16 files changed, 1136 insertions, 0 deletions
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) {
+
+}
+