diff --git a/CLAUDE.md b/CLAUDE.md index 661014e..40fe941 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,5 +106,6 @@ The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` p - **C++17.** Match the surrounding code's style per file. - **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs. - **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code. +- **DPI / display scaling:** `schema::UI()` returns **logical (raw) px**; `Layout::dpiScale()` (= OS DPI × the in-app font-scale) is the single scale factor. The `Layout::k*()` helpers and `BeginOverlayDialog` already fold it in, and ImGui auto-layout scales via the DPI-rebuilt font atlas + `ScaleAllSizes` — so tabs/standard widgets scale for free. But **hand-drawn absolute geometry** (`dl->AddText` at manual `cy += 24.0f` offsets, `SetNextWindowSize`, explicit `ImVec2(width,0)` button sizes, `SameLine(x)` column strides) is immune to all of that and must be multiplied by `ui::Layout::dpiScale()` yourself, or it renders native-size (tiny) on a HiDPI display. Font metrics (`font->LegacySize`, `CalcTextSize`) are already scaled — don't double-scale those. Verify at scale with a full sweep run at `font_scale: 1.5` (same `dpiScale()==1.5` code path as OS 150%). - **i18n:** user-facing strings are translated via `src/util/i18n`; the English source of truth is the `strings_[...]` map in `src/util/i18n.cpp`, and the per-language translations live as the **source of truth** in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). **Edit those JSONs directly and additively** — write with `json.dump(..., indent=4, sort_keys=True, ensure_ascii=False)`; never bulk-regenerate/overwrite a whole file (that path silently dropped ~285 keys/language before). `scripts/add_missing_translations.py` back-fills only the keys missing from a JSON (non-destructive), and `scripts/build_cjk_subset.py` rebuilds the CJK subset font (`res/fonts/NotoSansCJK-Subset.ttf`) after new CJK glyphs are added. - **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`. diff --git a/src/app.cpp b/src/app.cpp index cdfff5b..9562636 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -627,7 +627,13 @@ void App::update() { PERF_SCOPE("Update.Total"); ImGuiIO& io = ImGui::GetIO(); - + + // Full UI screenshot sweep: demo state is injected once and must stay frozen. Skip every live + // op (refresh/connect/pumps) so a real daemon can't clobber it — on Windows a running node's + // refresh set connected=false (CDB error) and blanked the Send/Receive/History tabs mid-sweep. + // capture_mode_ is only set for the offline full sweep, so the live tab-only sweep is unaffected. + if (capture_mode_) return; + // Track user interaction for auto-lock if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 || io.MouseClicked[0] || io.MouseClicked[1] || @@ -3006,6 +3012,9 @@ void App::renderSeedBackupDialog() ui::material::Type().text(ui::material::TypeStyle::H6, TR("seed_backup_title")); ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + // Button widths are logical px; BeginOverlayDialog scales the card by dpiScale() so scale these + // to match (raw widths render small/left-packed at higher DPI / font scale). + const float dp = ui::Layout::dpiScale(); if (state_.isLocked()) { ImGui::TextWrapped("%s", TR("seed_backup_locked")); @@ -3027,11 +3036,11 @@ void App::renderSeedBackupDialog() ImGui::Spacing(); } ImGui::Separator(); - if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120, 0))) { + if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120 * dp, 0))) { copySecretToClipboard(seed_backup_phrase_); } ImGui::SameLine(); - if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150, 0))) { + if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150 * dp, 0))) { std::string path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt"; std::string content = seed_backup_phrase_ + "\n"; const bool ok = util::Platform::writeFileAtomically(path, content, @@ -3041,7 +3050,7 @@ void App::renderSeedBackupDialog() : std::string(TR("seed_backup_save_failed"))) + path; } ImGui::SameLine(); - if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) { + if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120 * dp, 0))) { closeAndWipe(); ui::material::EndOverlayDialog(); return; @@ -3054,7 +3063,7 @@ void App::renderSeedBackupDialog() ImGui::Spacing(); ImGui::Separator(); - if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) { + if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120 * dp, 0))) { closeAndWipe(); } ui::material::EndOverlayDialog(); @@ -3085,6 +3094,9 @@ void App::renderSeedMigrationDialog() if (!show_seed_migration_ && busy) show_seed_migration_ = true; const ImVec4 kMedium = ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()); + // BeginOverlayDialog scales the card by dpiScale(); the button widths below are authored in + // logical px and must be scaled the same way or they render small/left-packed at higher DPI. + const float dp = ui::Layout::dpiScale(); ui::material::Type().text(ui::material::TypeStyle::H6, TR("mig_title")); ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); @@ -3093,10 +3105,10 @@ void App::renderSeedMigrationDialog() ImGui::TextWrapped("%s", TR("mig_intro")); ImGui::Spacing(); ImGui::Separator(); - if (ui::material::StyledButton(TR("mig_create"), ImVec2(180, 0))) + if (ui::material::StyledButton(TR("mig_create"), ImVec2(180 * dp, 0))) beginCreateSeedWallet(); ImGui::SameLine(); - if (ui::material::StyledButton(TR("cancel"), ImVec2(120, 0))) + if (ui::material::StyledButton(TR("cancel"), ImVec2(120 * dp, 0))) close(); break; @@ -3117,10 +3129,10 @@ void App::renderSeedMigrationDialog() ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr")); ImGui::TextWrapped("%s", seed_migration_dest_.c_str()); ImGui::Spacing(); - if (ui::material::StyledButton(TR("mig_copy_seed"), ImVec2(120, 0))) + if (ui::material::StyledButton(TR("mig_copy_seed"), ImVec2(120 * dp, 0))) copySecretToClipboard(seed_migration_seed_); ImGui::SameLine(); - if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(130, 0))) { + if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(130 * dp, 0))) { std::string path = util::Platform::getConfigDir() + "/dragonx-seed-wallet-backup.txt"; std::string content = seed_migration_seed_ + "\nAddress: " + seed_migration_dest_ + "\n"; const bool ok = util::Platform::writeFileAtomically(path, content, /*restrict=*/true); @@ -3138,14 +3150,14 @@ void App::renderSeedMigrationDialog() ImGui::PopStyleColor(); ImGui::Separator(); if (!seed_migration_backed_up_) ImGui::BeginDisabled(); - if (ui::material::StyledButton(TR("mig_continue_sweep"), ImVec2(170, 0))) { + if (ui::material::StyledButton(TR("mig_continue_sweep"), ImVec2(170 * dp, 0))) { wipeSeed(); // the sweep/adopt steps only use the address, never the seed itself seed_migration_step_ = SeedMigrationStep::Sweep; refreshSeedMigrationBalance(); } if (!seed_migration_backed_up_) ImGui::EndDisabled(); ImGui::SameLine(); - if (ui::material::StyledButton(TR("mig_discard"), ImVec2(160, 0))) { + if (ui::material::StyledButton(TR("mig_discard"), ImVec2(160 * dp, 0))) { // Throw away the isolated new wallet + clear the pending state. if (!seed_migration_temp_dir_.empty()) { std::error_code ec; @@ -3181,15 +3193,15 @@ void App::renderSeedMigrationDialog() { const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0; if (!canSweep) ImGui::BeginDisabled(); - if (ui::material::StyledButton(TR("mig_sweep_all"), ImVec2(170, 0))) + if (ui::material::StyledButton(TR("mig_sweep_all"), ImVec2(170 * dp, 0))) beginSweepToSeedWallet(); if (!canSweep) ImGui::EndDisabled(); } ImGui::SameLine(); - if (ui::material::StyledButton(TR("refresh"), ImVec2(110, 0))) + if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0))) refreshSeedMigrationBalance(); ImGui::SameLine(); - if (ui::material::StyledButton(TR("mig_later"), ImVec2(100, 0))) + if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0))) close(); // pending state persists; resumes here next time break; } @@ -3219,22 +3231,22 @@ void App::renderSeedMigrationDialog() ImGui::Spacing(); ImGui::Separator(); if (confirmed && legacyEmpty) { - if (ui::material::StyledButton(TR("mig_adopt"), ImVec2(180, 0))) + if (ui::material::StyledButton(TR("mig_adopt"), ImVec2(180 * dp, 0))) beginAdoptSeedWallet(); } else if (confirmed && !legacyEmpty) { ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error()); ImGui::TextWrapped("%s", TR("mig_remainder")); ImGui::PopStyleColor(); - if (ui::material::StyledButton(TR("mig_sweep_remaining"), ImVec2(180, 0))) + if (ui::material::StyledButton(TR("mig_sweep_remaining"), ImVec2(180 * dp, 0))) beginSweepToSeedWallet(); } else { ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine")); } ImGui::SameLine(); - if (ui::material::StyledButton(TR("refresh"), ImVec2(110, 0))) + if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0))) pollSweepStatus(); ImGui::SameLine(); - if (ui::material::StyledButton(TR("mig_later"), ImVec2(100, 0))) + if (ui::material::StyledButton(TR("mig_later"), ImVec2(100 * dp, 0))) close(); // pending state + txid persist; resumes here next time break; } @@ -3260,7 +3272,7 @@ void App::renderSeedMigrationDialog() } ImGui::Spacing(); ImGui::Separator(); - if (ui::material::StyledButton(TR("mig_done_btn"), ImVec2(120, 0))) + if (ui::material::StyledButton(TR("mig_done_btn"), ImVec2(120 * dp, 0))) close(); break; @@ -3270,7 +3282,7 @@ void App::renderSeedMigrationDialog() ImGui::PopStyleColor(); ImGui::Spacing(); ImGui::Separator(); - if (ui::material::StyledButton(TR("close"), ImVec2(120, 0))) + if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0))) close(); break; } diff --git a/src/app_security.cpp b/src/app_security.cpp index 4363e19..86a10cd 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -824,12 +824,19 @@ void App::renderLockScreen() { MarkBlurOverlayDrawn(nullptr); // suppress foreground theme-effect bleed + re-capture on unlock } - // Card + // Card. The lock screen is fully hand-drawn (dl->AddText at manual y offsets), so — unlike + // BeginOverlayDialog cards — nothing scales automatically. Multiply every geometry literal + // (card size, logo, input/button widths, vertical gaps) by dpiScale() so the card grows with + // OS DPI / font scale instead of stranding a native-size card in a large window. The font + // metrics (LegacySize / CalcTextSize) are already DPI-scaled via the atlas, so leave those. + const float dp = ui::Layout::dpiScale(); const auto& S = ui::schema::UI(); float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f); float cardH = S.drawElement("screens.lock-screen", "card").height; if (cardW <= 0) cardW = 400.0f; if (cardH <= 0) cardH = 320.0f; + cardW *= dp; + cardH *= dp; float cardX = winPos.x + (winSize.x - cardW) * 0.5f; float cardY = winPos.y + (winSize.y - cardH) * 0.5f; @@ -845,17 +852,17 @@ void App::renderLockScreen() { cardGlass.borderWidth = 1.0f; DrawGlassPanel(dl, cardMin, cardMax, cardGlass); - float cy = cardY + 24.0f; + float cy = cardY + 24.0f * dp; // Logo - float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f); + float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f) * dp; if (logo_tex_ != 0) { float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f; float logoW = logoSize * aspect; float logoX = cardX + (cardW - logoW) * 0.5f; dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize)); } - cy += logoSize + 16.0f; + cy += logoSize + 16.0f * dp; // Title ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font); @@ -869,7 +876,7 @@ void App::renderLockScreen() { ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title); dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title); - cy += ts.y + 20.0f; + cy += ts.y + 20.0f * dp; } // Lockout timer @@ -882,7 +889,7 @@ void App::renderLockScreen() { ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg); - cy += captionFont->LegacySize + 12.0f; + cy += captionFont->LegacySize + 12.0f * dp; } // Check if PIN vault is available @@ -907,7 +914,7 @@ void App::renderLockScreen() { ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon); dl->AddText(captionFont, captionFont->LegacySize, ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText); - cy += std::max(iconSize.y, textSize.y) + 8.0f; + cy += std::max(iconSize.y, textSize.y) + 8.0f * dp; // Switch link ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel); @@ -924,7 +931,7 @@ void App::renderLockScreen() { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(switchX, cy), ui::material::Primary(), switchLabel); ImGui::PopStyleColor(); - cy += captionFont->LegacySize + 12.0f; + cy += captionFont->LegacySize + 12.0f * dp; } else { // No PIN vault — don't show toggle, force passphrase mode lock_use_pin_ = false; @@ -933,6 +940,7 @@ void App::renderLockScreen() { // Input field float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f); if (inputW <= 0) inputW = 320.0f; + inputW *= dp; float inputX = cardX + (cardW - inputW) * 0.5f; bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_; @@ -942,7 +950,7 @@ void App::renderLockScreen() { // PIN input ImGui::SetCursorScreenPos(ImVec2(inputX, cy)); ImGui::PushItemWidth(inputW); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp); ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal; if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue; submitted = ImGui::InputText("##lock_pin", lock_pin_buf_, @@ -953,7 +961,7 @@ void App::renderLockScreen() { // Passphrase input (original) ImGui::SetCursorScreenPos(ImVec2(inputX, cy)); ImGui::PushItemWidth(inputW); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp); ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password; if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue; submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_, @@ -965,9 +973,9 @@ void App::renderLockScreen() { // default frame is subtle on the glass card, so this shows the field is active and ready to type. if (ImGui::IsItemActive() || ImGui::IsItemFocused()) { dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), - ui::material::Primary(), 6.0f, 0, 2.0f); + ui::material::Primary(), 6.0f * dp, 0, 2.0f * dp); } - cy += 40.0f + 12.0f; + cy += (40.0f + 12.0f) * dp; // Focus the input when the lock screen first appears. // IsWindowAppearing() does not work here because the lock screen is @@ -985,13 +993,13 @@ void App::renderLockScreen() { dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(), lock_error_msg_.c_str()); - cy += captionFont->LegacySize + 8.0f; + cy += captionFont->LegacySize + 8.0f * dp; } // "Unlocking..." feedback while worker thread is running // Always reserve the vertical space so the button doesn't shift. { - float rowH = captionFont->LegacySize + 8.0f; + float rowH = captionFont->LegacySize + 8.0f * dp; if (lock_unlock_in_progress_) { // Animated spinner dots char msg[64]; @@ -1009,13 +1017,15 @@ void App::renderLockScreen() { float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height; if (unlockW <= 0) unlockW = 320.0f; if (unlockH <= 0) unlockH = 44.0f; + unlockW *= dp; + unlockH *= dp; float unlockX = cardX + (cardW - unlockW) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(unlockX, cy)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::BeginDisabled(!canSubmit); bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH)); ImGui::EndDisabled(); diff --git a/src/ui/windows/about_dialog.cpp b/src/ui/windows/about_dialog.cpp index cb3bf0f..d7c5205 100644 --- a/src/ui/windows/about_dialog.cpp +++ b/src/ui/windows/about_dialog.cpp @@ -24,7 +24,16 @@ void RenderAboutDialog(App* app, bool* p_open) auto closeBtn = S.button("dialogs.about", "close-button"); auto versionLbl = S.label("dialogs.about", "version-label"); auto editionLbl = S.label("dialogs.about", "edition-label"); - + + // Schema positions/widths are logical px. BeginOverlayDialog scales the card by dpiScale(), so + // scale the value-column x, the edition inset, and the button widths to match — raw values left + // the value column jammed against the labels and the buttons left-packed at higher DPI. + const float dp = Layout::dpiScale(); + const float valX = versionLbl.position * dp; + const float edX = editionLbl.position * dp; + const float linkW = linkBtn.width * dp; + const float closeW = closeBtn.width * dp; + if (!material::BeginOverlayDialog(TR("about_title"), p_open, win.width, 0.94f)) { return; } @@ -39,7 +48,7 @@ void RenderAboutDialog(App* app, bool* p_open) ImGui::PopFont(); ImGui::PopStyleColor(); - ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position); + ImGui::SameLine(ImGui::GetWindowWidth() - edX); ImGui::TextDisabled("%s", TR("about_edition")); ImGui::Spacing(); @@ -48,24 +57,24 @@ void RenderAboutDialog(App* app, bool* p_open) // Version info ImGui::Text("%s", TR("about_version")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text("%s", DRAGONX_VERSION); ImGui::Text("%s", TR("about_imgui")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text("%s", IMGUI_VERSION); ImGui::Text("%s", TR("about_build_date")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text("%s %s", __DATE__, __TIME__); #ifdef DRAGONX_DEBUG ImGui::Text("%s", TR("about_build_type")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug")); #else ImGui::Text("%s", TR("about_build_type")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text("%s", TR("about_release")); #endif @@ -76,20 +85,20 @@ void RenderAboutDialog(App* app, bool* p_open) ImGui::Spacing(); ImGui::Text("%s", TR("about_daemon")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected")); const auto& state = app->getWalletState(); ImGui::Text("%s", TR("about_chain")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text("ObsidianDragon"); ImGui::Text("%s", TR("about_block_height")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text("%d", state.sync.blocks); ImGui::Text("%s", TR("about_connections")); - ImGui::SameLine(versionLbl.position); + ImGui::SameLine(valX); ImGui::Text(TR("about_peers_count"), state.peers.size()); } @@ -121,22 +130,22 @@ void RenderAboutDialog(App* app, bool* p_open) ImGui::Spacing(); // Links - if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) { + if (material::StyledButton(TR("about_website"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { util::Platform::openUrl("https://dragonx.is"); } ImGui::SameLine(); - if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) { + if (material::StyledButton(TR("about_github"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon"); } ImGui::SameLine(); - if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) { + if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkW, 0), S.resolveFont(linkBtn.font))) { util::Platform::openUrl("https://explorer.dragonx.is"); } ImGui::Spacing(); // Close button - float button_width = closeBtn.width; + float button_width = closeW; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f); if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { *p_open = false; diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 250efeb..60da1ff 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -12,6 +12,7 @@ #include "../../util/i18n.h" #include "../material/colors.h" #include "../material/type.h" +#include "../layout.h" // Layout::dpiScale() #include "imgui.h" #include @@ -134,8 +135,11 @@ void RenderChatTab(App* app) const ImVec2 avail = ImGui::GetContentRegionAvail(); const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f); - const float pad = 10.0f; - const float rowH = 52.0f; + // Row geometry is logical px — scale by dpiScale() so rows/padding grow with the (DPI-scaled) + // fonts. Left raw, at higher DPI the row was too short for the enlarged text and the preview's + // right margin (rowW - pad) shrank to ~zero, clipping the last glyph mid-word. + const float pad = 10.0f * Layout::dpiScale(); + const float rowH = 52.0f * Layout::dpiScale(); ImFont* nameFont = material::Type().body2(); ImFont* metaFont = material::Type().caption(); diff --git a/src/ui/windows/seed_display.h b/src/ui/windows/seed_display.h index f0b2d62..c3750d1 100644 --- a/src/ui/windows/seed_display.h +++ b/src/ui/windows/seed_display.h @@ -10,6 +10,7 @@ #include #include "imgui.h" +#include "../layout.h" // Layout::dpiScale() namespace dragonx { namespace ui { @@ -31,14 +32,26 @@ inline std::vector SplitSeedWords(const std::string& phrase) } // Render the words as a numbered 4-column grid (matches the lite welcome wizard layout). +// The column stride is sized to the widest "NN. word" cell and scaled by dpiScale(): a raw px +// stride collided with the next column's index once the font grew at higher DPI / font scale +// (e.g. at 150% "13. elevator" ran straight into "14."). Sizing to content keeps a gap at any +// scale and for any word length. inline void RenderSeedWordGrid(const std::vector& words, float colSpacingPx = 130.0f) { + const float dp = Layout::dpiScale(); + float colStride = colSpacingPx * dp; + for (size_t i = 0; i < words.size(); ++i) { + char cell[96]; + std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str()); + const float need = ImGui::CalcTextSize(cell).x + 20.0f * dp; // cell width + inter-column gap + if (need > colStride) colStride = need; + } for (size_t i = 0; i < words.size(); ++i) { char cell[96]; std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str()); ImGui::TextUnformatted(cell); if ((i % 4) != 3 && i + 1 < words.size()) - ImGui::SameLine(((i % 4) + 1) * colSpacingPx); + ImGui::SameLine(((i % 4) + 1) * colStride); } } diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 6829e97..1f173ea 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -821,7 +821,7 @@ void RenderSendConfirmPopup(App* app) { if (s_sending) { Type().text(TypeStyle::Body2, TR("sending")); } else { - if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) { + if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) { // Re-validate against LIVE state — the confirm dialog persists across frames, so the // balance could have dropped or sync (re)started (or the fee bumped total over available) // since Review. Don't broadcast a now-invalid transaction. @@ -874,7 +874,7 @@ void RenderSendConfirmPopup(App* app) { } } ImGui::SameLine(); - if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) { + if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width * Layout::dpiScale(), std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) { s_show_confirm = false; } }