feat(contacts): avatar shapes, list scale & live settings preview
Add a settings-gear modal with a segmented avatar-shape picker (circle / rounded square / full-row left tab), a list-scale slider, and a live two-row preview. Smooth-scroll the address list and fix a latent wheel double-scroll on the outer scroll child. (settings.h also refreshes the portfolio-style doc comment.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -196,6 +196,8 @@ bool Settings::load(const std::string& path)
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
|
||||
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
|
||||
loadScalar(j, "animate_avatars", animate_avatars_);
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||
@@ -467,6 +469,8 @@ bool Settings::save(const std::string& path)
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["contacts_view_mode"] = contacts_view_mode_;
|
||||
j["contacts_avatar_shape"] = contacts_avatar_shape_;
|
||||
j["contacts_list_scale"] = contacts_list_scale_;
|
||||
j["animate_avatars"] = animate_avatars_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
|
||||
@@ -226,13 +226,19 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
|
||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
|
||||
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts customization (gear modal): avatar shape + card/list row scale.
|
||||
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
|
||||
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
|
||||
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
float getContactsListScale() const { return contacts_list_scale_; }
|
||||
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
|
||||
bool getAnimateAvatars() const { return animate_avatars_; }
|
||||
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
||||
@@ -553,8 +559,10 @@ private:
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
|
||||
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
|
||||
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
|
||||
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
|
||||
bool scanline_enabled_ = true;
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace ui {
|
||||
static int s_selected_index = -1;
|
||||
static bool s_show_add_dialog = false;
|
||||
static bool s_show_edit_dialog = false;
|
||||
static bool s_show_contacts_settings = false; // Contacts customization modal (gear button)
|
||||
static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens
|
||||
static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms
|
||||
static char s_edit_label[128] = "";
|
||||
@@ -81,6 +82,8 @@ struct AvatarTex {
|
||||
static std::unordered_map<std::string, AvatarTex> s_avatarTexCache;
|
||||
static int s_avatarLoadsThisFrame = 0;
|
||||
static bool s_animateAvatars = true; // mirrors the setting; refreshed each frame in RenderContactsTab
|
||||
static int s_contactsShape = 0; // avatar shape (mirrors getContactsAvatarShape: 0/1/2)
|
||||
static float s_contactsScale = 1.0f; // card/list row scale (mirrors getContactsListScale)
|
||||
static bool s_avatarAnimatedThisFrame = false; // set when a live animated frame is drawn -> keep redrawing
|
||||
static constexpr int kAvatarLoadsPerFrame = 2;
|
||||
static constexpr int kAvatarMaxFrames = 300; // cap frames per animated avatar (bounds VRAM/decode)
|
||||
@@ -134,22 +137,48 @@ static void invalidateAvatarTexture(const std::string& path) {
|
||||
s_avatarTexCache.erase(it);
|
||||
}
|
||||
|
||||
// Draw a contact's avatar into the circle at `c` (radius `r`): a custom image (circular-cropped), a
|
||||
// Material icon in a tinted circle, or — the default — the Z/T type badge.
|
||||
// Draw a contact's avatar: a custom image, a Material icon in a tinted shape, or the Z/T type badge.
|
||||
// `shape` picks the outline: 0 = circle (centered at `c`, radius `r`); 1 = rounded square (same box as
|
||||
// the circle, all corners rounded); 2 = a full-row-height tab spanning [tabMin, tabMax] with rounded
|
||||
// LEFT corners (radius tabRound) + a straight right edge. The inner glyph is sized to the shape, so it
|
||||
// stays proportional at any list scale.
|
||||
static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::AddressBookEntry& e,
|
||||
bool shielded, ImU32 typeCol, bool light, float dp,
|
||||
ImFont* letterFont, ImFont* iconFont, bool onScreen = true) {
|
||||
ImFont* letterFont, ImFont* iconFont, bool onScreen = true, int shape = 0,
|
||||
ImVec2 tabMin = ImVec2(0, 0), ImVec2 tabMax = ImVec2(0, 0), float tabRound = 0.0f) {
|
||||
const bool circle = (shape == 0);
|
||||
// Resolve the shape rectangle, its rounding, the content center + radius, and the glyph size.
|
||||
ImVec2 cc = c; float cr = r;
|
||||
ImVec2 sMin(c.x - r, c.y - r), sMax(c.x + r, c.y + r);
|
||||
float sRound = r * 0.42f;
|
||||
ImDrawFlags sFlags = ImDrawFlags_RoundCornersAll;
|
||||
if (shape == 2) {
|
||||
sMin = tabMin; sMax = tabMax; sRound = tabRound; sFlags = ImDrawFlags_RoundCornersLeft;
|
||||
cc = ImVec2((tabMin.x + tabMax.x) * 0.5f, (tabMin.y + tabMax.y) * 0.5f);
|
||||
cr = (tabMax.y - tabMin.y) * 0.5f;
|
||||
}
|
||||
const float glyphSz = cr * 0.92f; // ~46% of the shape diameter — matches the classic badge ratio
|
||||
auto fillShape = [&](ImU32 col) {
|
||||
if (circle) dl->AddCircleFilled(cc, cr, col);
|
||||
else dl->AddRectFilled(sMin, sMax, col, sRound, sFlags);
|
||||
};
|
||||
auto borderShape = [&](ImU32 col, float th) {
|
||||
if (circle) dl->AddCircle(cc, cr, col, 0, th);
|
||||
else dl->AddRect(sMin, sMax, col, sRound, sFlags, th);
|
||||
};
|
||||
const std::string& av = e.avatar;
|
||||
if (av.rfind("img:", 0) == 0) {
|
||||
const AvatarTex* t = getAvatarTexture(av.substr(4));
|
||||
ImTextureID tex = currentAvatarFrame(t, onScreen);
|
||||
if (tex) {
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1; // centre-crop to a square so the circle isn't stretched
|
||||
if (t->w > t->h) { float m = (t->w - t->h) * 0.5f / t->w; u0 = m; u1 = 1 - m; }
|
||||
else if (t->h > t->w) { float m = (t->h - t->w) * 0.5f / t->h; v0 = m; v1 = 1 - m; }
|
||||
dl->AddImageRounded(tex, ImVec2(c.x - r, c.y - r), ImVec2(c.x + r, c.y + r),
|
||||
ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE, r);
|
||||
dl->AddCircle(c, r, material::WithAlpha(material::OnSurface(), 45), 0, 1.0f * dp);
|
||||
const float aspect = (sMax.x - sMin.x) / std::max(1.0f, sMax.y - sMin.y); // crop to the shape's aspect
|
||||
float u0 = 0, v0 = 0, u1 = 1, v1 = 1;
|
||||
const float tw = static_cast<float>(t->w), th2 = static_cast<float>(t->h);
|
||||
if (tw / th2 > aspect) { float keep = aspect * th2 / tw; float m = (1 - keep) * 0.5f; u0 = m; u1 = 1 - m; }
|
||||
else { float keep = (tw / aspect) / th2; float m = (1 - keep) * 0.5f; v0 = m; v1 = 1 - m; }
|
||||
dl->AddImageRounded(tex, sMin, sMax, ImVec2(u0, v0), ImVec2(u1, v1), IM_COL32_WHITE,
|
||||
circle ? cr : sRound, circle ? ImDrawFlags_RoundCornersAll : sFlags);
|
||||
borderShape(material::WithAlpha(material::OnSurface(), 45), 1.0f * dp);
|
||||
return;
|
||||
}
|
||||
// fall through to the type badge if the image failed to load
|
||||
@@ -158,19 +187,19 @@ static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::Add
|
||||
const bool known = (iconName == material::project_icons::kPickaxeName) ||
|
||||
(material::project_icons::glyphForName(iconName) != nullptr);
|
||||
if (known) {
|
||||
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
|
||||
material::project_icons::drawByName(dl, iconName, c, typeCol, iconFont, iconFont->LegacySize);
|
||||
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
|
||||
material::project_icons::drawByName(dl, iconName, cc, typeCol, iconFont, glyphSz);
|
||||
return;
|
||||
}
|
||||
// fall through to the type badge if the icon name is unknown
|
||||
}
|
||||
// Default: Z/T type badge.
|
||||
dl->AddCircleFilled(c, r, material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
dl->AddCircle(c, r, material::WithAlpha(typeCol, 190), 0, 1.4f * dp);
|
||||
fillShape(material::WithAlpha(typeCol, light ? 45 : 60));
|
||||
borderShape(material::WithAlpha(typeCol, 190), 1.4f * dp);
|
||||
const char* letter = shielded ? "Z" : "T";
|
||||
const ImVec2 ls = letterFont->CalcTextSizeA(letterFont->LegacySize, FLT_MAX, 0, letter);
|
||||
dl->AddText(letterFont, letterFont->LegacySize, ImVec2(c.x - ls.x * 0.5f, c.y - ls.y * 0.5f), typeCol, letter);
|
||||
const ImVec2 ls = letterFont->CalcTextSizeA(glyphSz, FLT_MAX, 0, letter);
|
||||
dl->AddText(letterFont, glyphSz, ImVec2(cc.x - ls.x * 0.5f, cc.y - ls.y * 0.5f), typeCol, letter);
|
||||
}
|
||||
|
||||
static bool isShieldedAddr(const std::string& a) {
|
||||
@@ -268,6 +297,8 @@ void RenderContactsTab(App* app)
|
||||
{
|
||||
s_avatarLoadsThisFrame = 0; // reset the per-frame avatar-decode budget (see getAvatarTexture)
|
||||
s_animateAvatars = !app->settings() || app->settings()->getAnimateAvatars();
|
||||
s_contactsShape = app->settings() ? app->settings()->getContactsAvatarShape() : 0;
|
||||
s_contactsScale = app->settings() ? app->settings()->getContactsListScale() : 1.0f;
|
||||
auto& S = schema::UI();
|
||||
// Reuse the existing address-book schema/column config for the table + add/edit form.
|
||||
auto addrTable = S.table("dialogs.address-book", "address-table");
|
||||
@@ -416,9 +447,12 @@ void RenderContactsTab(App* app)
|
||||
const bool sh = isShieldedAddr(s_edit_address);
|
||||
const ImU32 tu = contactTypeColor(sh, light);
|
||||
ImVec2 avC(pMin.x + pad + avR, pMin.y + ph * 0.5f);
|
||||
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF);
|
||||
const float pTabW = ph; // full preview-height square tab (shape 2)
|
||||
drawContactAvatar(pdl, avC, avR, pv, sh, tu, light, dp, letF, icoF, true,
|
||||
s_contactsShape, pMin, ImVec2(pMin.x + pTabW, pMax.y), 10.0f * dp);
|
||||
|
||||
float tx = avC.x + avR + Layout::spacingMd();
|
||||
float tx = (s_contactsShape == 2) ? (pMin.x + pTabW + Layout::spacingMd())
|
||||
: (avC.x + avR + Layout::spacingMd());
|
||||
float textRight = pMax.x - pad;
|
||||
float ty = pMin.y + (ph - blockH) * 0.5f;
|
||||
const bool hasName = s_edit_label[0] != '\0';
|
||||
@@ -819,7 +853,8 @@ void RenderContactsTab(App* app)
|
||||
// Inline tab content lives in a scroll child (mirrors peers_tab / explorer_tab).
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##ContactsScroll", avail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse); // inner ##contactList owns the wheel (smooth scroll)
|
||||
|
||||
// Toolbar — icon + label actions; the primary "Add New" is accented so it stands out.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
@@ -885,8 +920,10 @@ void RenderContactsTab(App* app)
|
||||
const char* segIcons[3] = { ICON_MD_VIEW_AGENDA, ICON_MD_VIEW_LIST, ICON_MD_TABLE_ROWS };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const float segW = 44.0f * dp * 3.0f;
|
||||
const float segGap = 6.0f * dp;
|
||||
const float grpW = segW + segGap + segH; // view toggle + gap + a square settings gear
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - segW));
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - grpW));
|
||||
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH,
|
||||
segIcons, 3, viewMode, material::Type().iconSmall(),
|
||||
@@ -896,11 +933,21 @@ void RenderContactsTab(App* app)
|
||||
app->settings()->save();
|
||||
viewMode = segClk;
|
||||
}
|
||||
ImGui::Dummy(ImVec2(segW, segH));
|
||||
// Contacts-customization gear, immediately right of the view toggle.
|
||||
ImGui::SetCursorScreenPos(ImVec2(segOrigin.x + segW + segGap, segOrigin.y));
|
||||
material::IconButtonStyle gst;
|
||||
gst.tooltip = TR("contacts_settings_tip");
|
||||
gst.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
gst.restBg = material::WithAlpha(material::OnSurface(), 20);
|
||||
gst.bgRounding = 7.0f * dp;
|
||||
if (material::IconButton("##contactsSettings", ICON_MD_SETTINGS, material::Type().iconSmall(),
|
||||
ImVec2(segH, segH), gst))
|
||||
s_show_contacts_settings = true;
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(grpW, segH));
|
||||
}
|
||||
|
||||
// Search / filter
|
||||
ImGui::Spacing();
|
||||
// Search / filter (tight against the toolbar row above — no extra spacer)
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
|
||||
s_search, sizeof(s_search));
|
||||
@@ -1009,9 +1056,12 @@ void RenderContactsTab(App* app)
|
||||
s_selected_index = static_cast<int>(i); s_confirm_delete_idx = -1; openContextMenu = true;
|
||||
}
|
||||
bool tAvOnScreen = ImGui::IsRectVisible(tAvP, ImVec2(tAvP.x + tAvR * 2.0f, tAvP.y + tLineH));
|
||||
// The table is a compact inline view: a full-height "tab" (shape 2) can't render inside a
|
||||
// table cell, so it falls back to the rounded square there (table scale is fixed too).
|
||||
const int tShape = (s_contactsShape == 2) ? 1 : s_contactsShape;
|
||||
drawContactAvatar(ImGui::GetWindowDrawList(), ImVec2(tAvP.x + tAvR, tAvP.y + tLineH * 0.5f),
|
||||
tAvR, entry, shielded, typeColor(shielded), lightTheme, dp,
|
||||
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen);
|
||||
material::Type().caption(), material::Type().iconSmall(), tAvOnScreen, tShape);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T");
|
||||
@@ -1044,7 +1094,9 @@ void RenderContactsTab(App* app)
|
||||
});
|
||||
}
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0,0,0,0));
|
||||
ImGui::BeginChild("##contactList", ImVec2(0, listH), false, ImGuiWindowFlags_NoBackground);
|
||||
ImGui::BeginChild("##contactList", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
material::ApplySmoothScroll(); // wheel-lerp scroll, matching the rest of the app
|
||||
if (visibleRows.empty()) {
|
||||
emptyState(ICON_MD_CONTACTS, book.empty() ? "address_book_empty" : "contacts_search_no_match");
|
||||
} else {
|
||||
@@ -1056,10 +1108,13 @@ void RenderContactsTab(App* app)
|
||||
ImFont* lblF = material::Type().subtitle2();
|
||||
ImFont* adrF = material::Type().caption();
|
||||
ImFont* icoF = material::Type().iconSmall();
|
||||
const float sc = s_contactsScale; // card/list scale (avatar + text + row height; not the table)
|
||||
const float pad = 12.0f * dp;
|
||||
const float avR = 15.0f * dp;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + (asCard ? 16.0f * dp : 12.0f * dp);
|
||||
const float round = 10.0f * dp;
|
||||
const float lblSz = lblF->LegacySize * sc; // scaled label + address text
|
||||
const float adrSz = adrF->LegacySize * sc;
|
||||
bool rowDeleteRequested = false; // per-row delete is deferred until after the loop
|
||||
for (size_t vi = 0; vi < visibleRows.size(); ++vi) {
|
||||
size_t i = visibleRows[vi];
|
||||
@@ -1103,18 +1158,23 @@ void RenderContactsTab(App* app)
|
||||
// Only animate when the row is actually on-screen, so a scrolled-off animated avatar
|
||||
// doesn't hold the app awake (this loop has no clipper).
|
||||
const bool rowOnScreen = ImGui::IsRectVisible(mn, mx);
|
||||
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen);
|
||||
// Shape 2 = a full-row-height left tab (a square the height of the row, rounded-left to
|
||||
// match the row corner); shapes 0/1 are inset badges. Text shifts to clear the tab.
|
||||
const float tabW = rowH;
|
||||
drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen,
|
||||
s_contactsShape, mn, ImVec2(mn.x + tabW, mx.y), asCard ? round : 0.0f);
|
||||
// Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions.
|
||||
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
|
||||
const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;
|
||||
const float trailW = 3.0f * actHit + 6.0f * dp + globeW;
|
||||
const float cy = mn.y + rowH * 0.5f;
|
||||
const float tx = mn.x + pad + avR * 2.0f + pad;
|
||||
const float tx = (s_contactsShape == 2) ? (mn.x + tabW + pad)
|
||||
: (mn.x + pad + avR * 2.0f + pad);
|
||||
const float textMaxX = mx.x - pad - trailW;
|
||||
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
|
||||
const float blockH = lblSz + adrSz + 3.0f * dp;
|
||||
const float ty = cy - blockH * 0.5f;
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||
dl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str());
|
||||
dl->PopClipRect();
|
||||
// Un-collapse to the full address on hover (clipped to the text column so it never
|
||||
// runs under the trailing actions); middle-truncated otherwise.
|
||||
@@ -1122,7 +1182,7 @@ void RenderContactsTab(App* app)
|
||||
? entry.address
|
||||
: util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true);
|
||||
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
|
||||
dl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp),
|
||||
material::OnSurfaceMedium(), addr.c_str());
|
||||
dl->PopClipRect();
|
||||
// Trailing: the globe badge stays pinned far-right (global contacts); per-row copy/edit/
|
||||
@@ -1226,6 +1286,119 @@ void RenderContactsTab(App* app)
|
||||
ImGui::EndChild();
|
||||
|
||||
renderEntryDialog();
|
||||
|
||||
// ---- Contacts customization modal (opened by the toolbar gear) — house BlurFloat overlay ----
|
||||
if (s_show_contacts_settings && app->settings()) {
|
||||
auto* st = app->settings();
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("contacts_settings_title");
|
||||
ov.p_open = &s_show_contacts_settings;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 460.0f; ov.idSuffix = "contactsettings";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
const float ctrlW = 210.0f * dp;
|
||||
auto rowStart = [&](const char* label) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW));
|
||||
ImGui::SetNextItemWidth(ctrlW);
|
||||
};
|
||||
// Draw one sample contact card (avatar + label + muted address) at a given shape/scale, using
|
||||
// the SAME drawContactAvatar + row geometry the real list uses — so the preview matches reality.
|
||||
auto drawSampleRow = [&](ImDrawList* pdl, ImVec2 mn, float w,
|
||||
const data::AddressBookEntry& e, int shape, float sc) {
|
||||
ImFont* lblF = material::Type().subtitle2();
|
||||
ImFont* adrF = material::Type().caption();
|
||||
ImFont* icoF = material::Type().iconSmall();
|
||||
const float pad = 12.0f * dp;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + 16.0f * dp;
|
||||
const float round = 10.0f * dp;
|
||||
const float lblSz = lblF->LegacySize * sc;
|
||||
const float adrSz = adrF->LegacySize * sc;
|
||||
const ImVec2 mx(mn.x + w, mn.y + rowH);
|
||||
pdl->AddRectFilled(mn, mx, material::WithAlpha(material::OnSurface(), 18), round);
|
||||
const bool shielded = isShieldedAddr(e.address);
|
||||
const ImU32 tu = contactTypeColor(shielded, lightTheme);
|
||||
const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f);
|
||||
const float tabW = rowH; // shape 2 = full-row-height square tab on the left
|
||||
drawContactAvatar(pdl, avC, avR, e, shielded, tu, lightTheme, dp, lblF, icoF, true,
|
||||
shape, mn, ImVec2(mn.x + tabW, mx.y), round);
|
||||
const float tx = (shape == 2) ? (mn.x + tabW + pad) : (mn.x + pad + avR * 2.0f + pad);
|
||||
const float blockH = lblSz + adrSz + 3.0f * dp;
|
||||
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
|
||||
const std::string addr = util::truncateMiddle(e.address, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
pdl->PushClipRect(ImVec2(tx, mn.y), ImVec2(mx.x - pad, mx.y), true);
|
||||
pdl->AddText(lblF, lblSz, ImVec2(tx, ty), material::OnSurface(), e.label.c_str());
|
||||
pdl->AddText(adrF, adrSz, ImVec2(tx, ty + lblSz + 3.0f * dp), material::OnSurfaceMedium(), addr.c_str());
|
||||
pdl->PopClipRect();
|
||||
};
|
||||
// ---- Live preview: two sample cards at the current shape + scale (updates as controls move). ----
|
||||
{
|
||||
const float sc = st->getContactsListScale();
|
||||
const int shape = st->getContactsAvatarShape();
|
||||
data::AddressBookEntry sA; sA.label = "Ava Reyes"; sA.address = "zs1preview0shielded0contact0demo0address0q9k";
|
||||
data::AddressBookEntry sB; sB.label = "Node Ops"; sB.address = "t1PreviewTransparentContactDemoAddr8xQ";
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
const float padIn = 10.0f * dp, gap = 8.0f * dp;
|
||||
const float w = ImGui::GetContentRegionAvail().x;
|
||||
const float avR = 15.0f * dp * sc;
|
||||
const float rowH = avR * 2.0f + 16.0f * dp;
|
||||
const float panelH = 2.0f * rowH + gap + 2.0f * padIn;
|
||||
const ImVec2 pOrigin = ImGui::GetCursorScreenPos();
|
||||
material::GlassPanelSpec g; g.rounding = 12.0f * dp; g.fillAlpha = 14; g.borderAlpha = 34;
|
||||
material::DrawGlassPanel(pdl, pOrigin, ImVec2(pOrigin.x + w, pOrigin.y + panelH), g);
|
||||
const ImVec2 rmn(pOrigin.x + padIn, pOrigin.y + padIn);
|
||||
const float rowW = w - 2.0f * padIn;
|
||||
drawSampleRow(pdl, rmn, rowW, sA, shape, sc);
|
||||
drawSampleRow(pdl, ImVec2(rmn.x, rmn.y + rowH + gap), rowW, sB, shape, sc);
|
||||
ImGui::Dummy(ImVec2(w, panelH));
|
||||
ImGui::Dummy(ImVec2(0.0f, 10.0f * dp));
|
||||
}
|
||||
// Avatar shape — circle, rounded square, or a full-row-height left tab (segmented control,
|
||||
// matching the chat settings modal's segmented style; the live preview above reflects it).
|
||||
{
|
||||
rowStart(TR("contacts_avatar_shape"));
|
||||
const char* items[] = { TR("contacts_shape_circle"), TR("contacts_shape_square"),
|
||||
TR("contacts_shape_tab") };
|
||||
const float segH = ImGui::GetFrameHeight();
|
||||
const ImVec2 segOrigin = ImGui::GetCursorScreenPos();
|
||||
const int cur = st->getContactsAvatarShape();
|
||||
const int clk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, ctrlW, segH,
|
||||
items, 3, cur, material::Type().caption(),
|
||||
"##contactsShape", dp);
|
||||
ImGui::SetCursorScreenPos(segOrigin);
|
||||
ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow
|
||||
if (clk >= 0 && clk != cur) { st->setContactsAvatarShape(clk); st->save(); }
|
||||
}
|
||||
// Card/list row scale (does not affect the table view).
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0.0f, 4.0f * dp));
|
||||
rowStart(TR("contacts_list_scale"));
|
||||
float v = st->getContactsListScale();
|
||||
if (ImGui::SliderFloat("##contactsScale", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
st->setContactsListScale(v); st->save();
|
||||
}
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0.0f, 8.0f * dp));
|
||||
{
|
||||
// Done: sized to its text (+ a little shoulder room) and centered, not full width.
|
||||
const char* doneLbl = TR("chat_settings_done");
|
||||
ImGui::PushFont(material::Type().button());
|
||||
const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp;
|
||||
ImGui::PopFont();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f));
|
||||
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)));
|
||||
if (material::TactileButton(doneLbl, ImVec2(bw, 0)))
|
||||
s_show_contacts_settings = false;
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ContactsSweepOpenEditDialog(int avatarMode)
|
||||
|
||||
Reference in New Issue
Block a user