diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/apps/example_app.jai | 33 | ||||
| -rw-r--r-- | src/apps/maze_app.jai | 257 | ||||
| -rw-r--r-- | src/debug_overlay.jai | 48 | ||||
| -rw-r--r-- | src/main.jai | 207 | ||||
| -rw-r--r-- | src/menu_app.jai | 217 | ||||
| -rw-r--r-- | src/ui_panel.jai | 252 | ||||
| -rw-r--r-- | src/widget_interface.jai | 12 |
7 files changed, 1026 insertions, 0 deletions
diff --git a/src/apps/example_app.jai b/src/apps/example_app.jai new file mode 100644 index 0000000..2298fb7 --- /dev/null +++ b/src/apps/example_app.jai @@ -0,0 +1,33 @@ +#import "Basic"; +#import "Math"; + +Example_Data :: struct { + timer: float; +} + +init_example_widget :: (widget: *Widget_State) { + data := New(Example_Data); + data.timer = 0; + widget.data = data; + + widget.init = (app: *Widget_State) {}; + + widget.update = (widget: *Widget_State, dt: float) { + data := cast(*Example_Data) widget.data; + data.timer += dt; + }; + + widget.handle_input = (widget: *Widget_State, occluded: bool) -> (occludes: bool) {return false;}; + + widget.render = (widget: *Widget_State) { + data := cast(*Example_Data) widget.data; + + // Pulse background color to red to show it's updating and distinct from menu + r := 0.5 + 0.5 * sin(data.timer * 2.0); + Simp.clear_render_target(r, 0.1, 0.1, 1.0); + }; + + widget.shutdown = (widget: *Widget_State) { + free(widget.data); + }; +} diff --git a/src/apps/maze_app.jai b/src/apps/maze_app.jai new file mode 100644 index 0000000..b3b588c --- /dev/null +++ b/src/apps/maze_app.jai @@ -0,0 +1,257 @@ +#import "Basic"; +#import "Math"; +#import "String"; + +Maze_Data :: struct { + rows: int; + cols: int; + grid: [..] bool; // false = empty, true = wall + + // Camera + camera_x: float; + camera_y: float; + zoom: float; + + // Panning State + is_panning: bool; + last_mouse_x: float; + last_mouse_y: float; + + // UI State + width_input: [..] u8; + height_input: [..] u8; + + font: *Simp.Dynamic_Font; + panel: UI_Panel; +} + +init_maze_app :: (widget: *Widget_State) { + data := New(Maze_Data); + data.rows = 10; + data.cols = 10; + array_resize(*data.grid, data.rows * data.cols); + for 0..data.grid.count-1 { data.grid[it] = false; } + + data.camera_x = cast(float) window_width / 2.0 - (5.0 * 50.0); + data.camera_y = cast(float) window_height / 2.0 - (5.0 * 50.0); + data.zoom = 50.0; // 50 pixels per cell + data.is_panning = false; + + // prefill text inputs + // TODO reconsider if the UI element should just manage this directly and parse the given value into backing string buffer + str_w := sprint("%", data.cols); + defer free(str_w); + for 0..str_w.count-1 array_add(*data.width_input, str_w[it]); + + str_h := sprint("%", data.rows); + defer free(str_h); + for 0..str_h.count-1 array_add(*data.height_input, str_h[it]); + + // Setup UI Panel + data.panel.x = 20.0; + data.panel.y = cast(float)window_height - 300.0; + data.panel.w = 200.0; + data.panel.h = 240.0; + data.panel.userdata = data; // Give callback access to our data! + data.panel.app_owner = widget; // Bind panel visibility to this app! + + array_add(*data.panel.elements, .{type=.HEADER, id=5, x0=0.0, y0=data.panel.h - 40.0, x1=data.panel.w, y1=data.panel.h, label="Controls"}); + + // Inputs (from top to bottom, note Simp Y=0 is bottom, so highest Y is top) + array_add(*data.panel.elements, .{type=.TEXT_INPUT, id=1, x0=10.0, y0=data.panel.h - 100.0, x1=190.0, y1=data.panel.h - 60.0, label="Width:", input_buf=*data.width_input}); + array_add(*data.panel.elements, .{type=.TEXT_INPUT, id=2, x0=10.0, y0=data.panel.h - 160.0, x1=190.0, y1=data.panel.h - 120.0, label="Height:", input_buf=*data.height_input}); + + // Back button at bottom + array_add(*data.panel.elements, .{type=.BUTTON, id=4, x0=10.0, y0=10.0, x1=190.0, y1=50.0, label="Back", action=(userdata: *void) { switch_app(*menu_state); }}); + + widget.data = data; + + widget.init = (widget: *Widget_State) { + data := cast(*Maze_Data) widget.data; + data.panel.font = Simp.get_font_at_size("data", "Anonymous_Pro.ttf", 32); + data.font = data.panel.font; + ui_register_panel(*data.panel); + }; + + widget.update = (widget: *Widget_State, dt: float) { + data := cast(*Maze_Data) widget.data; + + // maze gen / solving step happens in here maybe? + }; + + widget.shutdown = (widget: *Widget_State) { + data := cast(*Maze_Data) widget.data; + ui_unregister_panel(*data.panel); + }; + + widget.update = (widget: *Widget_State, dt: float) {}; + widget.handle_input = handle_input; + widget.render = render; + + widget.shutdown = (widget: *Widget_State) { + data := cast(*Maze_Data) widget.data; + array_free(data.grid); + array_free(data.width_input); + array_free(data.height_input); + free(data); + }; +} + +#scope_file + +apply_resize :: (data: *Maze_Data) { + w_str: string; + w_str.data = data.width_input.data; + w_str.count = data.width_input.count; + + h_str: string; + h_str.data = data.height_input.data; + h_str.count = data.height_input.count; + + new_cols, ok_w := string_to_int(w_str); + new_rows, ok_h := string_to_int(h_str); + + if ok_w && ok_h && new_cols > 0 && new_rows > 0 { + new_grid: [..] bool; + array_resize(*new_grid, new_rows * new_cols); + for r: 0..new_rows-1 { + for c: 0..new_cols-1 { + val := false; + if r < data.rows && c < data.cols { + val = data.grid[r * data.cols + c]; + } + new_grid[r * new_cols + c] = val; + } + } + array_free(data.grid); + data.grid = new_grid; + data.rows = new_rows; + data.cols = new_cols; + } +} + +handle_input :: (widget: *Widget_State, occluded: bool) -> (occludes: bool) { + data := cast(*Maze_Data) widget.data; + + mouse_x, mouse_y, success := get_mouse_pointer_position(window, true); + if !success return occluded; + mouse_x_f := cast(float) mouse_x; + mouse_y_f := cast(float) mouse_y; + + space_state := Input.input_button_states[Input.Key_Code.SPACEBAR]; + space_down := (space_state & Input.Key_Current_State.DOWN) != 0; + + for event: Input.events_this_frame { + if event.type == .MOUSE_WHEEL { + zoom_factor := 1.0; + if event.wheel_delta < 0 { + zoom_factor = 0.9; + } else if event.wheel_delta > 0 { + zoom_factor = 1.1; + } + if zoom_factor != 1.0 { + world_x := (mouse_x_f - data.camera_x) / data.zoom; + world_y := (mouse_y_f - data.camera_y) / data.zoom; + + data.zoom *= zoom_factor; + if data.zoom < 5.0 data.zoom = 5.0; + if data.zoom > 300.0 data.zoom = 300.0; + + data.camera_x = mouse_x_f - world_x * data.zoom; + data.camera_y = mouse_y_f - world_y * data.zoom; + } + } + + // if event.type == .TEXT_INPUT && data.panel.focus != 0 { + // print("we are text inputting into panel %", data.panel.focus); + // value := event.utf32; + // if value >= #char "0" && value <= #char "9" { + // if data.panel.focus == 1 array_add(*data.width_input, cast(u8)value); + // if data.panel.focus == 2 array_add(*data.height_input, cast(u8)value); + // } + // } + + if event.type == .KEYBOARD && event.key_pressed { + if event.key_code == .ENTER { + if data.panel.focus != 0 { + apply_resize(data); + data.panel.focus = 0; + } + } + + // Mouse clicks + if event.key_code == .MOUSE_BUTTON_LEFT { + if !occluded { + // data.panel.focus = 0; + + // Clicked on grid (only if not panning with spacebar) + if !space_down { + world_x := (mouse_x_f - data.camera_x) / data.zoom; + world_y := (mouse_y_f - data.camera_y) / data.zoom; + col := cast(int) world_x; + row := cast(int) world_y; + + // Check if within bounds + if col >= 0 && col < data.cols && row >= 0 && row < data.rows { + data.grid[row * data.cols + col] = !data.grid[row * data.cols + col]; + } + } + } + } + } + } + + mouse_down := (Input.input_button_states[Input.Key_Code.MOUSE_BUTTON_LEFT] & Input.Key_Current_State.DOWN) != 0; + + // Panning logic + if space_down && mouse_down { + if !data.is_panning { + data.is_panning = true; + data.last_mouse_x = mouse_x_f; + data.last_mouse_y = mouse_y_f; + } else { + dx := mouse_x_f - data.last_mouse_x; + dy := mouse_y_f - data.last_mouse_y; + data.camera_x += dx; + data.camera_y += dy; + data.last_mouse_x = mouse_x_f; + data.last_mouse_y = mouse_y_f; + } + } else { + data.is_panning = false; + } + + return false; +} + +render :: (widget: *Widget_State) { + data := cast(*Maze_Data) widget.data; + + Simp.clear_render_target(0.1, 0.1, 0.1, 1.0); + Simp.set_shader_for_color(true); + Simp.immediate_begin(); + Simp.immediate_set_2d_projection(window_width, window_height); + + + // Draw cells + for r: 0..data.rows-1 { + for c: 0..data.cols-1 { + x0 := data.camera_x + cast(float) c * data.zoom; + y0 := data.camera_y + cast(float) r * data.zoom; + + cx0 := x0 + 1.0; + cy0 := y0 + 1.0; + cx1 := x0 + data.zoom - 1.0; + cy1 := y0 + data.zoom - 1.0; + + // Culling optimization + if cx1 < 0 || cx0 > cast(float)window_width || cy1 < 0 || cy0 > cast(float)window_height continue; + + is_wall := data.grid[r * data.cols + c]; + color := ifx is_wall then Vector4.{0.8, 0.2, 0.2, 1.0} else Vector4.{0.6, 0.6, 0.6, 1.0}; + Simp.immediate_quad(cx0, cy0, cx1, cy1, color); + } + } + + // UI_Panel drawing is entirely handled by the global Window Manager in main.jai! +} diff --git a/src/debug_overlay.jai b/src/debug_overlay.jai new file mode 100644 index 0000000..90adc87 --- /dev/null +++ b/src/debug_overlay.jai @@ -0,0 +1,48 @@ +// Loaded into main.jai so it inherits global imports. + +#scope_file +debug_lines: [..] string; +debug_panel: UI_Panel; +panel_initialized := false; + +#scope_export + +debug_add_value :: (fmt: string, args: .. Any) { + builder: String_Builder; + print_to_builder(*builder, fmt, ..args); + array_add(*debug_lines, builder_to_string(*builder)); +} + +render_debug_overlay :: (font: *Simp.Dynamic_Font) { + if !font return; + + if !panel_initialized { + debug_panel.x = cast(float)window_width - 800.0; + debug_panel.y = cast(float)window_height - 40.0; + debug_panel.w = 300.0; + debug_panel.h = 40.0; + debug_panel.font = font; + ui_register_panel(*debug_panel); + panel_initialized = true; + } + + target_h := 50.0 + debug_lines.count * 32.0; + top := debug_panel.y + debug_panel.h; + debug_panel.h = target_h; + debug_panel.y = top - debug_panel.h; + + array_reset_keeping_memory(*debug_panel.elements); + + array_add(*debug_panel.elements, .{type=.HEADER, id=0, x0=0.0, y0=debug_panel.h - 40.0, x1=debug_panel.w, y1=debug_panel.h, label="Debug"}); + + y := debug_panel.h - 40.0; + for debug_lines { + y -= 32.0; + array_add(*debug_panel.elements, .{type=.DYNAMIC_TEXT, id=0, x0=10.0, y0=y, x1=debug_panel.w, y1=y+32.0, label=it}); + } +} + +clear_debug_overlay :: () { + for debug_lines free(it); + array_reset_keeping_memory(*debug_lines); +} diff --git a/src/main.jai b/src/main.jai new file mode 100644 index 0000000..897cfb7 --- /dev/null +++ b/src/main.jai @@ -0,0 +1,207 @@ +#import "Basic"; +#import "Window_Creation"; +Simp :: #import "Simp"; +Input :: #import "Input"; +Unicode :: #import "Unicode"; + +#load "widget_interface.jai"; +#load "menu_app.jai"; +#load "apps/example_app.jai"; +#load "apps/maze_app.jai"; +#load "ui_panel.jai"; +#load "debug_overlay.jai"; + +// Global reference to active app state +active_app: *Widget_State; + +// The different apps available +menu_state: Widget_State; +example_state: Widget_State; +maze_state: Widget_State; + +window: Window_Type; +window_width : s32 = 1920; +window_height : s32 = 1080; + +// silly +FRAMES :: 32_768; +frame_times : [FRAMES] float; +frame_times_sum: float; +frame_times_cursor: int; + +FONT_HEIGHT :: 32; + +switch_app :: (new_app: *Widget_State) { + active_app = new_app; +} + + +main :: () { + window = create_window(window_width, window_height, "Jai Sandbox"); + Simp.set_render_target(window); + // Initialize all apps + init_menu_app(*menu_state); + init_example_widget(*example_state); + init_maze_app(*maze_state); + + text_edit: [..] u8; + held_keys: String_Builder; + + if menu_state.init then menu_state.init(*menu_state); + if example_state.init then example_state.init(*example_state); + if maze_state.init then maze_state.init(*maze_state); + + the_font := (cast(*Menu_Data) menu_state.data).font; + active_app = *menu_state; + + quit := false; + last_time := seconds_since_init(); + + sub_frames := false; + + while !quit { + // FIXME lazy FPS cap + sleep_milliseconds(10); + + Input.update_window_events(); + + for Input.get_window_resizes() { + Simp.update_window(it.window); + if it.window == window { + + window_width = it.width; + window_height = it.height; + } + } + + for Input.events_this_frame { + if it.type == { + case .QUIT; + quit = true; + + case .KEYBOARD; + if it.key_pressed == 0 continue; + + // If escape is pressed, toggle back to menu + if it.key_code == .ESCAPE { + if active_app != *menu_state { + switch_app(*menu_state); + } else { + quit = true; // Escape on menu quits + } + } + + if it.key_pressed && it.key_code == .BACKSPACE { + // @FixMe this works fine for English characters since most of that character set + // is one-byte in UTF8, but will not work correctly for many other language characters. + if text_edit.count then pop(*text_edit); + } + + case .TEXT_INPUT; + value := it.utf32; + + str := Unicode.character_utf32_to_utf8(value); + for 0..str.count-1 { + array_add(*text_edit, str[it]); + } + free(str); + } + } + + for Input.input_button_states { + if it & .DOWN { + if it_index >= 0x20 && it_index < 0x7F { + value: u8 = cast(u8) it_index; + str: string; + str.data = *value; + str.count = 1; + append(*held_keys, str); + } else { + print_to_builder(*held_keys, "% ", cast(Input.Key_Code) it_index); + } + } + } + + held_keys_text := builder_to_string(*held_keys); + defer free(held_keys_text); + + now := seconds_since_init(); + dt := cast(float)(now - last_time); + last_time = now; + + if sub_frames { + frame_times_sum -= frame_times[frame_times_cursor]; + } + frame_times_sum += dt; + frame_times[frame_times_cursor] = dt; + frame_times_cursor += 1; + + if frame_times_cursor >= FRAMES { + sub_frames = true; + frame_times_cursor = 0; + } + + fps := 0.0; + if sub_frames { + fps = FRAMES / frame_times_sum; + } else { + fps = frame_times_cursor / frame_times_sum; + } + + text_edit_text: string; + text_edit_text.data = text_edit.data; + text_edit_text.count = text_edit.count; + + // Update active app + if active_app && active_app.update { + active_app.update(active_app, dt); + } + + ui_occludes := ui_handle_input(active_app); + debug_add_value("UI occludes: %", ui_occludes); + active_app.handle_input(active_app, ui_occludes); + + // Render active app + if active_app && active_app.render { + active_app.render(active_app); + } + + mouse_x, mouse_y, _ := get_mouse_pointer_position(window, true); + + // overlay + debug_add_value("FPS: %", fps); + debug_add_value("frame_times_sum: %", frame_times_sum); + debug_add_value("frame_times_cursor: %", frame_times_cursor); + debug_add_value("Window Size: %, %", window_width, window_height); + debug_add_value("Text Input: %", text_edit_text); + debug_add_value("Held keys: %", held_keys_text); + debug_add_value("mouse_pos: %, %", mouse_x, mouse_y); + + // Set up debug overlay elements + render_debug_overlay(the_font); + + // Render global UI panels (on top of active app) + ui_render(active_app); + clear_debug_overlay(); + + finish_frame(); + } + + if menu_state.shutdown then menu_state.shutdown(*menu_state); + if example_state.shutdown then example_state.shutdown(*example_state); +} + +draw_text :: (font: *Simp.Dynamic_Font, color: Vector4, x: int, y: int, fmt: string, args: .. Any) { + assert(font != null); + builder: String_Builder; + + print_to_builder(*builder, fmt, ..args); + text := builder_to_string(*builder); + defer free(text); + + Simp.draw_text(font, x, y, text, color); +} + +finish_frame :: () { + Simp.swap_buffers(window); +} diff --git a/src/menu_app.jai b/src/menu_app.jai new file mode 100644 index 0000000..1570a1f --- /dev/null +++ b/src/menu_app.jai @@ -0,0 +1,217 @@ +#import "Basic"; +#import "Math"; + +Button :: struct { + x0, y0, x1, y1: float; + text: string; + alpha: float = 0.4; + scale: float = 1.0; +} + +Menu_Data :: struct { + selection: int; + font: *Simp.Dynamic_Font; + buttons: [..] Button; +} + +init_menu_app :: (widget: *Widget_State) { + data := New(Menu_Data); + data.selection = 0; + data.font = null; + + // Define the buttons up front + array_add(*data.buttons, .{ 100.0, 500.0, 400.0, 600.0, "Example widget", 0.4, 1.0 }); + array_add(*data.buttons, .{ 100.0, 350.0, 400.0, 450.0, "Maze App", 0.4, 1.0 }); + array_add(*data.buttons, .{ 100.0, 200.0, 400.0, 300.0, "Quit", 0.4, 1.0 }); + + widget.data = data; + + widget.init = (widget: *Widget_State) { + data := cast(*Menu_Data) widget.data; + // Load the font + data.font = Simp.get_font_at_size("data", "Anonymous_Pro.ttf", 48); + + // Dynamically resize our buttons so they always fit their text! + if data.font { + for * data.buttons { + text_width := Simp.prepare_text(data.font, it.text); + center_x := (it.x0 + it.x1) / 2.0; + + // Ensure the button is at least as wide as the text + 60px of padding + needed_width := cast(float)text_width + 60.0; + actual_width := max(it.x1 - it.x0, needed_width); + + it.x0 = center_x - actual_width / 2.0; + it.x1 = center_x + actual_width / 2.0; + } + } + }; + + widget.update = update; + widget.handle_input = handle_input; + widget.render = render; + + widget.shutdown = (widget: *Widget_State) { + data := cast(*Menu_Data) widget.data; + array_free(data.buttons); + free(data); + }; +} + +#scope_file + +time_selected := 0.0; + +update :: (widget: *Widget_State, dt: float) { + data := cast(*Menu_Data) widget.data; + time_selected += dt; + debug_add_value("time_selected: %", time_selected); + + for * data.buttons { + // 1. Set our targets + target_scale := ifx data.selection == it_index then 1.05 else 1.0; + + target_alpha: float; + if data.selection == it_index { + // make button GROW + target_alpha = 1.0; + target_scale = target_scale + (sin(cast(float)time_selected * 2.0) * 0.03); + // STOP GROW + if time_selected > (3.14 / 4.0) { + target_scale = 1.08; + } + } else { + // Dimmer when unselected + target_alpha = 0.4; + } + + // 2. Smoothly lerp current values towards the targets using exponential decay + it.scale = it.scale + (target_scale - it.scale) * (15.0 * dt); + it.alpha = it.alpha + (target_alpha - it.alpha) * (15.0 * dt); + } +} + +handle_input :: (widget: *Widget_State, occluded: bool) -> (occludes: bool) { + data := cast(*Menu_Data) widget.data; + start_select := data.selection; + + occludes := false; + if !occluded { + // Query the mouse position! By passing true for right_handed, + // Y=0 is at the bottom, which perfectly matches our rendering projection. + mouse_x, mouse_y, success := get_mouse_pointer_position(window, true); + if success { + mouse_x_f := cast(float) mouse_x; + mouse_y_f := cast(float) mouse_y; + for data.buttons { + if mouse_x_f >= it.x0 && mouse_x_f <= it.x1 && mouse_y_f >= it.y0 && mouse_y_f <= it.y1 { + occludes = true; + data.selection = it_index; + } + } + } + if Input.mouse_delta_x + Input.mouse_delta_y + Input.mouse_delta_z != 0 { + if !occludes { + data.selection = -1; // Deselect if moving mouse outside + } + } + } else { + data.selection = -1; + } + + for event: Input.events_this_frame { + if event.type == .KEYBOARD && event.key_pressed { + if event.key_code == .ARROW_UP { + if data.selection <= 0 { + data.selection = data.buttons.count - 1; + } else { + data.selection -= 1; + } + } + if event.key_code == .ARROW_DOWN { + data.selection += 1; + if data.selection >= data.buttons.count then data.selection = 0; + } + if event.key_code == .ENTER || event.key_code == .MOUSE_BUTTON_LEFT { + // Execute button action + if data.selection == 0 { + switch_app(*example_state); + } else if data.selection == 1 { + switch_app(*maze_state); + } else if data.selection == 2 { + exit(0); + } + } + } + } + + if start_select != data.selection { + time_selected = 0; + } + + return occludes; +} + +render :: (widget: *Widget_State) { + data := cast(*Menu_Data) widget.data; + // Clear to a blueish color to represent the menu + Simp.clear_render_target(0.2, 0.3, 0.4, 1.0); + + Simp.set_shader_for_color(true); + Simp.immediate_begin(); + Simp.immediate_set_2d_projection(window_width, window_height); + + // Iterate through and draw all buttons dynamically based on their struct definition + for data.buttons { + color := ifx data.selection == it_index then Vector4.{0.8, 0.8, 0.2, it.alpha} else Vector4.{0.5, 0.5, 0.5, it.alpha}; + + // Scale the button from its center + center_x := (it.x0 + it.x1) / 2.0; + center_y := (it.y0 + it.y1) / 2.0; + width := it.x1 - it.x0; + height := it.y1 - it.y0; + + draw_x0 := center_x - (width / 2.0) * it.scale; + draw_x1 := center_x + (width / 2.0) * it.scale; + draw_y0 := center_y - (height / 2.0) * it.scale; + draw_y1 := center_y + (height / 2.0) * it.scale; + + Simp.immediate_quad(draw_x0, draw_y0, draw_x1, draw_y1, color); + } + + Simp.immediate_flush(); + + // Draw text on the buttons (perfectly centered!) + if data.font { + for data.buttons { + text_width := Simp.prepare_text(data.font, it.text); + + // Calculate the dead-center of the unscaled button + center_x := (it.x0 + it.x1) / 2.0; + center_y := (it.y0 + it.y1) / 2.0; + + // Calculate text placement (subtract half text width/height) + // Note: Font size is 48, so we subtract ~24 for vertical centering + text_x := center_x - (cast(float)text_width / 2.0); + text_y := center_y - 24.0 + 8.0; // 8.0 is a minor visual baseline tweak + + // 1. Generate the unscaled quads in world space + Simp.generate_quads_for_prepared_text(data.font, cast(int)text_x, cast(int)text_y, 0); + + // 2. Mathematically scale every quad outward from the button's center! + for * quad: data.font.current_quads { + quad.p0.x = center_x + (quad.p0.x - center_x) * it.scale; + quad.p0.y = center_y + (quad.p0.y - center_y) * it.scale; + quad.p1.x = center_x + (quad.p1.x - center_x) * it.scale; + quad.p1.y = center_y + (quad.p1.y - center_y) * it.scale; + quad.p2.x = center_x + (quad.p2.x - center_x) * it.scale; + quad.p2.y = center_y + (quad.p2.y - center_y) * it.scale; + quad.p3.x = center_x + (quad.p3.x - center_x) * it.scale; + quad.p3.y = center_y + (quad.p3.y - center_y) * it.scale; + } + + // 3. Draw the newly scaled quads + Simp.draw_generated_quads(data.font, .{0, 0, 0, it.alpha}); + } + } +} diff --git a/src/ui_panel.jai b/src/ui_panel.jai new file mode 100644 index 0000000..cc0a2e2 --- /dev/null +++ b/src/ui_panel.jai @@ -0,0 +1,252 @@ +// UI_Panel and UI_Element are loaded into main.jai so they inherit the global imports. + +UI_Element_Type :: enum { HEADER; BUTTON; TEXT_INPUT; DYNAMIC_TEXT; } + +UI_Element :: struct { + type: UI_Element_Type; + id: int; + + x0, y0, x1, y1: float; + label: string; + input_buf: *[..] u8; + + action: (userdata: *void) = null; +} + +UI_Panel :: struct { + x, y, w, h: float; + + is_dragging: bool; + drag_offset_x: float; + drag_offset_y: float; + + is_collapsed: bool; + focus: int; + elements: [..] UI_Element; + + app_owner: *void; // If non-null, panel is only visible when this app is active + userdata: *void; + font: *Simp.Dynamic_Font; +} + +#scope_file +active_panels: [..] *UI_Panel; +text_edit: *[..] u8; + +#scope_export +ui_register_panel :: (panel: *UI_Panel) { + array_add(*active_panels, panel); +} + +ui_unregister_panel :: (panel: *UI_Panel) { + array_ordered_remove_by_value(*active_panels, panel); +} + +ui_bring_to_front :: (panel: *UI_Panel) { + array_ordered_remove_by_value(*active_panels, panel); + array_add(*active_panels, panel); +} + +ui_handle_input :: (active_app: *void) -> (occludes: bool) { + occludes := false; + mouse_x, mouse_y, success := get_mouse_pointer_position(window, true); + if !success return false; + mouse_x_f := cast(float) mouse_x; + mouse_y_f := cast(float) mouse_y; + + mouse_down := (Input.input_button_states[Input.Key_Code.MOUSE_BUTTON_LEFT] & Input.Key_Current_State.DOWN) != 0; + + mouse_btn := false; + mouse_released := false; + dirty_text := false; + for Input.events_this_frame { + + if it.type == { + case .KEYBOARD; + + if it.key_pressed && it.key_code == .BACKSPACE { + + if text_edit { + dirty_text = true; + // @FixMe this works fine for English characters since most of that character set + // is one-byte in UTF8, but will not work correctly for many other language characters. + if text_edit.count then pop(text_edit); + } + } + + if it.key_code == .MOUSE_BUTTON_LEFT { + mouse_btn = true; + mouse_released = it.key_pressed == 0; + } + + case .TEXT_INPUT; + if text_edit { + dirty_text = true; + value := it.utf32; + + str := Unicode.character_utf32_to_utf8(value); + for 0..str.count-1 { + array_add(text_edit, str[it]); + } + free(str); + } + } + } + + // Process dragging + for panel: active_panels { + if panel.app_owner != null && panel.app_owner != active_app continue; + + if panel.is_dragging { + if mouse_down { + panel.x = mouse_x_f - panel.drag_offset_x; + panel.y = mouse_y_f - panel.drag_offset_y; + return true; + } else { + panel.is_dragging = false; + } + } + } + + // Hit test from top to bottom (end of array to start) + for < panel: active_panels { + if panel.app_owner != null && panel.app_owner != active_app continue; + + // if text input append to builder and rebuild string + + hit_panel := false; + if panel.is_collapsed { + for elem: panel.elements { + if elem.type == .HEADER { + abs_x0 := panel.x + elem.x0; + abs_y0 := panel.y + elem.y0; + abs_x1 := panel.x + elem.x1; + abs_y1 := panel.y + elem.y1; + if mouse_x_f >= abs_x0 && mouse_x_f <= abs_x1 && mouse_y_f >= abs_y0 && mouse_y_f <= abs_y1 { + hit_panel = true; + } + break; + } + } + } else { + if mouse_x_f >= panel.x && mouse_x_f <= panel.x + panel.w && + mouse_y_f >= panel.y && mouse_y_f <= panel.y + panel.h { + hit_panel = true; + } + } + + if hit_panel { + if mouse_btn { + ui_bring_to_front(panel); + + hit_element := false; + + for * elem: panel.elements { + if panel.is_collapsed && elem.type != .HEADER continue; + + abs_x0 := panel.x + elem.x0; + abs_y0 := panel.y + elem.y0; + abs_x1 := panel.x + elem.x1; + abs_y1 := panel.y + elem.y1; + + if mouse_x_f >= abs_x0 && mouse_x_f <= abs_x1 && mouse_y_f >= abs_y0 && mouse_y_f <= abs_y1 { + if elem.type == .HEADER { + if mouse_x_f >= abs_x1 - 30.0 && mouse_released { + panel.is_collapsed = !panel.is_collapsed; + } else if !mouse_released { + panel.is_dragging = true; + panel.drag_offset_x = mouse_x_f - panel.x; + panel.drag_offset_y = mouse_y_f - panel.y; + } + } else if elem.type == .TEXT_INPUT { + panel.focus = elem.id; + text_edit = elem.input_buf; + } else if elem.type == .BUTTON && mouse_released { + if elem.action elem.action(panel.userdata); + } + hit_element = true; + + break; + } + } + + + if !hit_element { + panel.focus = 0; + text_edit = null; + } + } + return true; + } + } + return false; +} + +ui_render :: (active_app: *void) { + mouse_x, mouse_y, _ := get_mouse_pointer_position(window, true); + mouse_x_f := cast(float) mouse_x; + mouse_y_f := cast(float) mouse_y; + + Simp.set_shader_for_color(true); + Simp.immediate_begin(); + Simp.immediate_set_2d_projection(window_width, window_height); + + // Draw from bottom to top + for panel: active_panels { + if panel.app_owner != null && panel.app_owner != active_app continue; + + if !panel.is_collapsed { + // Draw Panel Body Background + Simp.immediate_quad(panel.x, panel.y, panel.x + panel.w, panel.y + panel.h, .{0.15, 0.15, 0.15, 0.95}); + } + + + for elem: panel.elements { + if panel.is_collapsed && elem.type != .HEADER continue; + + abs_x0 := panel.x + elem.x0; + abs_y0 := panel.y + elem.y0; + abs_x1 := panel.x + elem.x1; + abs_y1 := panel.y + elem.y1; + + is_hovered := mouse_x_f >= abs_x0 && mouse_x_f <= abs_x1 && mouse_y_f >= abs_y0 && mouse_y_f <= abs_y1; + + if elem.type == .HEADER { + color := ifx is_hovered then Vector4.{0.3, 0.3, 0.3, 1.0} else Vector4.{0.2, 0.2, 0.2, 1.0}; + Simp.immediate_quad(abs_x0, abs_y0, abs_x1, abs_y1, color); + + // Collapse button + btn_color := ifx (is_hovered && mouse_x_f >= abs_x1 - 30.0) then Vector4.{0.8, 0.3, 0.3, 1.0} else Vector4.{0.4, 0.4, 0.4, 1.0}; + Simp.immediate_quad(abs_x1 - 30.0, abs_y0 + 5.0, abs_x1 - 5.0, abs_y1 - 5.0, btn_color); + + if panel.font { + Simp.draw_text(panel.font, cast(int)abs_x0 + 10, cast(int)abs_y0 + 6, elem.label, .{1,1,1,1}); + Simp.draw_text(panel.font, cast(int)abs_x1 - 25, cast(int)abs_y0 + 6, "-", .{1,1,1,1}); + } + } else if elem.type == .BUTTON { + color := ifx is_hovered then Vector4.{0.4, 0.4, 0.4, 1.0} else Vector4.{0.3, 0.3, 0.3, 1.0}; + Simp.immediate_quad(abs_x0, abs_y0, abs_x1, abs_y1, color); + if panel.font Simp.draw_text(panel.font, cast(int)abs_x0 + 60, cast(int)abs_y0 + 6, elem.label, .{1,1,1,1}); + } else if elem.type == .TEXT_INPUT { + color := ifx panel.focus == elem.id then Vector4.{0.4, 0.4, 0.8, 1.0} + else ifx is_hovered then Vector4.{0.3, 0.3, 0.3, 1.0} else Vector4.{0.2, 0.2, 0.2, 1.0}; + Simp.immediate_quad(abs_x0, abs_y0, abs_x1, abs_y1, color); + if panel.font { + Simp.draw_text(panel.font, cast(int)abs_x0 + 10, cast(int)abs_y0 + 6, elem.label, .{1,1,1,1}); + + val: string; + val.data = elem.input_buf.data; + val.count = elem.input_buf.count; + + // FIXME lol + offset_x := elem.label.count * 20; + + Simp.draw_text(panel.font, cast(int)abs_x0 + offset_x, cast(int)abs_y0 + 6, val, .{1,1,1,1}); + } + } else if elem.type == .DYNAMIC_TEXT { + if panel.font Simp.draw_text(panel.font, cast(int)abs_x0, cast(int)abs_y0, elem.label, .{0, 1, 0, 1}); + } + } + } + Simp.immediate_flush(); +} diff --git a/src/widget_interface.jai b/src/widget_interface.jai new file mode 100644 index 0000000..62c84a3 --- /dev/null +++ b/src/widget_interface.jai @@ -0,0 +1,12 @@ +#import "Basic"; + +Widget_State :: struct { + init: (widget: *Widget_State) -> (); + update: (widget: *Widget_State, dt: float) -> (); + handle_input: (widget: *Widget_State, occluded: bool) -> (occludes: bool); + render: (widget: *Widget_State) -> (); + shutdown: (widget: *Widget_State) -> (); + + // We can store a pointer to the specific widget's state here + data: *void; +} |
