Step 82: minimap

This commit is contained in:
Bill
2026-02-09 09:14:47 -07:00
parent 8bad31e94c
commit c040f6f936
5 changed files with 126 additions and 3 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -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<bool> 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;
}

View File

@@ -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);

View File

@@ -0,0 +1,62 @@
// Step 82 TDD Test: Minimap
//
// Tests:
// 1. Minimap width reported when enabled
#include <cassert>
#include <iostream>
#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<HighlightSpan> 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;
}