From 19dea53ef894ebcbc69f80a95e9d2bf349179303 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 15 Jul 2026 07:23:19 -0500 Subject: [PATCH] =?UTF-8?q?refactor(contacts):=20two-column=20edit=20dialo?= =?UTF-8?q?g=20=E2=80=94=20wider,=20shorter,=20roomier=20grid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The edit dialog was a tall, narrow single column that wasted ~half the horizontal space and cramped the icon grid into ~2.5 clipped rows crowding the footer. Rework it into the portfolio-editor two-column shape: - Card widened 560 → 880 logical. - Full-width live preview stays on top (now shows more of the address). - Body is two fixed-height columns: form (label / address+paste / notes / global) on the left, avatar picker (segmented + icon grid / image / badge hint) on the right, each filling its column so the grid gets ~6 columns and ~4 rows instead of clipping mid-row. - Overall modal is much shorter, so Save/Cancel no longer crowd the grid. - Badge-mode hint re-centered relative to the current cursor (it now sits below the segmented control inside the shared column, not a fresh child). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/contacts_tab.cpp | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index 256aa2d..fcf2d32 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -228,14 +228,13 @@ void RenderContactsTab(App* app) const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd"; const float dp = Layout::dpiScale(); const bool light = material::IsLightTheme(); - float formW = -1.0f; // stretch inputs to the card content width 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 = 560.0f; ov.idSuffix = id; // wider than a plain form — holds the icon grid + ov.cardWidth = 880.0f; ov.idSuffix = id; // two-column body: form (left) + avatar picker (right) ov.cardBottomViewportRatio = 0.92f; if (material::BeginOverlayDialog(ov)) { // End-truncate a string to fit maxW (whole UTF-8 code points), appending an ellipsis. @@ -289,15 +288,24 @@ void RenderContactsTab(App* app) hasAddr ? material::OnSurfaceMedium() : material::OnSurfaceDisabled(), ad.c_str()); ImGui::Dummy(ImVec2(pw, ph)); } - ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::Dummy(ImVec2(0, Layout::spacingMd())); + // ================= Two-column body: form (left) | avatar picker (right) ================= + const float bodyH = 300.0f * dp; + const float colGap = Layout::spacingLg(); + const float bodyAvail = ImGui::GetContentRegionAvail().x; + const float colW = (bodyAvail - colGap) * 0.5f; + + // ---- LEFT: label / address / notes / global ---------------------------------------- + ImGui::BeginChild("##contactFormCol", ImVec2(colW, bodyH), false, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); // Focus the first field the frame the dialog opens so it's keyboard-ready. if (s_focus_edit_field) { ImGui::SetKeyboardFocusHere(); s_focus_edit_field = false; } material::LabeledInput(TR("label"), isEdit ? "##EditLabel" : "##AddLabel", - s_edit_label, sizeof(s_edit_label), formW); + s_edit_label, sizeof(s_edit_label), -1.0f); ImGui::Spacing(); @@ -320,14 +328,18 @@ void RenderContactsTab(App* app) ImGui::Spacing(); material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes", - s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH)); + s_edit_notes, sizeof(s_edit_notes), ImVec2(-1.0f, notesH)); ImGui::Spacing(); ImGui::Checkbox(TR("contact_global"), &s_edit_global); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_tt")); + ImGui::EndChild(); // ##contactFormCol - // ---- Avatar picker: Badge / Icon / Image ------------------------------------------- - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::SameLine(0, colGap); + + // ---- RIGHT: avatar picker (Badge / Icon / Image), fills the column height ----------- + ImGui::BeginChild("##contactAvatarCol", ImVec2(colW, bodyH), false, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceMedium(), TR("contact_avatar")); { const char* segs[3] = { TR("contact_avatar_badge"), TR("contact_avatar_icon"), @@ -349,18 +361,18 @@ void RenderContactsTab(App* app) } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - // Fixed-height area so the modal height doesn't jump when switching avatar modes. - const float avatarAreaH = 176.0f * dp; - ImGui::BeginChild("##avArea", ImVec2(ImGui::GetContentRegionAvail().x, avatarAreaH), false, - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + // The mode content fills the rest of the avatar column (fixed height → no jump on switch). if (s_edit_avatar_mode == 0) { - // Badge: centered explanatory hint. + // Badge: explanatory hint, centered in the remaining space below the segmented control. const char* msg = TR("contact_avatar_badge_hint"); ImFont* f = material::Type().caption(); - ImVec2 a = ImGui::GetContentRegionAvail(); - ImVec2 ts = f->CalcTextSizeA(f->LegacySize, a.x - 16.0f * dp, 0, msg); - ImGui::SetCursorPos(ImVec2(std::max(0.0f, (a.x - ts.x) * 0.5f), a.y * 0.4f)); - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + std::min(ts.x, a.x)); + ImVec2 a = ImGui::GetContentRegionAvail(); // remaining w,h from the current cursor + float wrapW = a.x - 16.0f * dp; + ImVec2 ts = f->CalcTextSizeA(f->LegacySize, wrapW, 0, msg); + float x0 = ImGui::GetCursorPosX(), y0 = ImGui::GetCursorPosY(); + ImGui::SetCursorPosX(x0 + std::max(0.0f, (a.x - ts.x) * 0.5f)); + ImGui::SetCursorPosY(y0 + std::max(0.0f, (a.y - ts.y) * 0.4f)); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + std::min(ts.x, wrapW)); material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceDisabled(), msg); ImGui::PopTextWrapPos(); } else if (s_edit_avatar_mode == 1) { @@ -435,8 +447,9 @@ void RenderContactsTab(App* app) ImGui::Dummy(ImVec2(0, Layout::spacingXs())); material::Type().textColored(material::TypeStyle::Caption, material::OnSurfaceDisabled(), TR("contact_avatar_image_hint")); } - ImGui::EndChild(); // ##avArea + ImGui::EndChild(); // ##contactAvatarCol + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0; float actionW = std::max(110.0f * dp, btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("save")).x