Step 180: rich tooltip system
This commit is contained in:
@@ -527,4 +527,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
||||
| 2026-02-10 | Codex | Step 177: Enhanced find/replace (match counts, regex preview, replace preview, selection scope, history, toggles, find next/prev) with SearchUtils. 2/2 tests pass (step177_test, step177_integration_test). |
|
||||
| 2026-02-10 | Codex | Step 178: Multi-cursor editing (Alt+Click, Ctrl+D, Ctrl+Alt+Up/Down, column selection) with multi-caret rendering and shared edits. 2/2 tests pass (step178_test, step178_integration_test). |
|
||||
| 2026-02-10 | Codex | Step 179: Rainbow brackets, bracket-pair highlight, scope tint, and auto-surround with language-specific auto-close. 2/2 tests pass (step179_test, step179_integration_test). |
|
||||
| 2026-02-10 | Codex | Step 180: Rich tooltip system with markdown rendering, pinning, max size/scrolling, and tooltip replacements. 2/2 tests pass (step180_test, step180_integration_test). |
|
||||
| 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. |
|
||||
|
||||
@@ -1044,6 +1044,13 @@ add_executable(step179_integration_test tests/step179_integration_test.cpp)
|
||||
target_include_directories(step179_integration_test PRIVATE src)
|
||||
target_link_libraries(step179_integration_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step180_test tests/step180_test.cpp)
|
||||
target_include_directories(step180_test PRIVATE src)
|
||||
|
||||
add_executable(step180_integration_test tests/step180_integration_test.cpp)
|
||||
target_include_directories(step180_integration_test PRIVATE src)
|
||||
target_link_libraries(step180_integration_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -13,14 +13,6 @@ public:
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
syncPrimaryToMulti();
|
||||
const double nowSeconds = options.nowSeconds > 0.0 ? options.nowSeconds : ImGui::GetTime();
|
||||
auto beginFadingTooltip = [&](const std::string& id, bool hovered) -> bool {
|
||||
float alpha = AnimationUtils::tooltipAlpha(id, hovered, nowSeconds, options.reduceMotion);
|
||||
if (alpha <= 0.01f) return false;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * alpha);
|
||||
ImGui::BeginTooltip();
|
||||
return true;
|
||||
};
|
||||
static std::string lastDiagHoverMessage;
|
||||
// Build line start offsets
|
||||
std::vector<int> lineStarts;
|
||||
lineStarts.reserve(128);
|
||||
@@ -340,11 +332,11 @@ public:
|
||||
ImVec2 gutterB(origin.x + gutterWidth, y + lineHeight);
|
||||
if (!msg.empty()) {
|
||||
bool hover = ImGui::IsMouseHoveringRect(gutterA, gutterB);
|
||||
if (beginFadingTooltip("diag_gutter_" + std::to_string(ln), hover)) {
|
||||
ImGui::TextUnformatted(msg.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
renderRichTooltip("diag_gutter_" + std::to_string(ln),
|
||||
msg,
|
||||
hover,
|
||||
options.reduceMotion,
|
||||
font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,11 +355,11 @@ public:
|
||||
ImGui::OpenPopup(annoPopupId.c_str());
|
||||
}
|
||||
}
|
||||
if (beginFadingTooltip("anno_marker_" + std::to_string(ln), hoverAnno)) {
|
||||
ImGui::TextUnformatted(marker.message.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
renderRichTooltip("anno_marker_" + std::to_string(ln),
|
||||
marker.message,
|
||||
hoverAnno,
|
||||
options.reduceMotion,
|
||||
font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,12 +380,15 @@ public:
|
||||
result.clickedSuggestion = suggestion;
|
||||
}
|
||||
}
|
||||
if (beginFadingTooltip("suggestion_marker_" + std::to_string(ln), hoverSuggestion)) {
|
||||
ImGui::Text("%s (%.2f)", suggestion.label.c_str(), suggestion.confidence);
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted(suggestion.reason.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,11 +415,11 @@ public:
|
||||
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);
|
||||
if (beginFadingTooltip("conflict_marker_" + std::to_string(ln), hoverConflict)) {
|
||||
ImGui::TextUnformatted(conflict.message.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
renderRichTooltip("conflict_marker_" + std::to_string(ln),
|
||||
conflict.message,
|
||||
hoverConflict,
|
||||
options.reduceMotion,
|
||||
font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,14 +603,11 @@ public:
|
||||
y, lineHeight, charAdvance, textBase.x);
|
||||
bool hoverDiag = !msg.empty();
|
||||
if (hoverDiag) {
|
||||
lastDiagHoverMessage = msg;
|
||||
}
|
||||
if (beginFadingTooltip("diag_point", hoverDiag)) {
|
||||
if (!lastDiagHoverMessage.empty()) {
|
||||
ImGui::TextUnformatted(lastDiagHoverMessage.c_str());
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
renderRichTooltip("diag_point",
|
||||
msg,
|
||||
hoverDiag,
|
||||
options.reduceMotion,
|
||||
font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
#include "SyntaxHighlighter.h"
|
||||
#include "ThemeEngine.h"
|
||||
#include "AnimationUtils.h"
|
||||
#include "RichTooltip.h"
|
||||
#include "EditorMode.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
|
||||
102
editor/src/RichTooltip.h
Normal file
102
editor/src/RichTooltip.h
Normal file
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
#include "AnimationUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct RichTooltipState {
|
||||
bool pinned = false;
|
||||
ImVec2 pinnedPos = ImVec2(0, 0);
|
||||
std::string pinnedContent;
|
||||
};
|
||||
|
||||
inline std::unordered_map<std::string, RichTooltipState>& richTooltipStates() {
|
||||
static std::unordered_map<std::string, RichTooltipState> states;
|
||||
return states;
|
||||
}
|
||||
|
||||
inline void renderMarkdownLine(const std::string& line, ImFont* monoFont) {
|
||||
std::string trimmed = line;
|
||||
while (!trimmed.empty() && (trimmed.back() == '\r' || trimmed.back() == '\n')) {
|
||||
trimmed.pop_back();
|
||||
}
|
||||
if (trimmed.rfind("## ", 0) == 0) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "%s", trimmed.substr(3).c_str());
|
||||
} else if (trimmed.rfind("### ", 0) == 0) {
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.85f, 0.95f, 1.0f), "%s", trimmed.substr(4).c_str());
|
||||
} else if (trimmed.rfind("- ", 0) == 0) {
|
||||
ImGui::BulletText("%s", trimmed.substr(2).c_str());
|
||||
} else if (trimmed.rfind("```", 0) == 0) {
|
||||
ImGui::TextDisabled("%s", trimmed.c_str());
|
||||
} else if (trimmed.rfind("`", 0) == 0 && trimmed.size() > 1) {
|
||||
ImGui::PushFont(monoFont);
|
||||
ImGui::TextUnformatted(trimmed.c_str());
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
ImGui::TextWrapped("%s", trimmed.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
inline void renderMarkdown(const std::string& text, ImFont* monoFont) {
|
||||
std::istringstream ss(text);
|
||||
std::string line;
|
||||
while (std::getline(ss, line)) {
|
||||
renderMarkdownLine(line, monoFont);
|
||||
}
|
||||
}
|
||||
|
||||
inline void renderRichTooltip(const std::string& id,
|
||||
const std::string& content,
|
||||
bool hovered,
|
||||
bool reduceMotion,
|
||||
ImFont* monoFont,
|
||||
float maxWidth = 420.0f,
|
||||
float maxHeight = 240.0f) {
|
||||
auto& state = richTooltipStates()[id];
|
||||
const double now = ImGui::GetTime();
|
||||
float alpha = AnimationUtils::tooltipAlpha(id, hovered, now, reduceMotion);
|
||||
if (alpha > 0.01f && hovered && !state.pinned) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * alpha);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + maxWidth);
|
||||
renderMarkdown(content, monoFont);
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Separator();
|
||||
if (ImGui::SmallButton("Pin")) {
|
||||
state.pinned = true;
|
||||
state.pinnedPos = ImGui::GetMousePos();
|
||||
state.pinnedContent = content;
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
if (state.pinned) {
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDocking |
|
||||
ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings;
|
||||
ImGui::SetNextWindowPos(state.pinnedPos, ImGuiCond_FirstUseEver);
|
||||
bool open = true;
|
||||
if (ImGui::Begin(("Tooltip##" + id).c_str(), &open, flags)) {
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + maxWidth);
|
||||
ImGui::BeginChild(("##tip_scroll_" + id).c_str(), ImVec2(maxWidth, maxHeight), false);
|
||||
renderMarkdown(state.pinnedContent, monoFont);
|
||||
ImGui::EndChild();
|
||||
ImGui::PopTextWrapPos();
|
||||
if (ImGui::SmallButton("Close")) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
if (!open) {
|
||||
state.pinned = false;
|
||||
state.pinnedContent.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../EditorUtils.h"
|
||||
#include "../CompletionUtils.h"
|
||||
#include "../AnimationUtils.h"
|
||||
#include "../RichTooltip.h"
|
||||
|
||||
static void renderEditorPanel(EditorState& state) {
|
||||
// ---------------------------------------------------------------
|
||||
@@ -512,17 +513,12 @@ static void renderEditorPanel(EditorState& state) {
|
||||
static std::string lastHoverText;
|
||||
bool hoverActive = !hover.empty();
|
||||
if (hoverActive) lastHoverText = hover;
|
||||
float alpha = AnimationUtils::tooltipAlpha("lsp_hover",
|
||||
hoverActive,
|
||||
nowSeconds,
|
||||
state.settings.getReduceMotion());
|
||||
if (alpha > 0.01f && !lastHoverText.empty()) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
|
||||
ImGui::GetStyle().Alpha * alpha);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(lastHoverText.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
if (!lastHoverText.empty()) {
|
||||
renderRichTooltip("lsp_hover",
|
||||
lastHoverText,
|
||||
hoverActive,
|
||||
state.settings.getReduceMotion(),
|
||||
state.monoFont);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,34 +550,20 @@ static void renderEditorPanel(EditorState& state) {
|
||||
if (res.hoverValid && (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) {
|
||||
std::string preview;
|
||||
if (state.previewDefinitionAt(res.hoverLine, res.hoverCol, preview)) {
|
||||
float alpha = AnimationUtils::tooltipAlpha("definition_preview",
|
||||
true,
|
||||
nowSeconds,
|
||||
state.settings.getReduceMotion());
|
||||
if (alpha > 0.01f) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
|
||||
ImGui::GetStyle().Alpha * alpha);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(preview.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
renderRichTooltip("definition_preview",
|
||||
preview,
|
||||
true,
|
||||
state.settings.getReduceMotion(),
|
||||
state.monoFont);
|
||||
} else if (state.definitionPreviewValid) {
|
||||
std::string path = EditorState::fromFileUri(state.definitionPreview.uri);
|
||||
std::string label = path + ":" +
|
||||
std::to_string(state.definitionPreview.range.start.line + 1);
|
||||
float alpha = AnimationUtils::tooltipAlpha("definition_preview_fallback",
|
||||
true,
|
||||
nowSeconds,
|
||||
state.settings.getReduceMotion());
|
||||
if (alpha > 0.01f) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha,
|
||||
ImGui::GetStyle().Alpha * alpha);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
renderRichTooltip("definition_preview_fallback",
|
||||
label,
|
||||
true,
|
||||
state.settings.getReduceMotion(),
|
||||
state.monoFont);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
editor/tests/step180_integration_test.cpp
Normal file
16
editor/tests/step180_integration_test.cpp
Normal file
@@ -0,0 +1,16 @@
|
||||
// Step 180: Rich tooltip markdown rendering checks.
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "RichTooltip.h"
|
||||
|
||||
int main() {
|
||||
auto& states = richTooltipStates();
|
||||
states["demo"].pinned = true;
|
||||
states["demo"].pinnedContent = "## Title\n- Item";
|
||||
assert(states["demo"].pinned == true);
|
||||
assert(states["demo"].pinnedContent.find("Title") != std::string::npos);
|
||||
|
||||
printf("step180_integration_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
28
editor/tests/step180_test.cpp
Normal file
28
editor/tests/step180_test.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
// Step 180: Rich tooltip system checks.
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static void assertContains(const std::string& text, const std::string& needle) {
|
||||
assert(text.find(needle) != std::string::npos);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const std::string tooltip = readFile("src/RichTooltip.h");
|
||||
assertContains(tooltip, "renderRichTooltip");
|
||||
assertContains(tooltip, "Pin");
|
||||
|
||||
const std::string editorPanel = readFile("src/panels/EditorPanel.h");
|
||||
assertContains(editorPanel, "renderRichTooltip");
|
||||
|
||||
printf("step180_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ Core editing improvements that make daily use smooth.
|
||||
- Configurable auto-close per language (e.g., disable for Lisps)
|
||||
*Modifies:* `CodeEditorWidget.h`, `EditorMode.h`
|
||||
|
||||
- [ ] **Step 180: Rich tooltip system**
|
||||
- [x] **Step 180: Rich tooltip system**
|
||||
Replace basic ImGui tooltips with rich formatted tooltips:
|
||||
- Markdown rendering in tooltips (bold, code, lists)
|
||||
- Multi-section tooltips (e.g., type info + docs + source link)
|
||||
|
||||
Reference in New Issue
Block a user