actually making this something interesting
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Tue, 10 Aug 2021 19:09:31 +0000 (14:09 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Tue, 10 Aug 2021 19:09:31 +0000 (14:09 -0500)
20 files changed:
res/colors_dark.json
site/js/decompiler.js
src/app.onyx [deleted file]
src/app/app.onyx [new file with mode: 0644]
src/app/colors.onyx [new file with mode: 0644]
src/app/debug_log.onyx [new file with mode: 0644]
src/app/window_management.onyx [new file with mode: 0644]
src/app/window_switcher.onyx [new file with mode: 0644]
src/build.onyx
src/config.onyx
src/debug_log.onyx [deleted file]
src/features/hex_editor/feature.onyx [new file with mode: 0644]
src/features/hex_editor/hex_viewer.onyx [new file with mode: 0644]
src/features/load_features.onyx
src/features/text_editor/feature.onyx [new file with mode: 0644]
src/features/text_editor/text_editor.onyx [new file with mode: 0644]
src/features/wasm/feature.onyx
src/features/wasm/wasm.onyx
src/ui/cursor.onyx [new file with mode: 0644]
src/ui/window.onyx

index 439f912fcc7e3e14e4a63016ba4526354b9b4fb1..3d4f29e5a3aa30a53c627d489c2980296a7275ae 100644 (file)
@@ -7,9 +7,9 @@
     "value":     [ 0.8, 1.0, 0.8 ],
     "jumppoint": [ 1, 0.5, 0.5   ],
 
-    "primary":       [ 0.051, 0.278, 0.631 ],
+    "primary":       [ 0.032, 0.178, 0.431 ],
     "primary_light": [ 0.329, 0.451, 0.827 ],
-    "primary_dark":  [ 0,     0.129, 0.443 ],
+    "primary_dark":  [ 0,     0.065, 0.233 ],
     "primary_text":  [ 1.0,   1.0,   1.0   ],
 
     "secondary":       [ 0,     0.376, 0.392 ],
index 3027307b5ffb84eb184fd78dcb06cad6b1284c5d..169dbd3bce71b74b3559d9f00f1ff708263e4538 100644 (file)
@@ -27,5 +27,13 @@ window.ONYX_MODULES.push({
 
     refresh: function() {
         window.location.reload(true);
+    },
+
+    set_cursor: function(curptr, curlen) {
+        const decoder = new TextDecoder();
+        const data = new Uint8Array(window.ONYX_MEMORY.buffer, curptr, curlen);
+        const str = decoder.decode(data);
+
+        document.getElementById("main_canvas").style.cursor = str;
     }
 });
diff --git a/src/app.onyx b/src/app.onyx
deleted file mode 100644 (file)
index 2d18130..0000000
+++ /dev/null
@@ -1,407 +0,0 @@
-package app
-
-use package core
-
-#private_file events :: package js_events
-#private_file gl     :: package gl
-#private_file gfx    :: package immediate_mode
-#private_file ui     :: package ui
-#private_file config :: package config
-#private_file wasm   :: package wasm_utils
-#private_file debug  :: package debug
-
-use package debug { init as debug_init, debug_log, draw_debug_log }
-use package core.intrinsics.onyx { __initialize }
-
-@Relocate search_buffer: string.String_Buffer;
-
-background_tile_texture : gfx.Texture;
-
-on_file_load_callbacks : map.Map(u32, (file_event: ^events.Event) -> void);
-
-// The global application state.
-state : Application_State;
-
-Application_State :: struct {
-    has_active_file := false;
-    file := Active_File.{};
-}
-
-Active_File :: struct {
-    name := (#type [] u8).{ null, 0 };
-    data := (#type [] u8).{ null, 0 };
-}
-
-init :: () {
-    debug_init();
-
-    __initialize(^state);
-
-    gl.init("main_canvas");
-    events.init();
-    gfx.immediate_renderer_init();
-
-    ui.init_ui();
-
-    map.init(^on_file_load_callbacks);
-
-    color_file := events.request_file(config.color_scheme_file);
-    map.put(^on_file_load_callbacks, color_file, load_colors);
-
-    load_background_tile_texture();
-    load_fonts();
-
-    search_buffer = string.buffer_make(memory.make_slice(u8, 256));
-
-    gl.enable(gl.BLEND);
-    gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
-
-    {
-        use type_info;
-
-        // Look through all the types in the program
-        for type: type_table {
-            if type.kind != .Struct do continue;
-
-            ts := cast(^Type_Info_Struct) type;
-            if !string.starts_with(ts.name, "Feature_") do continue;
-
-            // Any types that are a structure and start with "Feature_" will be dynamically loaded
-
-            debug_log(.Info, "Found feature '{}'", string.advance(ts.name, 8));
-
-            for ^member: ts.members {
-                Hook_Function_Type :: #type (^Application_State) -> void;
-
-                if member.name == "setup" {
-                    assert(member.type == Hook_Function_Type, "setup has the wrong type.");
-                    assert(member.default != null, "setup has no default function.");
-
-                    (*(cast(^Hook_Function_Type) member.default))(null);
-                }
-
-                if member.name == "work" {
-                    assert(member.type == Hook_Function_Type, "work has the wrong type.");
-                    assert(member.default != null, "work has no default function.");
-
-                    (*(cast(^Hook_Function_Type) member.default))(null);
-                }
-            }
-        }
-    }
-
-    load_background_tile_texture :: () {
-        background_tile_texture = gfx.load_texture(32, 32, #file_contents "res/images/background_tile.data", gl.RGB, gl.RGB);
-        gl.bindTexture(gl.TEXTURE_2D, background_tile_texture.texture);
-
-        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
-        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
-        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
-        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
-
-        gl.bindTexture(gl.TEXTURE_2D, -1);
-    }
-
-    load_colors :: (event: ^events.Event) {
-        json :: package json
-
-        assert(event.kind == .FileRequest, "Bad event type");
-        assert(event.file.status == .Success, "Failed to load color file");
-
-        color_data := memory.make_slice(u8, event.file.size);
-        defer if color_data.count > 0 do cfree(color_data.data);
-        events.get_requested_file_data(event.file.file_id, color_data);
-
-        arena := alloc.arena.make(context.allocator, 4096);
-        defer alloc.arena.free(^arena);
-        colors := json.decode(color_data, alloc.arena.make_allocator(^arena));
-        defer json.free(colors);
-
-        config.Colors.dark_background = decode_color(colors.root["dark_background"]);
-        config.Colors.background      = decode_color(colors.root["background"]);
-        config.Colors.foreground      = decode_color(colors.root["foreground"]);
-
-        config.Colors.keyword         = decode_color(colors.root["keyword"]);
-        config.Colors.value           = decode_color(colors.root["value"]);
-        config.Colors.jumppoint       = decode_color(colors.root["jumppoint"]);
-
-        config.Colors.primary         = decode_color(colors.root["primary"]);
-        config.Colors.primary_light   = decode_color(colors.root["primary_light"]);
-        config.Colors.primary_dark    = decode_color(colors.root["primary_dark"]);
-        config.Colors.primary_text    = decode_color(colors.root["primary_text"]);
-
-        config.Colors.secondary       = decode_color(colors.root["secondary"]);
-        config.Colors.secondary_light = decode_color(colors.root["secondary_light"]);
-        config.Colors.secondary_dark  = decode_color(colors.root["secondary_dark"]);
-        config.Colors.secondary_text  = decode_color(colors.root["secondary_text"]);
-
-        update_ui_colors();
-
-        decode_color :: (v: ^json.Value) -> gfx.Color4 {
-            return .{
-                r = ~~v[0]->as_float(),
-                g = ~~v[1]->as_float(),
-                b = ~~v[2]->as_float(),
-            };
-        }
-
-        debug_log(.Info, "Successfully loaded colorscheme.", null);
-    }
-
-    load_fonts :: () {
-        use type_info;
-
-        // Dumb check to see if the array is uninitialized
-        if fonts_loading.capacity == 0 do array.init(^fonts_loading, 4);
-
-        fonts_info := cast(^Type_Info_Struct) get_type_info(config.Fonts_Container);
-        for ^member: fonts_info.members {
-            info := cast(^Type_Info_Struct) get_type_info(member.type);
-
-            font_name := member.name;
-
-            font_index    := *cast(^i32) info.parameters[0].data;
-            fnt_file_name := *cast(^str) info.parameters[1].data;
-            tex_file_name := *cast(^str) info.parameters[2].data;
-
-            debug_log(.Info, "Loading font '{}' with index {} from '{}' and '{}'\n", font_name, font_index, fnt_file_name, tex_file_name);
-
-            fnt_file_id := events.request_file(fnt_file_name);
-            tex_file_id := events.request_file(tex_file_name);
-
-            map.put(^on_file_load_callbacks, fnt_file_id, font_file_loaded);
-            map.put(^on_file_load_callbacks, tex_file_id, font_file_loaded);
-
-            array.push(^fonts_loading, .{ font_index, font_name, fnt_file_id, tex_file_id });
-        }
-
-        Loading_Font :: struct {
-            font_index  : u32;
-            font_name   : str;
-
-            fnt_file_id : u32;
-            tex_file_id : u32;
-
-            fnt_file_size := cast(u32) 0;
-            tex_file_size := cast(u32) 0;
-        }
-        #persist fonts_loading : [..] Loading_Font;
-
-        font_file_loaded :: (ev: ^events.Event) {
-            lf: ^Loading_Font = null;
-
-            for ^entry: fonts_loading {
-                if entry.fnt_file_id == ev.file.file_id { entry.fnt_file_size = ev.file.size; lf = entry; }
-                if entry.tex_file_id == ev.file.file_id { entry.tex_file_size = ev.file.size; lf = entry; }
-            }
-
-            assert(lf != null, "Loaded a file for a font that was not registered.");
-
-            if lf.fnt_file_size > 0 && lf.tex_file_size > 0 {
-                fnt_data := memory.make_slice(u8, lf.fnt_file_size);
-                tex_data := memory.make_slice(u8, lf.tex_file_size);
-                defer {
-                    cfree(fnt_data.data); 
-                    cfree(tex_data.data); 
-                }
-
-                @ErrorHandling
-                assert(events.get_requested_file_data(lf.fnt_file_id, fnt_data), "Failed to get bmfont file data.");
-                assert(events.get_requested_file_data(lf.tex_file_id, tex_data), "Failed to get texture data.");
-
-                font := ui.create_font(fnt_data, tex_data);
-                ui.register_font(lf.font_index, font);
-
-                debug_log(.Info, "Successfully loaded font '{}'.", lf.font_name);
-
-                ui.use_font(lf.font_index);
-            }
-        }
-    }
-}
-
-handle_event :: (event: ^events.Event) {
-    switch event.kind {
-        case .MouseDown do switch event.mouse.button {
-            case .Left      do ui.button_pressed(.Left);
-            case .Right     do ui.button_pressed(.Right);
-            case .Middle    do ui.button_pressed(.Middle);
-        }
-
-        case .MouseUp do switch event.mouse.button {
-            case .Left      do ui.button_released(.Left);
-            case .Right     do ui.button_released(.Right);
-            case .Middle    do ui.button_released(.Middle);
-        }
-
-        case .MouseMove do ui.update_mouse_position(~~ event.mouse.pos_x, ~~ event.mouse.pos_y);
-
-        case .MouseWheel do switch event.mouse.button {
-            case .WheelUp   do ui.button_pressed(.WheelUp);
-            case .WheelDown do ui.button_pressed(.WheelDown);
-        }
-
-        case .KeyDown, .KeyUp {
-            modifiers : ui.Keyboard_State.Key_Event.Modifiers;
-            if event.keyboard.modifiers & .CTRL  do modifiers |= .CTRL;
-            if event.keyboard.modifiers & .ALT   do modifiers |= .ALT;
-            if event.keyboard.modifiers & .META  do modifiers |= .META;
-            if event.keyboard.modifiers & .SHIFT do modifiers |= .SHIFT;
-
-            if event.kind == .KeyDown {
-                ui.key_down(event.keyboard.keycode, modifiers);   @KeycodeIsWrong // .keycode is apparently not browser independent...
-
-                if event.keyboard->get_name() == "F5" {
-                    refresh :: () -> void #foreign "decompiler" "refresh" ---
-                    refresh();
-                    break;
-                }
-
-                if event.keyboard->get_name() == "F7" {
-                    debug.debug_log_toggle();
-                    break;
-                }
-
-                if event.keyboard->get_name() == "Tab" {
-                    // toggle_sidebar(^analyzer_state);
-                    break;
-                }
-
-            } else {
-                ui.key_up(event.keyboard.keycode, modifiers);    @KeycodeIsWrong // see above
-            }
-        }
-
-        case .Resize {
-            gl.setSize(event.resize.width, event.resize.height);
-            gfx.set_window_size(event.resize.width, event.resize.height);
-        }
-
-        case .FileRequest {
-            if f := map.get(^on_file_load_callbacks, event.file.file_id); f != null_proc {
-                f(event);
-
-            } else {
-                printf("Warning: No callback set for file id {}.\n", event.file.file_id);
-            }
-        }
-
-        case .FileDropped {
-            data := memory.make_slice(u8, event.file.size);
-            name := memory.make_slice(u8, event.file.name_length);
-            events.get_requested_file_data(event.file.file_id, data, name);
-
-            debug_log(.Info, "File with size {} and name {} was dropped.\n", event.file.size, name);
-
-            if state.has_active_file {
-                memory.free_slice(^state.file.name);
-                memory.free_slice(^state.file.data);
-            }
-
-            state.has_active_file = true;
-            state.file.name = name;
-            state.file.data = data;
-
-            // This transfers ownership of wasm_data to the analyzer_state
-            // load_wasm_binary(^analyzer_state, wasm_data);
-        }
-    }
-}
-
-needs_redraw :: () -> bool {
-    return ui.has_active_animation() || debug.debug_log_transitioning();
-}
-
-update :: (dt: f32) {
-    debug.debug_log_update(dt);
-}
-
-draw :: () {
-    bg_color := config.Colors.background;
-    gl.clearColor(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
-    gl.clear(gl.COLOR_BUFFER_BIT);
-
-    window_width, window_height := gfx.get_window_size();
-    window_rectangle := ui.Rectangle.{ 0, 0, ~~window_width, ~~window_height };
-    menu_bar, main_area := ui.Flow.split_horizontal(window_rectangle, top_height=32);
-
-    ui.workspace_start(main_area);
-    draw_background_lines(~~window_width, ~~window_height, line_color=config.Colors.background);
-
-    ui.draw_rect(0, 0, 20, 20, color=.{1,0,0});
-
-    {
-        #persist window_state := ui.Window_State.{ position=gfx.Vector2.{100, 100}, size=gfx.Vector2.{800, 600} };
-        ui.window_start(^window_state);
-        defer ui.window_end();
-
-        if state.has_active_file {
-            buffer: [512] u8;
-            name_text := conv.str_format("File name: {}", ~~buffer, state.file.name);
-            ui.draw_text(.{ 0, 0, 300, 200 }, name_text);
-
-            size_text := conv.str_format("File size: {} bytes", ~~buffer, state.file.data.count);
-            ui.draw_text(.{ 0, 32, 300, 200 }, size_text);
-
-        } else {
-            ui.draw_text(.{ 0, 0, 300, 200 }, "No file loaded.");
-        }
-    }
-
-    ui.workspace_end();
-
-    // draw_sidebar(^analyzer_state);
-
-    // Menu bar drawing
-    {
-        #insert gfx.save_matrix;
-        
-        gfx.identity();
-        ui.menubar(menu_bar, ^search_buffer, ~~ui.Menu_Bar_Option.[
-            .{ label = "File" },
-            .{ label = "Test" },
-        ]);
-    }
-
-    // Debug log drawing
-    draw_debug_log(window_rectangle);
-
-    gfx.flush();
-    ui.end_frame();
-
-    draw_background_lines :: (width: f32, height: f32, line_color := gfx.Color4.{0.2, 0.2, 0.2}, line_spacing := 32.0f) {
-        gl :: package gl
-
-        #insert gfx.save_matrix;
-
-        trans := gfx.global_renderer->get_transform();
-        sx    := trans.scale.x * line_spacing;
-        sy    := trans.scale.y * line_spacing;
-        tx    := -trans.translation.x / sx;
-        ty    := -trans.translation.y / sy;
-
-        gfx.identity();
-        gfx.set_texture(^background_tile_texture);
-        gfx.textured_rect(.{ 0, 0 }, .{ width, height }, .{ tx, ty }, .{ width / sx, height / sy }, color=line_color); 
-        gfx.set_texture(); 
-    }
-}
-
-#private_file background_tile_texture : gfx.Texture;
-
-
-update_ui_colors :: () {
-    ui.default_text_theme.text_color = config.Colors.foreground;
-
-    ui.default_button_theme.text_color       = config.Colors.primary_text;
-    ui.default_button_theme.background_color = config.Colors.primary_dark;
-    ui.default_button_theme.hover_color      = config.Colors.primary;
-    ui.default_button_theme.click_color      = config.Colors.primary_light;
-    ui.default_button_theme.border_color     = config.Colors.primary;
-
-    ui.default_textbox_theme.text_color       = config.Colors.primary_text;
-    ui.default_textbox_theme.background_color = config.Colors.primary_dark;
-    ui.default_textbox_theme.hover_color      = config.Colors.primary;
-    ui.default_textbox_theme.click_color      = config.Colors.primary_light;
-    ui.default_textbox_theme.border_color     = config.Colors.primary;
-}
diff --git a/src/app/app.onyx b/src/app/app.onyx
new file mode 100644 (file)
index 0000000..b9c8c1d
--- /dev/null
@@ -0,0 +1,439 @@
+package app
+
+use package core
+
+#private_file events :: package js_events
+#private_file gl     :: package gl
+#private_file gfx    :: package immediate_mode
+#private_file ui     :: package ui
+#private_file config :: package config
+#private_file wasm   :: package wasm_utils
+#private_file debug  :: package debug
+
+use package debug { init as debug_init, debug_log, draw_debug_log }
+use package core.intrinsics.onyx { __initialize }
+
+@Relocate search_buffer: string.String_Buffer;
+on_file_load_callbacks : map.Map(u32, (file_event: ^events.Event) -> void);
+
+@Relocate
+Tool :: struct {
+    id:   str;
+    name: str;
+    open: () -> void;
+}
+@Relocate registered_tools : [..] Tool;
+
+// The global application state.
+state : Application_State;
+
+Application_State :: struct {
+    colorscheme: Colorscheme;
+
+    has_active_file := false;
+    file := Active_File.{};
+
+    windows_store  : alloc.pool.PoolAllocator(Application_Window);
+    windows_map    : map.Map(str, ^Application_Window);
+    windows_sorted : [..] ^Application_Window;
+
+    window_switcher_state : Window_Switcher_State;
+    workspace_state       : ui.Workspace_State = .{};
+}
+
+Active_File :: struct {
+    name := null_str;
+    data := (#type [] u8).{ null, 0 };
+}
+
+#private_file MAXIMUM_WINDOWS :: 128
+Application_Window :: struct {
+    id: str;
+
+    window_state : ui.Window_State;
+
+    draw_data    : rawptr;
+    draw         : (data: rawptr, window: ^Application_Window) -> void;
+}
+
+init :: () {
+    debug_init();
+
+    __initialize(^state);
+    window_buffer := memory.make_slice(Application_Window, MAXIMUM_WINDOWS);
+    state.windows_store = alloc.pool.make(window_buffer);
+    map.init(^state.windows_map, hash_count=16);
+    array.init(^state.windows_sorted);
+    state.window_switcher_state->init();
+
+    gl.init("main_canvas");
+    events.init();
+    gfx.immediate_renderer_init();
+
+    ui.init_ui();
+
+    array.init(^registered_tools);
+    map.init(^on_file_load_callbacks);
+
+    colorscheme_switch(.Dark);
+
+    load_background_tile_texture();
+    load_fonts();
+
+    search_buffer = string.buffer_make(memory.make_slice(u8, 256));
+
+    gl.enable(gl.BLEND);
+    gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+    array.push(^registered_tools, .{
+        id   = "load_file_info",
+        name = "File Info",
+        open = () {
+            open_window("load_file_info", "Loaded file info", .{ 0, 0 }, (_: rawptr, win: ^Application_Window) {
+                if state.has_active_file {
+                    buffer: [512] u8;
+                    name_text := conv.str_format("File name: {}", ~~buffer, state.file.name);
+                    ui.draw_text(.{ 0, 0, 300, 200 }, name_text);
+
+                    size_text := conv.str_format("File size: {} bytes", ~~buffer, state.file.data.count);
+                    ui.draw_text(.{ 0, 32, 300, 200 }, size_text);
+
+                } else {
+                    ui.draw_text(.{ 0, 0, 300, 200 }, "No file loaded.");
+                }
+            });
+
+            move_window_to_top("load_file_info");
+            focus_window("load_file_info");
+        }
+    });
+
+    // Dynamically load things in the binary
+    {
+        use type_info;
+
+        // Look through all the types in the program
+        for type: type_table {
+            if type.kind != .Struct do continue;
+
+            ts := cast(^Type_Info_Struct) type;
+            if !string.starts_with(ts.name, "Feature_") do continue;
+
+            // Any types that are a structure and start with "Feature_" will be dynamically loaded
+
+            debug_log(.Info, "Found feature '{}'", string.advance(ts.name, 8));
+
+            for ^member: ts.members {
+                Hook_Function_Type :: #type () -> void;
+
+                if member.name == "setup" {
+                    assert(member.type == Hook_Function_Type, "setup has the wrong type.");
+                    assert(member.default != null, "setup has no default function.");
+
+                    (*(cast(^Hook_Function_Type) member.default))();
+                }
+            }
+        }
+    }
+
+    load_background_tile_texture :: () {
+        background_tile_texture = gfx.load_texture(32, 32, #file_contents "res/images/background_tile.data", gl.RGB, gl.RGB);
+        gl.bindTexture(gl.TEXTURE_2D, background_tile_texture.texture);
+
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
+
+        gl.bindTexture(gl.TEXTURE_2D, -1);
+    }
+
+    load_fonts :: () {
+        use type_info;
+
+        // Dumb check to see if the array is uninitialized
+        if fonts_loading.capacity == 0 do array.init(^fonts_loading, 4);
+
+        fonts_info := cast(^Type_Info_Struct) get_type_info(config.Fonts_Container);
+        for ^member: fonts_info.members {
+            info := cast(^Type_Info_Struct) get_type_info(member.type);
+
+            font_name := member.name;
+
+            font_index    := *cast(^i32) info.parameters[0].data;
+            fnt_file_name := *cast(^str) info.parameters[1].data;
+            tex_file_name := *cast(^str) info.parameters[2].data;
+
+            debug_log(.Info, "Loading font '{}' with index {} from '{}' and '{}'\n", font_name, font_index, fnt_file_name, tex_file_name);
+
+            fnt_file_id := events.request_file(fnt_file_name);
+            tex_file_id := events.request_file(tex_file_name);
+
+            map.put(^on_file_load_callbacks, fnt_file_id, font_file_loaded);
+            map.put(^on_file_load_callbacks, tex_file_id, font_file_loaded);
+
+            array.push(^fonts_loading, .{ font_index, font_name, fnt_file_id, tex_file_id });
+        }
+
+        Loading_Font :: struct {
+            font_index  : u32;
+            font_name   : str;
+
+            fnt_file_id : u32;
+            tex_file_id : u32;
+
+            fnt_file_size := cast(u32) 0;
+            tex_file_size := cast(u32) 0;
+        }
+        #persist fonts_loading : [..] Loading_Font;
+
+        font_file_loaded :: (ev: ^events.Event) {
+            lf: ^Loading_Font = null;
+
+            for ^entry: fonts_loading {
+                if entry.fnt_file_id == ev.file.file_id { entry.fnt_file_size = ev.file.size; lf = entry; }
+                if entry.tex_file_id == ev.file.file_id { entry.tex_file_size = ev.file.size; lf = entry; }
+            }
+
+            assert(lf != null, "Loaded a file for a font that was not registered.");
+
+            if lf.fnt_file_size > 0 && lf.tex_file_size > 0 {
+                fnt_data := memory.make_slice(u8, lf.fnt_file_size);
+                tex_data := memory.make_slice(u8, lf.tex_file_size);
+                defer {
+                    cfree(fnt_data.data); 
+                    cfree(tex_data.data); 
+                }
+
+                @ErrorHandling
+                assert(events.get_requested_file_data(lf.fnt_file_id, fnt_data), "Failed to get bmfont file data.");
+                assert(events.get_requested_file_data(lf.tex_file_id, tex_data), "Failed to get texture data.");
+
+                font := ui.create_font(fnt_data, tex_data);
+                ui.register_font(lf.font_index, font);
+
+                debug_log(.Info, "Successfully loaded font '{}'.", lf.font_name);
+
+                ui.use_font(lf.font_index);
+            }
+        }
+    }
+}
+
+handle_event :: (event: ^events.Event) {
+    switch event.kind {
+        case .MouseDown do switch event.mouse.button {
+            case .Left      do ui.button_pressed(.Left) ;
+            case .Right     do ui.button_pressed(.Right);
+            case .Middle    do ui.button_pressed(.Middle);
+        }
+
+        case .MouseUp do switch event.mouse.button {
+            case .Left      do ui.button_released(.Left);
+            case .Right     do ui.button_released(.Right);
+            case .Middle    do ui.button_released(.Middle);
+        }
+
+        case .MouseMove do ui.update_mouse_position(~~ event.mouse.pos_x, ~~ event.mouse.pos_y);
+
+        case .MouseWheel do switch event.mouse.button {
+            case .WheelUp   do ui.button_pressed(.WheelUp);
+            case .WheelDown do ui.button_pressed(.WheelDown);
+        }
+
+        case .KeyDown, .KeyUp {
+            modifiers : ui.Keyboard_State.Key_Event.Modifiers;
+            if event.keyboard.modifiers & .CTRL  do modifiers |= .CTRL;
+            if event.keyboard.modifiers & .ALT   do modifiers |= .ALT;
+            if event.keyboard.modifiers & .META  do modifiers |= .META;
+            if event.keyboard.modifiers & .SHIFT do modifiers |= .SHIFT;
+
+            if event.kind == .KeyDown {
+                ui.key_down(event.keyboard.keycode, modifiers);   @KeycodeIsWrong // .keycode is apparently not browser independent...
+
+                if event.keyboard->get_name() == "F5" {
+                    refresh :: () -> void #foreign "decompiler" "refresh" ---
+                    refresh();
+                    break;
+                }
+
+                if event.keyboard->get_name() == "F7" {
+                    debug.debug_log_toggle();
+                    break;
+                }
+
+                if event.keyboard->get_name() == "Tab" {
+                    state.window_switcher_state->toggle();
+                    break;
+                }
+
+                @Temporary // This should be a part of the workspace?
+                if event.keyboard->get_name() == "Space" {
+                    open_tool_opener();
+                }
+
+            } else {
+                ui.key_up(event.keyboard.keycode, modifiers);    @KeycodeIsWrong // see above
+            }
+        }
+
+        case .Resize {
+            gl.setSize(event.resize.width, event.resize.height);
+            gfx.set_window_size(event.resize.width, event.resize.height);
+        }
+
+        case .FileRequest {
+            if f := map.get(^on_file_load_callbacks, event.file.file_id); f != null_proc {
+                f(event);
+
+            } else {
+                printf("Warning: No callback set for file id {}.\n", event.file.file_id);
+            }
+        }
+
+        case .FileDropped {
+            data := memory.make_slice(u8, event.file.size);
+            name := memory.make_slice(u8, event.file.name_length);
+            events.get_requested_file_data(event.file.file_id, data, name);
+
+            debug_log(.Info, "File with size {} and name {} was dropped.\n", event.file.size, name);
+
+            if state.has_active_file {
+                memory.free_slice(^state.file.name);
+                memory.free_slice(^state.file.data);
+            }
+
+            state.has_active_file = true;
+            state.file.name = name;
+            state.file.data = data;
+        }
+    }
+}
+
+needs_redraw :: () -> bool {
+    return ui.has_active_animation()
+        || debug.debug_log_transitioning()
+        || state.window_switcher_state->is_animating();
+}
+
+update :: (dt: f32) {
+    debug.debug_log_update(dt);
+}
+
+draw :: () {
+    bg_color := config.Colors.background;
+    gl.clearColor(bg_color.r, bg_color.g, bg_color.b, bg_color.a);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+
+    ui.set_cursor(ui.Cursors.Default);
+
+    window_width, window_height := gfx.get_window_size();
+    window_rectangle := ui.Rectangle.{ 0, 0, ~~window_width, ~~window_height };
+    menu_bar, main_area := ui.Flow.split_horizontal(window_rectangle, top_height=32);
+
+    ui.workspace_start(main_area, state=^state.workspace_state);
+    draw_background_lines(~~window_width, ~~window_height, line_color=config.Colors.background);
+
+    // Used for detecting when a window is completely off-screen and shouldn't be rendered.
+    transformed_window_rect := window_rectangle;
+    {
+        trans := gfx.global_renderer->get_transform();
+        transformed_window_rect.x0 -= trans.translation.x / trans.scale.x;
+        transformed_window_rect.y0 -= trans.translation.y / trans.scale.y;
+        transformed_window_rect.x1 = transformed_window_rect.x0 + (~~window_width / trans.scale.x);
+        transformed_window_rect.y1 = transformed_window_rect.y0 + (~~window_height / trans.scale.y);
+    }
+
+    i := 0;
+    new_top: ^Application_Window = null;
+    for window: state.windows_sorted {
+        defer i += 1;
+
+        if !(window.window_state->get_rectangle() |> ui.Rectangle.intersects(transformed_window_rect)) do continue;
+
+        if ui.window_start(^window.window_state, increment=hash.to_u32(window.id)) {
+            new_top = window;
+        }
+        defer ui.window_end();
+
+        window.window_state.background_color = config.Colors.background;
+        window.window_state.border_color = config.Colors.primary_dark;
+
+        window.draw(window.draw_data, window);
+
+        if window.window_state.should_close {
+            close_window(window.id);
+        }
+    }
+
+    if new_top != null {
+        move_window_to_top(new_top.id);
+    }
+
+    ui.workspace_end();
+
+    state.window_switcher_state->draw(window_rectangle);
+
+    // Menu bar drawing
+    {
+        #insert gfx.save_matrix;
+        
+        gfx.identity();
+        ui.menubar(menu_bar, ^search_buffer, ~~ui.Menu_Bar_Option.[
+            .{ label = "File" },
+            .{ label = "Test" },
+        ]);
+    }
+
+    // Debug log drawing
+    draw_debug_log(window_rectangle);
+
+    gfx.flush();
+    ui.end_frame();
+}
+
+open_tool_opener :: () {
+    mouse_pos := gfx.transform_point(^state.workspace_state.transform, .{ ui.mouse_state.x_, ui.mouse_state.y_ });
+
+    close_window("tool_opener");
+    open_window("tool_opener", "Tool Opener", .{ mouse_pos.x, mouse_pos.y }, init_size=.{ 200, 400 },
+        draw=(_: rawptr, win: ^Application_Window) {
+        window_rect := ui.Rectangle.{ 0, 0, win.window_state.size.x, win.window_state.size.y };
+
+        ui.scrollable_region_start(window_rect, maximum_y=~~(registered_tools.count - 1) * 40.0f);
+        defer ui.scrollable_region_stop();
+
+        button_rect : ui.Rectangle;
+        button_theme := ui.default_button_theme;
+
+        for ^entry: registered_tools {
+            button_theme.background_color = config.Colors.secondary if window_is_open(entry.id) else config.Colors.primary_dark;
+
+            button_rect, window_rect = ui.Flow.split_horizontal(window_rect, top_height=40);
+            if ui.button(button_rect, entry.name, theme=^button_theme, increment=hash.to_u32(entry.id)) {
+                entry.open();
+
+                @Setting
+                close_window("tool_opener");
+            }
+        }
+    });
+}
+
+#private_file background_tile_texture : gfx.Texture;
+#private_file draw_background_lines :: (width: f32, height: f32, line_color := gfx.Color4.{0.2, 0.2, 0.2}, line_spacing := 32.0f) {
+    gl :: package gl
+
+    #insert gfx.save_matrix;
+
+    trans := gfx.global_renderer->get_transform();
+    sx    := trans.scale.x * line_spacing;
+    sy    := trans.scale.y * line_spacing;
+    tx    := -trans.translation.x / sx;
+    ty    := -trans.translation.y / sy;
+
+    gfx.identity();
+    gfx.set_texture(^background_tile_texture);
+    gfx.textured_rect(.{ 0, 0 }, .{ width, height }, .{ tx, ty }, .{ width / sx, height / sy }, color=line_color); 
+    gfx.set_texture(); 
+}
diff --git a/src/app/colors.onyx b/src/app/colors.onyx
new file mode 100644 (file)
index 0000000..86784bf
--- /dev/null
@@ -0,0 +1,104 @@
+package app
+
+use package core
+
+#private_file events :: package js_events
+#private_file gl     :: package gl
+#private_file gfx    :: package immediate_mode
+#private_file ui     :: package ui
+#private_file config :: package config
+#private_file wasm   :: package wasm_utils
+#private_file debug  :: package debug
+
+use package debug { init as debug_init, debug_log, draw_debug_log }
+use package core.intrinsics.onyx { __initialize }
+
+Colorscheme :: enum {
+    Undefined;
+    Dark;
+    Light;
+}
+
+colorscheme_switch :: (new_scheme: Colorscheme) {
+    if state.colorscheme == new_scheme do return;
+
+    state.colorscheme = new_scheme;
+
+    colors_scheme_file: str;
+    switch state.colorscheme {
+        case .Dark do colors_scheme_file = config.dark_color_scheme_file;
+        case .Light do colors_scheme_file = config.light_color_scheme_file;
+        case #default do assert(false, "Bad colorscheme setting.");
+    }
+
+    color_file := events.request_file(colors_scheme_file);
+    map.put(^on_file_load_callbacks, color_file, load_colors);
+}
+
+load_colors :: (event: ^events.Event) {
+    json :: package json
+
+    assert(event.kind == .FileRequest, "Bad event type");
+    assert(event.file.status == .Success, "Failed to load color file");
+
+    color_data := memory.make_slice(u8, event.file.size);
+    defer if color_data.count > 0 do cfree(color_data.data);
+    events.get_requested_file_data(event.file.file_id, color_data);
+
+    arena := alloc.arena.make(context.allocator, 4096);
+    defer alloc.arena.free(^arena);
+    colors := json.decode(color_data, alloc.arena.make_allocator(^arena));
+    defer json.free(colors);
+
+    config.Colors.dark_background = decode_color(colors.root["dark_background"]);
+    config.Colors.background      = decode_color(colors.root["background"]);
+    config.Colors.foreground      = decode_color(colors.root["foreground"]);
+
+    config.Colors.keyword         = decode_color(colors.root["keyword"]);
+    config.Colors.value           = decode_color(colors.root["value"]);
+    config.Colors.jumppoint       = decode_color(colors.root["jumppoint"]);
+
+    config.Colors.primary         = decode_color(colors.root["primary"]);
+    config.Colors.primary_light   = decode_color(colors.root["primary_light"]);
+    config.Colors.primary_dark    = decode_color(colors.root["primary_dark"]);
+    config.Colors.primary_text    = decode_color(colors.root["primary_text"]);
+
+    config.Colors.secondary       = decode_color(colors.root["secondary"]);
+    config.Colors.secondary_light = decode_color(colors.root["secondary_light"]);
+    config.Colors.secondary_dark  = decode_color(colors.root["secondary_dark"]);
+    config.Colors.secondary_text  = decode_color(colors.root["secondary_text"]);
+
+    update_ui_colors();
+
+    decode_color :: (v: ^json.Value) -> gfx.Color4 {
+        return .{
+            r = ~~v[0]->as_float(),
+            g = ~~v[1]->as_float(),
+            b = ~~v[2]->as_float(),
+        };
+    }
+
+    debug_log(.Info, "Successfully loaded colorscheme '{}'.", state.colorscheme);
+}
+
+update_ui_colors :: () {
+    ui.default_text_theme.text_color = config.Colors.foreground;
+
+    ui.default_button_theme.text_color       = config.Colors.primary_text;
+    ui.default_button_theme.background_color = config.Colors.primary_dark;
+    ui.default_button_theme.hover_color      = config.Colors.primary;
+    ui.default_button_theme.click_color      = config.Colors.primary_light;
+    ui.default_button_theme.border_color     = config.Colors.primary;
+
+    ui.default_textbox_theme.text_color       = config.Colors.primary_text;
+    ui.default_textbox_theme.background_color = config.Colors.primary_dark;
+    ui.default_textbox_theme.hover_color      = config.Colors.primary;
+    ui.default_textbox_theme.click_color      = config.Colors.primary_light;
+    ui.default_textbox_theme.border_color     = config.Colors.primary;
+
+    ui.default_radio_theme.text_color = config.Colors.foreground;
+    ui.default_radio_theme.radio_color = config.Colors.primary_dark;
+    ui.default_radio_theme.selected_color = config.Colors.secondary;
+    ui.default_radio_theme.hover_color = config.Colors.primary;
+    ui.default_radio_theme.click_color = config.Colors.primary_light;
+}
diff --git a/src/app/debug_log.onyx b/src/app/debug_log.onyx
new file mode 100644 (file)
index 0000000..72b1c81
--- /dev/null
@@ -0,0 +1,139 @@
+package debug
+
+use package core
+#private_file ui  :: package ui
+#private_file gfx :: package immediate_mode
+#private_file config :: package config
+
+#private y_scroll := 0.0f;
+#private debug_log_y_offset := 0.0f;
+#private debug_log_y_offset_target := 0.0f;
+
+init :: () {
+    log_buffer.line_arena = alloc.arena.make(context.allocator, 4096);
+    log_buffer.lines = array.make(str);
+
+    debug_log(.Debug, "Debug system initialized.", null);
+}
+
+Severity :: enum {
+    Debug;
+    Info;
+    Warning;
+    Error;
+}
+
+minimum_severity := Severity.Debug;
+
+debug_log_toggle :: () {
+    debug_log_y_offset_target = 1 - debug_log_y_offset_target;
+}
+
+debug_log_transitioning :: () -> bool {
+    return debug_log_y_offset != debug_log_y_offset_target;
+}
+
+debug_log_update :: (dt: f32) {
+    ui.move_towards(^debug_log_y_offset, debug_log_y_offset_target, 4 * dt);
+}
+
+debug_log_clear :: () {
+    array.clear(^log_buffer.lines);
+    alloc.arena.free(^log_buffer.line_arena);
+
+    log_buffer.line_arena = alloc.arena.make(context.allocator, 4096);
+}
+
+debug_log :: (severity: Severity, format: str, args: ..any) {
+    if severity < minimum_severity do return;
+
+    buffer1: [4096] u8;
+    s := conv.str_format_va(format, ~~buffer1, ~~args);
+    lines := string.split(s, #char "\n", context.temp_allocator);
+
+    line_alloc := alloc.arena.make_allocator(^log_buffer.line_arena);
+    for l: lines {
+        if l.count == 0 do continue;
+
+        buffer2: [4096] u8;
+        s = conv.str_format("[{}] {}\n", ~~buffer2, severity, l);
+        line := string.alloc_copy(s, line_alloc);
+
+        array.push(^log_buffer.lines, line);
+    }
+}
+
+draw_debug_log :: (window_rectangle: ui.Rectangle, site := #callsite) {
+    if debug_log_y_offset == 0.0f do return;
+
+    r, _ := ui.Flow.split_horizontal(window_rectangle, top_percent=.5);
+    height := ui.Rectangle.height(r);
+    r.y0 -= height * (1 - debug_log_y_offset);
+    r.y1 -= height * (1 - debug_log_y_offset);
+
+    hash := ui.get_site_hash(site);
+
+    mx, my := ui.get_mouse_position();
+    if ui.Rectangle.contains(r, mx, my) {
+        ui.set_hot_item(hash, false);
+    }
+
+    line_spacing := 28.0f;
+
+    if ui.is_hot_item(hash) {
+        if ui.mouse_state.dwheel > 0 do y_scroll -= 20.0f;
+        if ui.mouse_state.dwheel < 0 do y_scroll += 20.0f;
+
+        if ui.is_key_down(38) do y_scroll -= 20.0f;
+        if ui.is_key_down(40) do y_scroll += 20.0f;
+    }
+
+    x, y := ui.Rectangle.bottom_left(r);
+    w, h := ui.Rectangle.dimensions(r);
+
+    gfx.push_scissor(x, y - h, w, h);
+    defer gfx.pop_scissor();
+
+    background_color := config.Colors.background;
+    background_color.a = 0.8;
+    ui.draw_rect(r, color=background_color);
+
+    scale :: 0.75f;
+    y_offset := line_spacing * scale + y_scroll;
+
+    while i := log_buffer.lines.count - 1; cast(i32) i >= 0 {
+        defer i -= 1;
+
+        ui.draw_text_raw(log_buffer.lines[i], x, y - y_offset, color=.{ 0.2, 0.7, 0.2 }, font=1, size=scale);
+        y_offset += line_spacing * scale;
+    }
+
+    clear_button_rect := ui.Rectangle.{
+        x0 = r.x0, x1 = r.x0 + 200,
+        y0 = r.y0, y1 = r.y0 + 50,
+    };
+
+    if ui.button(clear_button_rect, "Clear log") {
+        debug_log_clear();
+    }
+
+    app :: package app
+    colorscheme := app.state.colorscheme;
+
+    clear_button_rect.x0 += 220;
+    clear_button_rect.x1 += 220;
+    switched := ui.radio(clear_button_rect, ^colorscheme, .Dark, "Dark mode");
+    clear_button_rect.x0 += 200;
+    clear_button_rect.x1 += 200;
+    switched = switched || ui.radio(clear_button_rect, ^colorscheme, .Light, "Light mode");
+
+    if switched {
+        app.colorscheme_switch(colorscheme);
+    }
+}
+
+#private_file log_buffer : struct {
+    line_arena : alloc.arena.ArenaState;
+    lines      : [..] str;
+}
+
diff --git a/src/app/window_management.onyx b/src/app/window_management.onyx
new file mode 100644 (file)
index 0000000..273f512
--- /dev/null
@@ -0,0 +1,106 @@
+package app
+
+use package core
+
+#private_file events :: package js_events
+#private_file gl     :: package gl
+#private_file gfx    :: package immediate_mode
+#private_file ui     :: package ui
+#private_file config :: package config
+#private_file wasm   :: package wasm_utils
+#private_file debug  :: package debug
+
+use package debug { init as debug_init, debug_log, draw_debug_log }
+use package core.intrinsics.onyx { __initialize }
+
+open_window :: (id: str,
+                    title: str,
+                    init_position: gfx.Vector2,
+                    draw: (rawptr, ^Application_Window) -> void,
+                    init_size := gfx.Vector2.{ 600, 600 }) {
+
+    if map.has(^state.windows_map, id) {
+        debug_log(.Warning, "Window with id '{}' is already registered.", id);
+        return;
+    }
+
+    debug_log(.Info, "Registering window with id '{}'.", id);
+
+    win := alloc.pool.pool_alloc(^state.windows_store);
+    __initialize(win);
+    __initialize(^win.window_state);
+
+    win.id = id;
+
+    win.window_state.background_color = config.Colors.background;
+    win.window_state.border_color = config.Colors.primary_dark;
+    win.window_state.position = init_position;
+    win.window_state.size = init_size;
+    win.window_state.title = title;
+
+    win.draw = draw;
+
+    map.put(^state.windows_map, id, win);
+    array.push(^state.windows_sorted, win);
+}
+
+move_window_to_top :: (id: str) {
+    if !map.has(^state.windows_map, id) {
+        debug_log(.Warning, "Window '{}' does not exist when trying to move to top.", id);
+        return;
+    }
+
+    win := map.get(^state.windows_map, id);
+
+    index := array.find(^state.windows_sorted, win);
+    assert(index >= 0, "Window not found.");
+
+    array.transplant(^state.windows_sorted, index, state.windows_sorted.count - 1);
+}
+
+focus_window :: (id: str) {
+    if !map.has(^state.windows_map, id) {
+        debug_log(.Warning, "Window '{}' does not exist when trying to focus.", id);
+        return;
+    }
+
+    win := map.get(^state.windows_map, id);
+
+    transform: gfx.Transform;
+    gfx.transform_identity(^transform);
+
+    entire_width, entire_height := gfx.get_window_size();
+    hw := cast(f32) entire_width / 2;
+    hh := cast(f32) entire_height / 2;
+
+    win_hw := win.window_state.size.x / 2;
+    win_hh := win.window_state.size.y / 2;
+
+    scale_x, scale_y := state.workspace_state.transform.scale.x, state.workspace_state.transform.scale.y;
+
+    gfx.transform_translate(^transform, .{hw, hh});
+    gfx.transform_scale(^transform, .{ scale_x, scale_y });
+    gfx.transform_apply(^transform, .{
+        translation = .{ -win.window_state.position.x - win_hw, -win.window_state.position.y - win_hh },
+        scale       = .{ 1, 1 }
+    });
+
+    state.workspace_state.target_transform = transform;
+    state.workspace_state.transform_transition = 1.0f;
+}
+
+close_window :: (id: str) {
+    if !map.has(^state.windows_map, id) {
+        return;
+    }
+
+    win := map.get(^state.windows_map, id);
+
+    alloc.pool.pool_free(^state.windows_store, win);
+    map.delete(^state.windows_map, id);
+    array.remove(^state.windows_sorted, win);
+}
+
+window_is_open :: (id: str) -> bool {
+    return map.has(^state.windows_map, id);
+}
\ No newline at end of file
diff --git a/src/app/window_switcher.onyx b/src/app/window_switcher.onyx
new file mode 100644 (file)
index 0000000..e54e90b
--- /dev/null
@@ -0,0 +1,124 @@
+package app
+
+#private_file ui  :: package ui
+#private_file gfx :: package immediate_mode
+#private_file config :: package config
+#private_file math :: package core.math
+#private_file string :: package core.string
+#private_file memory :: package core.memory
+use package core.intrinsics.onyx { __initialize }
+
+Window_Switcher_State :: struct {
+    visibility := 0.0f;
+    visibility_target := 0.0f;
+
+    window := ui.Window_State.{
+        gfx.Vector2.zero, gfx.Vector2.zero,
+        resizable = false, draggable = false,
+    };
+
+    search_buffer: string.String_Buffer;
+    x_scroll := 0.0f;
+    y_scroll := 0.0f;
+
+    selected_index: i32 = 0;
+
+    init :: (use state: ^Window_Switcher_State) {
+        __initialize(state);
+        search_buffer = string.buffer_make(memory.make_slice(u8, 256));
+    }
+
+    toggle :: (use this: ^Window_Switcher_State) {
+        visibility_target = 1 - visibility_target;
+        if visibility_target == 0 do this->reset();
+    }
+
+    reset :: (use this: ^Window_Switcher_State) {
+        selected_index = state.windows_sorted.count - 1;
+        string.buffer_clear(^search_buffer);
+    }
+
+    is_animating :: (use state: ^Window_Switcher_State) -> bool {
+        return visibility_target != visibility;
+    }
+
+    select_window :: (use state: ^Window_Switcher_State) {
+        window := (package app).state.windows_sorted[selected_index];
+        state->toggle();
+
+        (package app).move_window_to_top(window.id);
+        (package app).focus_window(window.id);
+    }
+
+    draw :: (use s: ^Window_Switcher_State, window_rectangle: ui.Rectangle) {
+        ui.move_towards(^visibility, visibility_target, 0.08f);
+        if visibility == 0.0f do return;
+
+        #insert gfx.save_matrix;
+        gfx.identity();
+
+        quater_width  := ui.Rectangle.width(window_rectangle) / 4;
+        quater_height := ui.Rectangle.width(window_rectangle) / 4;
+
+        wv := math.lerp(visibility, quater_width, 100);
+        hv := math.lerp(visibility, quater_height, 100);
+        switcher_rect := ui.Flow.padding(window_rectangle, wv, hv, wv, hv);
+        window.position = .{ switcher_rect.x0, switcher_rect.y0 };
+        window.size     = .{ switcher_rect.x1 - switcher_rect.x0, switcher_rect.y1 - switcher_rect.y0 };
+        window.title    = "Window switcher";
+
+        window.background_color = config.Colors.background;
+        window.border_color = config.Colors.primary_dark;
+
+        ui.window_start(^window);
+            // Enter is pressed.
+            if ui.is_key_just_down(13) do s->select_window();
+
+            if ui.is_key_just_down(38) do selected_index += 1;
+            if ui.is_key_just_down(40) do selected_index -= 1;
+
+            selected_index = math.clamp(selected_index, 0, state.windows_sorted.count - 1);
+
+            window_rect := ui.Rectangle.{ 0, 0, window.size.x, window.size.y };
+
+            textbox_rect : ui.Rectangle;
+            textbox_rect, window_rect = ui.Flow.split_horizontal(window_rect, top_height=48);
+
+            textbox_theme := ui.default_textbox_theme;
+            textbox_theme.border_color = config.Colors.primary_dark;
+
+            ui.textbox(textbox_rect, ^search_buffer, "Search for a window...", theme=^textbox_theme);
+
+            text_rect : ui.Rectangle;
+            mx, my := ui.get_mouse_position();
+
+            i := cast(i32) state.windows_sorted.count - 1;
+            while i >= 0 {
+                window := state.windows_sorted[i];
+                defer i -= 1;
+
+                text_rect, window_rect = ui.Flow.split_horizontal(window_rect, top_height=48);
+
+                if ui.Rectangle.contains(text_rect, mx, my) {
+                    selected_index = i;
+                    #if #defined(ui.set_cursor) { ui.set_cursor(ui.Cursors.Pointer); }
+                }
+
+                if selected_index == i {
+                    ui.draw_rect(text_rect, color=config.Colors.primary_light);
+                }
+
+                text_rect = ui.Flow.padding(text_rect, left=24);
+                ui.draw_text(text_rect, window.window_state.title);
+            }
+
+        ui.window_end();
+
+        if window.should_close {
+            window.should_close = false;
+            visibility_target = 0.0f;
+
+            s->reset();
+        }
+    }
+}
index 4cc582dbe7d79984c720151b44d5436e424dd8ef..a1c11506b7e71f47b962f506e26e8ba54a3113de 100644 (file)
     #load "modules/ui/module"
     #load "modules/js_events/module"
     #load "modules/bmfont/module"
-
-    #load "src/main"
     
-    #load "src/app"
-    #load "src/features/load_features"
-
     #load "src/ui/window"
     #load "src/ui/menubar"
-    #load "src/debug_log"
+    #load "src/ui/cursor"
+
+    #load "src/app/app"
+    #load "src/app/debug_log"
+    #load "src/app/window_switcher"
+    #load "src/app/window_management"
+    #load "src/app/colors"
+
+    #load "src/features/load_features"
+
+    #load "src/main"
 }
 
 #if (package runtime).Runtime == (package runtime).Runtime_Wasi {
index 8f11ac62023990962eaae203017b0a22661e1393..cd3f1e0ccbcf26f4dbe0a18f4fb37f457f0f1434 100644 (file)
@@ -6,7 +6,8 @@ use package immediate_mode { Color4 }
 
 ONLY_REDRAW_ON_EVENTS :: true
 
-color_scheme_file :: "/res/colors_dark.json"
+light_color_scheme_file :: "/res/colors_light.json"
+dark_color_scheme_file  :: "/res/colors_dark.json"
 
 Colors : struct {
     dark_background : Color4;
diff --git a/src/debug_log.onyx b/src/debug_log.onyx
deleted file mode 100644 (file)
index c014014..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-package debug
-
-use package core
-#private_file ui  :: package ui
-#private_file gfx :: package immediate_mode
-#private_file config :: package config
-
-#private y_scroll := 0.0f;
-#private debug_log_y_offset := 0.0f;
-#private debug_log_y_offset_target := 0.0f;
-
-init :: () {
-    log_buffer.line_arena = alloc.arena.make(context.allocator, 4096);
-    log_buffer.lines = array.make(str);
-
-    debug_log(.Debug, "Debug system initialized.", null);
-}
-
-Severity :: enum {
-    Debug;
-    Info;
-    Warning;
-    Error;
-}
-
-minimum_severity := Severity.Debug;
-
-debug_log_toggle :: () {
-    debug_log_y_offset_target = 1 - debug_log_y_offset;
-}
-
-debug_log_transitioning :: () -> bool {
-    return debug_log_y_offset != debug_log_y_offset_target;
-}
-
-debug_log_update :: (dt: f32) {
-    ui.move_towards(^debug_log_y_offset, debug_log_y_offset_target, 4 * dt);
-}
-
-debug_log_clear :: () {
-    array.clear(^log_buffer.lines);
-    alloc.arena.free(^log_buffer.line_arena);
-
-    log_buffer.line_arena = alloc.arena.make(context.allocator, 4096);
-}
-
-debug_log :: (severity: Severity, format: str, args: ..any) {
-    if severity < minimum_severity do return;
-
-    buffer1: [4096] u8;
-    s := conv.str_format_va(format, ~~buffer1, ~~args);
-    lines := string.split(s, #char "\n", context.temp_allocator);
-
-    line_alloc := alloc.arena.make_allocator(^log_buffer.line_arena);
-    for l: lines {
-        if l.count == 0 do continue;
-
-        buffer2: [4096] u8;
-        s = conv.str_format("[{}] {}\n", ~~buffer2, severity, l);
-        line := string.alloc_copy(s, line_alloc);
-
-        array.push(^log_buffer.lines, line);
-    }
-}
-
-draw_debug_log :: (window_rectangle: ui.Rectangle, site := #callsite) {
-    if debug_log_y_offset == 0.0f do return;
-
-    r, _ := ui.Flow.split_horizontal(window_rectangle, top_percent=.5);
-    height := ui.Rectangle.height(r);
-    r.y0 -= height * (1 - debug_log_y_offset);
-    r.y1 -= height * (1 - debug_log_y_offset);
-
-    hash := ui.get_site_hash(site);
-
-    mx, my := ui.get_mouse_position();
-    if ui.Rectangle.contains(r, mx, my) {
-        ui.set_hot_item(hash, false);
-    }
-
-    line_spacing := 28.0f;
-
-    if ui.is_hot_item(hash) {
-        if ui.mouse_state.dwheel > 0 do y_scroll -= 20.0f;
-        if ui.mouse_state.dwheel < 0 do y_scroll += 20.0f;
-
-        if ui.is_key_down(38) do y_scroll -= 20.0f;
-        if ui.is_key_down(40) do y_scroll += 20.0f;
-    }
-
-    x, y := ui.Rectangle.bottom_left(r);
-    w, h := ui.Rectangle.dimensions(r);
-
-    gfx.push_scissor(x, y - h, w, h);
-    defer gfx.pop_scissor();
-
-    background_color := config.Colors.background;
-    background_color.a = 0.8;
-    ui.draw_rect(r, color=background_color);
-
-    scale :: 0.75f;
-    y_offset := line_spacing * scale + y_scroll;
-
-    while i := log_buffer.lines.count - 1; cast(i32) i >= 0 {
-        defer i -= 1;
-
-        ui.draw_text_raw(log_buffer.lines[i], x, y - y_offset, color=.{ 0.2, 0.7, 0.2 }, font=1, size=scale);
-        y_offset += line_spacing * scale;
-    }
-
-    clear_button_rect := ui.Rectangle.{
-        x0 = r.x0, x1 = r.x0 + 200,
-        y0 = r.y0, y1 = r.y0 + 50,
-    };
-
-    if ui.button(clear_button_rect, "Clear log") {
-        debug_log_clear();
-    }
-}
-
-#private_file log_buffer : struct {
-    line_arena : alloc.arena.ArenaState;
-    lines      : [..] str;
-}
-
diff --git a/src/features/hex_editor/feature.onyx b/src/features/hex_editor/feature.onyx
new file mode 100644 (file)
index 0000000..6b8548e
--- /dev/null
@@ -0,0 +1,7 @@
+package feature.hex_viewer
+
+#load "./hex_viewer"
+
+Feature_Hex_Viewer :: struct {
+    setup := setup;
+}
\ No newline at end of file
diff --git a/src/features/hex_editor/hex_viewer.onyx b/src/features/hex_editor/hex_viewer.onyx
new file mode 100644 (file)
index 0000000..17cf620
--- /dev/null
@@ -0,0 +1,131 @@
+package feature.hex_viewer
+
+#private_file app :: package app
+#private_file ui :: package ui
+#private_file config :: package config
+
+use package core
+use package debug { debug_log }
+
+window_id :: "hex_viewer"
+window_name :: "Hex Viewer"
+
+Hex_Viewer_State :: struct {
+    scrollable_region := ui.Scrollable_Region_State.{};
+}
+
+// Should this be global? Or should each viewer window get their own?
+#private viewer_state := Hex_Viewer_State.{};
+
+window_draw :: (_: rawptr, win: ^app.Application_Window) {
+    orig_r: ui.Rectangle = .{ 0, 0, win.window_state.size.x, win.window_state.size.y };
+    r := orig_r;
+
+    text_theme := ui.default_text_theme;
+    text_theme.font = config.Fonts.FiraCode.index;
+    text_theme.font_size = 1.0f;
+
+    ui.use_font(text_theme.font);
+    em_height := ui.get_text_height("M", text_theme.font_size) / 2;
+
+    win.window_state.max_size.x = (10 + 1 + 3 * 16 + 2 + 16) * em_height;
+    win.window_state.background_color = config.Colors.dark_background;
+
+    if !app.state.has_active_file {
+        ui.draw_text(r, "No loaded file.", theme=^text_theme);
+        return;
+    }
+
+    file_data := app.state.file.data;
+
+    {
+        // This is a hack to get the region to be "scrollable" without actually applying any of the scrolling effects
+        ui.scrollable_region_start(r, maximum_y=em_height*~~(file_data.count / 8 - 1), state=^viewer_state.scrollable_region);
+        ui.scrollable_region_stop();
+    }
+
+    line_data: [128] u8;
+    line := string.buffer_make(~~line_data);
+
+    first_line: i32 = ~~math.floor(-viewer_state.scrollable_region.transform.translation.y / (2*em_height));
+    line_count: i32 = ~~math.ceil(win.window_state.size.y / (em_height));
+
+    visible_lines := first_line .. (first_line + line_count);
+
+    for i: visible_lines {
+        if i * 16 >= file_data.count do break;
+
+        offset_data: [16] u8;
+        output := conv.str_format("{w8b16} ", ~~offset_data, i * 16);
+        ui.draw_text(r, output, theme=^text_theme);
+        r = ui.Flow.padding(r, top=32);
+    }
+
+    r = ui.Flow.padding(orig_r, left=10*em_height);
+
+    for i: visible_lines {
+        if i * 16 >= file_data.count do break;
+
+        string.buffer_clear(^line);
+
+        for ch: 16 {
+            index := i * 16 + ch;
+            if index >= file_data.count do break;
+
+            ch_data: [8] u8;
+            output := conv.str_format("{w2b16} ", ~~ch_data, cast(u32) file_data[index]);
+            string.buffer_append(^line, output);
+        }
+
+        ui.draw_text(r, string.buffer_to_str(^line), theme=^text_theme);
+        r = ui.Flow.padding(r, top=32);
+    }
+
+    r = ui.Flow.padding(orig_r, left=3*em_height*16 + 10*em_height + 32);
+
+    for i: visible_lines {
+        if i * 16 >= file_data.count do break;
+
+        string.buffer_clear(^line);
+
+        for ch: 16 {
+            index := i * 16 + ch;
+            if index >= file_data.count do break;
+
+            char := file_data[index];
+
+            data: [1] u8;
+            if char >= 32 && char <= 126 do data[0] = char;
+            else                         do data[0] = #char ".";
+
+            string.buffer_append(^line, ~~data);
+        }
+
+        ui.draw_text(r, string.buffer_to_str(^line), theme=^text_theme);
+        r = ui.Flow.padding(r, top=32);
+    }
+}
+
+open_window :: () {
+    app.open_window(window_id, window_name, .{ 0, 0 }, window_draw, .{ 1300, 650 });
+    app.move_window_to_top(window_id);
+    app.focus_window(window_id);
+
+/*
+    app.open_window("dummy", "DUMMY TESTING", .{ 0, 0 }, (_: rawptr, _: ^app.Application_Window) {
+        buffer: [1024] u8;
+        s := conv.str_format("{p}", ~~buffer, viewer_state);
+        ui.draw_text(.{0,0,0,0}, s);
+    });
+*/
+}
+
+setup :: () {
+    debug_log(.Debug, "Initializing hex viewer...", 0);
+
+    array.push(^app.registered_tools, .{
+        id   = window_id,
+        name = window_name,
+        open = open_window,
+    });
+}
\ No newline at end of file
index 6e111df4b03dd8503631fab36a16f8966c5802a0..367ab17f4460ceea074dbf38bd30c0df6bbe508b 100644 (file)
@@ -1,4 +1,6 @@
 
 // Add #load statements for the features
 
-#load "./wasm/feature"
\ No newline at end of file
+#load "./wasm/feature"
+#load "./hex_editor/feature"
+#load "./text_editor/feature"
\ No newline at end of file
diff --git a/src/features/text_editor/feature.onyx b/src/features/text_editor/feature.onyx
new file mode 100644 (file)
index 0000000..8bb1fc4
--- /dev/null
@@ -0,0 +1,7 @@
+package feature.text_editor
+
+#load "./text_editor"
+
+Feature_Text_Editor :: struct {
+    setup := setup;
+}
\ No newline at end of file
diff --git a/src/features/text_editor/text_editor.onyx b/src/features/text_editor/text_editor.onyx
new file mode 100644 (file)
index 0000000..8948eb8
--- /dev/null
@@ -0,0 +1,37 @@
+package feature.text_editor
+
+#private_file app :: package app
+#private_file ui  :: package ui
+#private_file config :: package config
+
+use package core
+use package debug { debug_log }
+
+window_id :: "text_editor"
+window_name :: "Text Editor"
+
+open_text_editor :: () {
+    app.open_window(window_id, window_name, .{ 0, 0 }, (_: rawptr, win: ^app.Application_Window) {
+        if !app.state.has_active_file do return;
+
+        ui.scrollable_region_start(.{ 0, 0, win.window_state.size.x, win.window_state.size.y });
+        defer ui.scrollable_region_stop();
+        
+        text_theme := ui.default_text_theme;
+        text_theme.font = config.Fonts.FiraCode.index;
+        ui.draw_text(.{ 0, 0, 200, 200 }, app.state.file.data, theme=^text_theme);
+    });
+
+    app.move_window_to_top(window_id);
+    app.focus_window(window_id);
+}
+
+setup :: () {
+    debug_log(.Debug, "Initializing text editor...", 0);
+
+    array.push(^app.registered_tools, .{
+        id   = window_id,
+        name = window_name,
+        open = open_text_editor,
+    });
+}
\ No newline at end of file
index 3fd098367ba5f5eb62126a88a978c6a81fa4467b..cf47133c82b043a8662a309267a8972edd3ed328 100644 (file)
@@ -1,4 +1,4 @@
-package feature_wasm
+package feature.wasm
 
 #load "./wasm"
 
index 74472381f1b037ee7368884c146684aafe4bc68c..cac42be62c1ca6030a36ebdcf0b2e9f68d9ebf8e 100644 (file)
@@ -1,8 +1,8 @@
-package feature_wasm
+package feature.wasm
 
 use package app { Application_State }
 use package debug { debug_log }
 
-setup :: (use app: ^Application_State) {
+setup :: () {
     debug_log(.Info, "Wasm Loader Loaded from {}", #file);
 }
\ No newline at end of file
diff --git a/src/ui/cursor.onyx b/src/ui/cursor.onyx
new file mode 100644 (file)
index 0000000..70ead9d
--- /dev/null
@@ -0,0 +1,17 @@
+// Maybe this should become part of the core ui library?
+
+package ui
+
+Cursors :: struct {
+    Default :: "default";
+    Text    :: "text";
+    Pointer :: "pointer";
+
+    NS_Resize   :: "ns-resize";
+    EW_Resize   :: "ew-resize";
+    NWSE_Resize :: "nwse-resize";
+
+    Move :: "move";
+}
+
+set_cursor :: (cursor_name: str) -> void #foreign "decompiler" "set_cursor" ---
\ No newline at end of file
index db54a63465325028514c08cacd80b1de00bfcc91..b147c7a488182135f25a3ff220f96e758df6993f 100644 (file)
@@ -6,18 +6,33 @@ Window_State :: struct {
     position: gfx.Vector2;
     size:     gfx.Vector2;
 
-    border_width := 4.0f;
+    max_size := gfx.Vector2.{ 100000.0f, 100000.0f };
+
+    title := "(undefined)";
+
+    border_width := 10.0f;
     border_color := gfx.Color4.{ 1, 0, 0 };
     background_color := gfx.Color4.{ 0.2, 0.2, 0.2 };
-    active_color     := gfx.Color4.{ 0.3, 0.3, 0.3 };
+
+    bar_height := 34.0f;
+
+    draggable := true;
+    resizable := true;
+
+    should_close := false;
+    dragging     := false;
+    resizing     := false;
 
     get_rectangle :: (use w: ^Window_State) -> Rectangle {
-        return .{ position.x, position.y, position.x + size.x, position.y + size.y };
+        bw := border_width;
+        if dragging do bw *= 5;
+
+        return .{ position.x - bw, position.y - bw - bar_height,
+                  position.x + size.x + bw, position.y + size.y + bw };
     }
 }
 
-// I don't know if this needs the site and increment parameters
-window_start :: (use state: ^Window_State, site := #callsite, increment := 0) {
+window_start :: (use state: ^Window_State, site := #callsite, increment := 0) -> bool {
 
     hash := get_site_hash(site, increment);
     animation_state := map.get(^animation_states, hash);
@@ -28,17 +43,66 @@ window_start :: (use state: ^Window_State, site := #callsite, increment := 0) {
     }
 
     if is_hot_item(hash) {
-        move_towards(^animation_state.hover_time, 1.0f, 0.06);
+        if mouse_state.left_button_just_down && !state.dragging && state.draggable {
+            state.dragging = true;
+            set_active_item(hash);
+        }
+
+        if mouse_state.right_button_just_down && !state.dragging && state.resizable {
+            state.resizing = true;
+            set_active_item(hash);
+        }
+
+        if state.dragging {
+            if !mouse_state.left_button_down {
+                state.dragging = false;
+                set_active_item(0);
+
+            } else {
+                dx, dy := get_mouse_delta();
+                state.position.x -= dx;
+                state.position.y -= dy;
+            }
+        }
+
+        if state.resizing {
+            if !mouse_state.right_button_down {
+                state.resizing = false;
+                set_active_item(0);
+
+            } else {
+                state.size.x = mx - state.position.x;
+                state.size.y = my - state.position.y;
+            }
+        }
+
     } else {
-        move_towards(^animation_state.hover_time, 0.0f, 0.06);
+        state.dragging = false;
+        state.resizing = false;
+    }
+
+    #if #defined(set_cursor) {
+        if state.dragging do set_cursor(Cursors.Move);
+        if state.resizing do set_cursor(Cursors.NWSE_Resize);
     }
 
-    x, y := position.x + border_width, position.y + border_width;
-    w, h := size.x - border_width * 2, size.y - border_width * 2;
+    size.x = math.clamp(size.x, 300, max_size.x);
+    size.y = math.clamp(size.y, 100, max_size.y);
+
+    x, y := position.x, position.y;
+    w, h := size.x, size.y;
+
+    draw_rect(x - border_width, y - border_width - bar_height, w + border_width * 2, h + border_width * 2 + bar_height, color=border_color);
+    draw_rect(x, y, w, h, color=state.background_color);
 
-    bg_color := color_lerp(animation_state.hover_time, background_color, active_color);
-    draw_rect(position.x, position.y, size.x, size.y, color=border_color);
-    draw_rect(x, y, w, h, bg_color);
+    title_theme := default_text_theme;
+    title_theme.font_size = 1.0f;
+    title_theme.text_color = .{ 1, 1, 1 }; @ThemeConfiguration
+    draw_text(.{ x, y - bar_height, x + w, y }, state.title, theme=^title_theme);
+
+    if button(.{ x + w - 48, y - bar_height - border_width, x + w + border_width, y }, "X", increment=increment + hash) {
+        state.should_close = true;
+    }
 
     if animation_state.click_time > 0 || animation_state.hover_time > 0 {
         map.put(^animation_states, hash, animation_state);
@@ -46,9 +110,11 @@ window_start :: (use state: ^Window_State, site := #callsite, increment := 0) {
         map.delete(^animation_states, hash);
     }
 
-    gfx.push_scissor(x, y, w, h);
     gfx.push_matrix();
+    gfx.push_scissor(x, y, w, h);
     gfx.apply_transform(.{ translation = .{ x, y }, scale = .{ 1, 1 } });
+
+    return is_active_item(hash);
 }
 
 window_end :: () {