From 06632c99a611c980190aea415d054a1fd9819048 Mon Sep 17 00:00:00 2001 From: Brendan Hansen Date: Tue, 10 Aug 2021 14:09:31 -0500 Subject: [PATCH] actually making this something interesting --- res/colors_dark.json | 4 +- site/js/decompiler.js | 8 + src/{ => app}/app.onyx | 246 ++++++++++++---------- src/app/colors.onyx | 104 +++++++++ src/{ => app}/debug_log.onyx | 16 +- src/app/window_management.onyx | 106 ++++++++++ src/app/window_switcher.onyx | 124 +++++++++++ src/build.onyx | 17 +- src/config.onyx | 3 +- src/features/hex_editor/feature.onyx | 7 + src/features/hex_editor/hex_viewer.onyx | 131 ++++++++++++ src/features/load_features.onyx | 4 +- src/features/text_editor/feature.onyx | 7 + src/features/text_editor/text_editor.onyx | 37 ++++ src/features/wasm/feature.onyx | 2 +- src/features/wasm/wasm.onyx | 4 +- src/ui/cursor.onyx | 17 ++ src/ui/window.onyx | 92 ++++++-- 18 files changed, 795 insertions(+), 134 deletions(-) rename src/{ => app}/app.onyx (63%) create mode 100644 src/app/colors.onyx rename src/{ => app}/debug_log.onyx (86%) create mode 100644 src/app/window_management.onyx create mode 100644 src/app/window_switcher.onyx create mode 100644 src/features/hex_editor/feature.onyx create mode 100644 src/features/hex_editor/hex_viewer.onyx create mode 100644 src/features/text_editor/feature.onyx create mode 100644 src/features/text_editor/text_editor.onyx create mode 100644 src/ui/cursor.onyx diff --git a/res/colors_dark.json b/res/colors_dark.json index 439f912..3d4f29e 100644 --- a/res/colors_dark.json +++ b/res/colors_dark.json @@ -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 ], diff --git a/site/js/decompiler.js b/site/js/decompiler.js index 3027307..169dbd3 100644 --- a/site/js/decompiler.js +++ b/site/js/decompiler.js @@ -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/app.onyx similarity index 63% rename from src/app.onyx rename to src/app/app.onyx index 2d18130..b9c8c1d 100644 --- a/src/app.onyx +++ b/src/app/app.onyx @@ -14,28 +14,57 @@ 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); +@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 := (#type [] u8).{ null, 0 }; + 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(); @@ -43,10 +72,10 @@ init :: () { ui.init_ui(); + array.init(^registered_tools); 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); + colorscheme_switch(.Dark); load_background_tile_texture(); load_fonts(); @@ -56,6 +85,30 @@ init :: () { 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; @@ -71,20 +124,13 @@ init :: () { debug_log(.Info, "Found feature '{}'", string.advance(ts.name, 8)); for ^member: ts.members { - Hook_Function_Type :: #type (^Application_State) -> void; + 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))(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); + (*(cast(^Hook_Function_Type) member.default))(); } } } @@ -102,52 +148,6 @@ init :: () { 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; @@ -223,7 +223,7 @@ init :: () { handle_event :: (event: ^events.Event) { switch event.kind { case .MouseDown do switch event.mouse.button { - case .Left do ui.button_pressed(.Left); + case .Left do ui.button_pressed(.Left) ; case .Right do ui.button_pressed(.Right); case .Middle do ui.button_pressed(.Middle); } @@ -263,10 +263,15 @@ handle_event :: (event: ^events.Event) { } if event.keyboard->get_name() == "Tab" { - // toggle_sidebar(^analyzer_state); + 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 } @@ -301,15 +306,14 @@ handle_event :: (event: ^events.Event) { 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(); + return ui.has_active_animation() + || debug.debug_log_transitioning() + || state.window_switcher_state->is_animating(); } update :: (dt: f32) { @@ -321,36 +325,54 @@ draw :: () { 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); + ui.workspace_start(main_area, state=^state.workspace_state); draw_background_lines(~~window_width, ~~window_height, line_color=config.Colors.background); - ui.draw_rect(0, 0, 20, 20, color=.{1,0,0}); - + // Used for detecting when a window is completely off-screen and shouldn't be rendered. + transformed_window_rect := window_rectangle; { - #persist window_state := ui.Window_State.{ position=gfx.Vector2.{100, 100}, size=gfx.Vector2.{800, 600} }; - ui.window_start(^window_state); + 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(); - 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); + window.window_state.background_color = config.Colors.background; + window.window_state.border_color = config.Colors.primary_dark; - size_text := conv.str_format("File size: {} bytes", ~~buffer, state.file.data.count); - ui.draw_text(.{ 0, 32, 300, 200 }, size_text); + window.draw(window.draw_data, window); - } else { - ui.draw_text(.{ 0, 0, 300, 200 }, "No file loaded."); + if window.window_state.should_close { + close_window(window.id); } } + if new_top != null { + move_window_to_top(new_top.id); + } + ui.workspace_end(); - // draw_sidebar(^analyzer_state); + state.window_switcher_state->draw(window_rectangle); // Menu bar drawing { @@ -368,40 +390,50 @@ draw :: () { 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 +open_tool_opener :: () { + mouse_pos := gfx.transform_point(^state.workspace_state.transform, .{ ui.mouse_state.x_, ui.mouse_state.y_ }); - #insert gfx.save_matrix; + 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 }; - 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; + ui.scrollable_region_start(window_rect, maximum_y=~~(registered_tools.count - 1) * 40.0f); + defer ui.scrollable_region_stop(); - 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(); - } + 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; -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; + 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; - 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; + 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 index 0000000..86784bf --- /dev/null +++ b/src/app/colors.onyx @@ -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/debug_log.onyx b/src/app/debug_log.onyx similarity index 86% rename from src/debug_log.onyx rename to src/app/debug_log.onyx index c014014..72b1c81 100644 --- a/src/debug_log.onyx +++ b/src/app/debug_log.onyx @@ -26,7 +26,7 @@ Severity :: enum { minimum_severity := Severity.Debug; debug_log_toggle :: () { - debug_log_y_offset_target = 1 - debug_log_y_offset; + debug_log_y_offset_target = 1 - debug_log_y_offset_target; } debug_log_transitioning :: () -> bool { @@ -116,6 +116,20 @@ draw_debug_log :: (window_rectangle: ui.Rectangle, site := #callsite) { 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 { diff --git a/src/app/window_management.onyx b/src/app/window_management.onyx new file mode 100644 index 0000000..273f512 --- /dev/null +++ b/src/app/window_management.onyx @@ -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 index 0000000..e54e90b --- /dev/null +++ b/src/app/window_switcher.onyx @@ -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(); + } + } +} diff --git a/src/build.onyx b/src/build.onyx index 4cc582d..a1c1150 100644 --- a/src/build.onyx +++ b/src/build.onyx @@ -11,15 +11,20 @@ #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 { diff --git a/src/config.onyx b/src/config.onyx index 8f11ac6..cd3f1e0 100644 --- a/src/config.onyx +++ b/src/config.onyx @@ -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/features/hex_editor/feature.onyx b/src/features/hex_editor/feature.onyx new file mode 100644 index 0000000..6b8548e --- /dev/null +++ b/src/features/hex_editor/feature.onyx @@ -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 index 0000000..17cf620 --- /dev/null +++ b/src/features/hex_editor/hex_viewer.onyx @@ -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 diff --git a/src/features/load_features.onyx b/src/features/load_features.onyx index 6e111df..367ab17 100644 --- a/src/features/load_features.onyx +++ b/src/features/load_features.onyx @@ -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 index 0000000..8bb1fc4 --- /dev/null +++ b/src/features/text_editor/feature.onyx @@ -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 index 0000000..8948eb8 --- /dev/null +++ b/src/features/text_editor/text_editor.onyx @@ -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 diff --git a/src/features/wasm/feature.onyx b/src/features/wasm/feature.onyx index 3fd0983..cf47133 100644 --- a/src/features/wasm/feature.onyx +++ b/src/features/wasm/feature.onyx @@ -1,4 +1,4 @@ -package feature_wasm +package feature.wasm #load "./wasm" diff --git a/src/features/wasm/wasm.onyx b/src/features/wasm/wasm.onyx index 7447238..cac42be 100644 --- a/src/features/wasm/wasm.onyx +++ b/src/features/wasm/wasm.onyx @@ -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 index 0000000..70ead9d --- /dev/null +++ b/src/ui/cursor.onyx @@ -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 diff --git a/src/ui/window.onyx b/src/ui/window.onyx index db54a63..b147c7a 100644 --- a/src/ui/window.onyx +++ b/src/ui/window.onyx @@ -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 :: () { -- 2.25.1