From a6b98e0613304c7802d9e0a155f5fd0c178babb1 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Tue, 15 Sep 2026 02:36:23 -0700 Subject: [PATCH] Added console concept --- engine/include/sophia/ui/debug_ui.hpp | 2 + engine/src/ui/debug_ui.cpp | 10 ++ engine/src/ui/panels/console_panel.cpp | 136 +++++++++++++++++++++++++ engine/src/ui/panels/console_panel.hpp | 50 +++++++++ 4 files changed, 198 insertions(+) create mode 100644 engine/src/ui/panels/console_panel.cpp create mode 100644 engine/src/ui/panels/console_panel.hpp diff --git a/engine/include/sophia/ui/debug_ui.hpp b/engine/include/sophia/ui/debug_ui.hpp index 45e78fd..09b4c17 100644 --- a/engine/include/sophia/ui/debug_ui.hpp +++ b/engine/include/sophia/ui/debug_ui.hpp @@ -18,12 +18,14 @@ class DebugUI { void showPanelRegistry(bool show = true); void showFrameStats(bool show = true); + void showConsole(bool show = true); private: PanelRegistry& registry; PanelHandle panelRegistryHandle; PanelHandle frameStatsHandle; + PanelHandle consoleHandle; }; } // namespace sophia diff --git a/engine/src/ui/debug_ui.cpp b/engine/src/ui/debug_ui.cpp index 2803ea4..fe3a19a 100644 --- a/engine/src/ui/debug_ui.cpp +++ b/engine/src/ui/debug_ui.cpp @@ -1,5 +1,6 @@ #include +#include "panels/console_panel.hpp" #include "panels/frame_stats_panel.hpp" #include "panels/panel_registry_panel.hpp" @@ -9,17 +10,20 @@ DebugUI::DebugUI(PanelRegistry& registry) : registry{registry} { // #ifndef NDEBUG this->panelRegistryHandle = registry.emplace(registry); this->frameStatsHandle = registry.emplace(); + this->consoleHandle = registry.emplace(); // #endif // !NDEBUG } void DebugUI::show() { showPanelRegistry(true); showFrameStats(true); + showConsole(true); } void DebugUI::hide() { showPanelRegistry(false); showFrameStats(false); + showConsole(false); } void DebugUI::showPanelRegistry(bool show) { @@ -34,4 +38,10 @@ void DebugUI::showFrameStats(bool show) { } } +void DebugUI::showConsole(bool show) { + if (registry.isValid(consoleHandle)) { + registry.get(consoleHandle).setVisible(show); + } +} + } // namespace sophia diff --git a/engine/src/ui/panels/console_panel.cpp b/engine/src/ui/panels/console_panel.cpp new file mode 100644 index 0000000..b9962dc --- /dev/null +++ b/engine/src/ui/panels/console_panel.cpp @@ -0,0 +1,136 @@ +#include "console_panel.hpp" + +#include + +namespace sophia { + +namespace { +constexpr ImVec4 COLOR_DEFAULT = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); +constexpr ImVec4 COLOR_COMMAND = ImVec4(1.0f, 0.78f, 0.37f, 1.0f); +constexpr ImVec4 COLOR_ERROR = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); +constexpr ImVec4 COLOR_INFO = ImVec4(0.4f, 0.7f, 1.0f, 1.0f); +} // namespace + +void ConsolePanel::registerDefaultCommands() { + log("Console ready. Type 'help' for a list of commands."); +} + +void ConsolePanel::log(const std::string& message) { + entries.push_back(Entry{message, COLOR_DEFAULT}); + scrollToBottom = true; +} + +void ConsolePanel::clear() { entries.clear(); } + +void ConsolePanel::execute(const std::string& command) { + entries.push_back(Entry{"> " + command, COLOR_COMMAND}); + scrollToBottom = true; + + historyPos = -1; + history.erase(std::remove(history.begin(), history.end(), command), + history.end()); + history.push_back(command); + + if (command == "help") { + entries.push_back( + Entry{"Available commands: help, clear, history", COLOR_INFO}); + } else if (command == "clear") { + clear(); + } else if (command == "history") { + for (const std::string& past : history) { + entries.push_back(Entry{past, COLOR_DEFAULT}); + } + } else if (commandCallback) { + commandCallback(*this, command); + } else { + entries.push_back(Entry{"Unknown command: '" + command + "'", COLOR_ERROR}); + } +} + +int ConsolePanel::textEditCallback(ImGuiInputTextCallbackData* data) { + if (data->EventFlag != ImGuiInputTextFlags_CallbackHistory) { + return 0; + } + + const int previousPos = historyPos; + if (data->EventKey == ImGuiKey_UpArrow) { + if (historyPos == -1) { + historyPos = static_cast(history.size()) - 1; + } else if (historyPos > 0) { + historyPos--; + } + } else if (data->EventKey == ImGuiKey_DownArrow) { + if (historyPos != -1 && ++historyPos >= static_cast(history.size())) { + historyPos = -1; + } + } + + if (previousPos != historyPos) { + const std::string& historyStr = + historyPos >= 0 ? history[static_cast(historyPos)] : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, historyStr.c_str()); + } + + return 0; +} + +void ConsolePanel::onUpdate(const FrameInfo&) { + ImGui::Begin("Console"); + + if (ImGui::SmallButton("Clear")) { + clear(); + } + ImGui::SameLine(); + ImGui::Checkbox("Auto-scroll", &autoScroll); + + ImGui::Separator(); + + const float footerHeight = + ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("ScrollRegion", ImVec2(0, -footerHeight), false, + ImGuiWindowFlags_HorizontalScrollbar); + + for (const Entry& entry : entries) { + ImGui::PushStyleColor(ImGuiCol_Text, entry.color); + ImGui::TextUnformatted(entry.text.c_str()); + ImGui::PopStyleColor(); + } + + if (scrollToBottom || + (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) { + ImGui::SetScrollHereY(1.0f); + } + scrollToBottom = false; + + ImGui::EndChild(); + ImGui::Separator(); + + ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | + ImGuiInputTextFlags_CallbackHistory; + + bool reclaimFocus = false; + if (ImGui::InputText( + "Input", inputBuffer, sizeof(inputBuffer), inputFlags, + [](ImGuiInputTextCallbackData* data) -> int { + auto* console = static_cast(data->UserData); + return console->textEditCallback(data); + }, + this)) { + std::string command{inputBuffer}; + if (!command.empty()) { + execute(command); + } + inputBuffer[0] = '\0'; + reclaimFocus = true; + } + + ImGui::SetItemDefaultFocus(); + if (reclaimFocus) { + ImGui::SetKeyboardFocusHere(-1); + } + + ImGui::End(); +} + +} // namespace sophia diff --git a/engine/src/ui/panels/console_panel.hpp b/engine/src/ui/panels/console_panel.hpp new file mode 100644 index 0000000..0afb993 --- /dev/null +++ b/engine/src/ui/panels/console_panel.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace sophia { + +class ConsolePanel : public Panel { + public: + using CommandCallback = + std::function; + + ConsolePanel() : Panel{"Console"} { registerDefaultCommands(); } + + void onUpdate(const FrameInfo& frameInfo) override; + + void log(const std::string& message); + void clear(); + + void setCommandCallback(CommandCallback callback) { + this->commandCallback = std::move(callback); + } + + private: + struct Entry { + std::string text; + ImVec4 color; + }; + + void registerDefaultCommands(); + void execute(const std::string& command); + int textEditCallback(struct ImGuiInputTextCallbackData* data); + + std::vector entries; + std::vector history; + int historyPos = -1; + + char inputBuffer[256] = {}; + bool autoScroll = true; + bool scrollToBottom = false; + + CommandCallback commandCallback; +}; + +} // namespace sophia