diff options
| author | dam <dam@gudinoff> | 2023-08-17 20:28:47 +0100 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2023-08-17 20:28:47 +0100 |
| commit | 709879ee56d31fe543a0ad882713bd4e3d17d2d2 (patch) | |
| tree | 12a35282bdd0f1f8a2159ade147944c89254db24 /kscurses/ui | |
| parent | fa1b8ea54646f1a0f3eadef33e3a660b875cc1ff (diff) | |
| download | task-time-tracker-709879ee56d31fe543a0ad882713bd4e3d17d2d2.tar.zst task-time-tracker-709879ee56d31fe543a0ad882713bd4e3d17d2d2.zip | |
Added kscurses and testing program.
Diffstat (limited to 'kscurses/ui')
| -rw-r--r-- | kscurses/ui/button.jai | 25 | ||||
| -rw-r--r-- | kscurses/ui/element.jai | 156 | ||||
| -rw-r--r-- | kscurses/ui/group.jai | 44 | ||||
| -rw-r--r-- | kscurses/ui/line_input.jai | 122 | ||||
| -rw-r--r-- | kscurses/ui/links.jai | 94 | ||||
| -rw-r--r-- | kscurses/ui/master.jai | 105 | ||||
| -rw-r--r-- | kscurses/ui/parent.jai | 33 | ||||
| -rw-r--r-- | kscurses/ui/popup_manager.jai | 65 | ||||
| -rw-r--r-- | kscurses/ui/progress_bar.jai | 37 | ||||
| -rw-r--r-- | kscurses/ui/scalable_group.jai | 88 | ||||
| -rw-r--r-- | kscurses/ui/scene_manager.jai | 48 | ||||
| -rw-r--r-- | kscurses/ui/select_list.jai | 79 | ||||
| -rw-r--r-- | kscurses/ui/style.jai | 118 | ||||
| -rw-r--r-- | kscurses/ui/table.jai | 87 | ||||
| -rw-r--r-- | kscurses/ui/text_buf.jai | 15 | ||||
| -rw-r--r-- | kscurses/ui/tilemap.jai | 20 |
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) { + +} + |
