// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Minimal in-app markdown renderer for release notes (the daemon / miner updater bodies are GitHub- // flavoured markdown). Supports the subset those notes actually use: # / ## / ### headings, // **bold**, `inline code`, - / * / + bullets, and blank-line paragraph breaks. It lays text out with // the ImGui draw list (word-wrapped, mixed fonts) and reserves the block height via a Dummy so a // scrollable parent scrolls correctly. Not a full CommonMark implementation — just enough to make // release notes readable. #pragma once #include #include #include #include #include "imgui.h" #include "../layout.h" #include "colors.h" #include "type.h" namespace dragonx { namespace ui { namespace material { // Render `md` into the current window, wrapping to `wrapW` (defaults to the content region width). // Advances the ImGui cursor past the rendered block. inline void RenderMarkdown(const std::string& md, float wrapW = 0.0f) { const float dp = Layout::dpiScale(); if (wrapW <= 0.0f) wrapW = ImGui::GetContentRegionAvail().x; if (wrapW <= 1.0f) return; ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* fBody = Type().body2(); // 14 Regular ImFont* fBold = Type().subtitle2(); // 14 Medium (bold at body size) ImFont* fCode = Type().mono(); ImFont* fH1 = Type().h6(); // 20 Medium ImFont* fH2 = Type().subtitle2(); // heading level 2+ = medium + top spacing const ImU32 cText = OnSurface(); const ImU32 cCode = Primary(); const ImU32 cBullet = OnSurfaceMedium(); const ImVec2 origin = ImGui::GetCursorScreenPos(); const float xLeft = origin.x; const float maxX = xLeft + wrapW; float y = origin.y; struct Run { std::string text; ImFont* font; ImU32 col; bool code; }; // Split a line into styled runs on **bold** and `code`. auto parseInline = [&](const std::string& line, ImFont* base, ImU32 col) { std::vector runs; std::string cur; auto flush = [&]() { if (!cur.empty()) { runs.push_back({cur, base, col, false}); cur.clear(); } }; for (size_t i = 0; i < line.size();) { if (line[i] == '*' && i + 1 < line.size() && line[i + 1] == '*') { size_t e = line.find("**", i + 2); if (e == std::string::npos) { cur += line[i]; ++i; continue; } flush(); runs.push_back({line.substr(i + 2, e - (i + 2)), fBold, col, false}); i = e + 2; } else if (line[i] == '`') { size_t e = line.find('`', i + 1); if (e == std::string::npos) { cur += line[i]; ++i; continue; } flush(); runs.push_back({line.substr(i + 1, e - (i + 1)), fCode, cCode, true}); i = e + 1; } else { cur += line[i]; ++i; } } flush(); return runs; }; // Lay runs out as words, wrapping to [xStart, maxX] and hanging at xHang on wrap. Advances y. auto layout = [&](const std::vector& runs, float xStart, float xHang) { float x = xStart; float lineH = 0.0f; for (const Run& r : runs) { size_t i = 0; while (i < r.text.size()) { size_t ws = r.text.find(' ', i); std::string word = (ws == std::string::npos) ? r.text.substr(i) : r.text.substr(i, ws - i + 1); i = (ws == std::string::npos) ? r.text.size() : ws + 1; if (word.empty()) continue; float wW = r.font->CalcTextSizeA(r.font->LegacySize, FLT_MAX, 0, word.c_str()).x; if (x + wW > maxX && x > xHang + 0.5f) { // wrap y += (lineH > 0.0f ? lineH : r.font->LegacySize * 1.4f); x = xHang; lineH = 0.0f; } lineH = std::max(lineH, r.font->LegacySize * 1.4f); if (r.code) { std::string trimmed = word; while (!trimmed.empty() && trimmed.back() == ' ') trimmed.pop_back(); float tw = r.font->CalcTextSizeA(r.font->LegacySize, FLT_MAX, 0, trimmed.c_str()).x; dl->AddRectFilled(ImVec2(x - 2.0f * dp, y), ImVec2(x + tw + 2.0f * dp, y + r.font->LegacySize + 3.0f * dp), WithAlpha(OnSurface(), 26), 3.0f * dp); } dl->AddText(r.font, r.font->LegacySize, ImVec2(x, y), r.col, word.c_str()); x += wW; } } y += (lineH > 0.0f ? lineH : fBody->LegacySize * 1.4f); }; std::istringstream ss(md); std::string raw; bool prevBlank = true; while (std::getline(ss, raw)) { if (!raw.empty() && raw.back() == '\r') raw.pop_back(); size_t a = raw.find_first_not_of(" \t"); std::string t = (a == std::string::npos) ? std::string() : raw.substr(a); if (t.empty()) { y += fBody->LegacySize * 0.55f; prevBlank = true; continue; } if (t[0] == '#') { // heading size_t h = t.find_first_not_of('#'); std::string txt; if (h != std::string::npos) { size_t ts = t.find_first_not_of(" \t", h); if (ts != std::string::npos) txt = t.substr(ts); } if (!prevBlank) y += fBody->LegacySize * 0.5f; // breathing room above a heading ImFont* hf = (h <= 1) ? fH1 : fH2; layout(parseInline(txt, hf, cText), xLeft, xLeft); prevBlank = false; continue; } if ((t[0] == '*' || t[0] == '-' || t[0] == '+') && t.size() > 1 && t[1] == ' ') { // bullet const float indent = fBody->LegacySize * 1.15f; dl->AddText(fBody, fBody->LegacySize, ImVec2(xLeft + fBody->LegacySize * 0.35f, y), cBullet, "\xE2\x80\xA2"); // • layout(parseInline(t.substr(2), fBody, cText), xLeft + indent, xLeft + indent); prevBlank = false; continue; } layout(parseInline(t, fBody, cText), xLeft, xLeft); // paragraph prevBlank = false; } ImGui::SetCursorScreenPos(origin); ImGui::Dummy(ImVec2(wrapW, std::max(0.0f, y - origin.y))); } } // namespace material } // namespace ui } // namespace dragonx