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:
2026-08-18 23:15:39 -05:00
co-authored by Claude Opus 4.8
parent ce8c7696d4
commit 9205addf55
11 changed files with 127 additions and 42 deletions
+13 -2
View File
@@ -198,7 +198,15 @@ void App::renderFirstRunWizard() {
const float scrollY = s_wizScroll;
// --- 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);
if (logo_tex_ != 0) {
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.
{
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) {
float trackH = winSize.y - 8.0f * dp;
float thumbH = std::min(trackH, std::max(32.0f * dp, trackH * (winSize.y / s_wizContentH)));