feat(settings): two-column page masonry for the section cards (A)
Render the section cards in two columns when the page is wide (>1000px): left = Theme, Wallet, Node & Security; right = Explorer, Chat & Contacts, About; Debug spans full width below. Driven by shadowing `availWidth` to the column width for the whole card region and floating the right column with Indent() (a one-shot cursor set can't hold a column across line-advances); each card's own responsive gates collapse their internal columns at the narrower width. Collapses to one column below the breakpoint. Fix the half-width overflows a parallel audit surfaced (cards previously only ever rendered full-width): - WALLET Tools & Actions: merge-to-address was a 3rd button on a 2-button row (unconditional SameLine) — flip the separators so 2-per-row pairs cleanly. - EXPLORER: wrap the Block Explorer button to its own row when the checkboxes + button won't fit (measured, locale-safe). - NODE & SECURITY: wrap "Remove Encryption" below when the encrypted row's three buttons + status don't fit the column. - ABOUT: 2x2 button grid instead of 1x4 so labels don't clip. - THEME: reserve the real gaps + custom-skin "*" marker in the theme combo width so Refresh doesn't spill past the edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -643,6 +643,27 @@ void RenderSettingsPage(App* app) {
|
||||
ImFont* body2 = Type().body2();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
|
||||
// ====================================================================
|
||||
// Masonry: render the section cards in two columns when the page is wide
|
||||
// enough. Left column: Theme, Wallet, Node & Security. Right column:
|
||||
// Explorer, Chat & Contacts, About. Debug spans full width below. We shadow
|
||||
// `availWidth` to the column width for the whole card region (every card
|
||||
// reads it), and float the right column with Indent() (a one-shot cursor
|
||||
// set can't hold a column across line-advances). Each card's own responsive
|
||||
// gates collapse their internal columns at this narrower width.
|
||||
// ====================================================================
|
||||
const float msFullWidth = availWidth;
|
||||
const float msColGap = Layout::spacingXl();
|
||||
const bool msTwoCol = availWidth > 1000.0f * Layout::dpiScale();
|
||||
const float msColW = msTwoCol ? (msFullWidth - msColGap) * 0.5f : msFullWidth;
|
||||
const ImVec2 msColTop = ImGui::GetCursorPos();
|
||||
float msLeftBottomY = 0.0f;
|
||||
if (msTwoCol) {
|
||||
availWidth = msColW;
|
||||
inputW = std::max(S.drawElement("components.settings-page", "input-min-width").size,
|
||||
availWidth - labelW - pad * 2);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit
|
||||
// which breaks BeginCombo popup rendering in some ImGui versions)
|
||||
@@ -1037,8 +1058,11 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::TextUnformatted(TR("theme"));
|
||||
ImGui::SameLine(labelW);
|
||||
|
||||
// Reserve the real gaps (default ItemSpacing, not spacingSm) plus the
|
||||
// custom-skin "*" marker so the Refresh button never spills past the edge.
|
||||
float themeComboW = std::max(S.drawElement("components.settings-page", "theme-combo-min-width").size,
|
||||
availWidth - pad * 2 - labelW - refreshBtnW - Layout::spacingSm());
|
||||
availWidth - pad * 2 - labelW - refreshBtnW - ImGui::GetStyle().ItemSpacing.x
|
||||
- (active_is_custom ? (ImGui::GetStyle().ItemSpacing.x + ImGui::CalcTextSize("*").x) : 0.0f));
|
||||
ImGui::SetNextItemWidth(themeComboW);
|
||||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||||
renderThemeComboPopup();
|
||||
@@ -1377,11 +1401,11 @@ void RenderSettingsPage(App* app) {
|
||||
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining"));
|
||||
ImGui::SameLine(0, btnSpacing);
|
||||
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
||||
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
|
||||
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
|
||||
ImGui::SameLine(0, btnSpacing);
|
||||
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
|
||||
s_settingsState.confirm_clear_ztx = true;
|
||||
}
|
||||
@@ -1498,6 +1522,9 @@ void RenderSettingsPage(App* app) {
|
||||
// also gets the RPC-backed encrypt/change/lock/remove block, includeRpcEncrypt).
|
||||
// `secX` is the row start X (== content-column left); `secColW` its width.
|
||||
auto renderSecuritySection = [&](float secX, float secColW, bool includeRpcEncrypt) {
|
||||
// Narrow (two-column) card: the encrypt buttons, auto-lock and PIN
|
||||
// controls can't all share one row, so wrap groups onto fresh rows.
|
||||
const bool secNarrow = secColW < 740.0f * Layout::dpiScale();
|
||||
// Encrypt / Change passphrase / Lock / Remove — RPC-backed, full-node only.
|
||||
// In lite the wallet's encryption controls live in the WALLET block above.
|
||||
if (includeRpcEncrypt) {
|
||||
@@ -1535,8 +1562,14 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::SameLine(0, Layout::spacingXs());
|
||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked"));
|
||||
}
|
||||
// Remove Encryption button — trails the row.
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
// Remove Encryption button — trails the row, or wraps below
|
||||
// when the column is too narrow for three buttons + status.
|
||||
if (secNarrow) {
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
|
||||
} else {
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
}
|
||||
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||
app->showDecryptDialog();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
|
||||
@@ -1558,7 +1591,7 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
// In a narrow (two-column) card the encrypt controls + auto-lock + PIN don't fit on
|
||||
// one row, so wrap the auto-lock/PIN group onto its own row at the section's left edge.
|
||||
const bool secWrap = includeRpcEncrypt && secColW < 740.0f * Layout::dpiScale();
|
||||
const bool secWrap = includeRpcEncrypt && secNarrow;
|
||||
if (includeRpcEncrypt && !secWrap) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
} else {
|
||||
@@ -2338,7 +2371,17 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
// End of the left column (Theme / Wallet / Node & Security). In two-column
|
||||
// mode jump back to the top and indent so Explorer / Chat / About stack in
|
||||
// the right column; otherwise just add the usual inter-card gap.
|
||||
if (msTwoCol) {
|
||||
msLeftBottomY = ImGui::GetCursorPosY();
|
||||
ImGui::SetCursorPosY(msColTop.y);
|
||||
ImGui::Indent(msColW + msColGap);
|
||||
ImGui::SetCursorPosX(msColTop.x + msColW + msColGap);
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// EXPLORER & OPTIONS — full-width card
|
||||
@@ -2376,13 +2419,21 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::PopFont();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Row 2: Checkboxes + Block Explorer button (on one line)
|
||||
// Row 2: Checkboxes + Block Explorer button. Keep the two checkboxes
|
||||
// side-by-side, but wrap the Block Explorer button onto its own row when
|
||||
// it won't fit the (narrow, two-column) card — measured, so it's locale-safe.
|
||||
const float expRowRight = ImGui::GetCursorScreenPos().x + contentW;
|
||||
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
const float expBtnW = ImGui::CalcTextSize(TR("block_explorer")).x
|
||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
||||
if (ImGui::GetItemRectMax().x + expBtnW <= expRowRight)
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||
}
|
||||
@@ -2488,7 +2539,11 @@ void RenderSettingsPage(App* app) {
|
||||
}
|
||||
{
|
||||
float fullContentW = availWidth - pad * 2;
|
||||
float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f;
|
||||
// 2x2 grid in the narrow (two-column) card so labels don't clip;
|
||||
// one 1x4 row at full width.
|
||||
const bool aboutGrid = fullContentW < 720.0f * Layout::dpiScale();
|
||||
float aboutBtnW = aboutGrid ? (fullContentW - Layout::spacingMd()) / 2.0f
|
||||
: (fullContentW - Layout::spacingMd() * 3) / 4.0f;
|
||||
|
||||
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||
util::Platform::openUrl("https://dragonx.is");
|
||||
@@ -2499,7 +2554,7 @@ void RenderSettingsPage(App* app) {
|
||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
||||
}
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
if (aboutGrid) ImGui::Dummy(ImVec2(0, Layout::spacingXs())); else ImGui::SameLine(0, Layout::spacingMd());
|
||||
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||
saveSettingsPageState(app->settings());
|
||||
Notifications::instance().success("Settings saved");
|
||||
@@ -2541,6 +2596,15 @@ void RenderSettingsPage(App* app) {
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
}
|
||||
|
||||
// End of the right column. Un-indent, drop below the taller of the two
|
||||
// columns, and restore full width so Debug spans the page.
|
||||
if (msTwoCol) {
|
||||
ImGui::Unindent(msColW + msColGap);
|
||||
const float msRightBottomY = ImGui::GetCursorPosY();
|
||||
ImGui::SetCursorPosX(msColTop.x);
|
||||
ImGui::SetCursorPosY(std::max(msLeftBottomY, msRightBottomY));
|
||||
availWidth = msFullWidth;
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
|
||||
// ====================================================================
|
||||
|
||||
Reference in New Issue
Block a user