diff options
| author | hachem <im@hachem.wtf> | 2026-08-28 01:01:01 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-28 01:01:01 +0200 |
| commit | 801c6a8916a495162b200fff0d14cfbf89ea854c (patch) | |
| tree | c68fb131e87e3098559643c707142eaeac835baa /src | |
| parent | 705e3914e4e820c3ac92892c32c93479691a09e7 (diff) | |
add: ui view and renderer
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/application.cpp | 10 | ||||
| -rw-r--r-- | src/core/application.h | 2 | ||||
| -rw-r--r-- | src/ui/source_view.cpp | 221 | ||||
| -rw-r--r-- | src/ui/source_view.h | 39 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 809 | ||||
| -rw-r--r-- | src/ui/ui.h | 79 |
6 files changed, 1159 insertions, 1 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp index 1e6d343..7047d6a 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -5,6 +5,7 @@ #include "core/window.h" #include "core/window_manager.h" #include "debugger/debugger.h" +#include "ui/ui.h" #include <glad/gl.h> @@ -41,10 +42,12 @@ namespace Hsdbg m_window = &m_window_manager->create_window(window_spec); m_debugger = std::make_unique<Debugger>(); + m_ui = std::make_unique<Ui>(*m_window); } Application::~Application() { + m_ui.reset(); m_debugger.reset(); m_window = nullptr; @@ -110,10 +113,15 @@ namespace Hsdbg auto Application::render() -> void { + m_ui->begin_frame(); + m_ui->draw(*m_debugger); + glViewport(0, 0, static_cast<GLsizei>(m_window->framebuffer_width()), static_cast<GLsizei>(m_window->framebuffer_height())); - glClearColor(0.09f, 0.09f, 0.11f, 1.0f); + glClearColor(0.05f, 0.05f, 0.06f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + m_ui->end_frame(); } } diff --git a/src/core/application.h b/src/core/application.h index 2f5ffbe..fd72af2 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -7,6 +7,7 @@ namespace Hsdbg { class Debugger; + class Ui; class Window; class WindowManager; @@ -51,6 +52,7 @@ namespace Hsdbg ApplicationSpec m_spec; std::unique_ptr<WindowManager> m_window_manager; std::unique_ptr<Debugger> m_debugger; + std::unique_ptr<Ui> m_ui; Window* m_window = nullptr; bool m_running = false; float m_delta_time = 0.0f; diff --git a/src/ui/source_view.cpp b/src/ui/source_view.cpp new file mode 100644 index 0000000..37ceffc --- /dev/null +++ b/src/ui/source_view.cpp @@ -0,0 +1,221 @@ +#include "ui/source_view.h" + +#include "core/log.h" +#include "debugger/debugger.h" + +#include <imgui.h> +#include <imgui_internal.h> +#include <misc/cpp/imgui_stdlib.h> + +#include <algorithm> +#include <fstream> + +namespace Hsdbg +{ + namespace + { + constexpr float BREAKPOINT_RADIUS = 5.0f; + + // the dot sits in the gutter with this much room on either side, and the + // line numbers pick the same gap up again so the column reads evenly + constexpr float GUTTER_MARGIN = 6.0f; + constexpr float GUTTER_WIDTH = BREAKPOINT_RADIUS * 2.0f + GUTTER_MARGIN * 2.0f; + + const ImU32 BREAKPOINT_COLOR = IM_COL32(226, 84, 84, 255); + const ImU32 BREAKPOINT_DISABLED_COLOR = IM_COL32(120, 90, 90, 255); + const ImU32 BREAKPOINT_HOVER_COLOR = IM_COL32(226, 84, 84, 90); + const ImU32 HIGHLIGHT_COLOR = IM_COL32(58, 72, 46, 255); + + auto line_number_width(size_t line_count) -> float + { + return ImGui::CalcTextSize(std::to_string(line_count).c_str()).x; + } + } + + auto SourceView::open(const std::filesystem::path& path) -> Result<void> + { + std::ifstream file(path); + if (!file.is_open()) + return fail("could not open '{}'", path.string()); + + std::vector<std::string> lines; + std::string line; + + while (std::getline(file, line)) + { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + + lines.push_back(std::move(line)); + } + + m_path = path; + m_lines = std::move(lines); + m_path_input = path.string(); + m_error.clear(); + m_highlighted_line = 0; + + Log::info("source: opened '{}' ({} lines)", m_path.string(), m_lines.size()); + + return {}; + } + + auto SourceView::close() -> void + { + m_path.clear(); + m_lines.clear(); + m_error.clear(); + m_highlighted_line = 0; + } + + auto SourceView::set_highlighted_line(uint32_t line) -> void + { + m_highlighted_line = line; + m_scroll_to_highlight = line != 0; + } + + auto SourceView::draw(Debugger& debugger) -> void + { + draw_open_bar(); + + ImGui::Separator(); + + if (m_lines.empty()) + { + ImGui::TextDisabled("no source file open"); + return; + } + + draw_lines(debugger); + } + + auto SourceView::draw_open_bar() -> void + { + ImGui::SetNextItemWidth(-ImGui::CalcTextSize("open").x - ImGui::GetStyle().FramePadding.x * 4.0f); + + const bool submitted = ImGui::InputTextWithHint("##source_path", + "path to a source file", + &m_path_input, + ImGuiInputTextFlags_EnterReturnsTrue); + + ImGui::SameLine(); + + if (ImGui::Button("open") || submitted) + { + if (const auto result = open(m_path_input); !result) + { + m_error = result.error(); + Log::error("{}", m_error); + } + } + + if (!m_error.empty()) + ImGui::TextColored(ImVec4(0.89f, 0.33f, 0.33f, 1.0f), "%s", m_error.c_str()); + } + + auto SourceView::draw_lines(Debugger& debugger) -> void + { + const float text_height = ImGui::GetTextLineHeightWithSpacing(); + const float number_width = line_number_width(m_lines.size()); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + if (!ImGui::BeginChild("##source_lines", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) + { + ImGui::EndChild(); + return; + } + + if (m_scroll_to_highlight && m_highlighted_line != 0) + { + ImGui::SetScrollY(static_cast<float>(m_highlighted_line - 1) * text_height - + ImGui::GetWindowHeight() * 0.4f); + m_scroll_to_highlight = false; + } + + ImGuiListClipper clipper; + clipper.Begin(static_cast<int>(m_lines.size()), text_height); + + while (clipper.Step()) + { + for (int index = clipper.DisplayStart; index < clipper.DisplayEnd; ++index) + { + const uint32_t line_number = static_cast<uint32_t>(index) + 1; + const ImVec2 row_start = ImGui::GetCursorScreenPos(); + + if (line_number == m_highlighted_line) + { + draw_list->AddRectFilled(row_start, + ImVec2(row_start.x + ImGui::GetContentRegionAvail().x, + row_start.y + text_height), + HIGHLIGHT_COLOR); + } + + ImGui::PushID(index); + + if (ImGui::InvisibleButton("##gutter", ImVec2(GUTTER_WIDTH, text_height))) + { + const Breakpoint* existing = nullptr; + + for (const Breakpoint& breakpoint : debugger.breakpoints()) + { + if (breakpoint.file == m_path && breakpoint.line == line_number) + { + existing = &breakpoint; + break; + } + } + + if (existing != nullptr) + debugger.remove_breakpoint(existing->id); + else + debugger.add_breakpoint(m_path, line_number); + } + + const bool gutter_hovered = ImGui::IsItemHovered(); + + ImGui::PopID(); + + const Breakpoint* breakpoint = nullptr; + + for (const Breakpoint& candidate : debugger.breakpoints()) + { + if (candidate.file == m_path && candidate.line == line_number) + { + breakpoint = &candidate; + break; + } + } + + const ImVec2 marker_center(row_start.x + GUTTER_WIDTH * 0.5f, + row_start.y + text_height * 0.5f); + + if (breakpoint != nullptr) + { + draw_list->AddCircleFilled(marker_center, + BREAKPOINT_RADIUS, + breakpoint->enabled ? BREAKPOINT_COLOR + : BREAKPOINT_DISABLED_COLOR); + } + else if (gutter_hovered) + { + draw_list->AddCircleFilled(marker_center, BREAKPOINT_RADIUS, BREAKPOINT_HOVER_COLOR); + } + + const std::string number_text = std::to_string(line_number); + + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + number_width - + ImGui::CalcTextSize(number_text.c_str()).x); + ImGui::TextDisabled("%s", number_text.c_str()); + + ImGui::SameLine(0.0f, GUTTER_MARGIN * 2.0f); + ImGui::TextUnformatted(m_lines[static_cast<size_t>(index)].c_str()); + } + } + + clipper.End(); + + ImGui::EndChild(); + } +} diff --git a/src/ui/source_view.h b/src/ui/source_view.h new file mode 100644 index 0000000..94855e8 --- /dev/null +++ b/src/ui/source_view.h @@ -0,0 +1,39 @@ +#pragma once + +#include "core/result.h" + +#include <cstdint> +#include <filesystem> +#include <string> +#include <vector> + +namespace Hsdbg +{ + class Debugger; + + class SourceView + { + public: + auto open(const std::filesystem::path& path) -> Result<void>; + auto close() -> void; + + auto draw(Debugger& debugger) -> void; + + auto set_highlighted_line(uint32_t line) -> void; + + auto path() const -> const std::filesystem::path& { return m_path; } + auto line_count() const -> size_t { return m_lines.size(); } + auto is_open() const -> bool { return !m_lines.empty(); } + + private: + auto draw_open_bar() -> void; + auto draw_lines(Debugger& debugger) -> void; + + std::filesystem::path m_path; + std::vector<std::string> m_lines; + std::string m_path_input; + std::string m_error; + uint32_t m_highlighted_line = 0; + bool m_scroll_to_highlight = false; + }; +} diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp new file mode 100644 index 0000000..91fab0a --- /dev/null +++ b/src/ui/ui.cpp @@ -0,0 +1,809 @@ +#include "ui/ui.h" + +#include "core/log.h" +#include "core/window.h" +#include "debugger/debugger.h" + +#include <imgui.h> +#include <imgui_internal.h> +#include <misc/cpp/imgui_stdlib.h> + +#include <backends/imgui_impl_glfw.h> +#include <backends/imgui_impl_opengl3.h> + +#include <filesystem> +#include <format> + +namespace Hsdbg +{ + namespace + { + constexpr const char* PANEL_SOURCE = "source"; + constexpr const char* PANEL_BREAKPOINTS = "breakpoints"; + constexpr const char* PANEL_CALL_STACK = "call stack"; + constexpr const char* PANEL_THREADS = "threads"; + constexpr const char* PANEL_LOCALS = "locals"; + constexpr const char* PANEL_REGISTERS = "registers"; + constexpr const char* PANEL_CONSOLE = "console"; + + constexpr const char* LOAD_TARGET_POPUP = "load target"; + + // macos gives us a 4.1 core context, which speaks glsl 1.50 + // #portability + constexpr const char* GLSL_VERSION = "#version 150"; + + constexpr size_t MAX_CONSOLE_LINES = 2048; + + // breathing room between the window edge and everything docked inside it + const ImVec2 ROOT_PADDING(8.0f, 6.0f); + const ImVec2 TOOLBAR_PADDING(4.0f, 4.0f); + + auto state_color(TargetState state) -> ImVec4 + { + switch (state) + { + case TargetState::Running: return ImVec4(0.45f, 0.78f, 0.45f, 1.0f); + case TargetState::Stopped: return ImVec4(0.95f, 0.76f, 0.35f, 1.0f); + case TargetState::Crashed: return ImVec4(0.89f, 0.33f, 0.33f, 1.0f); + case TargetState::Exited: return ImVec4(0.60f, 0.60f, 0.65f, 1.0f); + default: return ImVec4(0.55f, 0.55f, 0.60f, 1.0f); + } + } + + auto draw_variable(const Variable& variable) -> void + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + if (variable.children.empty()) + { + ImGui::TreeNodeEx(variable.name.c_str(), + ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | + ImGuiTreeNodeFlags_SpanFullWidth); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(variable.type.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(variable.value.c_str()); + + return; + } + + const bool open = ImGui::TreeNodeEx(variable.name.c_str(), ImGuiTreeNodeFlags_SpanFullWidth); + + ImGui::TableNextColumn(); + ImGui::TextUnformatted(variable.type.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(variable.value.c_str()); + + if (!open) + return; + + for (const Variable& child : variable.children) + draw_variable(child); + + ImGui::TreePop(); + } + } + + Ui::Ui(Window& window) + : m_window(&window) + { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigWindowsMoveFromTitleBarOnly = true; + + // a saved layout beats the built in one, so only build when there is none + m_layout_built = io.IniFilename != nullptr && std::filesystem::exists(io.IniFilename); + + apply_style(); + + ImGui_ImplGlfw_InitForOpenGL(m_window->handle(), true); + ImGui_ImplOpenGL3_Init(GLSL_VERSION); + + Log::info("imgui {}", IMGUI_VERSION); + } + + Ui::~Ui() + { + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + } + + auto Ui::begin_frame() -> void + { + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + } + + auto Ui::end_frame() -> void + { + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + + auto Ui::open_source(const std::filesystem::path& path) -> void + { + if (const auto result = m_source_view.open(path); !result) + { + Log::error("{}", result.error()); + push_console(std::format("error: {}", result.error())); + } + } + + auto Ui::draw(Debugger& debugger) -> void + { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + + constexpr ImGuiWindowFlags root_flags = + ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ROOT_PADDING); + + ImGui::Begin("##hsdbg_root", nullptr, root_flags); + + ImGui::PopStyleVar(3); + + draw_menu_bar(debugger); + draw_toolbar(debugger); + + const ImGuiID dockspace_id = ImGui::GetID("##hsdbg_dockspace"); + const float status_bar_height = ImGui::GetTextLineHeight() + + ImGui::GetStyle().ItemSpacing.y * 3.0f; + + ImGui::DockSpace(dockspace_id, + ImVec2(0.0f, ImGui::GetContentRegionAvail().y - status_bar_height), + ImGuiDockNodeFlags_PassthruCentralNode); + + if (!m_layout_built) + { + build_default_layout(dockspace_id); + m_layout_built = true; + } + + draw_status_bar(debugger); + draw_load_target_popup(debugger); + + ImGui::End(); + + draw_source_panel(debugger); + draw_breakpoints_panel(debugger); + draw_call_stack_panel(debugger); + draw_threads_panel(debugger); + draw_locals_panel(debugger); + draw_registers_panel(debugger); + draw_console_panel(debugger); + + if (m_visible.demo) + ImGui::ShowDemoWindow(&m_visible.demo); + } + + auto Ui::build_default_layout(uint32_t dockspace_id) -> void + { + ImGui::DockBuilderRemoveNode(dockspace_id); + ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->WorkSize); + + ImGuiID center_id = dockspace_id; + const ImGuiID left_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Left, 0.20f, nullptr, ¢er_id); + const ImGuiID right_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.28f, nullptr, ¢er_id); + const ImGuiID bottom_id = ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.28f, nullptr, ¢er_id); + + ImGuiID left_top_id = left_id; + const ImGuiID left_bottom_id = ImGui::DockBuilderSplitNode(left_top_id, ImGuiDir_Down, 0.5f, nullptr, &left_top_id); + + ImGuiID right_top_id = right_id; + const ImGuiID right_bottom_id = ImGui::DockBuilderSplitNode(right_top_id, ImGuiDir_Down, 0.5f, nullptr, &right_top_id); + + ImGui::DockBuilderDockWindow(PANEL_SOURCE, center_id); + ImGui::DockBuilderDockWindow(PANEL_THREADS, left_top_id); + ImGui::DockBuilderDockWindow(PANEL_CALL_STACK, left_bottom_id); + ImGui::DockBuilderDockWindow(PANEL_LOCALS, right_top_id); + ImGui::DockBuilderDockWindow(PANEL_REGISTERS, right_bottom_id); + ImGui::DockBuilderDockWindow(PANEL_BREAKPOINTS, bottom_id); + ImGui::DockBuilderDockWindow(PANEL_CONSOLE, bottom_id); + + ImGui::DockBuilderFinish(dockspace_id); + } + + auto Ui::draw_menu_bar(Debugger& debugger) -> void + { + if (!ImGui::BeginMenuBar()) + return; + + if (ImGui::BeginMenu("file")) + { + // a menu is its own popup, so the request has to be handed back to the + // root window or BeginPopupModal never sees a matching id + if (ImGui::MenuItem("load target...")) + { + m_target_input = debugger.target_path().string(); + m_load_target_pending = true; + } + + if (ImGui::MenuItem("unload target", nullptr, false, debugger.has_target())) + debugger.unload_target(); + + ImGui::Separator(); + + if (ImGui::MenuItem("quit", "cmd+q")) + m_window->set_should_close(true); + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("run")) + { + const bool has_target = debugger.has_target(); + + if (ImGui::MenuItem("continue", "f5", false, has_target)) + report(debugger.resume(), "continue"); + + if (ImGui::MenuItem("pause", nullptr, false, debugger.is_running())) + report(debugger.pause(), "pause"); + + if (ImGui::MenuItem("stop", nullptr, false, has_target)) + report(debugger.terminate(), "stop"); + + ImGui::Separator(); + + if (ImGui::MenuItem("step over", "f10", false, has_target)) + report(debugger.step_over(), "step over"); + + if (ImGui::MenuItem("step into", "f11", false, has_target)) + report(debugger.step_into(), "step into"); + + if (ImGui::MenuItem("step out", "shift+f11", false, has_target)) + report(debugger.step_out(), "step out"); + + ImGui::Separator(); + + if (ImGui::MenuItem("clear breakpoints", nullptr, false, !debugger.breakpoints().empty())) + debugger.clear_breakpoints(); + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("view")) + { + ImGui::MenuItem(PANEL_SOURCE, nullptr, &m_visible.source); + ImGui::MenuItem(PANEL_BREAKPOINTS, nullptr, &m_visible.breakpoints); + ImGui::MenuItem(PANEL_CALL_STACK, nullptr, &m_visible.call_stack); + ImGui::MenuItem(PANEL_THREADS, nullptr, &m_visible.threads); + ImGui::MenuItem(PANEL_LOCALS, nullptr, &m_visible.locals); + ImGui::MenuItem(PANEL_REGISTERS, nullptr, &m_visible.registers); + ImGui::MenuItem(PANEL_CONSOLE, nullptr, &m_visible.console); + + ImGui::Separator(); + + if (ImGui::MenuItem("reset layout")) + m_layout_built = false; + + ImGui::MenuItem("imgui demo", nullptr, &m_visible.demo); + + ImGui::EndMenu(); + } + + ImGui::EndMenuBar(); + } + + auto Ui::draw_toolbar(Debugger& debugger) -> void + { + const bool has_target = debugger.has_target(); + const bool running = debugger.is_running(); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, TOOLBAR_PADDING); + ImGui::BeginChild("##toolbar", + ImVec2(0.0f, ImGui::GetFrameHeight() + TOOLBAR_PADDING.y * 2.0f)); + + ImGui::BeginDisabled(!has_target || running); + if (ImGui::Button("run")) + { + LaunchSpec spec; + spec.executable = debugger.target_path(); + + report(debugger.launch(spec), "run"); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::BeginDisabled(!running); + if (ImGui::Button("pause")) + report(debugger.pause(), "pause"); + ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::BeginDisabled(!has_target); + if (ImGui::Button("stop")) + report(debugger.terminate(), "stop"); + + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + + if (ImGui::Button("step over")) + report(debugger.step_over(), "step over"); + + ImGui::SameLine(); + + if (ImGui::Button("step into")) + report(debugger.step_into(), "step into"); + + ImGui::SameLine(); + + if (ImGui::Button("step out")) + report(debugger.step_out(), "step out"); + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(state_color(debugger.state()), "%s", to_string(debugger.state()).data()); + + ImGui::EndChild(); + ImGui::PopStyleVar(2); + } + + auto Ui::draw_status_bar(Debugger& debugger) -> void + { + ImGui::Separator(); + + ImGui::AlignTextToFramePadding(); + ImGui::TextColored(state_color(debugger.state()), "%s", to_string(debugger.state()).data()); + + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + + if (debugger.has_target()) + ImGui::Text("%s", debugger.target_path().filename().string().c_str()); + else + ImGui::TextDisabled("no target"); + + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + + ImGui::Text("%zu breakpoints", debugger.breakpoints().size()); + + const std::string frame_stats = std::format("{:.1f} fps", ImGui::GetIO().Framerate); + const float text_width = ImGui::CalcTextSize(frame_stats.c_str()).x; + + ImGui::SameLine(ImGui::GetContentRegionMax().x - text_width - ImGui::GetStyle().ItemSpacing.x); + ImGui::TextDisabled("%s", frame_stats.c_str()); + } + + auto Ui::draw_load_target_popup(Debugger& debugger) -> void + { + if (m_load_target_pending) + { + ImGui::OpenPopup(LOAD_TARGET_POPUP); + m_load_target_pending = false; + } + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 center(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f, + viewport->WorkPos.y + viewport->WorkSize.y * 0.5f); + + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + if (!ImGui::BeginPopupModal(LOAD_TARGET_POPUP, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + return; + + ImGui::TextDisabled("path to an executable to debug"); + + ImGui::SetNextItemWidth(420.0f); + + const bool submitted = ImGui::InputText("##target_path", + &m_target_input, + ImGuiInputTextFlags_EnterReturnsTrue); + + if (ImGui::Button("load") || submitted) + { + if (const auto result = debugger.load_target(m_target_input); result) + { + push_console(std::format("loaded target {}", debugger.target_path().string())); + ImGui::CloseCurrentPopup(); + } + else + { + push_console(std::format("error: {}", result.error())); + } + } + + ImGui::SameLine(); + + if (ImGui::Button("cancel")) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); + } + + auto Ui::draw_source_panel(Debugger& debugger) -> void + { + if (!m_visible.source) + return; + + if (ImGui::Begin(PANEL_SOURCE, &m_visible.source)) + m_source_view.draw(debugger); + + ImGui::End(); + } + + auto Ui::draw_breakpoints_panel(Debugger& debugger) -> void + { + if (!m_visible.breakpoints) + return; + + if (ImGui::Begin(PANEL_BREAKPOINTS, &m_visible.breakpoints)) + { + if (debugger.breakpoints().empty()) + { + ImGui::TextDisabled("no breakpoints, click a line gutter in the source panel"); + } + else + { + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_SizingStretchProp | + ImGuiTableFlags_ScrollY; + + if (ImGui::BeginTable("##breakpoints", 5, flags)) + { + ImGui::TableSetupColumn("on", ImGuiTableColumnFlags_WidthFixed, 26.0f); + ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_WidthFixed, 30.0f); + ImGui::TableSetupColumn("location"); + ImGui::TableSetupColumn("hits", ImGuiTableColumnFlags_WidthFixed, 40.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 26.0f); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + uint32_t pending_removal = 0; + + for (const Breakpoint& breakpoint : debugger.breakpoints()) + { + ImGui::PushID(static_cast<int>(breakpoint.id)); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + bool enabled = breakpoint.enabled; + if (ImGui::Checkbox("##enabled", &enabled)) + debugger.set_breakpoint_enabled(breakpoint.id, enabled); + + ImGui::TableNextColumn(); + ImGui::Text("%u", breakpoint.id); + + ImGui::TableNextColumn(); + if (breakpoint.line != 0) + { + ImGui::Text("%s:%u", + breakpoint.file.filename().string().c_str(), + breakpoint.line); + } + else + { + ImGui::Text("%s()", breakpoint.function.c_str()); + } + + if (ImGui::IsItemHovered() && !breakpoint.file.empty()) + ImGui::SetTooltip("%s", breakpoint.file.string().c_str()); + + ImGui::TableNextColumn(); + ImGui::Text("%u", breakpoint.hit_count); + + ImGui::TableNextColumn(); + if (ImGui::SmallButton("x")) + pending_removal = breakpoint.id; + + ImGui::PopID(); + } + + ImGui::EndTable(); + + if (pending_removal != 0) + debugger.remove_breakpoint(pending_removal); + } + } + } + + ImGui::End(); + } + + auto Ui::draw_call_stack_panel(Debugger& debugger) -> void + { + if (!m_visible.call_stack) + return; + + if (ImGui::Begin(PANEL_CALL_STACK, &m_visible.call_stack)) + { + if (debugger.call_stack().empty()) + { + ImGui::TextDisabled("no call stack"); + } + else + { + for (const StackFrame& frame : debugger.call_stack()) + { + const bool selected = frame.index == debugger.selected_frame(); + const std::string label = std::format("{:>2} {}", frame.index, frame.function); + + if (ImGui::Selectable(label.c_str(), selected)) + debugger.select_frame(frame.index); + + if (frame.line == 0) + continue; + + ImGui::SameLine(); + ImGui::TextDisabled("%s:%u", frame.file.filename().string().c_str(), frame.line); + } + } + } + + ImGui::End(); + } + + auto Ui::draw_threads_panel(Debugger& debugger) -> void + { + if (!m_visible.threads) + return; + + if (ImGui::Begin(PANEL_THREADS, &m_visible.threads)) + { + if (debugger.threads().empty()) + { + ImGui::TextDisabled("no threads"); + } + else + { + for (const Thread& thread : debugger.threads()) + { + const bool selected = thread.id == debugger.selected_thread(); + const std::string label = std::format("{} {}", thread.id, thread.name); + + if (ImGui::Selectable(label.c_str(), selected)) + debugger.select_thread(thread.id); + + ImGui::SameLine(); + ImGui::TextDisabled("%s", to_string(thread.stop_reason).data()); + } + } + } + + ImGui::End(); + } + + auto Ui::draw_locals_panel(Debugger& debugger) -> void + { + if (!m_visible.locals) + return; + + if (ImGui::Begin(PANEL_LOCALS, &m_visible.locals)) + { + if (debugger.locals().empty()) + { + ImGui::TextDisabled("no locals"); + } + else + { + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY; + + if (ImGui::BeginTable("##locals", 3, flags)) + { + ImGui::TableSetupColumn("name"); + ImGui::TableSetupColumn("type"); + ImGui::TableSetupColumn("value"); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + for (const Variable& variable : debugger.locals()) + draw_variable(variable); + + ImGui::EndTable(); + } + } + } + + ImGui::End(); + } + + auto Ui::draw_registers_panel(Debugger& debugger) -> void + { + if (!m_visible.registers) + return; + + if (ImGui::Begin(PANEL_REGISTERS, &m_visible.registers)) + { + if (debugger.registers().empty()) + { + ImGui::TextDisabled("no registers"); + } + else + { + constexpr ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_ScrollY; + + if (ImGui::BeginTable("##registers", 2, flags)) + { + ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("value"); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + for (const Register& entry : debugger.registers()) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(entry.name.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("0x%016llx", static_cast<unsigned long long>(entry.value)); + } + + ImGui::EndTable(); + } + } + } + + ImGui::End(); + } + + auto Ui::draw_console_panel(Debugger& debugger) -> void + { + if (!m_visible.console) + return; + + if (ImGui::Begin(PANEL_CONSOLE, &m_visible.console)) + { + const float input_height = ImGui::GetFrameHeightWithSpacing(); + + if (ImGui::BeginChild("##console_output", ImVec2(0.0f, -input_height))) + { + for (const std::string& line : debugger.console_output()) + ImGui::TextUnformatted(line.c_str()); + + for (const std::string& line : m_console_lines) + ImGui::TextUnformatted(line.c_str()); + + if (m_console_scroll_pending) + { + ImGui::SetScrollHereY(1.0f); + m_console_scroll_pending = false; + } + } + + ImGui::EndChild(); + + ImGui::SetNextItemWidth(-1.0f); + + const bool submitted = ImGui::InputTextWithHint("##console_input", + "expression to evaluate", + &m_console_input, + ImGuiInputTextFlags_EnterReturnsTrue); + + if (submitted && !m_console_input.empty()) + { + push_console(std::format("> {}", m_console_input)); + + if (const auto result = debugger.evaluate(m_console_input); result) + push_console(*result); + else + push_console(std::format("error: {}", result.error())); + + m_console_input.clear(); + ImGui::SetKeyboardFocusHere(-1); + } + } + + ImGui::End(); + } + + auto Ui::push_console(std::string line) -> void + { + m_console_lines.push_back(std::move(line)); + + if (m_console_lines.size() > MAX_CONSOLE_LINES) + m_console_lines.erase(m_console_lines.begin()); + + m_console_scroll_pending = true; + } + + auto Ui::report(const Result<void>& result, std::string_view action) -> void + { + if (result) + return; + + push_console(std::format("{} failed: {}", action, result.error())); + } + + auto Ui::apply_style() -> void + { + ImGuiStyle& style = ImGui::GetStyle(); + + style.WindowRounding = 4.0f; + style.ChildRounding = 4.0f; + style.FrameRounding = 4.0f; + style.PopupRounding = 4.0f; + style.ScrollbarRounding = 6.0f; + style.GrabRounding = 4.0f; + style.TabRounding = 4.0f; + + style.WindowBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + style.WindowPadding = ImVec2(8.0f, 8.0f); + style.FramePadding = ImVec2(8.0f, 4.0f); + style.ItemSpacing = ImVec2(8.0f, 5.0f); + style.ScrollbarSize = 11.0f; + style.GrabMinSize = 9.0f; + style.WindowTitleAlign = ImVec2(0.0f, 0.5f); + style.SeparatorTextBorderSize = 1.0f; + + ImVec4* colors = style.Colors; + + colors[ImGuiCol_Text] = ImVec4(0.86f, 0.86f, 0.88f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.44f, 0.44f, 0.48f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.09f, 0.09f, 0.11f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.12f, 0.12f, 0.15f, 1.00f); + colors[ImGuiCol_Border] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.16f, 0.19f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.21f, 0.21f, 0.26f, 1.00f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.25f, 0.25f, 0.31f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.07f, 0.07f, 0.09f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.11f, 0.11f, 0.14f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.07f, 0.07f, 0.09f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.11f, 0.11f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.09f, 0.09f, 0.11f, 1.00f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.24f, 0.29f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.43f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.30f, 0.72f, 0.78f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.30f, 0.72f, 0.78f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.38f, 0.80f, 0.86f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.18f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.24f, 0.40f, 0.44f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.28f, 0.52f, 0.58f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.20f, 0.20f, 0.25f, 1.00f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.24f, 0.40f, 0.44f, 1.00f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.28f, 0.52f, 0.58f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.72f, 0.78f, 0.60f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.72f, 0.78f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.29f, 1.00f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.30f, 0.72f, 0.78f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.30f, 0.72f, 0.78f, 1.00f); + colors[ImGuiCol_Tab] = ImVec4(0.11f, 0.11f, 0.14f, 1.00f); + colors[ImGuiCol_TabHovered] = ImVec4(0.24f, 0.40f, 0.44f, 1.00f); + colors[ImGuiCol_TabSelected] = ImVec4(0.18f, 0.30f, 0.34f, 1.00f); + colors[ImGuiCol_TabSelectedOverline] = ImVec4(0.30f, 0.72f, 0.78f, 1.00f); + colors[ImGuiCol_TabDimmed] = ImVec4(0.09f, 0.09f, 0.11f, 1.00f); + colors[ImGuiCol_TabDimmedSelected] = ImVec4(0.14f, 0.20f, 0.22f, 1.00f); + colors[ImGuiCol_DockingPreview] = ImVec4(0.30f, 0.72f, 0.78f, 0.50f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.07f, 0.07f, 0.09f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.14f, 0.14f, 0.17f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f); + colors[ImGuiCol_TableBorderLight] = ImVec4(0.16f, 0.16f, 0.19f, 1.00f); + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.02f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.30f, 0.72f, 0.78f, 0.35f); + colors[ImGuiCol_NavCursor] = ImVec4(0.30f, 0.72f, 0.78f, 1.00f); + } +} diff --git a/src/ui/ui.h b/src/ui/ui.h new file mode 100644 index 0000000..20d4e1b --- /dev/null +++ b/src/ui/ui.h @@ -0,0 +1,79 @@ +#pragma once + +#include "core/result.h" +#include "ui/source_view.h" + +#include <cstdint> +#include <filesystem> +#include <string> +#include <string_view> +#include <vector> + +namespace Hsdbg +{ + class Debugger; + class Window; + + class Ui + { + public: + explicit Ui(Window& window); + ~Ui(); + + Ui(const Ui&) = delete; + Ui(Ui&&) = delete; + auto operator=(const Ui&) -> Ui& = delete; + auto operator=(Ui&&) -> Ui& = delete; + + auto begin_frame() -> void; + auto draw(Debugger& debugger) -> void; + auto end_frame() -> void; + + // where the ui will jump when a frame is selected or a breakpoint is hit + auto open_source(const std::filesystem::path& path) -> void; + + private: + struct PanelVisibility + { + bool source = true; + bool breakpoints = true; + bool call_stack = true; + bool threads = true; + bool locals = true; + bool registers = true; + bool console = true; + bool demo = false; + }; + + auto apply_style() -> void; + auto build_default_layout(uint32_t dockspace_id) -> void; + + auto draw_menu_bar(Debugger& debugger) -> void; + auto draw_toolbar(Debugger& debugger) -> void; + auto draw_status_bar(Debugger& debugger) -> void; + auto draw_load_target_popup(Debugger& debugger) -> void; + + auto draw_source_panel(Debugger& debugger) -> void; + auto draw_breakpoints_panel(Debugger& debugger) -> void; + auto draw_call_stack_panel(Debugger& debugger) -> void; + auto draw_threads_panel(Debugger& debugger) -> void; + auto draw_locals_panel(Debugger& debugger) -> void; + auto draw_registers_panel(Debugger& debugger) -> void; + auto draw_console_panel(Debugger& debugger) -> void; + + auto push_console(std::string line) -> void; + auto report(const Result<void>& result, std::string_view action) -> void; + + Window* m_window = nullptr; + SourceView m_source_view; + PanelVisibility m_visible; + + std::vector<std::string> m_console_lines; + std::string m_console_input; + std::string m_target_input; + + bool m_layout_built = false; + bool m_console_scroll_pending = false; + bool m_load_target_pending = false; + }; +} |
