Step 82: minimap
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
62
editor/tests/step82_test.cpp
Normal file
62
editor/tests/step82_test.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user