#pragma once #include "imgui.h" #include "AnimationUtils.h" #include #include #include #include #include struct RichTooltipState { bool pinned = false; ImVec2 pinnedPos = ImVec2(0, 0); std::string pinnedContent; }; inline std::unordered_map& richTooltipStates() { static std::unordered_map states; return states; } inline void richRenderMarkdownLine(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 richRenderMarkdown(const std::string& text, ImFont* monoFont) { std::istringstream ss(text); std::string line; while (std::getline(ss, line)) { richRenderMarkdownLine(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); richRenderMarkdown(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); richRenderMarkdown(state.pinnedContent, monoFont); ImGui::EndChild(); ImGui::PopTextWrapPos(); if (ImGui::SmallButton("Close")) { open = false; } } ImGui::End(); if (!open) { state.pinned = false; state.pinnedContent.clear(); } } }