diff options
author | rtk0c <[email protected]> | 2023-10-19 22:50:07 -0700 |
---|---|---|
committer | rtk0c <[email protected]> | 2025-08-16 11:31:16 -0700 |
commit | 297232d21594b138bb368a42b5b0d085ff9ed6aa (patch) | |
tree | 075d5407e1e12a9d35cbee6e4c20ad34e0765c42 /src/brussel.engine/EditorCommandPalette.cpp | |
parent | d5cd34ff69f7fd134d5450696f298af1a864afbc (diff) |
The great renaming: switch to "module style"
Diffstat (limited to 'src/brussel.engine/EditorCommandPalette.cpp')
-rw-r--r-- | src/brussel.engine/EditorCommandPalette.cpp | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/src/brussel.engine/EditorCommandPalette.cpp b/src/brussel.engine/EditorCommandPalette.cpp new file mode 100644 index 0000000..0e7b894 --- /dev/null +++ b/src/brussel.engine/EditorCommandPalette.cpp @@ -0,0 +1,406 @@ +#include "EditorCommandPalette.hpp" + +#include "AppConfig.hpp" +#include "EditorUtils.hpp" +#include "FuzzyMatch.hpp" +#include "Utils.hpp" + +#include <GLFW/glfw3.h> +#include <imgui.h> +#include <misc/cpp/imgui_stdlib.h> +#include <algorithm> +#include <limits> +#include <utility> + +#define IMGUI_DEFINE_MATH_OPERATORS +#include <imgui_internal.h> + +using namespace std::literals; + +bool EditorCommandExecuteContext::IsInitiated() const { + return mCommand != nullptr; +} + +const EditorCommand* EditorCommandExecuteContext::GetCurrentCommand() const { + return mCommand; +} + +void EditorCommandExecuteContext::Initiate(const EditorCommand& command) { + if (mCommand == nullptr) { + mCommand = &command; + } +} + +void EditorCommandExecuteContext::Prompt(std::vector<std::string> options) { + assert(mCommand != nullptr); + mCurrentOptions = std::move(options); + ++mDepth; +} + +void EditorCommandExecuteContext::Finish() { + assert(mCommand != nullptr); + mCommand = nullptr; + mCurrentOptions.clear(); + mDepth = 0; +} + +int EditorCommandExecuteContext::GetExecutionDepth() const { + return mDepth; +} + +struct EditorCommandPalette::SearchResult { + int itemIndex; + int score; + int matchCount; + uint8_t matches[32]; +}; + +struct EditorCommandPalette::Item { + bool hovered = false; + bool held = false; +}; + +EditorCommandPalette::EditorCommandPalette() = default; +EditorCommandPalette::~EditorCommandPalette() = default; + +namespace P6503_UNITY_ID { +std::string MakeCommandName(std::string_view category, std::string_view name) { + std::string result; + constexpr auto infix = ": "sv; + result.reserve(category.size() + infix.size() + name.size()); + result.append(category); + result.append(infix); + result.append(name); + return result; +} +} // namespace P6503_UNITY_ID + +void EditorCommandPalette::AddCommand(std::string_view category, std::string_view name, EditorCommand command) { + command.name = P6503_UNITY_ID::MakeCommandName(category, name); + + auto location = std::lower_bound( + mCommands.begin(), + mCommands.end(), + command, + [](const EditorCommand& a, const EditorCommand& b) -> bool { + return a.name < b.name; + }); + auto iter = mCommands.insert(location, std::move(command)); + + InvalidateSearchResults(); +} + +void EditorCommandPalette::RemoveCommand(std::string_view category, std::string_view name) { + auto commandName = P6503_UNITY_ID::MakeCommandName(category, name); + RemoveCommand(commandName); +} + +void EditorCommandPalette::RemoveCommand(const std::string& commandName) { + struct Comparator { + bool operator()(const EditorCommand& command, const std::string& str) const { + return command.name < str; + } + + bool operator()(const std::string& str, const EditorCommand& command) const { + return str < command.name; + } + }; + + auto range = std::equal_range(mCommands.begin(), mCommands.end(), commandName, Comparator{}); + mCommands.erase(range.first, range.second); + + InvalidateSearchResults(); +} + +void EditorCommandPalette::Show(bool* open) { + // Center window horizontally, align top vertically + ImGui::SetNextWindowPos(ImVec2(ImGui::GetMainViewport()->Size.x / 2, 0), ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowSizeRelScreen(0.3f, 0.0f); + + ImGui::Begin("Command Palette", open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); + float width = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x; + + if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || mShouldCloseNextFrame) { + // Close popup when user unfocused the command palette window (clicking elsewhere) + // or some action requested closing this window + mShouldCloseNextFrame = false; + if (open) { + *open = false; + } + } + + if (ImGui::IsWindowAppearing() || mFocusSearchBox) { + mFocusSearchBox = false; + + // Focus the search box when user first brings command palette window up + // Note: this only affects the next frame + ImGui::SetKeyboardFocusHere(0); + } + ImGui::SetNextItemWidth(width); + if (ImGui::InputText("##", &mSearchText)) { + // Search string updated, update search results + + mFocusedItemId = 0; + mSearchResults.clear(); + + size_t itemCount; + if (mExeCtx.GetExecutionDepth() == 0) { + itemCount = mCommands.size(); + } else { + itemCount = mExeCtx.mCurrentOptions.size(); + } + + for (size_t i = 0; i < itemCount; ++i) { + const char* text; + if (mExeCtx.GetExecutionDepth() == 0) { + text = mCommands[i].name.c_str(); + } else { + text = mExeCtx.mCurrentOptions[i].c_str(); + } + + SearchResult result{ + .itemIndex = (int)i, + }; + if (FuzzyMatch::Search(mSearchText.c_str(), text, result.score, result.matches, std::size(result.matches), result.matchCount)) { + mSearchResults.push_back(result); + } + } + + std::sort( + mSearchResults.begin(), + mSearchResults.end(), + [](const SearchResult& a, const SearchResult& b) -> bool { + // We want the biggest element first + return a.score > b.score; + }); + } + + ImGui::BeginChild("SearchResults", ImVec2(width, 300), false, ImGuiWindowFlags_AlwaysAutoResize); + auto window = ImGui::GetCurrentWindow(); + + auto& io = ImGui::GetIO(); + auto dlSharedData = ImGui::GetDrawListSharedData(); + + auto textColor = ImGui::GetColorU32(ImGuiCol_Text); + auto itemHoveredColor = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + auto itemActiveColor = ImGui::GetColorU32(ImGuiCol_HeaderActive); + auto itemSelectedColor = ImGui::GetColorU32(ImGuiCol_Header); + + int itemCount = GetItemCount(); + if (mItems.size() < itemCount) { + mItems.resize(itemCount); + } + + // Flag used to delay item selection until after the loop ends + bool selectFocusedItem = false; + for (size_t i = 0; i < itemCount; ++i) { + auto id = window->GetID(static_cast<int>(i)); + + ImVec2 size{ + ImGui::GetContentRegionAvail().x, + dlSharedData->Font->FontSize, + }; + ImRect rect{ + window->DC.CursorPos, + window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f), + }; + + bool& hovered = mItems[i].hovered; + bool& held = mItems[i].held; + if (held && hovered) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemActiveColor); + } else if (hovered) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemHoveredColor); + } else if (mFocusedItemId == i) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemSelectedColor); + } + + auto item = GetItem(i); + if (item.indexType == SearchResultIndex) { + // Iterating search results: draw text with highlights at matched chars + + auto& searchResult = mSearchResults[i]; + auto textPos = window->DC.CursorPos; + int rangeBegin; + int rangeEnd; + int lastRangeEnd = 0; + + auto DrawCurrentRange = [&]() -> void { + if (rangeBegin != lastRangeEnd) { + // Draw normal text between last highlighted range end and current highlighted range start + auto begin = item.text + lastRangeEnd; + auto end = item.text + rangeBegin; + window->DrawList->AddText(textPos, textColor, begin, end); + + auto segmentSize = dlSharedData->Font->CalcTextSizeA(dlSharedData->Font->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end); + textPos.x += segmentSize.x; + } + + auto begin = item.text + rangeBegin; + auto end = item.text + rangeEnd; + window->DrawList->AddText(AppConfig::fontBold, AppConfig::fontBold->FontSize, textPos, textColor, begin, end); + + auto segmentSize = AppConfig::fontBold->CalcTextSizeA(AppConfig::fontBold->FontSize, std::numeric_limits<float>::max(), 0.0f, begin, end); + textPos.x += segmentSize.x; + }; + + assert(searchResult.matchCount >= 1); + rangeBegin = searchResult.matches[0]; + rangeEnd = rangeBegin; + + int lastCharIdx = -1; + for (int j = 0; j < searchResult.matchCount; ++j) { + int charIdx = searchResult.matches[j]; + + if (charIdx == lastCharIdx + 1) { + // These 2 indices are equal, extend our current range by 1 + ++rangeEnd; + } else { + DrawCurrentRange(); + lastRangeEnd = rangeEnd; + rangeBegin = charIdx; + rangeEnd = charIdx + 1; + } + + lastCharIdx = charIdx; + } + + // Draw the remaining range (if any) + if (rangeBegin != rangeEnd) { + DrawCurrentRange(); + } + + // Draw the text after the last range (if any) + window->DrawList->AddText(textPos, textColor, item.text + rangeEnd); // Draw until \0 + } else { + // Iterating everything else: draw text as-is, there is no highlights + + window->DrawList->AddText(window->DC.CursorPos, textColor, item.text); + } + + ImGui::ItemSize(rect); + if (!ImGui::ItemAdd(rect, id)) { + continue; + } + if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) { + mFocusedItemId = i; + selectFocusedItem = true; + } + } + + if (ImGui::IsKeyPressed(GLFW_KEY_UP)) { + mFocusedItemId = std::max(mFocusedItemId - 1, 0); + } else if (ImGui::IsKeyPressed(GLFW_KEY_DOWN)) { + mFocusedItemId = std::min(mFocusedItemId + 1, itemCount - 1); + } + if (ImGui::IsKeyPressed(GLFW_KEY_ENTER) || selectFocusedItem) { + SelectFocusedItem(); + } + + ImGui::EndChild(); + + ImGui::End(); +} + +size_t EditorCommandPalette::GetItemCount() const { + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + if (mSearchText.empty()) { + return mCommands.size(); + } else { + return mSearchResults.size(); + } + } else { + if (mSearchText.empty()) { + return mExeCtx.mCurrentOptions.size(); + } else { + return mSearchResults.size(); + } + } +} + +EditorCommandPalette::ItemInfo EditorCommandPalette::GetItem(size_t idx) const { + ItemInfo option; + + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + if (mSearchText.empty()) { + option.text = mCommands[idx].name.c_str(); + option.command = &mCommands[idx]; + option.itemId = idx; + option.indexType = DirectIndex; + } else { + auto id = mSearchResults[idx].itemIndex; + option.text = mCommands[id].name.c_str(); + option.command = &mCommands[id]; + option.itemId = id; + option.indexType = SearchResultIndex; + } + option.itemType = CommandItem; + } else { + assert(mExeCtx.GetCurrentCommand() != nullptr); + if (mSearchText.empty()) { + option.text = mExeCtx.mCurrentOptions[idx].c_str(); + option.command = mExeCtx.GetCurrentCommand(); + option.itemId = idx; + option.indexType = DirectIndex; + } else { + auto id = mSearchResults[idx].itemIndex; + option.text = mExeCtx.mCurrentOptions[id].c_str(); + option.command = mExeCtx.GetCurrentCommand(); + option.itemId = id; + option.indexType = SearchResultIndex; + } + option.itemType = CommandOptionItem; + } + + return option; +} + +void EditorCommandPalette::SelectFocusedItem() { + if (mFocusedItemId < 0 || mFocusedItemId >= GetItemCount()) { + return; + } + + auto selectedItem = GetItem(mFocusedItemId); + auto& command = *selectedItem.command; + + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + assert(!mExeCtx.IsInitiated()); + + mExeCtx.Initiate(*selectedItem.command); + if (command.callback) { + command.callback(mExeCtx); + + mFocusSearchBox = true; + // Don't invalidate search results if no further actions have been requested (returning to global list of commands) + if (mExeCtx.IsInitiated()) { + InvalidateSearchResults(); + } + } else { + mExeCtx.Finish(); + } + } else { + assert(mExeCtx.IsInitiated()); + assert(command.subsequentCallback); + command.subsequentCallback(mExeCtx, selectedItem.itemId); + + mFocusSearchBox = true; + InvalidateSearchResults(); + } + + // This action terminated execution, close command palette window + if (!mExeCtx.IsInitiated()) { + if (command.terminate) { + command.terminate(); + } + mShouldCloseNextFrame = true; + } +} + +void EditorCommandPalette::InvalidateSearchResults() { + mSearchText.clear(); + mSearchResults.clear(); + mFocusedItemId = 0; +} |