fix(ui): DPI-scale hand-drawn overlays + freeze sweep demo state

Hand-drawn absolute geometry (dl->AddText offsets, SetNextWindowSize,
explicit ImVec2 button widths, SameLine strides) is immune to the app's
DPI machinery (font-atlas rebuild + ScaleAllSizes), so several overlays
rendered native-size (tiny) on HiDPI / Windows 150% displays. Multiply the
raw logical-px geometry by Layout::dpiScale():

- seed_display: content-aware, DPI-scaled seed-grid column stride (a raw
  130px stride let words collide with the next column's index at 150%)
- lock screen: card/logo/input/unlock-button + all vertical offsets
- migrate-to-seed + seed-backup dialogs: button widths
- send-confirm: confirm/cancel button widths
- about dialog: value-column x, edition inset, link/close button widths
- chat conversation list: row height + padding (fixes the preview clip)

Also freeze demo state during the full UI sweep: guard App::update() on
capture_mode_ so a running daemon's refresh can't clobber the injected
demo state (it blanked Send/Receive/History with a CDB error mid-sweep).

Verified on Windows 4K@150% and locally at font_scale=1.5 (same
dpiScale()==1.5 code path). Adds a DPI-scaling convention note to CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 18:58:54 -05:00
parent 0d9908019d
commit 6c19fac6f5
7 changed files with 104 additions and 55 deletions

View File

@@ -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. - **C++17.** Match the surrounding code's style per file.
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs. - **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. - **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. - **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`. - **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.

View File

@@ -627,7 +627,13 @@ void App::update()
{ {
PERF_SCOPE("Update.Total"); PERF_SCOPE("Update.Total");
ImGuiIO& io = ImGui::GetIO(); 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 // Track user interaction for auto-lock
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 || if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
io.MouseClicked[0] || io.MouseClicked[1] || 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")); ui::material::Type().text(ui::material::TypeStyle::H6, TR("seed_backup_title"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); 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()) { if (state_.isLocked()) {
ImGui::TextWrapped("%s", TR("seed_backup_locked")); ImGui::TextWrapped("%s", TR("seed_backup_locked"));
@@ -3027,11 +3036,11 @@ void App::renderSeedBackupDialog()
ImGui::Spacing(); ImGui::Spacing();
} }
ImGui::Separator(); 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_); copySecretToClipboard(seed_backup_phrase_);
} }
ImGui::SameLine(); 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 path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt";
std::string content = seed_backup_phrase_ + "\n"; std::string content = seed_backup_phrase_ + "\n";
const bool ok = util::Platform::writeFileAtomically(path, content, const bool ok = util::Platform::writeFileAtomically(path, content,
@@ -3041,7 +3050,7 @@ void App::renderSeedBackupDialog()
: std::string(TR("seed_backup_save_failed"))) + path; : std::string(TR("seed_backup_save_failed"))) + path;
} }
ImGui::SameLine(); 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(); closeAndWipe();
ui::material::EndOverlayDialog(); ui::material::EndOverlayDialog();
return; return;
@@ -3054,7 +3063,7 @@ void App::renderSeedBackupDialog()
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); 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(); closeAndWipe();
} }
ui::material::EndOverlayDialog(); ui::material::EndOverlayDialog();
@@ -3085,6 +3094,9 @@ void App::renderSeedMigrationDialog()
if (!show_seed_migration_ && busy) show_seed_migration_ = true; if (!show_seed_migration_ && busy) show_seed_migration_ = true;
const ImVec4 kMedium = ImGui::ColorConvertU32ToFloat4(ui::material::OnSurfaceMedium()); 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")); ui::material::Type().text(ui::material::TypeStyle::H6, TR("mig_title"));
ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm()));
@@ -3093,10 +3105,10 @@ void App::renderSeedMigrationDialog()
ImGui::TextWrapped("%s", TR("mig_intro")); ImGui::TextWrapped("%s", TR("mig_intro"));
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
if (ui::material::StyledButton(TR("mig_create"), ImVec2(180, 0))) if (ui::material::StyledButton(TR("mig_create"), ImVec2(180 * dp, 0)))
beginCreateSeedWallet(); beginCreateSeedWallet();
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::StyledButton(TR("cancel"), ImVec2(120, 0))) if (ui::material::StyledButton(TR("cancel"), ImVec2(120 * dp, 0)))
close(); close();
break; break;
@@ -3117,10 +3129,10 @@ void App::renderSeedMigrationDialog()
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr")); ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
ImGui::TextWrapped("%s", seed_migration_dest_.c_str()); ImGui::TextWrapped("%s", seed_migration_dest_.c_str());
ImGui::Spacing(); 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_); copySecretToClipboard(seed_migration_seed_);
ImGui::SameLine(); 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 path = util::Platform::getConfigDir() + "/dragonx-seed-wallet-backup.txt";
std::string content = seed_migration_seed_ + "\nAddress: " + seed_migration_dest_ + "\n"; std::string content = seed_migration_seed_ + "\nAddress: " + seed_migration_dest_ + "\n";
const bool ok = util::Platform::writeFileAtomically(path, content, /*restrict=*/true); const bool ok = util::Platform::writeFileAtomically(path, content, /*restrict=*/true);
@@ -3138,14 +3150,14 @@ void App::renderSeedMigrationDialog()
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::Separator(); ImGui::Separator();
if (!seed_migration_backed_up_) ImGui::BeginDisabled(); 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 wipeSeed(); // the sweep/adopt steps only use the address, never the seed itself
seed_migration_step_ = SeedMigrationStep::Sweep; seed_migration_step_ = SeedMigrationStep::Sweep;
refreshSeedMigrationBalance(); refreshSeedMigrationBalance();
} }
if (!seed_migration_backed_up_) ImGui::EndDisabled(); if (!seed_migration_backed_up_) ImGui::EndDisabled();
ImGui::SameLine(); 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. // Throw away the isolated new wallet + clear the pending state.
if (!seed_migration_temp_dir_.empty()) { if (!seed_migration_temp_dir_.empty()) {
std::error_code ec; std::error_code ec;
@@ -3181,15 +3193,15 @@ void App::renderSeedMigrationDialog()
{ {
const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0; const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0;
if (!canSweep) ImGui::BeginDisabled(); 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(); beginSweepToSeedWallet();
if (!canSweep) ImGui::EndDisabled(); if (!canSweep) ImGui::EndDisabled();
} }
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::StyledButton(TR("refresh"), ImVec2(110, 0))) if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0)))
refreshSeedMigrationBalance(); refreshSeedMigrationBalance();
ImGui::SameLine(); 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 close(); // pending state persists; resumes here next time
break; break;
} }
@@ -3219,22 +3231,22 @@ void App::renderSeedMigrationDialog()
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
if (confirmed && legacyEmpty) { 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(); beginAdoptSeedWallet();
} else if (confirmed && !legacyEmpty) { } else if (confirmed && !legacyEmpty) {
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error()); ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error());
ImGui::TextWrapped("%s", TR("mig_remainder")); ImGui::TextWrapped("%s", TR("mig_remainder"));
ImGui::PopStyleColor(); 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(); beginSweepToSeedWallet();
} else { } else {
ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine")); ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine"));
} }
ImGui::SameLine(); ImGui::SameLine();
if (ui::material::StyledButton(TR("refresh"), ImVec2(110, 0))) if (ui::material::StyledButton(TR("refresh"), ImVec2(110 * dp, 0)))
pollSweepStatus(); pollSweepStatus();
ImGui::SameLine(); 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 close(); // pending state + txid persist; resumes here next time
break; break;
} }
@@ -3260,7 +3272,7 @@ void App::renderSeedMigrationDialog()
} }
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); 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(); close();
break; break;
@@ -3270,7 +3282,7 @@ void App::renderSeedMigrationDialog()
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::Spacing(); ImGui::Spacing();
ImGui::Separator(); ImGui::Separator();
if (ui::material::StyledButton(TR("close"), ImVec2(120, 0))) if (ui::material::StyledButton(TR("close"), ImVec2(120 * dp, 0)))
close(); close();
break; break;
} }

View File

@@ -824,12 +824,19 @@ void App::renderLockScreen() {
MarkBlurOverlayDrawn(nullptr); // suppress foreground theme-effect bleed + re-capture on unlock 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(); const auto& S = ui::schema::UI();
float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f); float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f);
float cardH = S.drawElement("screens.lock-screen", "card").height; float cardH = S.drawElement("screens.lock-screen", "card").height;
if (cardW <= 0) cardW = 400.0f; if (cardW <= 0) cardW = 400.0f;
if (cardH <= 0) cardH = 320.0f; if (cardH <= 0) cardH = 320.0f;
cardW *= dp;
cardH *= dp;
float cardX = winPos.x + (winSize.x - cardW) * 0.5f; float cardX = winPos.x + (winSize.x - cardW) * 0.5f;
float cardY = winPos.y + (winSize.y - cardH) * 0.5f; float cardY = winPos.y + (winSize.y - cardH) * 0.5f;
@@ -845,17 +852,17 @@ void App::renderLockScreen() {
cardGlass.borderWidth = 1.0f; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass); DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
float cy = cardY + 24.0f; float cy = cardY + 24.0f * dp;
// Logo // 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) { if (logo_tex_ != 0) {
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f; float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
float logoW = logoSize * aspect; float logoW = logoSize * aspect;
float logoX = cardX + (cardW - logoW) * 0.5f; float logoX = cardX + (cardW - logoW) * 0.5f;
dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize)); dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize));
} }
cy += logoSize + 16.0f; cy += logoSize + 16.0f * dp;
// Title // Title
ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font); 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); ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize, dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title); ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
cy += ts.y + 20.0f; cy += ts.y + 20.0f * dp;
} }
// Lockout timer // Lockout timer
@@ -882,7 +889,7 @@ void App::renderLockScreen() {
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg); ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg); 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 // Check if PIN vault is available
@@ -907,7 +914,7 @@ void App::renderLockScreen() {
ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon); ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon);
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText); 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 // Switch link
ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel); ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel);
@@ -924,7 +931,7 @@ void App::renderLockScreen() {
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(switchX, cy), ui::material::Primary(), switchLabel); ImVec2(switchX, cy), ui::material::Primary(), switchLabel);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
cy += captionFont->LegacySize + 12.0f; cy += captionFont->LegacySize + 12.0f * dp;
} else { } else {
// No PIN vault — don't show toggle, force passphrase mode // No PIN vault — don't show toggle, force passphrase mode
lock_use_pin_ = false; lock_use_pin_ = false;
@@ -933,6 +940,7 @@ void App::renderLockScreen() {
// Input field // Input field
float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f); float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f);
if (inputW <= 0) inputW = 320.0f; if (inputW <= 0) inputW = 320.0f;
inputW *= dp;
float inputX = cardX + (cardW - inputW) * 0.5f; float inputX = cardX + (cardW - inputW) * 0.5f;
bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_; bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_;
@@ -942,7 +950,7 @@ void App::renderLockScreen() {
// PIN input // PIN input
ImGui::SetCursorScreenPos(ImVec2(inputX, cy)); ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
ImGui::PushItemWidth(inputW); ImGui::PushItemWidth(inputW);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal; ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal;
if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue; if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
submitted = ImGui::InputText("##lock_pin", lock_pin_buf_, submitted = ImGui::InputText("##lock_pin", lock_pin_buf_,
@@ -953,7 +961,7 @@ void App::renderLockScreen() {
// Passphrase input (original) // Passphrase input (original)
ImGui::SetCursorScreenPos(ImVec2(inputX, cy)); ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
ImGui::PushItemWidth(inputW); ImGui::PushItemWidth(inputW);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password; ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password;
if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue; if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_, 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. // default frame is subtle on the glass card, so this shows the field is active and ready to type.
if (ImGui::IsItemActive() || ImGui::IsItemFocused()) { if (ImGui::IsItemActive() || ImGui::IsItemFocused()) {
dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), 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. // Focus the input when the lock screen first appears.
// IsWindowAppearing() does not work here because the lock screen is // IsWindowAppearing() does not work here because the lock screen is
@@ -985,13 +993,13 @@ void App::renderLockScreen() {
dl->AddText(captionFont, captionFont->LegacySize, dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(), ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
lock_error_msg_.c_str()); lock_error_msg_.c_str());
cy += captionFont->LegacySize + 8.0f; cy += captionFont->LegacySize + 8.0f * dp;
} }
// "Unlocking..." feedback while worker thread is running // "Unlocking..." feedback while worker thread is running
// Always reserve the vertical space so the button doesn't shift. // 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_) { if (lock_unlock_in_progress_) {
// Animated spinner dots // Animated spinner dots
char msg[64]; char msg[64];
@@ -1009,13 +1017,15 @@ void App::renderLockScreen() {
float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height; float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height;
if (unlockW <= 0) unlockW = 320.0f; if (unlockW <= 0) unlockW = 320.0f;
if (unlockH <= 0) unlockH = 44.0f; if (unlockH <= 0) unlockH = 44.0f;
unlockW *= dp;
unlockH *= dp;
float unlockX = cardX + (cardW - unlockW) * 0.5f; float unlockX = cardX + (cardW - unlockW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(unlockX, cy)); ImGui::SetCursorScreenPos(ImVec2(unlockX, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary())); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary())); 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); ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH)); bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
ImGui::EndDisabled(); ImGui::EndDisabled();

View File

@@ -24,7 +24,16 @@ void RenderAboutDialog(App* app, bool* p_open)
auto closeBtn = S.button("dialogs.about", "close-button"); auto closeBtn = S.button("dialogs.about", "close-button");
auto versionLbl = S.label("dialogs.about", "version-label"); auto versionLbl = S.label("dialogs.about", "version-label");
auto editionLbl = S.label("dialogs.about", "edition-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)) { if (!material::BeginOverlayDialog(TR("about_title"), p_open, win.width, 0.94f)) {
return; return;
} }
@@ -39,7 +48,7 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::PopFont(); ImGui::PopFont();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::SameLine(ImGui::GetWindowWidth() - editionLbl.position); ImGui::SameLine(ImGui::GetWindowWidth() - edX);
ImGui::TextDisabled("%s", TR("about_edition")); ImGui::TextDisabled("%s", TR("about_edition"));
ImGui::Spacing(); ImGui::Spacing();
@@ -48,24 +57,24 @@ void RenderAboutDialog(App* app, bool* p_open)
// Version info // Version info
ImGui::Text("%s", TR("about_version")); ImGui::Text("%s", TR("about_version"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text("%s", DRAGONX_VERSION); ImGui::Text("%s", DRAGONX_VERSION);
ImGui::Text("%s", TR("about_imgui")); ImGui::Text("%s", TR("about_imgui"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text("%s", IMGUI_VERSION); ImGui::Text("%s", IMGUI_VERSION);
ImGui::Text("%s", TR("about_build_date")); ImGui::Text("%s", TR("about_build_date"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text("%s %s", __DATE__, __TIME__); ImGui::Text("%s %s", __DATE__, __TIME__);
#ifdef DRAGONX_DEBUG #ifdef DRAGONX_DEBUG
ImGui::Text("%s", TR("about_build_type")); 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")); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug"));
#else #else
ImGui::Text("%s", TR("about_build_type")); ImGui::Text("%s", TR("about_build_type"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text("%s", TR("about_release")); ImGui::Text("%s", TR("about_release"));
#endif #endif
@@ -76,20 +85,20 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::Spacing(); ImGui::Spacing();
ImGui::Text("%s", TR("about_daemon")); 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")); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected"));
const auto& state = app->getWalletState(); const auto& state = app->getWalletState();
ImGui::Text("%s", TR("about_chain")); ImGui::Text("%s", TR("about_chain"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text("ObsidianDragon"); ImGui::Text("ObsidianDragon");
ImGui::Text("%s", TR("about_block_height")); ImGui::Text("%s", TR("about_block_height"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text("%d", state.sync.blocks); ImGui::Text("%d", state.sync.blocks);
ImGui::Text("%s", TR("about_connections")); ImGui::Text("%s", TR("about_connections"));
ImGui::SameLine(versionLbl.position); ImGui::SameLine(valX);
ImGui::Text(TR("about_peers_count"), state.peers.size()); ImGui::Text(TR("about_peers_count"), state.peers.size());
} }
@@ -121,22 +130,22 @@ void RenderAboutDialog(App* app, bool* p_open)
ImGui::Spacing(); ImGui::Spacing();
// Links // 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"); util::Platform::openUrl("https://dragonx.is");
} }
ImGui::SameLine(); 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"); util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
} }
ImGui::SameLine(); 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"); util::Platform::openUrl("https://explorer.dragonx.is");
} }
ImGui::Spacing(); ImGui::Spacing();
// Close button // Close button
float button_width = closeBtn.width; float button_width = closeW;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f); ImGui::SetCursorPosX((ImGui::GetWindowWidth() - button_width) * 0.5f);
if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) { if (material::StyledButton(TR("close"), ImVec2(button_width, 0), S.resolveFont(closeBtn.font))) {
*p_open = false; *p_open = false;

View File

@@ -12,6 +12,7 @@
#include "../../util/i18n.h" #include "../../util/i18n.h"
#include "../material/colors.h" #include "../material/colors.h"
#include "../material/type.h" #include "../material/type.h"
#include "../layout.h" // Layout::dpiScale()
#include "imgui.h" #include "imgui.h"
#include <algorithm> #include <algorithm>
@@ -134,8 +135,11 @@ void RenderChatTab(App* app)
const ImVec2 avail = ImGui::GetContentRegionAvail(); const ImVec2 avail = ImGui::GetContentRegionAvail();
const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f); const float listW = std::clamp(avail.x * 0.32f, 220.0f, 360.0f);
const float pad = 10.0f; // Row geometry is logical px — scale by dpiScale() so rows/padding grow with the (DPI-scaled)
const float rowH = 52.0f; // 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* nameFont = material::Type().body2();
ImFont* metaFont = material::Type().caption(); ImFont* metaFont = material::Type().caption();

View File

@@ -10,6 +10,7 @@
#include <vector> #include <vector>
#include "imgui.h" #include "imgui.h"
#include "../layout.h" // Layout::dpiScale()
namespace dragonx { namespace dragonx {
namespace ui { namespace ui {
@@ -31,14 +32,26 @@ inline std::vector<std::string> SplitSeedWords(const std::string& phrase)
} }
// Render the words as a numbered 4-column grid (matches the lite welcome wizard layout). // 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<std::string>& words, float colSpacingPx = 130.0f) inline void RenderSeedWordGrid(const std::vector<std::string>& 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) { for (size_t i = 0; i < words.size(); ++i) {
char cell[96]; char cell[96];
std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str()); std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
ImGui::TextUnformatted(cell); ImGui::TextUnformatted(cell);
if ((i % 4) != 3 && i + 1 < words.size()) if ((i % 4) != 3 && i + 1 < words.size())
ImGui::SameLine(((i % 4) + 1) * colSpacingPx); ImGui::SameLine(((i % 4) + 1) * colStride);
} }
} }

View File

@@ -821,7 +821,7 @@ void RenderSendConfirmPopup(App* app) {
if (s_sending) { if (s_sending) {
Type().text(TypeStyle::Body2, TR("sending")); Type().text(TypeStyle::Body2, TR("sending"));
} else { } 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 // 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) // balance could have dropped or sync (re)started (or the fee bumped total over available)
// since Review. Don't broadcast a now-invalid transaction. // since Review. Don't broadcast a now-invalid transaction.
@@ -874,7 +874,7 @@ void RenderSendConfirmPopup(App* app) {
} }
} }
ImGui::SameLine(); 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; s_show_confirm = false;
} }
} }