diff --git a/PROGRESS.md b/PROGRESS.md index 65b3f43..54dd170 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -177,6 +177,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 79: **IMPLEMENTED** — Auto-indent and smart editing: EditorMode integration, enter auto-indent, tab inserts spaces, auto-close brackets/quotes (3/3 tests pass) - [x] Step 80: **IMPLEMENTED** — Scroll/selection/clipboard: shift-extend selection, double/triple click selection, Ctrl+C/X/V clipboard, drag select (3/3 tests pass) - [x] Step 81: **IMPLEMENTED** — Code folding: tree-sitter fold regions, gutter fold toggles, hidden ranges with placeholders (2/2 tests pass) +- [x] Step 82: **IMPLEMENTED** — Minimap: right-side overview with viewport indicator and click-to-scroll (1/1 tests pass) --- @@ -237,6 +238,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 79:** Compile and pass (3/3) **Step 80:** Compile and pass (3/3) **Step 81:** Compile and pass (2/2) +**Step 82:** Compile and pass (1/1) --- @@ -326,3 +328,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 79: Auto-indent and smart editing. EditorMode wired for newline indent, tab spacing, and bracket auto-close. 3/3 tests pass. | | 2026-02-09 | Codex | Step 80: Selection and clipboard operations (Ctrl+C/X/V, shift-extend, double/triple click). 3/3 tests pass. | | 2026-02-09 | Codex | Step 81: Code folding with tree-sitter fold regions and gutter toggles. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 82: Minimap overview with viewport indicator and click-to-scroll. 1/1 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 3ed5ed6..5fc5951 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -343,6 +343,10 @@ add_executable(step81_test tests/step81_test.cpp) target_include_directories(step81_test PRIVATE src) target_link_libraries(step81_test PRIVATE imgui::imgui unofficial::tree-sitter::tree-sitter tree_sitter_python tree_sitter_cpp tree_sitter_elisp) +add_executable(step82_test tests/step82_test.cpp) +target_include_directories(step82_test PRIVATE src) +target_link_libraries(step82_test PRIVATE imgui::imgui unofficial::tree-sitter::tree-sitter tree_sitter_python tree_sitter_cpp tree_sitter_elisp) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index f06f711..3a2a829 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -20,6 +20,7 @@ struct CodeEditorOptions { bool readOnly = false; EditorMode* mode = nullptr; bool enableFolding = false; + bool showMinimap = false; }; struct CodeEditorResult { @@ -30,6 +31,10 @@ struct CodeEditorResult { int currentLine = 0; int foldCount = 0; bool anyFolded = false; + bool minimapEnabled = false; + float minimapWidth = 0.0f; + float minimapViewportStart = 0.0f; + float minimapViewportEnd = 0.0f; }; struct FoldRegion { @@ -81,6 +86,7 @@ public: const float lineHeight = ImGui::GetTextLineHeightWithSpacing(); const float charAdvance = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, "M").x; const float gutterWidth = calcGutterWidth(lineCount, font, charAdvance); + const float minimapWidth = options.showMinimap ? 80.0f : 0.0f; // Estimate content size for scrollbars int maxLineLen = 0; @@ -90,7 +96,7 @@ public: int len = std::max(0, end - start); maxLineLen = std::max(maxLineLen, len); } - ImVec2 contentSize(gutterWidth + maxLineLen * charAdvance + 4.0f, + ImVec2 contentSize(gutterWidth + maxLineLen * charAdvance + minimapWidth + 4.0f, lineCount * lineHeight + 4.0f); ImGui::BeginChild(id, size, false, ImGuiWindowFlags_HorizontalScrollbar); @@ -104,6 +110,9 @@ public: const float scrollY = ImGui::GetScrollY(); 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(); @@ -113,6 +122,17 @@ public: 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; + } int clickCount = ImGui::GetIO().MouseClickedCount[0]; if (mouse.x < origin.x + gutterWidth || clickCount >= 3) { int line = lineFromMouseY(mouse.y, gutterBase.y, lineHeight, lineCount); @@ -141,6 +161,7 @@ public: } selecting_ = true; } + after_mouse: if (hovered && selecting_ && ImGui::IsMouseDown(ImGuiMouseButton_Left)) { const ImVec2 mouse = ImGui::GetMousePos(); if (mouse.x >= origin.x + gutterWidth) { @@ -162,8 +183,7 @@ public: 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, - (ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x) - gutterWidth); + const float textAreaWidth = std::max(0.0f, windowWidth - gutterWidth - minimapWidth); const int currentLine = lineFromPos(cursor_, lineStarts); std::vector hiddenLines(lineCount, false); @@ -278,6 +298,29 @@ public: } } + // 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, IM_COL32(18, 18, 18, 255)); + for (int ln = 0; ln < lineCount; ++ln) { + float y = minimapBaseY + ln * lineHeight; + ImU32 color = IM_COL32(80, 80, 80, 255); + if (ln == currentLine) color = 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, IM_COL32(120, 160, 220, 180)); + } + ImGui::EndChild(); result.cursorByte = cursor_; @@ -287,6 +330,14 @@ public: 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; } diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 182c5a4..70634bc 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -64,6 +64,7 @@ struct EditorState { // Custom editor widget state CodeEditorWidget codeWidget; bool showWhitespace = false; + bool showMinimap = false; void init() { std::string defaultContent = @@ -563,6 +564,7 @@ int main(int, char**) { } if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Show Whitespace", nullptr, &state.showWhitespace); + ImGui::MenuItem("Show Minimap", nullptr, &state.showMinimap); ImGui::EndMenu(); } if (ImGui::BeginMenu("Language")) { @@ -667,6 +669,7 @@ int main(int, char**) { opts.showWhitespace = state.showWhitespace; opts.mode = &state.mode; opts.enableFolding = true; + opts.showMinimap = state.showMinimap; CodeEditorResult res = state.codeWidget.render("##editor", state.editBuf, state.highlights, opts, avail, monoFont); diff --git a/editor/tests/step82_test.cpp b/editor/tests/step82_test.cpp new file mode 100644 index 0000000..26ac59b --- /dev/null +++ b/editor/tests/step82_test.cpp @@ -0,0 +1,62 @@ +// Step 82 TDD Test: Minimap +// +// Tests: +// 1. Minimap width reported when enabled + +#include +#include +#include "imgui.h" +#include "CodeEditorWidget.h" +#include "EditorMode.h" + +static void beginFrame() { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(800, 600); + io.DeltaTime = 1.0f / 60.0f; + ImGui::NewFrame(); +} + +static void endFrame() { + ImGui::Render(); +} + +int main() { + int passed = 0; + int failed = 0; + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontDefault(); + io.Fonts->Build(); + + // --- Test 1: Minimap enabled reports width --- + { + beginFrame(); + ImGui::SetNextWindowFocus(); + ImGui::Begin("TestWindow"); + CodeEditorWidget widget; + EditorMode mode("python"); + std::string text = "def f():\n x = 1\n y = 2\n"; + std::vector spans; + CodeEditorOptions opts; + opts.mode = &mode; + opts.enableFolding = true; + opts.showMinimap = true; + + CodeEditorResult res = widget.render("##editor", text, spans, opts, ImVec2(300, 200), ImGui::GetFont()); + ImGui::End(); + endFrame(); + + assert(res.minimapEnabled); + assert(res.minimapWidth > 0.0f); + std::cout << "Test 1 PASS: Minimap width reported" << std::endl; + ++passed; + } + + ImGui::DestroyContext(); + + std::cout << "\n=== Step 82 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}