diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index dfe8202..85df33f 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -453,8 +453,17 @@ void App::buildSweepCatalog() }; std::vector rels; rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28", - "## What's new\n- Faster initial block sync\n- BIP39 seed-phrase wallet support\n" - "- Mempool handling fixes and lower memory use\n\n## Checksums\n| File | SHA-256 |\n" + "## What is DragonX?\n\n" + "DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. " + "It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n" + "## Bug Fixes\n" + "* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n" + "* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n" + "## Key Features\n" + "* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n" + "* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n" + "* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n" + "## Checksums\n| File | SHA-256 |\n" "|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false)); rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14", "## Notes\n- Stability improvements\n- RPC fixes\n", false)); diff --git a/src/ui/material/markdown.h b/src/ui/material/markdown.h new file mode 100644 index 0000000..97a9689 --- /dev/null +++ b/src/ui/material/markdown.h @@ -0,0 +1,154 @@ +// 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 diff --git a/src/ui/windows/daemon_download_dialog.h b/src/ui/windows/daemon_download_dialog.h index 9fec06c..5c8cdbc 100644 --- a/src/ui/windows/daemon_download_dialog.h +++ b/src/ui/windows/daemon_download_dialog.h @@ -24,6 +24,7 @@ #include "../../util/i18n.h" #include "../material/colors.h" #include "../material/draw_helpers.h" +#include "../material/markdown.h" #include "../material/type.h" #include "../theme.h" #include "release_list_view.h" @@ -184,8 +185,8 @@ private: } } - // Release-notes text with the checksum table / signature block stripped (the body is mostly a - // machine-readable checksum table). Returns "" when nothing human-readable remains. + // Release-notes markdown with the machine-readable checksum table / signature block cut off (the + // body ends with a "| file | sha |" table). Keeps the markdown so RenderMarkdown can style it. static std::string cleanNotes(const std::string& body) { std::istringstream ss(body); std::string line, out; @@ -198,16 +199,7 @@ private: if (!lt.empty() && lt[0] == '|') break; // markdown table (checksums) if (low.rfind("## checksum", 0) == 0 || low.rfind("#### checksum", 0) == 0 || low.rfind("checksums", 0) == 0 || low.rfind("### sha", 0) == 0) break; - // Strip a leading markdown heading marker ("## Foo" -> "Foo") for cleaner plain-text display. - std::string disp = lt; - if (!disp.empty() && disp[0] == '#') { - size_t h = disp.find_first_not_of('#'); - if (h != std::string::npos) disp = disp.substr(disp.find_first_not_of(" \t", h)); - else disp.clear(); - } else { - disp = line; // keep original indentation for non-heading lines (bullets etc.) - } - out += disp; + out += line; out += '\n'; } while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || @@ -241,13 +233,19 @@ private: const float masterW = std::min(std::max(contentW * 0.34f, 240.0f * dp), 340.0f * dp); const float detailW = contentW - masterW - gap; - // ---- LEFT: scrollable version list ---- - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingXs(), Layout::spacingXs())); - ImGui::BeginChild("##daemonVersions", ImVec2(masterW, bodyH), true); + // ---- LEFT: scrollable version list — borderless; each version is a tactile Material card ---- + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::BeginChild("##daemonVersions", ImVec2(masterW, bodyH), false); { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, Layout::spacingSm())); + // Transparent Selectable highlight — we draw our own Material card fill/hover/press. + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0, 0, 0, 0)); ImDrawList* mdl = ImGui::GetWindowDrawList(); - const float rowH = Layout::spacingSm() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize; + const float rowH = Layout::spacingMd() * 2.0f + bodyF->LegacySize + Layout::spacingXs() + capF->LegacySize; const float rowW = ImGui::GetContentRegionAvail().x; + const float round = 10.0f * dp; for (int i = 0; i < (int)s_rows.size(); ++i) { const ReleaseRow& r = s_rows[i]; ImGui::PushID(i); @@ -256,36 +254,47 @@ private: if (ImGui::Selectable("##ver", sel, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) { if (!active) s_sel = i; } + bool hov = ImGui::IsItemHovered(); + bool held = ImGui::IsItemActive(); ImVec2 rmx(rmn.x + rowW, rmn.y + rowH); - // Accent bar: installed = green, newest = primary, else a dim rule. - ImU32 accent = r.installed ? Success() : (i == 0 ? Primary() : WithAlpha(OnSurface(), 55)); - mdl->AddRectFilled(ImVec2(rmn.x, rmn.y + 4.0f * dp), - ImVec2(rmn.x + 3.0f * dp, rmx.y - 4.0f * dp), accent, 1.5f * dp); - float x = rmn.x + Layout::spacingSm() + 4.0f * dp; - float tagY = rmn.y + Layout::spacingSm(); + // Material card: filled surface, brighter on hover, primary-tinted + outlined when selected. + ImU32 fill = sel ? WithAlpha(Primary(), held ? 55 : 42) + : held ? WithAlpha(OnSurface(), 34) + : hov ? WithAlpha(OnSurface(), 24) + : WithAlpha(OnSurface(), 12); + mdl->AddRectFilled(rmn, rmx, fill, round); + if (sel) mdl->AddRect(rmn, rmx, WithAlpha(Primary(), 150), round, 0, 1.6f * dp); + if (hov || held) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + // Left accent bar (rounded), keyed to the version's status. + ImU32 accent = r.installed ? Success() : (i == 0 ? Primary() : WithAlpha(OnSurface(), 70)); + mdl->AddRectFilled(ImVec2(rmn.x + 4.0f * dp, rmn.y + 8.0f * dp), + ImVec2(rmn.x + 7.0f * dp, rmx.y - 8.0f * dp), accent, 2.0f * dp); + float x = rmn.x + Layout::spacingMd() + 5.0f * dp; + float tagY = rmn.y + Layout::spacingMd(); mdl->AddText(bodyF, bodyF->LegacySize, ImVec2(x, tagY), r.hasAsset ? OnSurface() : OnSurfaceDisabled(), r.tag.c_str()); float bx = x + bodyF->CalcTextSizeA(bodyF->LegacySize, FLT_MAX, 0, r.tag.c_str()).x + Layout::spacingSm(); float by = tagY + (bodyF->LegacySize - capF->LegacySize) * 0.5f; auto badge = [&](const char* t, ImU32 c) { float tw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, t).x; - if (bx + tw > rmx.x - Layout::spacingXs()) return; // no room: skip (long tag) + if (bx + tw > rmx.x - Layout::spacingSm()) return; // no room: skip (long tag) mdl->AddText(capF, capF->LegacySize, ImVec2(bx, by), c, t); bx += tw + Layout::spacingSm(); }; if (r.installed) badge(TR("upd_installed_badge"), Success()); else if (i == 0) badge(TR("daemon_update_latest_badge"), Primary()); if (r.prerelease) badge(TR("upd_prerelease"), Warning()); - // Date line. if (!r.date.empty()) mdl->AddText(capF, capF->LegacySize, - ImVec2(x, rmx.y - Layout::spacingSm() - capF->LegacySize), + ImVec2(x, rmx.y - Layout::spacingMd() - capF->LegacySize), OnSurfaceMedium(), r.date.c_str()); ImGui::PopID(); } + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); // ItemSpacing } ImGui::EndChild(); - ImGui::PopStyleVar(); + ImGui::PopStyleVar(); // WindowPadding ImGui::SameLine(0.0f, gap); @@ -363,17 +372,21 @@ private: // (never let a min-height notes child push the button into a scroll region). const float roomForNotes = ImGui::GetContentRegionAvail().y - reserve; if (roomForNotes >= 44.0f * dp) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingSm(), Layout::spacingSm())); - ImGui::BeginChild("##daemonNotes", ImVec2(0, roomForNotes), true); + // Filled, rounded Material surface (no thin outline); notes rendered as markdown. + ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 14)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 12.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingMd())); + ImGui::BeginChild("##daemonNotes", ImVec2(0, roomForNotes), false); { const std::string notes = cleanNotes(rel.body); if (notes.empty()) Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("daemon_update_no_notes")); else - ImGui::TextWrapped("%s", notes.c_str()); + RenderMarkdown(notes); } ImGui::EndChild(); - ImGui::PopStyleVar(); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); ImGui::Spacing(); }