#pragma once // Included inside CodeEditorWidget (rendering + folds). public: static int annotationShapeIndex(const std::string& label) { size_t h = 0; for (unsigned char c : label) h = h * 131 + c; return (int)(h % 5); } static void drawAnnotationShape(ImDrawList* drawList, const ImVec2& center, float size, ImU32 color, int shape) { switch (shape) { case 0: { // circle drawList->AddCircleFilled(center, size, color); break; } case 1: { // triangle ImVec2 top(center.x, center.y - size); ImVec2 left(center.x - size, center.y + size); ImVec2 right(center.x + size, center.y + size); drawList->AddTriangleFilled(top, left, right, color); break; } case 2: { // square ImVec2 a(center.x - size, center.y - size); ImVec2 b(center.x + size, center.y + size); drawList->AddRectFilled(a, b, color, 0.0f); break; } case 3: { // diamond ImVec2 top(center.x, center.y - size); ImVec2 right(center.x + size, center.y); ImVec2 bottom(center.x, center.y + size); ImVec2 left(center.x - size, center.y); drawList->AddQuadFilled(top, right, bottom, left, color); break; } default: { // star (outline) ImVec2 pts[10]; for (int i = 0; i < 10; ++i) { float angle = (float)(IM_PI * 0.5 + i * (IM_PI / 5.0)); float radius = (i % 2 == 0) ? size : size * 0.5f; pts[i] = ImVec2(center.x + std::cos(angle) * radius, center.y - std::sin(angle) * radius); } drawList->AddPolyline(pts, 10, color, true, 1.5f); break; } } } CodeEditorResult render(const char* id, std::string& text, const std::vector& spans, const CodeEditorOptions& options, const ImVec2& size, ImFont* font) { CodeEditorResult result; if (!font) font = ImGui::GetFont(); ImGuiIO& io = ImGui::GetIO(); syncPrimaryToMulti(); const double nowSeconds = options.nowSeconds > 0.0 ? options.nowSeconds : ImGui::GetTime(); // Build line start offsets std::vector lineStarts; lineStarts.reserve(128); lineStarts.push_back(0); for (int i = 0; i < (int)text.size(); ++i) { if (text[i] == '\n') lineStarts.push_back(i + 1); } const int lineCount = (int)lineStarts.size(); auto isOpenBracket = [](char c) { return c == '(' || c == '[' || c == '{'; }; auto isCloseBracket = [](char c) { return c == ')' || c == ']' || c == '}'; }; auto matchingOpen = [](char c) { if (c == ')') return '('; if (c == ']') return '['; if (c == '}') return '{'; return '\0'; }; auto isBracketChar = [&](char c) { return isOpenBracket(c) || isCloseBracket(c); }; std::vector bracketDepth(text.size(), -1); std::vector bracketMatch(text.size(), -1); std::vector stack; std::vector stackChars; stack.reserve(128); stackChars.reserve(128); for (int i = 0; i < (int)text.size(); ++i) { char c = text[i]; if (isOpenBracket(c)) { stack.push_back(i); stackChars.push_back(c); bracketDepth[i] = (int)stack.size() - 1; } else if (isCloseBracket(c)) { char open = matchingOpen(c); if (!stackChars.empty() && stackChars.back() == open) { int openPos = stack.back(); stack.pop_back(); stackChars.pop_back(); int depth = (int)stack.size(); bracketDepth[i] = depth; bracketMatch[i] = openPos; bracketMatch[openPos] = i; } } } std::array rainbow = { ThemeEngine::instance().editorColor("bracket_1", IM_COL32(242, 120, 75, 255)), ThemeEngine::instance().editorColor("bracket_2", IM_COL32(241, 196, 15, 255)), ThemeEngine::instance().editorColor("bracket_3", IM_COL32(46, 204, 113, 255)), ThemeEngine::instance().editorColor("bracket_4", IM_COL32(52, 152, 219, 255)), ThemeEngine::instance().editorColor("bracket_5", IM_COL32(155, 89, 182, 255)), ThemeEngine::instance().editorColor("bracket_6", IM_COL32(231, 76, 60, 255)) }; int focusBracketPos = -1; int focusBracketMatch = -1; if (cursor_ > 0 && cursor_ - 1 < (int)text.size() && isBracketChar(text[cursor_ - 1])) { focusBracketPos = cursor_ - 1; } else if (cursor_ < (int)text.size() && isBracketChar(text[cursor_])) { focusBracketPos = cursor_; } if (focusBracketPos >= 0 && focusBracketPos < (int)bracketMatch.size()) { focusBracketMatch = bracketMatch[focusBracketPos]; } int scopeStartLine = -1; int scopeEndLine = -1; if (focusBracketMatch >= 0) { int start = std::min(focusBracketPos, focusBracketMatch); int end = std::max(focusBracketPos, focusBracketMatch); scopeStartLine = lineFromPos(start, lineStarts); scopeEndLine = lineFromPos(end, lineStarts); } // Clamp cursor if (cursor_ < 0) cursor_ = 0; if (cursor_ > (int)text.size()) cursor_ = (int)text.size(); // Build per-byte category map std::vector charCats(text.size(), TokenCategory::Plain); for (const auto& span : spans) { uint32_t end = std::min(span.end, (uint32_t)charCats.size()); for (uint32_t i = span.start; i < end; ++i) { charCats[i] = span.category; } } if (options.enableFolding && options.mode) { updateFolds(text, options.mode->getLanguage()); } else { folds_.clear(); } // Measure const float lineHeight = ImGui::GetTextLineHeightWithSpacing() * std::max(0.5f, options.lineHeightScale); const float baseAdvance = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, "M").x; const float charAdvance = baseAdvance + std::max(0.0f, options.letterSpacing); const float gutterWidth = options.showLineNumbers ? calcGutterWidth(lineCount, font, charAdvance) : 12.0f; const float minimapWidth = options.showMinimap ? 80.0f : 0.0f; // Estimate content size for scrollbars int maxLineLen = 0; for (int ln = 0; ln < lineCount; ++ln) { int start = lineStarts[ln]; int end = (ln + 1 < lineCount) ? lineStarts[ln + 1] - 1 : (int)text.size(); int len = std::max(0, end - start); maxLineLen = std::max(maxLineLen, len); } ImVec2 contentSize(gutterWidth + maxLineLen * charAdvance + minimapWidth + 4.0f, lineCount * lineHeight + 4.0f); ImGui::BeginChild(id, size, false, ImGuiWindowFlags_HorizontalScrollbar); if (options.syncScrollX && !options.scrollMaster) { ImGui::SetScrollX(*options.syncScrollX); } if (options.syncScrollY && !options.scrollMaster) { ImGui::SetScrollY(*options.syncScrollY); } std::string annoPopupId = std::string(id) + "_AnnoPopup"; ImVec2 origin = ImGui::GetCursorScreenPos(); // Dummy to set scroll extents ImGui::Dummy(contentSize); ImDrawList* drawList = ImGui::GetWindowDrawList(); const float scrollX = ImGui::GetScrollX(); const float scrollY = ImGui::GetScrollY(); if (options.syncScrollX && options.scrollMaster) { *options.syncScrollX = scrollX; } if (options.syncScrollY && options.scrollMaster) { *options.syncScrollY = scrollY; } ImVec2 gutterBase(origin.x, origin.y - scrollY); ImVec2 textBase(origin.x + gutterWidth - scrollX, origin.y - scrollY); const float windowWidth = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x; const float minimapBaseX = origin.x + std::max(0.0f, windowWidth - minimapWidth); const float minimapBaseY = origin.y - scrollY; // Focus and input const bool hovered = ImGui::IsWindowHovered(); const bool focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); // Mouse handling if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { ImGui::SetKeyboardFocusHere(-1); const ImVec2 mouse = ImGui::GetMousePos(); if (options.showMinimap && mouse.x >= minimapBaseX) { float miniHeight = lineCount * lineHeight; if (miniHeight > 0.0f) { float localY = mouse.y - minimapBaseY; float t = std::max(0.0f, std::min(1.0f, localY / miniHeight)); float targetScroll = t * lineCount * lineHeight; ImGui::SetScrollY(targetScroll); } selecting_ = false; goto after_mouse; } bool ctrlClick = io.KeyCtrl || io.KeySuper; bool altClick = io.KeyAlt; bool columnSelect = io.KeyAlt && io.KeyShift; int clickCount = ImGui::GetIO().MouseClickedCount[0]; if (mouse.x < origin.x + gutterWidth || clickCount >= 3) { int line = lineFromMouseY(mouse.y, gutterBase.y, lineHeight, lineCount); result.lineClicked = true; result.clickedLine = line; const FoldRegion* fold = findFoldAtLine(line); if (fold && mouse.x < origin.x + 12.0f) { toggleFoldAtLine(line); } int lineStart = lineStarts[line]; int lineEnd = (line + 1 < lineCount) ? lineStarts[line + 1] : (int)text.size(); cursor_ = lineStart; selStart_ = lineStart; selEnd_ = lineEnd; } else if (columnSelect) { int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight); int line = lineFromPos(pos, lineStarts); int col = pos - lineStarts[line]; columnSelectActive_ = true; columnAnchorLine_ = line; columnAnchorCol_ = col; updateColumnSelection(text, lineStarts, columnAnchorLine_, columnAnchorCol_, line, col); selecting_ = true; goto after_mouse; } else if (altClick) { int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight); addCursorAt(pos); selecting_ = false; goto after_mouse; } else { clearExtraCursors(); int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight); result.lineClicked = true; result.clickedLine = lineFromPos(pos, lineStarts); if (ctrlClick) { result.ctrlClick = true; result.ctrlClickPos = pos; result.ctrlClickLine = result.clickedLine; result.ctrlClickCol = pos - lineStarts[result.ctrlClickLine]; } if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { selectWordAt(text, pos); } else if (ImGui::GetIO().KeyShift) { if (!hasSelection()) selStart_ = cursor_; cursor_ = pos; selEnd_ = cursor_; } else { cursor_ = pos; selStart_ = cursor_; selEnd_ = cursor_; } } selecting_ = true; } after_mouse: if (hovered && selecting_ && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { const ImVec2 mouse = ImGui::GetMousePos(); if (mouse.x >= origin.x + gutterWidth) { int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight); if (columnSelectActive_ && (io.KeyAlt && io.KeyShift)) { int line = lineFromPos(pos, lineStarts); int col = pos - lineStarts[line]; updateColumnSelection(text, lineStarts, columnAnchorLine_, columnAnchorCol_, line, col); } else { selEnd_ = pos; } } } if (selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { selecting_ = false; columnSelectActive_ = false; } if (hovered) { const ImVec2 mouse = ImGui::GetMousePos(); if (mouse.x >= origin.x + gutterWidth && (!options.showMinimap || mouse.x < minimapBaseX)) { int pos = positionFromMouse(mouse, textBase, lineStarts, text, charAdvance, lineHeight); int line = lineFromPos(pos, lineStarts); result.hoverValid = true; result.hoverPos = pos; result.hoverLine = line; result.hoverCol = pos - lineStarts[line]; } } // Keyboard input if (focused && !options.readOnly) { handleKeyboard(text, result.changed, lineStarts, options.mode); } // Render visible lines const int firstLine = std::max(0, (int)(scrollY / lineHeight)); const int visibleLines = (int)(ImGui::GetContentRegionAvail().y / lineHeight) + 2; const int lastLine = std::min(lineCount - 1, firstLine + visibleLines); const float textAreaWidth = std::max(0.0f, windowWidth - gutterWidth - minimapWidth); const int currentLine = lineFromPos(cursor_, lineStarts); std::vector hiddenLines(lineCount, false); for (const auto& f : folds_) { if (!f.folded) continue; for (int l = f.startLine + 1; l <= f.endLine && l < lineCount; ++l) { hiddenLines[l] = true; } } for (int ln = firstLine; ln <= lastLine; ++ln) { if (ln >= 0 && ln < lineCount && hiddenLines[ln]) continue; int start = lineStarts[ln]; int end = (ln + 1 < lineCount) ? lineStarts[ln + 1] - 1 : (int)text.size(); int len = std::max(0, end - start); float y = textBase.y + ln * lineHeight; // Gutter background ImVec2 gutterA(origin.x, y); ImVec2 gutterB(origin.x + gutterWidth, y + lineHeight); drawList->AddRectFilled( gutterA, gutterB, ThemeEngine::instance().editorColor("gutter_bg", IM_COL32(20, 20, 20, 255))); // Line number (right-aligned) if (options.showLineNumbers) { char numBuf[16]; snprintf(numBuf, sizeof(numBuf), "%d", ln + 1); ImVec2 numSize = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, numBuf); ImVec2 numPos(origin.x + gutterWidth - 4.0f - numSize.x, y); drawList->AddText(font, font->FontSize, numPos, ThemeEngine::instance().editorColor("gutter_text", IM_COL32(120, 120, 120, 255)), numBuf); } // Diagnostics marker bool hasError = lineIn(options.errorLines, ln); bool hasWarn = lineIn(options.warningLines, ln); if (hasError || hasWarn) { ImU32 color = hasError ? ThemeEngine::instance().editorColor("diag_error", IM_COL32(220, 80, 80, 255)) : ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255)); ImVec2 center(origin.x + 3.0f, y + lineHeight * 0.5f); drawList->AddCircleFilled(center, 3.0f, color); } DiagnosticRange securityDiag; bool hasSecurity = options.diagnostics && securityDiagnosticAtLine(*options.diagnostics, ln, securityDiag); if (hasSecurity) { ImU32 secColor = (securityDiag.severity == 1) ? ThemeEngine::instance().editorColor("diag_error", IM_COL32(220, 80, 80, 255)) : (securityDiag.severity == 2) ? ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255)) : ThemeEngine::instance().editorColor("diag_info", IM_COL32(90, 160, 220, 255)); ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f); ImVec2 top(center.x, center.y - 4.0f); ImVec2 left(center.x - 4.0f, center.y - 1.0f); ImVec2 right(center.x + 4.0f, center.y - 1.0f); ImVec2 bottom(center.x, center.y + 4.0f); drawList->AddTriangleFilled(top, left, right, secColor); drawList->AddTriangleFilled(left, right, bottom, secColor); } if (options.diagnostics) { std::string msg = diagnosticMessageAtLine(*options.diagnostics, ln); ImVec2 gutterA(origin.x, y); ImVec2 gutterB(origin.x + gutterWidth, y + lineHeight); if (!msg.empty()) { bool hover = ImGui::IsMouseHoveringRect(gutterA, gutterB); renderRichTooltip("diag_gutter_" + std::to_string(ln), msg, hover, options.reduceMotion, font); } if (hasSecurity && !securityDiag.message.empty()) { bool hover = ImGui::IsMouseHoveringRect(gutterA, gutterB); renderRichTooltip("security_gutter_" + std::to_string(ln), securityDiag.message, hover, options.reduceMotion, font); } } // Annotation marker if (options.annotations) { AnnotationMarker marker; if (annotationAtLine(*options.annotations, ln, marker)) { ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f); if (options.useAnnotationShapes) { int shape = annotationShapeIndex(marker.message); drawAnnotationShape(drawList, center, 3.5f, marker.color, shape); } else { drawList->AddCircleFilled(center, 3.0f, marker.color); } ImVec2 a(center.x - 4.0f, center.y - 4.0f); ImVec2 b(center.x + 4.0f, center.y + 4.0f); bool hoverAnno = ImGui::IsMouseHoveringRect(a, b); if (hoverAnno) { if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { annotationPopupMessage_ = marker.message; ImGui::OpenPopup(annoPopupId.c_str()); } } renderRichTooltip("anno_marker_" + std::to_string(ln), marker.message, hoverAnno, options.reduceMotion, font); } } // Suggestion marker (lightbulb) if (options.suggestions) { SuggestionMarker suggestion; if (suggestionAtLine(*options.suggestions, ln, suggestion)) { ImVec2 center(origin.x + 20.0f, y + lineHeight * 0.5f); ImU32 color = ThemeEngine::instance().editorColor("suggestion", IM_COL32(240, 200, 40, 255)); drawList->AddCircleFilled(center, 3.0f, color); ImVec2 a(center.x - 4.0f, center.y - 4.0f); ImVec2 b(center.x + 4.0f, center.y + 4.0f); bool hoverSuggestion = ImGui::IsMouseHoveringRect(a, b); if (hoverSuggestion) { if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { result.suggestionClicked = true; result.clickedSuggestion = suggestion; } } if (hoverSuggestion) { std::string tip = suggestion.label + " (" + std::to_string(suggestion.confidence) + ")\n" + suggestion.reason; renderRichTooltip("suggestion_marker_" + std::to_string(ln), tip, hoverSuggestion, options.reduceMotion, font); } } } // Conflict markers: highlight and connecting line if (options.conflicts) { AnnotationConflictMarker conflict; if (conflictAtLine(*options.conflicts, ln, conflict)) { ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f); drawList->AddCircle(center, 5.0f, ThemeEngine::instance().editorColor("conflict", IM_COL32(220, 80, 80, 255)), 12, 1.5f); if (ln == std::min(conflict.childLine, conflict.parentLine) && conflict.childLine >= 0 && conflict.parentLine >= 0) { float y1 = textBase.y + conflict.childLine * lineHeight + lineHeight * 0.5f; float y2 = textBase.y + conflict.parentLine * lineHeight + lineHeight * 0.5f; drawList->AddLine(ImVec2(origin.x + 12.0f, y1), ImVec2(origin.x + 12.0f, y2), ThemeEngine::instance().editorColor("conflict_line", IM_COL32(220, 80, 80, 180)), 1.0f); } ImVec2 a(center.x - 5.0f, center.y - 5.0f); ImVec2 b(center.x + 5.0f, center.y + 5.0f); bool hoverConflict = ImGui::IsMouseHoveringRect(a, b); renderRichTooltip("conflict_marker_" + std::to_string(ln), conflict.message, hoverConflict, options.reduceMotion, font); } } // Fold indicator const FoldRegion* fold = findFoldAtLine(ln); if (fold) { ImVec2 triCenter(origin.x + 6.0f, y + lineHeight * 0.5f); if (fold->folded) { drawTriangle(drawList, triCenter, ThemeEngine::instance().editorColor("fold_indicator", IM_COL32(160, 160, 160, 255)), true); } else { drawTriangle(drawList, triCenter, ThemeEngine::instance().editorColor("fold_indicator", IM_COL32(160, 160, 160, 255)), false); } } // Current line highlight (text area only) if (options.showCurrentLine && ln == currentLine) { ImVec2 hlA(textBase.x, y); ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); drawList->AddRectFilled(hlA, hlB, ThemeEngine::instance().editorColor("line_highlight", IM_COL32(40, 40, 40, 120))); } if (scopeStartLine >= 0 && ln >= scopeStartLine && ln <= scopeEndLine) { ImVec2 hlA(textBase.x, y); ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); drawList->AddRectFilled(hlA, hlB, ThemeEngine::instance().editorColor("scope_highlight", IM_COL32(60, 60, 90, 40))); } if (options.highlightLine >= 0 && ln == options.highlightLine) { ImVec2 hlA(textBase.x, y); ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); drawList->AddRectFilled(hlA, hlB, options.highlightLineColor); } if (options.highlightLines) { if (std::find(options.highlightLines->begin(), options.highlightLines->end(), ln) != options.highlightLines->end()) { ImVec2 hlA(textBase.x, y); ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); drawList->AddRectFilled(hlA, hlB, options.highlightLineColor); } } if (options.searchPulseLine >= 0 && ln == options.searchPulseLine) { float pulse = AnimationUtils::pulseAlpha(nowSeconds, options.searchPulseStart, 0.6f, options.reduceMotion); if (pulse > 0.0f) { ImU32 base = ThemeEngine::instance().editorColor("search_pulse", IM_COL32(240, 200, 80, 160)); ImVec4 c = ImGui::ColorConvertU32ToFloat4(base); c.w *= pulse; ImU32 col = ImGui::ColorConvertFloat4ToU32(c); ImVec2 hlA(textBase.x, y); ImVec2 hlB(textBase.x + textAreaWidth, y + lineHeight); drawList->AddRectFilled(hlA, hlB, col); } } // Selection background for (const auto& c : cursors_) { if (c.selStart < 0 || c.selEnd < 0 || c.selStart == c.selEnd) continue; int selA = std::min(c.selStart, c.selEnd); int selB = std::max(c.selStart, c.selEnd); int lineSelStart = std::max(selA, start); int lineSelEnd = std::min(selB, end); if (lineSelStart < lineSelEnd) { int colA = lineSelStart - start; int colB = lineSelEnd - start; ImVec2 a(textBase.x + colA * charAdvance, y); ImVec2 b(textBase.x + colB * charAdvance, y + lineHeight); drawList->AddRectFilled(a, b, ThemeEngine::instance().editorColor("selection", IM_COL32(60, 100, 160, 120))); } } // Bracket pair highlight if (focusBracketPos >= 0 && focusBracketMatch >= 0) { int matchPositions[2] = {focusBracketPos, focusBracketMatch}; for (int i = 0; i < 2; ++i) { int pos = matchPositions[i]; if (pos < start || pos >= end) continue; int col = pos - start; ImVec2 a(textBase.x + col * charAdvance, y); ImVec2 b(textBase.x + (col + 1) * charAdvance, y + lineHeight); drawList->AddRectFilled(a, b, ThemeEngine::instance().editorColor("bracket_match", IM_COL32(120, 140, 200, 100))); } } // Render text with colors int pos = start; while (pos < end) { TokenCategory cat = TokenCategory::Plain; if (pos < (int)charCats.size()) cat = charCats[pos]; if (pos < (int)text.size() && isBracketChar(text[pos]) && bracketDepth[pos] >= 0) { ImU32 col = rainbow[bracketDepth[pos] % rainbow.size()]; char buf[2] = {text[pos], 0}; ImVec2 p(textBase.x + (pos - start) * charAdvance, y); drawList->AddText(font, font->FontSize, p, col, buf); ++pos; continue; } int spanEnd = pos + 1; while (spanEnd < end) { if (spanEnd < (int)text.size() && isBracketChar(text[spanEnd]) && bracketDepth[spanEnd] >= 0) { break; } TokenCategory nextCat = TokenCategory::Plain; if (spanEnd < (int)charCats.size()) nextCat = charCats[spanEnd]; if (nextCat != cat) break; ++spanEnd; } std::string chunk = text.substr(pos, spanEnd - pos); if (options.showWhitespace) { for (auto& c : chunk) { if (c == ' ') c = '.'; else if (c == '\t') c = '>'; } } if (!chunk.empty() && chunk[chunk.size() - 1] == '\n') { chunk.pop_back(); } ImVec2 p(textBase.x + (pos - start) * charAdvance, y); drawList->AddText(font, font->FontSize, p, colorFor(cat), chunk.c_str()); pos = spanEnd; } // Inline annotation tag if (options.showAnnotations && options.annotations) { AnnotationMarker marker; if (annotationAtLine(*options.annotations, ln, marker)) { ImVec2 tagPos; if (options.annotationLayout == 1) { tagPos = ImVec2(origin.x + 18.0f, y); } else if (options.annotationLayout == 2) { tagPos = ImVec2(textBase.x + len * charAdvance + 8.0f, y); } else { tagPos = ImVec2(textBase.x, y - lineHeight * 0.7f); } ImVec2 textSize = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, marker.message.c_str()); ImVec2 bgA(tagPos.x - 2.0f, tagPos.y); ImVec2 bgB(tagPos.x + textSize.x + 6.0f, tagPos.y + lineHeight * 0.8f); drawList->AddRectFilled(bgA, bgB, ThemeEngine::instance().editorColor("annotation_tag_bg", IM_COL32(30, 30, 30, 220)), 2.0f); drawList->AddText(font, font->FontSize, ImVec2(tagPos.x + 2.0f, tagPos.y), marker.color, marker.message.c_str()); } } // Folded placeholder if (fold && fold->folded) { ImVec2 p(textBase.x + len * charAdvance + 6.0f, y); drawList->AddText(font, font->FontSize, p, ThemeEngine::instance().editorColor("fold_placeholder", IM_COL32(140, 140, 140, 255)), "{...}"); } if (options.diagnostics) { renderSquiggles(*options.diagnostics, ln, y, lineHeight, charAdvance, textBase.x, len, drawList); if (hovered) { ImVec2 mouse = ImGui::GetMousePos(); std::string msg = diagnosticMessageAtPoint(*options.diagnostics, ln, mouse, y, lineHeight, charAdvance, textBase.x); bool hoverDiag = !msg.empty(); if (hoverDiag) { renderRichTooltip("diag_point", msg, hoverDiag, options.reduceMotion, font); } } } } if (ImGui::BeginPopup(annoPopupId.c_str())) { ImGui::TextUnformatted("Annotation Editor (TODO)"); ImGui::Separator(); ImGui::TextUnformatted(annotationPopupMessage_.c_str()); ImGui::EndPopup(); } // Cursor if (focused) { float blinkAlpha = AnimationUtils::blinkAlpha(nowSeconds, options.cursorBlinkRate, options.reduceMotion); if (blinkAlpha > 0.02f) { for (size_t i = 0; i < cursors_.size(); ++i) { const auto& c = cursors_[i]; int curLine = lineFromPos(c.cursor, lineStarts); int lineStart = lineStarts[curLine]; int col = c.cursor - lineStart; float x = textBase.x + col * charAdvance; float y = textBase.y + curLine * lineHeight; ImU32 base = ThemeEngine::instance().editorColor("caret", IM_COL32(240, 240, 240, 255)); ImVec4 color = ImGui::ColorConvertU32ToFloat4(base); color.w *= (i == 0) ? blinkAlpha : (blinkAlpha * 0.6f); ImU32 col32 = ImGui::ColorConvertFloat4ToU32(color); drawList->AddLine(ImVec2(x, y), ImVec2(x, y + lineHeight), col32, 1.0f); } } } // Minimap if (options.showMinimap && minimapWidth > 0.0f) { float miniHeight = lineCount * lineHeight; ImVec2 miniA(minimapBaseX, minimapBaseY); ImVec2 miniB(minimapBaseX + minimapWidth, minimapBaseY + miniHeight); drawList->AddRectFilled(miniA, miniB, ThemeEngine::instance().editorColor("minimap_bg", IM_COL32(18, 18, 18, 255))); for (int ln = 0; ln < lineCount; ++ln) { float y = minimapBaseY + ln * lineHeight; ImU32 color = ThemeEngine::instance().editorColor("minimap_line", IM_COL32(80, 80, 80, 255)); if (ln == currentLine) { color = ThemeEngine::instance().editorColor("minimap_line_active", IM_COL32(120, 120, 160, 255)); } drawList->AddRectFilled(ImVec2(minimapBaseX + 2.0f, y), ImVec2(minimapBaseX + minimapWidth - 2.0f, y + 2.0f), color); } float viewStart = (lineCount > 0) ? (scrollY / (lineCount * lineHeight)) : 0.0f; float viewEnd = viewStart + (ImGui::GetContentRegionAvail().y / (lineCount * lineHeight)); viewStart = std::max(0.0f, std::min(1.0f, viewStart)); viewEnd = std::max(0.0f, std::min(1.0f, viewEnd)); ImVec2 vA(minimapBaseX, minimapBaseY + viewStart * miniHeight); ImVec2 vB(minimapBaseX + minimapWidth, minimapBaseY + viewEnd * miniHeight); drawList->AddRect(vA, vB, ThemeEngine::instance().editorColor("minimap_view", IM_COL32(120, 160, 220, 180))); } ImGui::EndChild(); syncMultiToPrimary(); result.cursorByte = cursor_; result.lineCount = lineCount; result.gutterWidth = gutterWidth; result.currentLine = currentLine; result.foldCount = (int)folds_.size(); result.anyFolded = std::any_of(folds_.begin(), folds_.end(), [](const FoldRegion& f) { return f.folded; }); result.minimapEnabled = options.showMinimap; result.minimapWidth = minimapWidth; if (options.showMinimap && lineCount > 0) { float miniHeight = lineCount * lineHeight; result.minimapViewportStart = (scrollY / (lineCount * lineHeight)) * miniHeight; result.minimapViewportEnd = result.minimapViewportStart + (ImGui::GetContentRegionAvail().y / (lineCount * lineHeight)) * miniHeight; } return result; } void setCursor(int pos) { cursor_ = pos; syncPrimaryToMulti(); } int getCursor() const { return cursor_; } bool hasSelectionRange() const { return hasSelection(); } void getSelectionRange(int& start, int& end) const { if (!hasSelection()) { start = end = -1; return; } start = std::min(selStart_, selEnd_); end = std::max(selStart_, selEnd_); } void setSelectionRange(int start, int end) { selStart_ = start; selEnd_ = end; } const std::vector& getFoldRegions() const { return folds_; } std::vector getFoldedLines() const { std::vector lines; for (const auto& f : folds_) { if (f.folded) lines.push_back(f.startLine); } return lines; } void setDesiredFoldedLines(const std::vector& lines) { desiredFoldedLines_ = lines; applyDesiredFoldState_ = true; } void toggleFoldAtLine(int line) { for (auto& f : folds_) { if (f.startLine == line) { f.folded = !f.folded; return; } } } private: int cursor_ = 0; int selStart_ = -1; int selEnd_ = -1; bool selecting_ = false; std::vector cursors_; bool columnSelectActive_ = false; int columnAnchorLine_ = 0; int columnAnchorCol_ = 0; std::vector folds_; std::string lastFoldText_; std::string lastFoldLang_; std::string annotationPopupMessage_; std::vector desiredFoldedLines_; bool applyDesiredFoldState_ = false; #include "CodeEditorRenderHelpers.h"