feat(contacts): tall edit dialog — fill vertical space, accent Save
The dialog was a squat auto-height box with a big dead zone below and an icon grid that clipped at ~3 rows. Make it a fixed, tall card (up to 84% of the viewport) whose body flexes to fill the height: - The icon grid grows into the space — ~8–9 rows visible instead of 3. - Notes expands to fill the left column above the now bottom-pinned Global checkbox, so the left side uses the height too. - The image-mode preview circle is bigger (r44 → r56) and vertically centered, with glyphs scaled to it. - Footer pins to the bottom; Save/Add is now accent-colored so the primary action reads above Cancel. Notes/actionButton schema lookups that only fed the old fixed heights are gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -156,7 +156,6 @@ void RenderContactsTab(App* app)
|
|||||||
auto addrTable = S.table("dialogs.address-book", "address-table");
|
auto addrTable = S.table("dialogs.address-book", "address-table");
|
||||||
auto addrFrontLbl = S.label("dialogs.address-book", "address-front-label");
|
auto addrFrontLbl = S.label("dialogs.address-book", "address-front-label");
|
||||||
auto addrBackLbl = S.label("dialogs.address-book", "address-back-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 actionBtn = S.button("dialogs.address-book", "action-button");
|
||||||
|
|
||||||
auto& book = app->addressBook();
|
auto& book = app->addressBook();
|
||||||
@@ -228,14 +227,16 @@ void RenderContactsTab(App* app)
|
|||||||
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
|
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
|
||||||
const float dp = Layout::dpiScale();
|
const float dp = Layout::dpiScale();
|
||||||
const bool light = material::IsLightTheme();
|
const bool light = material::IsLightTheme();
|
||||||
float notesH = (notesInput.height > 0 ? notesInput.height : 56.0f) * dp;
|
|
||||||
ImFont* btnFont = S.resolveFont(actionBtn.font);
|
ImFont* btnFont = S.resolveFont(actionBtn.font);
|
||||||
|
|
||||||
material::OverlayDialogSpec ov;
|
material::OverlayDialogSpec ov;
|
||||||
ov.title = title; ov.p_open = open;
|
ov.title = title; ov.p_open = open;
|
||||||
ov.style = material::OverlayStyle::BlurFloat;
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
ov.cardWidth = 880.0f; ov.idSuffix = id; // two-column body: form (left) + avatar picker (right)
|
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)) {
|
if (material::BeginOverlayDialog(ov)) {
|
||||||
// End-truncate a string to fit maxW (whole UTF-8 code points), appending an ellipsis.
|
// End-truncate a string to fit maxW (whole UTF-8 code points), appending an ellipsis.
|
||||||
auto fitText = [&](std::string s, ImFont* f, float maxW) {
|
auto fitText = [&](std::string s, ImFont* f, float maxW) {
|
||||||
@@ -305,7 +306,10 @@ void RenderContactsTab(App* app)
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
|
||||||
// ================= Two-column body: form (left) | avatar picker (right) =================
|
// ================= 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 colGap = Layout::spacingLg();
|
||||||
const float bodyAvail = ImGui::GetContentRegionAvail().x;
|
const float bodyAvail = ImGui::GetContentRegionAvail().x;
|
||||||
const float colW = (bodyAvail - colGap) * 0.5f;
|
const float colW = (bodyAvail - colGap) * 0.5f;
|
||||||
@@ -341,8 +345,15 @@ void RenderContactsTab(App* app)
|
|||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
|
// Notes grows to fill the column above the Global checkbox, which is pinned to the bottom.
|
||||||
s_edit_notes, sizeof(s_edit_notes), ImVec2(-1.0f, notesH));
|
{
|
||||||
|
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::Spacing();
|
||||||
ImGui::Checkbox(TR("contact_global"), &s_edit_global);
|
ImGui::Checkbox(TR("contact_global"), &s_edit_global);
|
||||||
@@ -494,15 +505,20 @@ void RenderContactsTab(App* app)
|
|||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
} else {
|
} 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;
|
const bool hasImg = s_edit_avatar.rfind("img:", 0) == 0;
|
||||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||||
ImFont* capF = material::Type().caption();
|
ImFont* capF = material::Type().caption();
|
||||||
const float prR = 44.0f * dp;
|
const float prR = 56.0f * dp;
|
||||||
ImVec2 a = ImGui::GetContentRegionAvail();
|
ImVec2 a = ImGui::GetContentRegionAvail();
|
||||||
ImVec2 origin = ImGui::GetCursorScreenPos();
|
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 cx = origin.x + a.x * 0.5f;
|
||||||
float cy = origin.y + Layout::spacingSm() + prR;
|
float cy = origin.y + topPad + prR;
|
||||||
ImVec2 cc(cx, cy);
|
ImVec2 cc(cx, cy);
|
||||||
if (hasImg) {
|
if (hasImg) {
|
||||||
if (const AvatarTex* t = getAvatarTexture(s_edit_avatar.substr(4))) {
|
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.
|
// Image set but no longer loadable (moved/deleted) — show a broken-image glyph.
|
||||||
mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::ReadableError(), 35));
|
mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::ReadableError(), 35));
|
||||||
mdl->AddCircle(cc, prR, material::WithAlpha(material::ReadableError(), 150), 0, 1.5f * dp);
|
mdl->AddCircle(cc, prR, material::WithAlpha(material::ReadableError(), 150), 0, 1.5f * dp);
|
||||||
ImFont* gf = material::Type().iconLarge();
|
ImFont* gf = material::Type().iconXL();
|
||||||
ImVec2 gs = gf->CalcTextSizeA(gf->LegacySize, FLT_MAX, 0, ICON_MD_BROKEN_IMAGE);
|
float gsz = prR * 0.72f;
|
||||||
mdl->AddText(gf, gf->LegacySize, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f),
|
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);
|
material::ReadableError(), ICON_MD_BROKEN_IMAGE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::OnSurface(), 18));
|
mdl->AddCircleFilled(cc, prR, material::WithAlpha(material::OnSurface(), 18));
|
||||||
mdl->AddCircle(cc, prR, material::WithAlpha(material::OnSurface(), 55), 0, 1.5f * dp);
|
mdl->AddCircle(cc, prR, material::WithAlpha(material::OnSurface(), 55), 0, 1.5f * dp);
|
||||||
ImFont* gf = material::Type().iconLarge();
|
ImFont* gf = material::Type().iconXL();
|
||||||
ImVec2 gs = gf->CalcTextSizeA(gf->LegacySize, FLT_MAX, 0, ICON_MD_ADD_PHOTO_ALTERNATE);
|
float gsz = prR * 0.72f;
|
||||||
mdl->AddText(gf, gf->LegacySize, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f),
|
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);
|
material::OnSurfaceDisabled(), ICON_MD_ADD_PHOTO_ALTERNATE);
|
||||||
}
|
}
|
||||||
float rowY = cy + prR + Layout::spacingSm();
|
float rowY = cy + prR + Layout::spacingSm();
|
||||||
@@ -571,8 +589,14 @@ void RenderContactsTab(App* app)
|
|||||||
|
|
||||||
if (!canSubmit) ImGui::BeginDisabled();
|
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");
|
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.
|
// Trim the label/address (a pasted address often carries a trailing newline); keep notes as-is.
|
||||||
auto trimAB = [](std::string s) {
|
auto trimAB = [](std::string s) {
|
||||||
while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin());
|
while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin());
|
||||||
|
|||||||
Reference in New Issue
Block a user