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

@@ -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();