feat(settings): About logo scales to card height; Node&Security Advanced expander; lite gating

- About: draw the logo deferred, scaled to the card's full height (aspect-preserved,
  capped to the reserved width) so there's no empty space below it.
- Node & Security: split the daemon toolbar — Refresh + Test connection stay visible;
  the rare/destructive actions (Install bundled, Rescan, Delete blockchain, Repair
  wallet) move behind a collapsible "Advanced" header (reusing the DEBUG LOGGING idiom).
- Lite gating: hide the DEBUG LOGGING card (dragonxd debug= categories, no daemon in
  lite) and the RPC-backed encrypt/change/lock/remove block (lite has its own encryption
  in the NODE column) behind full-node guards; label the lite left column "WALLET" not
  "NODE". (Auto-lock + PIN remain in both.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:13:40 -05:00
parent 14f5fdc559
commit a6964621a8
2 changed files with 89 additions and 43 deletions

View File

@@ -1327,7 +1327,9 @@ security-combo-width = { size = 120.0 }
port-input-min-width = { size = 60.0 }
port-input-width-ratio = { size = 0.4 }
idle-combo-width = { size = 64.0 }
about-logo-size = { size = 64.0 }
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
# card's actual height (aspect-preserved) and capped to this * aspect in width.
about-logo-size = { size = 150.0 }
[components.main-layout]
app-bar-height = { size = 64.0 }

View File

@@ -144,6 +144,7 @@ struct SettingsPageState {
std::set<std::string> debug_categories;
bool debug_cats_dirty = false;
bool debug_expanded = false;
bool node_advanced_expanded = false; // Node & Security: rare/destructive daemon actions
bool effects_expanded = false;
bool tools_expanded = false;
bool confirm_clear_ztx = false;
@@ -1521,7 +1522,10 @@ void RenderSettingsPage(App* app) {
ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y),
ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node"));
// Lite has no daemon — this column holds wallet lifecycle/encryption, so label
// it "WALLET" rather than "NODE".
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
app->isLiteBuild() ? TR("wallet") : TR("node"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
@@ -2029,7 +2033,10 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
// Encrypt / Change passphrase / Lock buttons
// Encrypt / Change passphrase / Lock / Remove — RPC-backed, so full-node
// only. In lite the wallet's encryption controls live in the NODE column
// above (via the lite backend), so this duplicate is hidden.
if (!app->isLiteBuild()) {
bool isEncrypted = app->state().isEncrypted();
bool isLocked = app->state().isLocked();
@@ -2072,6 +2079,7 @@ void RenderSettingsPage(App* app) {
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
} // !isLiteBuild — RPC-backed encrypt/change/lock/remove block
// Auto-Lock timeout
{
@@ -2223,17 +2231,8 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// All node actions on one full-width toolbar row: daemon-binary actions first
// (Install bundled | Refresh), then maintenance (Test connection | Rescan |
// Delete blockchain | Repair wallet).
// Common actions, always visible: Refresh | Test connection.
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_reinstall_daemon = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.daemon_info_loaded = false;
}
@@ -2256,27 +2255,61 @@ void RenderSettingsPage(App* app) {
}
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_rescan = true;
// Re-probe the available block range each time the dialog is opened.
s_settingsState.rescan_height_detecting = false;
s_settingsState.rescan_height_detected = false;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_delete_blockchain = true;
// Advanced — rare + destructive daemon actions behind a collapsible header
// (reuses the DEBUG LOGGING expander idiom).
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{
ImVec2 advPos = ImGui::GetCursorScreenPos();
const char* advArrow = s_settingsState.node_advanced_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
if (ImGui::Button("##NodeAdvancedToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded;
}
ImGui::PopStyleColor(3);
ImFont* ovFont = Type().overline();
float textY = advPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(advPos.x, textY), OnSurfaceMedium(), TR("advanced"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = ovFont;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, advArrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(advPos.x + contentW - arrowW, textY), OnSurfaceMedium(), advArrow);
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_repair_wallet = true;
if (s_settingsState.node_advanced_expanded) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_reinstall_daemon = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_rescan = true;
// Re-probe the available block range each time the dialog is opened.
s_settingsState.rescan_height_detecting = false;
s_settingsState.rescan_height_detected = false;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_repair_wallet = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
ImGui::EndDisabled();
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
ImGui::EndDisabled();
}
}
@@ -2373,19 +2406,17 @@ void RenderSettingsPage(App* app) {
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
ImGui::Indent(pad);
// Logo on the left side of the about card
// Logo on the left side of the about card. Deferred: reserve horizontal space
// now, but draw the image after the card's final height is known so it scales to
// the full card height (no empty space below it).
ImTextureID logoTex = app->getLogoTexture();
float logoAreaW = 0;
ImVec2 logoPos = ImGui::GetCursorScreenPos();
float logoAspect = (app->getLogoHeight() > 0)
? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
float logoReserveH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(150.0f);
if (logoTex != 0) {
float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f);
float logoH = logoMaxH;
float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
float logoW = logoH * aspect;
ImVec2 logoPos = ImGui::GetCursorScreenPos();
dl->AddImage(logoTex,
ImVec2(logoPos.x, logoPos.y),
ImVec2(logoPos.x + logoW, logoPos.y + logoH));
logoAreaW = logoW + Layout::spacingLg();
logoAreaW = logoReserveH * logoAspect + Layout::spacingLg();
ImGui::Indent(logoAreaW);
}
@@ -2471,6 +2502,18 @@ void RenderSettingsPage(App* app) {
ImGui::Unindent(pad);
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
// Draw the logo now that the card height is known — scaled to the card height
// (inset by padding), aspect-preserved, and capped to the reserved width so it
// never overlaps the text. Still on the content channel (1), above the glass.
if (logoTex != 0) {
float reserveW = logoReserveH * logoAspect;
float logoH = std::max(16.0f, (cardMax.y - logoPos.y) - bottomPad);
float logoW = logoH * logoAspect;
if (logoW > reserveW) { logoW = reserveW; logoH = (logoAspect > 0.0f) ? logoW / logoAspect : logoH; }
dl->AddImage(logoTex, logoPos, ImVec2(logoPos.x + logoW, logoPos.y + logoH));
}
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
dl->ChannelsMerge();
@@ -2482,9 +2525,10 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, gap));
// ====================================================================
// DEBUG LOGGING — collapsible card with glass styling
// DEBUG LOGGING — collapsible card (full-node only: these are dragonxd
// daemon debug= categories written to DRAGONX.conf; lite has no daemon)
// ====================================================================
{
if (app->supportsFullNodeLifecycleActions()) {
// Clickable header row
ImVec2 headerPos = ImGui::GetCursorScreenPos();
const char* arrow = s_settingsState.debug_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;