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* body2 = Type().body2();
|
||||||
ImFont* sub1 = Type().subtitle1();
|
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
|
// THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit
|
||||||
// which breaks BeginCombo popup rendering in some ImGui versions)
|
// which breaks BeginCombo popup rendering in some ImGui versions)
|
||||||
@@ -1037,8 +1058,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::TextUnformatted(TR("theme"));
|
ImGui::TextUnformatted(TR("theme"));
|
||||||
ImGui::SameLine(labelW);
|
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,
|
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);
|
ImGui::SetNextItemWidth(themeComboW);
|
||||||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||||||
renderThemeComboPopup();
|
renderThemeComboPopup();
|
||||||
@@ -1377,11 +1401,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_shield_mining"));
|
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")))
|
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
|
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"))) {
|
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
|
||||||
s_settingsState.confirm_clear_ztx = true;
|
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).
|
// also gets the RPC-backed encrypt/change/lock/remove block, includeRpcEncrypt).
|
||||||
// `secX` is the row start X (== content-column left); `secColW` its width.
|
// `secX` is the row start X (== content-column left); `secColW` its width.
|
||||||
auto renderSecuritySection = [&](float secX, float secColW, bool includeRpcEncrypt) {
|
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.
|
// Encrypt / Change passphrase / Lock / Remove — RPC-backed, full-node only.
|
||||||
// In lite the wallet's encryption controls live in the WALLET block above.
|
// In lite the wallet's encryption controls live in the WALLET block above.
|
||||||
if (includeRpcEncrypt) {
|
if (includeRpcEncrypt) {
|
||||||
@@ -1535,8 +1562,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked"));
|
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked"));
|
||||||
}
|
}
|
||||||
// Remove Encryption button — trails the row.
|
// 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());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
|
}
|
||||||
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||||
app->showDecryptDialog();
|
app->showDecryptDialog();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
|
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
|
// 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.
|
// 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) {
|
if (includeRpcEncrypt && !secWrap) {
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
} else {
|
} else {
|
||||||
@@ -2338,7 +2371,17 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
}
|
||||||
|
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
// EXPLORER & OPTIONS — full-width card
|
// EXPLORER & OPTIONS — full-width card
|
||||||
@@ -2376,13 +2419,21 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
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);
|
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
||||||
|
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());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
|
else
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openUrl("https://explorer.dragonx.is");
|
util::Platform::openUrl("https://explorer.dragonx.is");
|
||||||
}
|
}
|
||||||
@@ -2488,7 +2539,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
float fullContentW = availWidth - pad * 2;
|
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"))) {
|
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||||
util::Platform::openUrl("https://dragonx.is");
|
util::Platform::openUrl("https://dragonx.is");
|
||||||
@@ -2499,7 +2554,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_report_bug"));
|
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"))) {
|
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
Notifications::instance().success("Settings saved");
|
Notifications::instance().success("Settings saved");
|
||||||
@@ -2541,6 +2596,15 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
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));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user