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

@@ -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;
}