aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore13
-rw-r--r--build.jai528
-rw-r--r--src/apps/example_app.jai33
-rw-r--r--src/apps/maze_app.jai257
-rw-r--r--src/debug_overlay.jai48
-rw-r--r--src/main.jai207
-rw-r--r--src/menu_app.jai217
-rw-r--r--src/ui_panel.jai252
-rw-r--r--src/widget_interface.jai12
9 files changed, 1567 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b5f69e3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+*.md
+
+.build/
+bin/
+.antigravitycli/
+tags
+
+maze-rs/
+
+data/
+
+how_to
+modules
diff --git a/build.jai b/build.jai
new file mode 100644
index 0000000..abec9b1
--- /dev/null
+++ b/build.jai
@@ -0,0 +1,528 @@
+is_absolute_path :: (path: string) -> bool { // This routine is probably not as correct as we'd like. We'd like to put in a better one! But maybe we will stop doing the cwd thing, or do it differently; hard to say.
+ if !path return false;
+
+ if path[0] == "/" return true; // Backslashes have not been converted to forward slashes by this point.
+ if path[0] == "\\" return true; // Backslashes have not been converted to forward slashes by this point.
+ if (path.count > 2) && (path[1] == #char ":") && (OS == .WINDOWS) return true; // Drive letter stuff. Probably incomplete.
+
+ if path.count >= 3 {
+ // @Robustness: Check for a drive letter in character 0? Anything else?
+ if path[1] == #char ":" return true;
+ }
+
+ return false;
+}
+
+build :: () {
+ //
+ // Create a workspace to contain the program we want to compile.
+ // We can pass a name to compiler_create_workspace that gets reported
+ // back to us in error messages:
+ //
+ w := compiler_create_workspace("Target Program");
+ if !w {
+ log_error("Workspace creation failed.\n");
+ return;
+ }
+
+ args := get_build_options().compile_time_command_line;
+
+ options := get_build_options(w);
+
+ check_bindings := true;
+ do_check := true; // Import modules/Check if true.
+
+ if verbose print("Arguments: %\n", args); // @Cleanup: 'verbose' cannot be true yet so this will never fire.
+
+ printed_help := false;
+ printed_version := false;
+
+ // User-processed options will give us entries for either 'files' (files to load)
+ // or 'run_strings' (strings to add to the build with #run in front.)
+ files: [..] string;
+ add_strings: [..] string;
+ run_strings: [..] string;
+ modules_paths: [..] string;
+
+ user_arguments: [..] string;
+
+ // Handle command-line arguments. Once we see + or -,
+ // we are done looking for our arguments.
+
+ got_error := false;
+
+ output_executable_name: string;
+ output_path: string;
+
+ intercept_flags: Intercept_Flags;
+
+ success, plugins_to_create, remaining_args := parse_plugin_arguments(args);
+ if !success got_error = true;
+
+ args = remaining_args;
+ index := 0;
+ while index < args.count {
+ defer index += 1;
+
+ it := args[index];
+
+ if !it continue; // @Temporary?
+
+ if it[0] == #char "-" {
+ if it == {
+ case "-";
+ // Everything after this is user arguments.
+ for i: index+1..args.count-1 array_add(*user_arguments, args[i]);
+ break;
+
+ case "-release";
+ log("*** Warning: -release will go away, because it's a confusing term inherited from Microsoft. Please use -o or -optimized instead.\n", flags=.WARNING);
+ set_optimization(*options, .VERY_OPTIMIZED);
+ options.stack_trace = false;
+
+ case "-release_debug";
+ log("*** Warning: -release_debug will go away, because 'release' is a confusing term inherited from Microsoft. Please use -od or -optimized_debug instead.\n", flags=.WARNING);
+ set_optimization(*options, .OPTIMIZED);
+ // Leave options.stack_trace == true...
+
+ case "-o"; #through;
+ case "-optimized";
+ set_optimization(*options, .VERY_OPTIMIZED);
+ options.stack_trace = false;
+
+ case "-od"; #through;
+ case "-optimized_debug";
+ set_optimization(*options, .OPTIMIZED);
+ // Leave options.stack_trace == true...
+
+ case "-very_debug";
+ set_optimization(*options, .VERY_DEBUG);
+
+ case "-no_inline";
+ options.enable_bytecode_inliner = false;
+
+ case "-quiet";
+ options.text_output_flags = 0;
+
+ case "-x64";
+ options.backend = .X64;
+ case "-llvm";
+ options.backend = .LLVM;
+
+ case "-no_dce";
+ options.dead_code_elimination = .NONE;
+
+ case "-no_split";
+ options.llvm_options.enable_split_modules = false;
+
+ case "-output_ir";
+ options.llvm_options.output_llvm_ir_before_optimizations = true;
+ options.llvm_options.output_llvm_ir = true;
+
+ case "-debug_for";
+ options.debug_for_expansions = true;
+
+ case "-msvc_format";
+ options.use_visual_studio_message_format = true;
+
+ case "-natvis";
+ options.use_natvis_compatible_types = true;
+
+ // We need to know about no_check bindings in pass 1 because that is how we determine if that plugin is in the set! Argh.
+ case "-no_check"; do_check = false;
+ case "-no_check_bindings"; check_bindings = false;
+ case "-check_bindings"; check_bindings = true; // Not necessary anymore since we changed the default. I just left it in for backwards compatibility. -rluba, 2022-03-23
+
+ case "-no_backtrace_on_crash";
+ options.backtrace_on_crash = .OFF;
+
+ case "-version";
+ s := compiler_get_version_info(null);
+ print("Version: %.\n", s);
+ printed_version = true;
+
+ case "-exe";
+ if index >= args.count-1 {
+ log_error("Command line: Missing argument to -exe.\n");
+ got_error = true;
+ break;
+ }
+
+ index += 1;
+ output_executable_name = args[index];
+
+ case "-output_path";
+ if index >= args.count-1 {
+ log_error("Command line: Missing argument to -output_path.\n");
+ got_error = true;
+ break;
+ }
+
+ index += 1;
+ output_path = args[index];
+
+ case "-add";
+
+ if index >= args.count-1 {
+ log_error("Command line: Missing argument to -add.\n");
+ got_error = true;
+ break;
+ }
+
+ index += 1;
+ array_add(*add_strings, args[index]);
+
+ case "-run";
+
+ if index >= args.count-1 {
+ log_error("Command line: Missing argument to -run.\n");
+ got_error = true;
+ break;
+ }
+
+ index += 1;
+ array_add(*run_strings, args[index]);
+
+ case "-help"; #through;
+ case "-?";
+ log("%", HELP_STRING);
+
+ printed_help = true;
+
+ case "-debugger";
+ options.interactive_bytecode_debugger = true;
+ set_build_options_dc(.{interactive_bytecode_debugger=true});
+
+ case "-context_size";
+ if index >= args.count-1 {
+ log_error("Command line: Missing argument to -context_size.\n");
+ got_error = true;
+ break;
+ }
+
+ index += 1;
+
+ value, success := to_integer(args[index]);
+ if success {
+ CONTEXT_SIZE_MAX :: 0x4_0000; // This has to be kind of reasonable because people declare Contexts on the stack and so forth...? Also must fit into 32 bits because that is the size of the value in Build_Options.
+ if value > CONTEXT_SIZE_MAX {
+ log_error("Command line: Invalid argument to -context_size. The context must be less than or equal to CONTEXT_SIZE_MAX, which is % (but the value provided was %). The size needs to be reasonable because contexts get declared on the stack, in other structs, and so forth. (If you think you should be able to exceed this limit, you can modify or replace Default_Metaprogram.)\n", CONTEXT_SIZE_MAX, value);
+ } else if value < size_of(Context_Base) {
+ log_error("Command line: Invalid argument to -context_size. The context must be at least as large as size_of(Context_Base), which is %.\n", size_of(Context_Base));
+ } else {
+ options.context_size_max = cast(s32) value;
+ }
+ } else {
+ log_error("Command line: Unable to parse an integer argument to context size; got '%'.\n", args[index]);
+ }
+ case "-import_dir";
+ if index >= args.count-1 {
+ log_error("Command line: Missing argument to -import_dir.\n");
+ got_error = true;
+ break;
+ }
+
+ index += 1;
+ array_add(*modules_paths, args[index]);
+
+ case "-no_color";
+ options.use_ansi_color = false;
+
+ case "-verbose";
+ verbose = true;
+
+ case;
+ log_error("Unknown argument '%'.\nExiting.\n", it);
+ got_error = true;
+ break;
+ }
+
+ continue;
+ }
+
+ // If we got here, it's a plain file. Add it to the array.
+ array_add(*files, it);
+ }
+
+ if got_error {
+ exit(1);
+ }
+
+ if do_check {
+ p := array_add(*plugins_to_create);
+ if check_bindings p.name = "Check";
+ else p.name = "Check(CHECK_BINDINGS=false)";
+ }
+
+ // Now that we know what the plugins are, init them.
+ success = init_plugins(plugins_to_create, *plugins, w);
+ if !success {
+ log_error("A plugin init() failed. Exiting.\n");
+ exit(0);
+ }
+
+ if !(files || add_strings || run_strings) {
+ if !(printed_help || printed_version) log("You need to provide an argument telling the compiler what to compile! Sorry. Pass -help for help.\n");
+ if printed_version && !printed_help exit(0);
+ if !printed_help exit(1); // If we printed help, we want to fall through to print help for all the modules, so don't exit yet.
+ }
+
+
+ old_wd := get_working_directory();
+ absolute_files: [..] string;
+ if files {
+ if output_path options.output_path = output_path;
+ if output_executable_name options.output_executable_name = output_executable_name;
+
+ array_reserve(*absolute_files, files.count);
+ for files {
+ absolute_file := it;
+ if !is_absolute_path(it) {
+ #if OS == .WINDOWS {
+ absolute_file = get_absolute_path(it);
+ } else {
+ absolute_file = sprint("%/%", old_wd, it);
+ }
+ }
+
+ array_add(*absolute_files, absolute_file);
+ }
+
+ basename, path := get_basename_and_path(absolute_files[0]);
+
+ if basename || path {
+ if !output_path {
+ options.output_path = sprint("%/bin", old_wd);
+ make_directory_if_it_does_not_exist(options.output_path);
+
+ options.intermediate_path = sprint("%/.build", old_wd);
+ make_directory_if_it_does_not_exist(options.intermediate_path);
+
+ if verbose print("options.output_path = \"%\";\n", options.output_path);
+ }
+
+ if basename && !output_executable_name {
+ options.output_executable_name = basename;
+ if verbose print("options.output_executable_name = \"%\";\n", basename);
+ }
+ }
+
+ if path {
+ if verbose print("Changing working directory to '%'.\n", path);
+ set_working_directory(path);
+ }
+ }
+
+ if printed_help {
+ log("\n\n");
+
+ for plugins {
+ name := plugins_to_create[it_index].name;
+
+ if it.log_help {
+ log("---- Help for plugin '%': ----\n\n", name);
+ it.log_help(it);
+ log("\n");
+ } else {
+ log("---- Plugin '%' provides no help.\n", name);
+ }
+ }
+
+ if !(files || run_strings) exit(0);
+ }
+
+ if modules_paths {
+ prefix := "";
+ if files {
+ // Because modules_paths are intended to be relative to the source code, but these aren't coming from the
+ // source code themselves, we need to tell the compiler where they live. We do this by just prefixing
+ // the import_paths with the path to the first source file. This is weird and kind of a hack, but it's unclear
+ // what is a better thing to do.
+ basename, path := get_basename_and_path(absolute_files[0]);
+ if path {
+ for * modules_paths if !is_absolute_path(it.*) { it.* = tprint("%0%", path, it.*); }
+ }
+ }
+
+ array_add(*modules_paths, ..options.import_path); // Put these behind whatever the user specified.
+ options.import_path = modules_paths;
+ }
+
+ options.compile_time_command_line = user_arguments;
+
+ // Make a fake location...
+ loc: Source_Code_Location;
+ loc.fully_pathed_filename = ifx absolute_files then absolute_files[0] else tprint("%/fake_file.fake", old_wd); // In case someone does a #run that imports stuff but adds no files!
+
+ set_build_options(options, w, loc=loc);
+
+ for plugins if it.before_intercept it.before_intercept(it, *intercept_flags);
+
+ // As the compiler builds the target program, we can listen in on messages
+ // that report the status of the program. In later examples we can use
+ // these messages to do sophisticated things, but for now, we'll just
+ // use them to report on the status of compilation.
+
+ // To tell the compiler to give us messages, we need to call compiler_begin_intercept
+ // before we add any code to the target workspace.
+ compiler_begin_intercept(w, intercept_flags);
+
+ if verbose {
+ print("Input files: %\n", absolute_files);
+ print("Add strings: %\n", add_strings);
+ print("Run strings: %\n", run_strings);
+ print("Plugins: %\n", plugins_to_create);
+ }
+
+ for plugins if it.add_source it.add_source(it);
+
+ for absolute_files {
+ add_build_file(it, w);
+ }
+
+ for add_strings add_build_string(tprint("%;", it), w);
+ for run_strings add_build_string(tprint("#run %;\n", it), w); // run_strings is kind of redundant now! We could remove it.
+
+ // Call message_loop(), which is a routine of ours below that will receive the messages.
+ message_loop(w);
+
+ // When we're done, message_loop will return.
+ // We call compiler_end_intercept to tell the compiler we are done.
+ compiler_end_intercept(w);
+
+ for plugins if it.finish it.finish (it);
+ for plugins if it.shutdown it.shutdown(it);
+
+ {
+ // None of the code in this file is intended to end up in an executable
+ // of any kind. So, we tell the compiler not to make an executable for us:
+
+ set_build_options_dc(.{do_output=false, write_added_strings=false});
+ }
+}
+
+#run,stallable build();
+
+//
+// message_loop() runs the event loop that reads the messages.
+// You can do whatever you want with those messages. The goal
+// of this example is just to show the different kinds of messages,
+// so we don't do anything crazy yet. But you can do some things
+// that are crazy.
+//
+message_loop :: (w: Workspace) {
+ while true {
+ // We ask the compiler for the next message. If one is not available,
+ // we will wait until it becomes available.
+ message := compiler_wait_for_message();
+ // Pass the message to all plugins.
+ for plugins if it.message it.message(it, message);
+
+ if message.kind == .COMPLETE break;
+ }
+}
+
+#import "Basic";
+#import "File";
+#import "Compiler";
+#import "Metaprogram_Plugins";
+#import "File_Utilities";
+
+
+verbose := false;
+plugins: [..] *Metaprogram_Plugin;
+
+
+HELP_STRING :: #string DONE
+Available Command-Line Arguments:
+
+-add arg Add the string 'arg' to the target program as code.
+ Example: -add "MY_VARIABLE :: 42";
+-context_size n Set the size of #Context, in bytes (you only need this if your program has a really big context).
+ Example: -context_size 2048
+-debugger If there is a crash in compile-time execution, drop into the interactive debugger.
+-debug_for Enable debugging of for_expansion macros. (Otherwise the debugger will never step into them to make stepping through for loops more convenient.)
+-exe name Set output_executable_name on the target workspace to 'name'.
+-import_dir arg Add this directory to the list of directories searched by #import. Can be
+ used multiple times.
+-llvm Use the LLVM backend by default (unless overridden by a metaprogram).
+ The LLVM backend is the default normally, so this isn't too useful.
+-msvc_format Use Visual Studio's message format for error messages.
+-natvis Use natvis compatible type names in debug info (array<T> instead of [] T, etc).
+-no_backtrace_on_crash Do not catch OS-level exceptions and print a stack trace when your program crashes.
+ Causes less code to be imported on startup. Depending on your OS (for example, on Windows),
+ crashes may look like silent exits.
+-no_color Disable ANSI terminal coloring in output messages.
+-no_dce Turn off dead code elimination. This is a temporary option,
+ provided because dead code elimination is a new and potentially
+ unstable feature. This will eventually be removed; the preferred way
+ to turn off dead code elimination is via Build_Options.
+-no_split Disable split modules when compiling with the LLVM backend.
+-no_check Do not import modules/Check and run it on the code. The result will be that you won't get
+ augmented error checking for stuff like print() calls. Use this if you want to substitute
+ your own error checking plugin, or for higher speeds in builds known to be good.
+-no_check_bindings Disable checking of module bindings when running modules/Check. If modules/Check is not run
+ due to -no_check, this does nothing.
+-no_inline Disable inlining throughout the program (useful when debugging).
+-o, -optimzed Build an optimized build (and disable stack traces).
+-od, -optimized_debug Make a build that is a little less optimized, and keeps user-level stack traces (which do cost CPU cycles, but can help with debugging).
+-output_path Set the path where your output files (such as the executable) will go.
+-quiet Run the compiler in quiet mode (not outputting unnecessary text).
+-run arg Start a #run directive that parses and runs 'arg' as code.
+ Example: -run write_string(\"Hello!\n\")
+ (The extra backslashes are the shell's fault.)
+-verbose Output some extra information about what this metaprogram is doing.
+-version Print the version of the compiler.
+-very_debug Build a very_debug build, i.e. add more debugging facilities than usual, which will cause it to run slower but catch more problems.
+-x64 Use the x64 backend by default (unless overridden by a metaprogram).
+
+- Every argument after - is ignored by the compiler itself,
+ and is passed to the user-level metaprogram for its own use.
+
+Any argument not starting with a -, and before a - by itself, is the name of a file to compile.
+
+Example: jai -x64 program.jai - info for -the compile_time execution
+
+
+==== Plugins ====
+
+In the section before the lone dash, but after all other such arguments listed above,
+you can have arguments that invoke plugins. These arguments begin with a + and then
+the name of the plugin module to invoke.
+
+Example: jai -x64 program.jai -verbose +Icon -icon icon_filename.ico +Autorun
+
+Each + invokes a new plugin, and all arguments between that + and the next + are sent
+to that plugin. In the above example, the -icon is a command-line option handled only
+by the Icon plugin (the name "-icon" is currently confusing and we'll change it!)
+
+Here's an example that uses the lone dash as well as a plugin invocation:
+
+Example: jai -x64 program.jai +Icon -icon icon_filename.ico +Autorun - arguments for the invoked metaprogram go here
+
+
+==== Compiler Hardcoded Options ====
+
+There are also a few very tweaky compiler-front-end options that almost nobody
+will ever care about. To see these, do:
+
+ jai -- help
+
+
+==== Other Functionality ====
+
+Unlike most contemporary compilers, we don't have a huge number of arcane arguments
+to control all kinds of minute things, that you have to spend a long time learning about.
+Our philosophy is, it's much more effective to do configuration from code, where
+you have a great deal of precise control over what's happening, options can be expressed
+as easy-to-understand data structures, and so forth. To get started, look into Build_Options
+in modules/Compiler/Compiler.jai, or look at how_to/400_workspaces.jai and
+how_to/420_command_line.jai.
+
+This text is generated by modules/Default_Metaprogram.jai, the code that starts up
+when you run the compiler. You can read the text of this file to learn how all this works
+(it's pretty simple!); you can make a copy of Default_Metaprogram and modify it
+to work however you want.
+DONE
+
+
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;
+}