diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index b1a9493..8250dd3 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -156,7 +156,6 @@ void RenderContactsTab(App* app) auto addrTable = S.table("dialogs.address-book", "address-table"); auto addrFrontLbl = S.label("dialogs.address-book", "address-front-label"); auto addrBackLbl = S.label("dialogs.address-book", "address-back-label"); - auto notesInput = S.input("dialogs.address-book", "notes-input"); auto actionBtn = S.button("dialogs.address-book", "action-button"); auto& book = app->addressBook(); @@ -228,14 +227,16 @@ void RenderContactsTab(App* app) const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd"; const float dp = Layout::dpiScale(); const bool light = material::IsLightTheme(); - float notesH = (notesInput.height > 0 ? notesInput.height : 56.0f) * dp; ImFont* btnFont = S.resolveFont(actionBtn.font); material::OverlayDialogSpec ov; ov.title = title; ov.p_open = open; ov.style = material::OverlayStyle::BlurFloat; ov.cardWidth = 880.0f; ov.idSuffix = id; // two-column body: form (left) + avatar picker (right) - ov.cardBottomViewportRatio = 0.92f; + // Fixed, tall card — fill the vertical space so the icon grid shows many rows and the notes + // field has room, rather than a squat auto-height box with dead space below. + const float vpH = ImGui::GetMainViewport()->Size.y; + ov.cardHeight = std::min(vpH * 0.84f, 820.0f * dp) / dp; // logical; framework re-applies dp if (material::BeginOverlayDialog(ov)) { // End-truncate a string to fit maxW (whole UTF-8 code points), appending an ellipsis. auto fitText = [&](std::string s, ImFont* f, float maxW) { @@ -305,7 +306,10 @@ void RenderContactsTab(App* app) ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // ================= Two-column body: form (left) | avatar picker (right) ================= - const float bodyH = 300.0f * dp; + // Body flexes to fill the tall card, leaving just the footer button row — so the icon grid + // and notes field grow into the vertical space instead of leaving it empty. + const float footerReserve = 44.0f * dp + Layout::spacingMd(); + const float bodyH = std::max(260.0f * dp, ImGui::GetContentRegionAvail().y - footerReserve); const float colGap = Layout::spacingLg(); const float bodyAvail = ImGui::GetContentRegionAvail().x; const float colW = (bodyAvail - colGap) * 0.5f; @@ -341,8 +345,15 @@ void RenderContactsTab(App* app) ImGui::Spacing(); - material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes", - s_edit_notes, sizeof(s_edit_notes), ImVec2(-1.0f, notesH)); + // Notes grows to fill the column above the Global checkbox, which is pinned to the bottom. + { + float globalReserve = ImGui::GetFrameHeight() + Layout::spacingSm(); + float labelLine = ImGui::GetTextLineHeightWithSpacing(); + float notesFill = std::max(60.0f * dp, + ImGui::GetContentRegionAvail().y - globalReserve - labelLine); + material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes", + s_edit_notes, sizeof(s_edit_notes), ImVec2(-1.0f, notesFill)); + } ImGui::Spacing(); ImGui::Checkbox(TR("contact_global"), &s_edit_global); @@ -494,15 +505,20 @@ void RenderContactsTab(App* app) ImGui::EndChild(); ImGui::PopStyleColor(); } else { - // Image: a big circular preview (or a placeholder), the filename, and choose / remove. + // Image: a big circular preview (or a placeholder), the filename, and choose / remove, + // vertically centered in the (now tall) column. const bool hasImg = s_edit_avatar.rfind("img:", 0) == 0; ImDrawList* mdl = ImGui::GetWindowDrawList(); ImFont* capF = material::Type().caption(); - const float prR = 44.0f * dp; + const float prR = 56.0f * dp; ImVec2 a = ImGui::GetContentRegionAvail(); ImVec2 origin = ImGui::GetCursorScreenPos(); + float btnH = 40.0f * dp; + float blockH = prR * 2.0f + Layout::spacingSm() + + (hasImg ? capF->LegacySize + Layout::spacingSm() : 0.0f) + btnH; + float topPad = std::max(Layout::spacingSm(), (a.y - blockH) * 0.32f); float cx = origin.x + a.x * 0.5f; - float cy = origin.y + Layout::spacingSm() + prR; + float cy = origin.y + topPad + prR; ImVec2 cc(cx, cy); if (hasImg) { if (const AvatarTex* t = getAvatarTexture(s_edit_avatar.substr(4))) { @@ -516,17 +532,19 @@ void RenderContactsTab(App* app) // Image set but no longer loadable (moved/deleted) — show a broken-image glyph. mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::ReadableError(), 35)); mdl->AddCircle(cc, prR, material::WithAlpha(material::ReadableError(), 150), 0, 1.5f * dp); - ImFont* gf = material::Type().iconLarge(); - ImVec2 gs = gf->CalcTextSizeA(gf->LegacySize, FLT_MAX, 0, ICON_MD_BROKEN_IMAGE); - mdl->AddText(gf, gf->LegacySize, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), + ImFont* gf = material::Type().iconXL(); + float gsz = prR * 0.72f; + ImVec2 gs = gf->CalcTextSizeA(gsz, FLT_MAX, 0, ICON_MD_BROKEN_IMAGE); + mdl->AddText(gf, gsz, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), material::ReadableError(), ICON_MD_BROKEN_IMAGE); } } else { mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::OnSurface(), 18)); mdl->AddCircle(cc, prR, material::WithAlpha(material::OnSurface(), 55), 0, 1.5f * dp); - ImFont* gf = material::Type().iconLarge(); - ImVec2 gs = gf->CalcTextSizeA(gf->LegacySize, FLT_MAX, 0, ICON_MD_ADD_PHOTO_ALTERNATE); - mdl->AddText(gf, gf->LegacySize, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), + ImFont* gf = material::Type().iconXL(); + float gsz = prR * 0.72f; + ImVec2 gs = gf->CalcTextSizeA(gsz, FLT_MAX, 0, ICON_MD_ADD_PHOTO_ALTERNATE); + mdl->AddText(gf, gsz, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), material::OnSurfaceDisabled(), ICON_MD_ADD_PHOTO_ALTERNATE); } float rowY = cy + prR + Layout::spacingSm(); @@ -571,8 +589,14 @@ void RenderContactsTab(App* app) if (!canSubmit) ImGui::BeginDisabled(); + // Accent the primary action (Save/Add) so it reads above Cancel. + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205))); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary())); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235))); const char* primaryLabel = isEdit ? TR("save") : TR("add"); - if (material::TactileButton(primaryLabel, ImVec2(actionW, 0), btnFont)) { + bool doSubmit = material::TactileButton(primaryLabel, ImVec2(actionW, 0), btnFont); + ImGui::PopStyleColor(3); + if (doSubmit) { // Trim the label/address (a pasted address often carries a trailing newline); keep notes as-is. auto trimAB = [](std::string s) { while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin());