From 93ad8a5cdeb8421bcf02195bc470257935062138 Mon Sep 17 00:00:00 2001 From: Alec Goncharow Date: Wed, 27 May 2026 14:34:32 -0400 Subject: Working widget idea, makings of a maze app Forgot to commit as I went over the weekend, which is fine, not a ton of interesting stuff just yet. --- src/apps/example_app.jai | 33 ++++++ src/apps/maze_app.jai | 257 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 src/apps/example_app.jai create mode 100644 src/apps/maze_app.jai (limited to 'src/apps') 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! +} -- cgit v1.2.3-70-g09d2