From 54f5bd1d809c08a7ed52df5ca01cb1d7c1396b01 Mon Sep 17 00:00:00 2001 From: paxcut <53811119+paxcut@users.noreply.github.com> Date: Sun, 15 Sep 2024 06:19:04 -0700 Subject: [PATCH] feat: Added underwaved text functions (#1889) ### Problem description Currently when errors are found the entire line where the error occurred is highlighted and one has to look at the error message in order to find where the error is located on the line. With this PR the line will no longer be highlighted and the location of the error will be marked with an red waved line under the error location. Hovering over the text where the error occurred produces an error overlay so if several errors occur on the same line they can all be seen separately. ### Implementation description The definition of error marker was switched to include column and size as well as line and message like before. This change required changing the way view pattern editor draws the error markers because the errors themselves don't have size information. Also, a new errorHoverBoxes type was defined to help in the detection of the floating error messages when error is hovered. Note that the underwave code depends on having a monospaced. If font is not monospaced the underwaved text can be short/long or displaced. ### Screenshots ![image](https://github.com/user-attachments/assets/f0b08e10-612c-404a-8863-d4f00054d198) ![image](https://github.com/user-attachments/assets/911fcacb-2a1e-431f-bbc8-8e05bcd61341) --- .../include/hex/ui/imgui_imhex_extensions.h | 2 + .../source/ui/imgui_imhex_extensions.cpp | 28 +++++ .../ColorTextEditor/include/TextEditor.h | 17 +-- .../ColorTextEditor/source/TextEditor.cpp | 104 ++++++++++++------ .../content/views/view_pattern_editor.hpp | 1 + .../content/views/view_pattern_editor.cpp | 23 +++- 6 files changed, 132 insertions(+), 43 deletions(-) diff --git a/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h b/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h index 304b45c21..67a0dc1bd 100644 --- a/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h +++ b/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h @@ -137,6 +137,8 @@ namespace ImGuiExt { void UnderlinedText(const char *label, ImColor color = ImGui::GetStyleColorVec4(ImGuiCol_Text), const ImVec2 &size_arg = ImVec2(0, 0)); + void UnderwavedText(const char *label, ImColor textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text), ImColor lineColor = ImGui::GetStyleColorVec4(ImGuiCol_Text), const ImVec2 &size_arg = ImVec2(0, 0)); + void TextSpinner(const char *label); void Header(const char *label, bool firstEntry = false); diff --git a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp index 5d4d6a589..6b54eae28 100644 --- a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp +++ b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp @@ -550,6 +550,34 @@ namespace ImGuiExt { PopStyleColor(); } + void UnderwavedText(const char *label, ImColor textColor, ImColor lineColor, const ImVec2 &size_arg) { + ImGuiWindow *window = GetCurrentWindow(); + std::string labelStr(label); + for (char letter : labelStr) { + std::string letterStr(1, letter); + const ImVec2 label_size = CalcTextSize(letterStr.c_str(), nullptr, true); + ImVec2 size = CalcItemSize(size_arg, label_size.x, label_size.y); + ImVec2 pos = window->DC.CursorPos; + float lineWidth = size.x / 3.0f; + float halfLineW = lineWidth / 2.0f; + float lineY = pos.y + size.y; + ImVec2 initial = ImVec2(pos.x, lineY); + ImVec2 pos1 = ImVec2(pos.x + lineWidth, lineY - 2.0f); + ImVec2 pos2 = ImVec2(pos.x + lineWidth + halfLineW, lineY); + ImVec2 pos3 = ImVec2(pos.x + lineWidth * 2 + halfLineW, lineY - 2.0f); + ImVec2 pos4 = ImVec2(pos.x + lineWidth * 3, lineY - 1.0f); + + PushStyleColor(ImGuiCol_Text, ImU32(textColor)); + TextEx(letterStr.c_str(), nullptr, ImGuiTextFlags_NoWidthForLargeClippedText); // Skip formatting + GetWindowDrawList()->AddLine(initial, pos1, ImU32(lineColor),0.4f); + GetWindowDrawList()->AddLine(pos1, pos2, ImU32(lineColor),0.3f); + GetWindowDrawList()->AddLine(pos2, pos3, ImU32(lineColor),0.4f); + GetWindowDrawList()->AddLine(pos3, pos4, ImU32(lineColor),0.3f); + PopStyleColor(); + window->DC.CursorPos = ImVec2(pos.x + size.x, pos.y); + } + } + void TextSpinner(const char *label) { Text("[%c] %s", "|/-\\"[ImU32(GetTime() * 20) % 4], label); } diff --git a/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h b/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h index f6172f080..ebe3784e4 100644 --- a/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h +++ b/lib/third_party/imgui/ColorTextEditor/include/TextEditor.h @@ -128,13 +128,14 @@ public: std::string mDeclaration; }; - typedef std::string String; - typedef std::unordered_map Identifiers; - typedef std::unordered_set Keywords; - typedef std::map ErrorMarkers; - typedef std::unordered_set Breakpoints; - typedef std::array Palette; - typedef uint8_t Char; + using String = std::string; + using Identifiers = std::unordered_map; + using Keywords = std::unordered_set ; + using ErrorMarkers = std::map>; + using ErrorHoverBoxes = std::map>; + using Breakpoints = std::unordered_set; + using Palette = std::array; + using Char = uint8_t ; struct Glyph { @@ -199,6 +200,7 @@ public: void SetErrorMarkers(const ErrorMarkers& aMarkers) { mErrorMarkers = aMarkers; } void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; } + ImVec2 Underwaves( ImVec2 pos, uint32_t nChars, ImColor color= ImGui::GetStyleColorVec4(ImGuiCol_Text), const ImVec2 &size_arg= ImVec2(0, 0)); void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false); void SetText(const std::string& aText); @@ -473,6 +475,7 @@ private: bool mCheckComments; Breakpoints mBreakpoints; ErrorMarkers mErrorMarkers; + ErrorHoverBoxes mErrorHoverBoxes; ImVec2 mCharAdvance; Coordinates mInteractiveStart, mInteractiveEnd; std::string mLineBuffer; diff --git a/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp b/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp index 0d3c714e2..8c540a4e4 100644 --- a/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp +++ b/lib/third_party/imgui/ColorTextEditor/source/TextEditor.cpp @@ -38,6 +38,37 @@ TextEditor::TextEditor() TextEditor::~TextEditor() { } + +ImVec2 TextEditor::Underwaves( ImVec2 pos ,uint32_t nChars, ImColor color, const ImVec2 &size_arg) { + auto save = ImGui::GetStyle().AntiAliasedLines; + ImGui::GetStyle().AntiAliasedLines = false; + ImGuiWindow *window = ImGui::GetCurrentWindow(); + window->DC.CursorPos =pos; + const ImVec2 label_size = ImGui::CalcTextSize("W", nullptr, true); + ImVec2 size = ImGui::CalcItemSize(size_arg, label_size.x, label_size.y); + float lineWidth = size.x / 3.0f + 0.5f; + float halfLineW = lineWidth / 2.0f; + + for (uint32_t i = 0; i < nChars; i++) { + pos = window->DC.CursorPos; + float lineY = pos.y + size.y; + + ImVec2 pos1_1 = ImVec2(pos.x + 0*lineWidth, lineY + halfLineW); + ImVec2 pos1_2 = ImVec2(pos.x + 1*lineWidth, lineY - halfLineW); + ImVec2 pos2_1 = ImVec2(pos.x + 2*lineWidth, lineY + halfLineW); + ImVec2 pos2_2 = ImVec2(pos.x + 3*lineWidth, lineY - halfLineW); + + ImGui::GetWindowDrawList()->AddLine(pos1_1, pos1_2, ImU32(color), 0.4f); + ImGui::GetWindowDrawList()->AddLine(pos1_2, pos2_1, ImU32(color), 0.4f); + ImGui::GetWindowDrawList()->AddLine(pos2_1, pos2_2, ImU32(color), 0.4f); + + window->DC.CursorPos = ImVec2(pos.x + size.x, pos.y); + } + auto ret = window->DC.CursorPos; + ret.y += size.y; + return ret; +} + void TextEditor::SetLanguageDefinition(const LanguageDefinition &aLanguageDef) { mLanguageDefinition = aLanguageDef; mRegexList.clear(); @@ -541,8 +572,8 @@ void TextEditor::RemoveLine(int aStart, int aEnd) { ErrorMarkers etmp; for (auto &i : mErrorMarkers) { - ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second); - if (e.first >= aStart && e.first <= aEnd) + ErrorMarkers::value_type e(i.first.mLine >= aStart ? Coordinates(i.first.mLine - 1,i.first.mColumn ) : i.first, i.second); + if (e.first.mLine >= aStart && e.first.mLine <= aEnd) continue; etmp.insert(e); } @@ -555,9 +586,10 @@ void TextEditor::RemoveLine(int aStart, int aEnd) { btmp.insert(i >= aStart ? i - 1 : i); } mBreakpoints = std::move(btmp); - - mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); - assert(!mLines.empty()); + if (aStart == 0 && aEnd == (int32_t)mLines.size() - 1) + mLines.erase(mLines.begin() + aStart, mLines.end()); + else + mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd + 1); mTextChanged = true; } @@ -568,8 +600,8 @@ void TextEditor::RemoveLine(int aIndex) { ErrorMarkers etmp; for (auto &i : mErrorMarkers) { - ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second); - if (e.first - 1 == aIndex) + ErrorMarkers::value_type e(i.first.mLine > aIndex ? Coordinates(i.first.mLine - 1 ,i.first.mColumn) : i.first, i.second); + if (e.first.mLine - 1 == aIndex) continue; etmp.insert(e); } @@ -594,7 +626,7 @@ TextEditor::Line &TextEditor::InsertLine(int aIndex) { ErrorMarkers etmp; for (auto &i : mErrorMarkers) - etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second)); + etmp.insert(ErrorMarkers::value_type(i.first.mLine >= aIndex ? Coordinates(i.first.mLine + 1,i.first.mColumn) : i.first, i.second)); mErrorMarkers = std::move(etmp); Breakpoints btmp; @@ -853,25 +885,6 @@ void TextEditor::Render() { auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); - // Draw error markers - auto errorIt = mErrorMarkers.find(lineNo + 1); - if (errorIt != mErrorMarkers.end()) { - auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); - drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::ErrorMarker]); - - if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) { - ImGui::BeginTooltip(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); - ImGui::Text("Error at line %d:", errorIt->first); - ImGui::PopStyleColor(); - ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.2f, 1.0f)); - ImGui::Text("%s", errorIt->second.c_str()); - ImGui::PopStyleColor(); - ImGui::EndTooltip(); - } - } - // Draw line number (right aligned) if (mShowLineNumbers) { snprintf(buf, 16, "%d ", lineNo + 1); @@ -891,7 +904,6 @@ void TextEditor::Render() { if (mState.mCursorPosition.mLine == lineNo && mShowCursor) { bool focused = ImGui::IsWindowFocused(); - ImGuiViewport *viewport = ImGui::GetWindowViewport(); // Highlight the current line (where the cursor is) if (!HasSelection()) { @@ -937,7 +949,14 @@ void TextEditor::Render() { for (int i = 0; i < line.size();) { auto &glyph = line[i]; auto color = GetGlyphColor(glyph); - + bool underwaved = false; + ErrorMarkers::iterator errorIt; + if (mErrorMarkers.size() > 0) { + errorIt = mErrorMarkers.find(Coordinates(lineNo+1,i)); + if (errorIt != mErrorMarkers.end()) { + underwaved = true; + } + } if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && !mLineBuffer.empty()) { const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); @@ -945,6 +964,13 @@ void TextEditor::Render() { bufferOffset.x += textSize.x; mLineBuffer.clear(); } + if (underwaved) { + auto textStart = TextDistanceToLineStart(Coordinates(lineNo, i-1)) + mTextStart; + auto begin = ImVec2(lineStartScreenPos.x + textStart, lineStartScreenPos.y); + auto end = Underwaves(begin, errorIt->second.first, mPalette[(int32_t) PaletteIndex::ErrorMarker]); + mErrorHoverBoxes[Coordinates(lineNo+1,i)]=std::make_pair(begin,end); + } + prevColor = color; if (glyph.mChar == '\t') { @@ -1021,6 +1047,22 @@ void TextEditor::Render() { mScrollToCursor = false; } + for (auto [key,value] : mErrorMarkers) { + auto start = mErrorHoverBoxes[key].first; + auto end = mErrorHoverBoxes[key].second; + if (ImGui::IsMouseHoveringRect(start, end)) { + ImGui::BeginTooltip(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Error at line %d:", key.mLine); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.2f, 1.0f)); + ImGui::Text("%s", value.second.c_str()); + ImGui::PopStyleColor(); + ImGui::EndTooltip(); + } + } + ImGuiPopupFlags_ popup_flags = ImGuiPopupFlags_None; ImGuiContext& g = *GImGui; auto oldTopMargin = mTopMargin; @@ -1788,7 +1830,7 @@ void TextEditor::Backspace() { ErrorMarkers etmp; for (auto &i : mErrorMarkers) - etmp.insert(ErrorMarkers::value_type(i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); + etmp.insert(ErrorMarkers::value_type(i.first.mLine - 1 == mState.mCursorPosition.mLine ? Coordinates(i.first.mLine - 1,i.first.mColumn) : i.first, i.second)); mErrorMarkers = std::move(etmp); RemoveLine(mState.mCursorPosition.mLine); @@ -2457,7 +2499,7 @@ void TextEditor::ColorizeRange(int aFromLine, int aToLine) { hasTokenizeResult = true; } - if (hasTokenizeResult == false) { + if (!hasTokenizeResult) { // todo : remove // printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); diff --git a/plugins/builtin/include/content/views/view_pattern_editor.hpp b/plugins/builtin/include/content/views/view_pattern_editor.hpp index f32b04646..db25fe728 100644 --- a/plugins/builtin/include/content/views/view_pattern_editor.hpp +++ b/plugins/builtin/include/content/views/view_pattern_editor.hpp @@ -250,6 +250,7 @@ namespace hex::plugin::builtin { PerProvider> m_lastEvaluationError; PerProvider> m_lastCompileError; + PerProvider> m_callStack; PerProvider> m_lastEvaluationOutVars; PerProvider> m_patternVariables; PerProvider> m_sections; diff --git a/plugins/builtin/source/content/views/view_pattern_editor.cpp b/plugins/builtin/source/content/views/view_pattern_editor.cpp index b0b086770..4435b9d44 100644 --- a/plugins/builtin/source/content/views/view_pattern_editor.cpp +++ b/plugins/builtin/source/content/views/view_pattern_editor.cpp @@ -1343,15 +1343,27 @@ namespace hex::plugin::builtin { }; TextEditor::ErrorMarkers errorMarkers; - if (m_lastEvaluationError->has_value()) { - errorMarkers[(*m_lastEvaluationError)->line] = processMessage((*m_lastEvaluationError)->message); + if (!m_callStack->empty()) { + for (const auto &frame : *m_callStack | std::views::reverse) { + auto location = frame->getLocation(); + std::string message; + if (location.source->source == pl::api::Source::DefaultSource) { + if (m_lastEvaluationError->has_value()) + message = processMessage((*m_lastEvaluationError)->message); + auto key = TextEditor::Coordinates(location.line, location.column); + errorMarkers[key] = std::make_pair(location.length, message); + } + } } if (!m_lastCompileError->empty()) { for (const auto &error : *m_lastCompileError) { - auto source = error.getLocation().source; - if (source != nullptr && source->source == pl::api::Source::DefaultSource) - errorMarkers[error.getLocation().line] = processMessage(error.getMessage()); + auto source = error.getLocation().source; + if (source != nullptr && source->source == pl::api::Source::DefaultSource) { + auto key = TextEditor::Coordinates(error.getLocation().line, error.getLocation().column); + if (!errorMarkers.contains(key) ||errorMarkers[key].first < error.getLocation().length) + errorMarkers[key] = std::make_pair(error.getLocation().length,processMessage(error.getMessage())); + } } } @@ -1794,6 +1806,7 @@ namespace hex::plugin::builtin { if (!m_lastEvaluationResult) { *m_lastEvaluationError = runtime.getEvalError(); *m_lastCompileError = runtime.getCompileErrors(); + *m_callStack = reinterpret_cast &>(runtime.getInternals().evaluator->getCallStack()); } TaskManager::doLater([code] {