feat(ui): width-responsiveness — content max-width cap + per-surface form/input clamps
Wide/ultrawide (1440-3440px) responsiveness was unhealthy: no page/content-level max-width cap existed, and every card/form/table derived width from raw GetContentRegionAvail().x with floor-only clamps, so surfaces stretched edge-to-edge (2000-3000px inputs, ballooning cards, giant grid cells, 2000px+ dead row-voids). Root cause: adopt the (previously dead-code) clamp helpers. - New Layout::kContentMaxWidth() (~1600dp, tunable via ui.toml [layout] content-max-width; <=0 disables). Cap ##ContentArea to it and center the column in wider windows — every tab derives from this child, so one change tames the app at wide widths. No-op below the cap (fills as before), so 1080p/1440p are unaffected. Per-surface upper-clamps (std::min(cap*dp, expr), floors preserved) where a single element is still too wide even within the capped column: - Settings: Theme/Layout/Language combos, the font-scale slider (~3000px -> 360dp), the effect sliders, Explorer URL and RPC credential fields. - Send / Receive: cap the compose / receive cards to a readable form width and center them (Indent(pad+offset) so the auto-layout fields align with the hand-drawn card); the recent-tx lists below keep the full column width. - Chat message bubbles + composer, mining pool URL/payout inputs + stats left/right split, contacts search, and the lite-network add-server row / server cards / status panel (capped + centered). - Wizard: vertically center the cards when they fit (was top-anchored, leaving a void on tall monitors), compensating the content-height measurement so it can't oscillate. The 1600 cap also subsumes the fixed-4-column balance grids (~400px cards) and the right-anchored row dead-gaps (voids shrink from ~2700px to ~800px), so those are left to the cap rather than blind column/row redesigns. Verified at 1024/1280 (and via a temporary 900dp cap to exercise the cap+center path, since the test display clamps to 1280) across full-node + Lite + Windows (ctest green) and an adversarial diff review (clean). The true wide/ultrawide look and the 1600dp cap value still want eyes on a real wide monitor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
src/app.cpp
15
src/app.cpp
@@ -1875,10 +1875,21 @@ void App::render()
|
|||||||
float caPadY = caWin.padding[1] > 0.0f ? caWin.padding[1] : ImGui::GetStyle().WindowPadding.y;
|
float caPadY = caWin.padding[1] > 0.0f ? caWin.padding[1] : ImGui::GetStyle().WindowPadding.y;
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(caPadX, caPadY));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(caPadX, caPadY));
|
||||||
|
|
||||||
// Capture content area screen position for edge fade mask
|
// Cap the content column to a readable max width and center it in wider windows. Every tab derives
|
||||||
|
// its cards/forms/tables from this child's width, so an uncapped fill stretches them edge-to-edge on
|
||||||
|
// wide/ultrawide displays. No-op below the cap (fills as before).
|
||||||
|
float caAvailW = ImGui::GetContentRegionAvail().x;
|
||||||
|
float caMaxW = ui::Layout::kContentMaxWidth();
|
||||||
|
float caChildW = 0.0f; // 0 -> fill remaining width (default, and whenever the window is below the cap)
|
||||||
|
if (caMaxW > 0.0f && caAvailW > caMaxW) {
|
||||||
|
caChildW = caMaxW;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (caAvailW - caMaxW) * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture content area screen position for edge fade mask (after any centering offset)
|
||||||
ImVec2 caScreenPos = ImGui::GetCursorScreenPos();
|
ImVec2 caScreenPos = ImGui::GetCursorScreenPos();
|
||||||
|
|
||||||
ImGui::BeginChild("##ContentArea", ImVec2(0, contentH), false, contentFlags);
|
ImGui::BeginChild("##ContentArea", ImVec2(caChildW, contentH), false, contentFlags);
|
||||||
|
|
||||||
// Persistent node/RPC error banner — drawn first (before the edge-fade vertex capture below,
|
// Persistent node/RPC error banner — drawn first (before the edge-fade vertex capture below,
|
||||||
// so it stays fully opaque) and above every page / overlay in the content column. It renders
|
// so it stays fully opaque) and above every page / overlay in the content column. It renders
|
||||||
|
|||||||
@@ -198,7 +198,15 @@ void App::renderFirstRunWizard() {
|
|||||||
const float scrollY = s_wizScroll;
|
const float scrollY = s_wizScroll;
|
||||||
|
|
||||||
// --- Header: Logo + Welcome ---
|
// --- Header: Logo + Welcome ---
|
||||||
float headerCy = winPos.y - scrollY + 20.0f * dp;
|
// Vertically center the content when it fits (mirrors the horizontal centering below): on a tall
|
||||||
|
// monitor top-anchoring leaves a large void under the cards. Using last frame's measured block
|
||||||
|
// height, when the content fits inside the window (and we're NOT overflowing, so this doesn't
|
||||||
|
// fight the scroll), push everything down by half the leftover space. No-op once content
|
||||||
|
// fills/exceeds the window (s_wizContentH >= winSize.y ⇒ wizMaxScroll > 0 ⇒ vCenter skipped).
|
||||||
|
float vCenter = 0.0f;
|
||||||
|
if (wizMaxScroll == 0.0f && s_wizContentH > 0.0f && s_wizContentH < winSize.y)
|
||||||
|
vCenter = std::max(0.0f, (winSize.y - s_wizContentH) * 0.5f);
|
||||||
|
float headerCy = winPos.y - scrollY + 20.0f * dp + vCenter;
|
||||||
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
|
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
|
||||||
if (logo_tex_ != 0) {
|
if (logo_tex_ != 0) {
|
||||||
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
||||||
@@ -1452,7 +1460,10 @@ void App::renderFirstRunWizard() {
|
|||||||
// window, draw a slim scroll indicator so the off-screen content is discoverable.
|
// window, draw a slim scroll indicator so the off-screen content is discoverable.
|
||||||
{
|
{
|
||||||
float contentBottom = std::max(card0Bot, std::max(card1Bot, card2Bot));
|
float contentBottom = std::max(card0Bot, std::max(card1Bot, card2Bot));
|
||||||
s_wizContentH = (contentBottom - winPos.y + scrollY) + 24.0f * dp;
|
// Subtract vCenter back out: everything below the header was shifted down by it, so the raw
|
||||||
|
// span includes it. We want s_wizContentH to be the true (un-centered) content height, or the
|
||||||
|
// vertical-centering above would feed on itself and oscillate frame-to-frame.
|
||||||
|
s_wizContentH = (contentBottom - winPos.y + scrollY - vCenter) + 24.0f * dp;
|
||||||
if (wizMaxScroll > 0.0f && s_wizContentH > 0.0f) {
|
if (wizMaxScroll > 0.0f && s_wizContentH > 0.0f) {
|
||||||
float trackH = winSize.y - 8.0f * dp;
|
float trackH = winSize.y - 8.0f * dp;
|
||||||
float thumbH = std::min(trackH, std::max(32.0f * dp, trackH * (winSize.y / s_wizContentH)));
|
float thumbH = std::min(trackH, std::max(32.0f * dp, trackH * (winSize.y / s_wizContentH)));
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ inline float kSidePanelMinWidth() { return schema::UI().drawElement("panels",
|
|||||||
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
|
inline float kSidePanelMaxWidth() { return schema::UI().drawElement("panels", "side-panel").getFloat("max-width", 450.0f) * dpiScale(); }
|
||||||
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
|
inline float kSidePanelWidthRatio() { return schema::UI().drawElement("panels", "side-panel").getFloat("width-ratio", 0.4f); }
|
||||||
|
|
||||||
|
// Overall content-column cap: the max width a tab's content should occupy before it is centered in wider
|
||||||
|
// windows. Prevents cards/forms/tables (which all derive their size from the content child's width) from
|
||||||
|
// stretching edge-to-edge at wide/ultrawide widths. <= 0 disables (fill full width). Tunable via ui.toml
|
||||||
|
// [layout] content-max-width; default is generous so data-dense screens stay comfortable.
|
||||||
|
inline float kContentMaxWidth() { return schema::UI().drawElement("layout", "content-max-width").sizeOr(1600.0f) * dpiScale(); }
|
||||||
|
|
||||||
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
|
inline float kTableMinHeight() { return schema::UI().drawElement("panels", "table").getFloat("min-height", 150.0f) * dpiScale(); }
|
||||||
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
|
inline float kTableHeightRatio() { return schema::UI().drawElement("panels", "table").getFloat("height-ratio", 0.45f); }
|
||||||
|
|
||||||
|
|||||||
@@ -766,7 +766,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// don't dpi-scale these terms or the budget over-reserves and the combos shrink needlessly.
|
// don't dpi-scale these terms or the budget over-reserves and the combos shrink needlessly.
|
||||||
float totalFixed = lblThemeW + lblLayoutW + lblLangW
|
float totalFixed = lblThemeW + lblLayoutW + lblLangW
|
||||||
+ comboGap * 2 + Layout::spacingSm() + refreshBtnW;
|
+ comboGap * 2 + Layout::spacingSm() + refreshBtnW;
|
||||||
float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f);
|
float comboW = std::min(std::max(80.0f, (contentW - totalFixed) / 3.0f), 300.0f * dp);
|
||||||
|
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted(TR("theme"));
|
ImGui::TextUnformatted(TR("theme"));
|
||||||
@@ -856,7 +856,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
{
|
{
|
||||||
ImGui::PushFont(body2);
|
ImGui::PushFont(body2);
|
||||||
ImGui::TextUnformatted(TR("font_scale"));
|
ImGui::TextUnformatted(TR("font_scale"));
|
||||||
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW);
|
float fontSliderW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW), 360.0f * dp);
|
||||||
ImGui::SetNextItemWidth(fontSliderW);
|
ImGui::SetNextItemWidth(fontSliderW);
|
||||||
s_settingsState.font_scale = Layout::userFontScale();
|
s_settingsState.font_scale = Layout::userFontScale();
|
||||||
float prev_font_scale = s_settingsState.font_scale;
|
float prev_font_scale = s_settingsState.font_scale;
|
||||||
@@ -956,7 +956,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
||||||
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
||||||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||||||
float ctrlW = std::max(effCtrlMinW, halfW);
|
float ctrlW = std::min(std::max(effCtrlMinW, halfW), 360.0f * dp);
|
||||||
float baseX = ImGui::GetCursorScreenPos().x;
|
float baseX = ImGui::GetCursorScreenPos().x;
|
||||||
float rightX = baseX + ctrlW + Layout::spacingLg();
|
float rightX = baseX + ctrlW + Layout::spacingLg();
|
||||||
|
|
||||||
@@ -1153,8 +1153,8 @@ void RenderSettingsPage(App* app) {
|
|||||||
{
|
{
|
||||||
ImGui::PushFont(body2);
|
ImGui::PushFont(body2);
|
||||||
ImGui::TextUnformatted(TR("font_scale"));
|
ImGui::TextUnformatted(TR("font_scale"));
|
||||||
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
float fontSliderW = std::min(std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||||||
availWidth - pad * 2);
|
availWidth - pad * 2), 360.0f * dp);
|
||||||
ImGui::SetNextItemWidth(fontSliderW);
|
ImGui::SetNextItemWidth(fontSliderW);
|
||||||
s_settingsState.font_scale = Layout::userFontScale();
|
s_settingsState.font_scale = Layout::userFontScale();
|
||||||
float prev_font_scale = s_settingsState.font_scale;
|
float prev_font_scale = s_settingsState.font_scale;
|
||||||
@@ -2218,7 +2218,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (fourAcross) {
|
if (fourAcross) {
|
||||||
// fieldW = (contentW - labels - per-field label gaps - 3 inter-field gaps) / 4
|
// fieldW = (contentW - labels - per-field label gaps - 3 inter-field gaps) / 4
|
||||||
float inputTotal = contentW - labelsW - Layout::spacingXs() * 4 - spMd * 3;
|
float inputTotal = contentW - labelsW - Layout::spacingXs() * 4 - spMd * 3;
|
||||||
float inputW = std::max(60.0f, std::floor(inputTotal / 4.0f));
|
float inputW = std::min(std::max(60.0f, std::floor(inputTotal / 4.0f)), 220.0f * dp);
|
||||||
field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false);
|
field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
|
||||||
ImGui::SameLine(0, spMd);
|
ImGui::SameLine(0, spMd);
|
||||||
@@ -2453,8 +2453,8 @@ void RenderSettingsPage(App* app) {
|
|||||||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||||||
float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs();
|
float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs();
|
||||||
float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs();
|
float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs();
|
||||||
float inputTxW = std::max(80.0f, halfW - lblTxW);
|
float inputTxW = std::min(std::max(80.0f, halfW - lblTxW), 460.0f * dp);
|
||||||
float inputAddrW = std::max(80.0f, halfW - lblAddrW);
|
float inputAddrW = std::min(std::max(80.0f, halfW - lblAddrW), 460.0f * dp);
|
||||||
|
|
||||||
// Row start X (indent-inclusive) — the Address column is placed relative
|
// Row start X (indent-inclusive) — the Address column is placed relative
|
||||||
// to it, not to a fixed `pad`, so it lands correctly in the right column.
|
// to it, not to a fixed `pad`, so it lands correctly in the right column.
|
||||||
|
|||||||
@@ -1313,7 +1313,7 @@ void RenderChatTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const float availW = ImGui::GetContentRegionAvail().x;
|
const float availW = ImGui::GetContentRegionAvail().x;
|
||||||
const float maxBubbleW = std::max(140.0f * dp, availW * 0.72f);
|
const float maxBubbleW = std::clamp(availW * 0.72f, 140.0f * dp, 560.0f * dp);
|
||||||
const float innerW = maxBubbleW - 2.0f * bpad;
|
const float innerW = maxBubbleW - 2.0f * bpad;
|
||||||
|
|
||||||
// ── Date separator (once per calendar day): a centered pill.
|
// ── Date separator (once per calendar day): a centered pill.
|
||||||
@@ -1599,7 +1599,8 @@ void RenderChatTab(App* app)
|
|||||||
const float ringR = std::max(7.0f, lineH * 0.42f);
|
const float ringR = std::max(7.0f, lineH * 0.42f);
|
||||||
const float ringPad = 9.0f * tdp;
|
const float ringPad = 9.0f * tdp;
|
||||||
const float ringSlot = 2.0f * ringR + ringPad * 1.6f;
|
const float ringSlot = 2.0f * ringR + ringPad * 1.6f;
|
||||||
const float inputW = std::max(ringSlot + 48.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW);
|
const float inputW = std::min(720.0f * tdp,
|
||||||
|
std::max(ringSlot + 48.0f * tdp, cw - emojiBtn - 2.0f * inGap - sendW));
|
||||||
const float sendX = inputX + inputW + inGap;
|
const float sendX = inputX + inputW + inGap;
|
||||||
const float textW = std::max(40.0f * tdp, inputW - ringSlot); // input area, left of the ring
|
const float textW = std::max(40.0f * tdp, inputW - ringSlot); // input area, left of the ring
|
||||||
const ImVec2 ringC(inputX + inputW - ringPad - ringR, rowY + composerBoxH - ringPad - ringR);
|
const ImVec2 ringC(inputX + inputW - ringPad - ringR, rowY + composerBoxH - ringPad - ringR);
|
||||||
|
|||||||
@@ -960,7 +960,7 @@ void RenderContactsTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search / filter (tight against the toolbar row above — no extra spacer)
|
// Search / filter (tight against the toolbar row above — no extra spacer)
|
||||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 700.0f * dp));
|
||||||
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
|
ImGui::InputTextWithHint("##ContactSearch", TR("contacts_search_placeholder"),
|
||||||
s_search, sizeof(s_search));
|
s_search, sizeof(s_search));
|
||||||
bool searchActive = ImGui::IsItemActive();
|
bool searchActive = ImGui::IsItemActive();
|
||||||
|
|||||||
@@ -189,8 +189,11 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
|
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
|
||||||
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
|
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
|
||||||
- Layout::spacingSm() - perGroupExtra * 2;
|
- Layout::spacingSm() - perGroupExtra * 2;
|
||||||
float urlW = std::max(60.0f, remainW * 0.30f);
|
// Floor keeps the inputs usable when cramped; the ceiling stops a
|
||||||
float wrkW = std::max(40.0f, remainW * 0.70f);
|
// single input sprawling absurdly wide on a large window (leftover
|
||||||
|
// space becomes right-side margin). Caps are logical px * dp.
|
||||||
|
float urlW = std::min(std::max(60.0f, remainW * 0.30f), 420.0f * dp);
|
||||||
|
float wrkW = std::min(std::max(40.0f, remainW * 0.70f), 560.0f * dp);
|
||||||
|
|
||||||
// Track positions for popup alignment
|
// Track positions for popup alignment
|
||||||
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
|
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||||
@@ -451,7 +454,9 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// --- Worker: Popup positioned below the input group ---
|
// --- Worker: Popup positioned below the input group ---
|
||||||
// Popup sized to fit full z-addresses without truncation;
|
// Popup sized to fit full z-addresses without truncation;
|
||||||
// zero horizontal padding so item highlights are flush with edges.
|
// zero horizontal padding so item highlights are flush with edges.
|
||||||
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
|
// Wide enough for full z-addresses, but ceiling it so it doesn't span
|
||||||
|
// the whole window on a large display (leftover -> unused margin).
|
||||||
|
float addrPopupW = std::min(std::max(wrkGroupW, availWidth * 0.55f), 640.0f * dp);
|
||||||
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
|
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
|
||||||
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
|
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
|
||||||
|
|||||||
@@ -399,7 +399,9 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
|||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
// A bit wider than the old 25%/180dp so the pool rows fit the "<hashrate> N% fee" text.
|
// A bit wider than the old 25%/180dp so the pool rows fit the "<hashrate> N% fee" text.
|
||||||
float leftW = std::clamp(availWidth * 0.30f, 210.0f * dp, availWidth * 0.45f);
|
// Ratio ceiling (0.45) never binds on a wide window, so give leftW an
|
||||||
|
// absolute cap too — the pool card doesn't need to be enormous.
|
||||||
|
float leftW = std::clamp(availWidth * 0.30f, 210.0f * dp, 440.0f * dp);
|
||||||
float colGap = gap;
|
float colGap = gap;
|
||||||
float rightW = availWidth - leftW - colGap;
|
float rightW = availWidth - leftW - colGap;
|
||||||
|
|
||||||
@@ -414,6 +416,15 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
|||||||
RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad,
|
RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad,
|
||||||
leftMin, leftMax, s_pool_url, s_pool_settings_dirty);
|
leftMin, leftMax, s_pool_url, s_pool_settings_dirty);
|
||||||
|
|
||||||
|
// rightW is uncapped, so the chart/hint band it draws would sprawl the
|
||||||
|
// full panel on a wide window. Constrain just the drawn content to a
|
||||||
|
// centered ~1000dp band inside the panel's padded content region; the
|
||||||
|
// glass panel itself still fills rightW, leftover -> side margin.
|
||||||
|
const float rightContentW = rightW - pad * 2.0f;
|
||||||
|
const float chartBandW = std::min(rightContentW, 1000.0f * dp);
|
||||||
|
const float chartBandX = rightMin.x + pad
|
||||||
|
+ std::max(0.0f, (rightContentW - chartBandW) * 0.5f);
|
||||||
|
|
||||||
// Right panel: live log (if toggled + available) else the sparkline.
|
// Right panel: live log (if toggled + available) else the sparkline.
|
||||||
if (showLogView) {
|
if (showLogView) {
|
||||||
float logPad = pad * 0.5f;
|
float logPad = pad * 0.5f;
|
||||||
@@ -422,14 +433,14 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
|||||||
state.pool_mining.log_lines, "##PoolLogText");
|
state.pool_mining.log_lines, "##PoolLogText");
|
||||||
} else if (hasChartContent) {
|
} else if (hasChartContent) {
|
||||||
DrawHashrateSparkline(dl,
|
DrawHashrateSparkline(dl,
|
||||||
ImVec2(rightMin.x + pad, rightMin.y + statRowH * 0.5f),
|
ImVec2(chartBandX, rightMin.y + statRowH * 0.5f),
|
||||||
ImVec2(rightMax.x - pad, rightMax.y),
|
ImVec2(chartBandX + chartBandW, rightMax.y),
|
||||||
chartHistory, capFont, dp);
|
chartHistory, capFont, dp);
|
||||||
} else {
|
} else {
|
||||||
const char* hint = TR("mining_chart_start");
|
const char* hint = TR("mining_chart_start");
|
||||||
ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint);
|
ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint);
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2((rightMin.x + rightMax.x - hs.x) * 0.5f,
|
ImVec2(chartBandX + (chartBandW - hs.x) * 0.5f,
|
||||||
(rightMin.y + rightMax.y - hs.y) * 0.5f),
|
(rightMin.y + rightMax.y - hs.y) * 0.5f),
|
||||||
OnSurfaceDisabled(), hint);
|
OnSurfaceDisabled(), hint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,9 +93,12 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
const bool connected = ws.connected;
|
const bool connected = ws.connected;
|
||||||
ImDrawList* sdl = ImGui::GetWindowDrawList();
|
ImDrawList* sdl = ImGui::GetWindowDrawList();
|
||||||
const float panelH = 56.0f * dp;
|
const float panelH = 56.0f * dp;
|
||||||
const float panelW = ImGui::GetContentRegionAvail().x;
|
const float availPanelW = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float panelW = std::min(1000.0f * dp, availPanelW);
|
||||||
|
const float panelOffsetX = std::max(0.0f, (availPanelW - panelW) * 0.5f);
|
||||||
const float pad2 = 12.0f * dp;
|
const float pad2 = 12.0f * dp;
|
||||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||||
|
pMin.x += panelOffsetX;
|
||||||
ImVec2 pMax(pMin.x + panelW, pMin.y + panelH);
|
ImVec2 pMax(pMin.x + panelW, pMin.y + panelH);
|
||||||
GlassPanelSpec sspec; sspec.rounding = 8.0f * dp;
|
GlassPanelSpec sspec; sspec.rounding = 8.0f * dp;
|
||||||
DrawGlassPanel(sdl, pMin, pMax, sspec);
|
DrawGlassPanel(sdl, pMin, pMax, sspec);
|
||||||
@@ -150,7 +153,7 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
Primary(), barH * 0.5f);
|
Primary(), barH * 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(panelW, panelH)); // reserve the panel (proper boundary growth)
|
ImGui::Dummy(ImVec2(availPanelW, panelH)); // reserve the full row (proper boundary growth)
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +177,8 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
{
|
{
|
||||||
const float availW = ImGui::GetContentRegionAvail().x;
|
const float availW = ImGui::GetContentRegionAvail().x;
|
||||||
const float addBtnW = 80.0f * dp;
|
const float addBtnW = 80.0f * dp;
|
||||||
const float urlW = (availW - addBtnW - 16.0f * dp) * 0.6f;
|
const float urlW = std::min(550.0f * dp, (availW - addBtnW - 16.0f * dp) * 0.6f);
|
||||||
const float lblW = (availW - addBtnW - 16.0f * dp) * 0.4f;
|
const float lblW = std::min(300.0f * dp, (availW - addBtnW - 16.0f * dp) * 0.4f);
|
||||||
ImGui::SetNextItemWidth(urlW);
|
ImGui::SetNextItemWidth(urlW);
|
||||||
ImGui::InputTextWithHint("##LiteAddUrl", TR("lite_net_add_url_hint"), s_addUrl, sizeof(s_addUrl));
|
ImGui::InputTextWithHint("##LiteAddUrl", TR("lite_net_add_url_hint"), s_addUrl, sizeof(s_addUrl));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -236,8 +239,11 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
auto it = probe.find(sv.url);
|
auto it = probe.find(sv.url);
|
||||||
if (it != probe.end()) pr = it->second;
|
if (it != probe.end()) pr = it->second;
|
||||||
|
|
||||||
const float cardW = ImGui::GetContentRegionAvail().x;
|
const float availCardW = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float cardW = std::min(1000.0f * dp, availCardW);
|
||||||
|
const float cardOffsetX = std::max(0.0f, (availCardW - cardW) * 0.5f);
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
|
cardMin.x += cardOffsetX;
|
||||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||||
const float rnd = 8.0f * dp;
|
const float rnd = 8.0f * dp;
|
||||||
const float cardBtnW = cardW - hideW;
|
const float cardBtnW = cardW - hideW;
|
||||||
@@ -326,8 +332,10 @@ void RenderLiteNetworkTab(App* app)
|
|||||||
|
|
||||||
// Advance to the next card via a real item (Dummy) below the card, so the scroll region's
|
// Advance to the next card via a real item (Dummy) below the card, so the scroll region's
|
||||||
// content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.
|
// content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
// Reset X to the (unshifted) region origin so next card's GetContentRegionAvail() isn't
|
||||||
ImGui::Dummy(ImVec2(cardW, gap));
|
// cumulatively narrowed by this card's centering offset.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x - cardOffsetX, cardMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(availCardW, gap));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Visible servers ────────────────────────────────────────────────────────
|
// ── Visible servers ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -581,21 +581,34 @@ void RenderReceiveTab(App* app)
|
|||||||
// MAIN CARD — single glass panel (channel split like Send tab)
|
// MAIN CARD — single glass panel (channel split like Send tab)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
{
|
||||||
|
// Cap + center the card so the address/amount column stops stretching while the
|
||||||
|
// QR plateaus. cardW never exceeds formW (only shrinks); leftover becomes margin.
|
||||||
|
// The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
|
||||||
|
float cardDp = Layout::dpiScale();
|
||||||
|
float cardW = std::min(formW, 860.0f * cardDp);
|
||||||
|
float cardLeftX = ImGui::GetCursorScreenPos().x;
|
||||||
|
float cardOffsetX = std::max(0.0f, (formW - cardW) * 0.5f);
|
||||||
|
if (cardOffsetX > 0.0f)
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
|
||||||
|
|
||||||
ImVec2 containerMin = ImGui::GetCursorScreenPos();
|
ImVec2 containerMin = ImGui::GetCursorScreenPos();
|
||||||
float pad = Layout::spacingLg();
|
float pad = Layout::spacingLg();
|
||||||
float innerW = formW - pad * 2;
|
float innerW = cardW - pad * 2;
|
||||||
float innerGap = Layout::spacingLg();
|
float innerGap = Layout::spacingLg();
|
||||||
|
|
||||||
// Channel split: content on ch1, glass background on ch0
|
// Channel split: content on ch1, glass background on ch0
|
||||||
dl->ChannelsSplit(2);
|
dl->ChannelsSplit(2);
|
||||||
dl->ChannelsSetCurrent(1);
|
dl->ChannelsSetCurrent(1);
|
||||||
|
|
||||||
ImGui::Indent(pad);
|
// Indent carries the centering offset too, so ImGui auto-layout content (the address
|
||||||
|
// dropdown/inputs/chips) lands at containerMin.x + pad — matching the hand-drawn geometry
|
||||||
|
// (glass panel / QR column) that keys off the offset containerMin.x.
|
||||||
|
ImGui::Indent(pad + cardOffsetX);
|
||||||
ImGui::Dummy(ImVec2(0, pad)); // top padding
|
ImGui::Dummy(ImVec2(0, pad)); // top padding
|
||||||
|
|
||||||
// ---- ADDRESS DROPDOWN + QR CODE — side by side ----
|
// ---- ADDRESS DROPDOWN + QR CODE — side by side ----
|
||||||
{
|
{
|
||||||
float qrColW = innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size;
|
float qrColW = std::min(innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size, 340.0f * cardDp);
|
||||||
float colGap = Layout::spacingLg();
|
float colGap = Layout::spacingLg();
|
||||||
float addrColW = innerW - qrColW - colGap;
|
float addrColW = innerW - qrColW - colGap;
|
||||||
float qrColX = containerMin.x + pad + addrColW + colGap;
|
float qrColX = containerMin.x + pad + addrColW + colGap;
|
||||||
@@ -876,7 +889,7 @@ void RenderReceiveTab(App* app)
|
|||||||
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
|
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
|
||||||
float footerH = innerGap + actionBtnH + pad;
|
float footerH = innerGap + actionBtnH + pad;
|
||||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||||
float targetCardH = std::min(Layout::mainCardTargetH(formW, vScale), recvCardCapH);
|
float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
|
||||||
float footerTopH = targetCardH - footerH;
|
float footerTopH = targetCardH - footerH;
|
||||||
if (currentCardH < footerTopH) {
|
if (currentCardH < footerTopH) {
|
||||||
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
|
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
|
||||||
@@ -887,7 +900,7 @@ void RenderReceiveTab(App* app)
|
|||||||
{
|
{
|
||||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
||||||
dl->AddLine(ImVec2(containerMin.x + pad, divPos.y),
|
dl->AddLine(ImVec2(containerMin.x + pad, divPos.y),
|
||||||
ImVec2(containerMin.x + formW - pad, divPos.y),
|
ImVec2(containerMin.x + cardW - pad, divPos.y),
|
||||||
ImGui::GetColorU32(Divider()), S.drawElement("tabs.receive", "divider-thickness").size);
|
ImGui::GetColorU32(Divider()), S.drawElement("tabs.receive", "divider-thickness").size);
|
||||||
}
|
}
|
||||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||||
@@ -967,24 +980,26 @@ void RenderReceiveTab(App* app)
|
|||||||
|
|
||||||
// Bottom padding
|
// Bottom padding
|
||||||
ImGui::Dummy(ImVec2(0, pad));
|
ImGui::Dummy(ImVec2(0, pad));
|
||||||
ImGui::Unindent(pad);
|
ImGui::Unindent(pad + cardOffsetX);
|
||||||
|
|
||||||
// Enforce shared card height (matches QR-driven target), capped so the reserved
|
// Enforce shared card height (matches QR-driven target), capped so the reserved
|
||||||
// RECENT RECEIVED slice below stays on-screen at HiDPI.
|
// RECENT RECEIVED slice below stays on-screen at HiDPI.
|
||||||
{
|
{
|
||||||
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
|
||||||
float targetCardH = std::min(Layout::mainCardTargetH(formW, vScale), recvCardCapH);
|
float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
|
||||||
if (currentCardH < targetCardH)
|
if (currentCardH < targetCardH)
|
||||||
ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
|
ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw glass panel background on channel 0
|
// Draw glass panel background on channel 0
|
||||||
ImVec2 containerMax(containerMin.x + formW, ImGui::GetCursorScreenPos().y);
|
ImVec2 containerMax(containerMin.x + cardW, ImGui::GetCursorScreenPos().y);
|
||||||
dl->ChannelsSetCurrent(0);
|
dl->ChannelsSetCurrent(0);
|
||||||
DrawGlassPanel(dl, containerMin, containerMax, glassSpec);
|
DrawGlassPanel(dl, containerMin, containerMax, glassSpec);
|
||||||
dl->ChannelsMerge();
|
dl->ChannelsMerge();
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
|
// Restore the original (uncapped) left edge so RECENT RECEIVED below renders
|
||||||
|
// full-width as-is, unaffected by the card's centering offset.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardLeftX, containerMax.y));
|
||||||
ImGui::Dummy(ImVec2(formW, 0));
|
ImGui::Dummy(ImVec2(formW, 0));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1266,6 +1266,12 @@ void RenderSendTab(App* app)
|
|||||||
float contentStartY = ImGui::GetCursorPosY();
|
float contentStartY = ImGui::GetCursorPosY();
|
||||||
|
|
||||||
float formAvailW = ImGui::GetContentRegionAvail().x;
|
float formAvailW = ImGui::GetContentRegionAvail().x;
|
||||||
|
// The compose form reads best as a centered fixed-width column, not edge-to-edge.
|
||||||
|
// Cap the card to a readable form width and center it; the recent-sends list below
|
||||||
|
// deliberately keeps the full column width (formAvailW).
|
||||||
|
const float sendDp = Layout::dpiScale();
|
||||||
|
float formCardW = std::min(formAvailW, 760.0f * sendDp);
|
||||||
|
float formOffsetX = std::max(0.0f, (formAvailW - formCardW) * 0.5f);
|
||||||
float formW = formAvailW;
|
float formW = formAvailW;
|
||||||
ImGui::BeginGroup();
|
ImGui::BeginGroup();
|
||||||
|
|
||||||
@@ -1280,7 +1286,15 @@ void RenderSendTab(App* app)
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// COMPOSE FORM — single container for all fields
|
// COMPOSE FORM — single container for all fields
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
// Full-column left edge, restored after the centered card so the recent-sends
|
||||||
|
// list below spans the full width again.
|
||||||
|
float formLeftX = ImGui::GetCursorPosX();
|
||||||
{
|
{
|
||||||
|
// Center the capped compose card: offset the cursor by the leftover half-margin,
|
||||||
|
// then derive every field/divider/button from the card width (not the full column).
|
||||||
|
float formW = formCardW;
|
||||||
|
if (formOffsetX > 0.0f)
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + formOffsetX);
|
||||||
ImVec2 containerMin = ImGui::GetCursorScreenPos();
|
ImVec2 containerMin = ImGui::GetCursorScreenPos();
|
||||||
float pad = Layout::spacingLg();
|
float pad = Layout::spacingLg();
|
||||||
float innerW = formW - pad * 2;
|
float innerW = formW - pad * 2;
|
||||||
@@ -1293,8 +1307,10 @@ void RenderSendTab(App* app)
|
|||||||
dl->ChannelsSplit(2);
|
dl->ChannelsSplit(2);
|
||||||
dl->ChannelsSetCurrent(1);
|
dl->ChannelsSetCurrent(1);
|
||||||
|
|
||||||
// Indent content by pad so every line is inset from the card edges
|
// Indent content by pad so every line is inset from the card edges. Fold in formOffsetX so the
|
||||||
ImGui::Indent(pad);
|
// auto-layout fields land inside the centered card (Indent() positions from the window's left, so
|
||||||
|
// without this the fields would sit at the un-offset column while the glass card is centered).
|
||||||
|
ImGui::Indent(pad + formOffsetX);
|
||||||
ImGui::Dummy(ImVec2(0, pad * vScale)); // top padding
|
ImGui::Dummy(ImVec2(0, pad * vScale)); // top padding
|
||||||
|
|
||||||
// ---- SOURCE ADDRESS ----
|
// ---- SOURCE ADDRESS ----
|
||||||
@@ -1593,7 +1609,7 @@ void RenderSendTab(App* app)
|
|||||||
|
|
||||||
// Add bottom padding
|
// Add bottom padding
|
||||||
ImGui::Dummy(ImVec2(0, pad * vScale));
|
ImGui::Dummy(ImVec2(0, pad * vScale));
|
||||||
ImGui::Unindent(pad);
|
ImGui::Unindent(pad + formOffsetX);
|
||||||
|
|
||||||
// Enforce shared card height (matches receive tab)
|
// Enforce shared card height (matches receive tab)
|
||||||
{
|
{
|
||||||
@@ -1626,7 +1642,8 @@ void RenderSendTab(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- RECENT SENDS ----
|
// ---- RECENT SENDS ---- (full column width; reset X off the centered card)
|
||||||
|
ImGui::SetCursorPosX(formLeftX);
|
||||||
RenderRecentSends(state, formW, capFont, app);
|
RenderRecentSends(state, formW, capFont, app);
|
||||||
|
|
||||||
ImGui::EndGroup();
|
ImGui::EndGroup();
|
||||||
|
|||||||
Reference in New Issue
Block a user