From 79d8f0d809477644257988812788479606ac55b8 Mon Sep 17 00:00:00 2001 From: dan_s Date: Sun, 12 Apr 2026 16:34:31 -0500 Subject: [PATCH] refactor: rewrite sidebar layout with two-pass architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace fragile Dummy()-based cursor flow with a deterministic two-pass layout system: - Pass 1: compute exact Y positions for all elements (pure math) - Pass 2: render at computed positions using SetCursorScreenPos + draw list Eliminates the dual-coordinate mismatch that caused persistent centering and overflow bugs. Height is computed once, not estimated then measured. Also tune sidebar spacing via ui.toml: - button-spacing: 4 → 6 - section-gap: 4 → 8 - Add section-label-pad-bottom (4px) below category labels - bottom-padding: 0 → 4 --- res/themes/ui.toml | 7 +- src/app.cpp | 17 +- src/ui/sidebar.h | 435 +++++++++++++++++++-------------------------- 3 files changed, 202 insertions(+), 257 deletions(-) diff --git a/res/themes/ui.toml b/res/themes/ui.toml index 5f9ae45..27f4759 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -1230,8 +1230,9 @@ width = { size = 140.0 } collapsed-width = { size = 64.0 } collapse-anim-speed = { size = 10.0 } auto-collapse-threshold = { size = 800.0 } -section-gap = { size = 4.0 } +section-gap = { size = 8.0 } section-label-pad-left = { size = 16.0 } +section-label-pad-bottom = { size = 4.0 } item-height = { size = 36.0 } item-pad-x = { size = 8.0 } min-height = { size = 360.0 } @@ -1248,8 +1249,8 @@ icon-half-size = { size = 7.0 } icon-label-gap = { size = 8.0 } badge-radius-dot = { size = 4.0 } badge-radius-number = { size = 8.0 } -button-spacing = { size = 4.0 } -bottom-padding = { size = 0.0 } +button-spacing = { size = 6.0 } +bottom-padding = { size = 4.0 } exit-icon-gap = { size = 4.0 } cutout-shadow-alpha = { size = 55 } cutout-highlight-alpha = { size = 8 } diff --git a/src/app.cpp b/src/app.cpp index 5194483..aa4bfbe 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1507,8 +1507,16 @@ void App::renderStatusBar() } if (s_blocks_per_sec > 0.1) { - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left, %.0f blk/s)", - state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec); + int eta_sec = (int)(blocksLeft / s_blocks_per_sec); + char eta[32]; + if (eta_sec >= 3600) + snprintf(eta, sizeof(eta), "%dh %dm", eta_sec / 3600, (eta_sec % 3600) / 60); + else if (eta_sec >= 60) + snprintf(eta, sizeof(eta), "%dm %ds", eta_sec / 60, eta_sec % 60); + else + snprintf(eta, sizeof(eta), "%ds", eta_sec); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left, %.0f blk/s, ~%s)", + state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec, eta); } else { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)", state_.sync.verification_progress * 100.0, blocksLeft); @@ -2799,11 +2807,10 @@ void App::renderLoadingOverlay(float contentH) const char* descText = state_.warmup_description.c_str(); ImFont* capFont = Type().caption(); if (!capFont) capFont = ImGui::GetFont(); - // Wrap to barW so long descriptions don't overflow - ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, barW, descText); + ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, descText); dl->AddText(capFont, capFont->LegacySize, ImVec2(wp.x + cx - ts.x * 0.5f, curY), - IM_COL32(160, 160, 160, 200), descText, nullptr, barW); + IM_COL32(160, 160, 160, 200), descText); curY += ts.y + gap; } diff --git a/src/ui/sidebar.h b/src/ui/sidebar.h index 6fdaa54..23e1e3a 100644 --- a/src/ui/sidebar.h +++ b/src/ui/sidebar.h @@ -415,46 +415,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei const float bottomPadding = sde("bottom-padding", 6.0f); const float exitIconGap = sde("exit-icon-gap", 4.0f); const float sbSectionLabelPadLeft = sde("section-label-pad-left", 16.0f); - const float sbItemPadX = sde("item-pad-x", 8.0f); // Base values for responsive scaling const float baseItemHeight = sde("item-height", 46.0f); const float baseNavGap = sde("nav-gap", 20.0f); - const float baseSectionGap = sde("section-gap", 12.0f); - const float baseButtonSpacing = sde("button-spacing", 3.0f); - - // Estimate total sidebar content height at base sizes to detect overflow. - // Fixed chrome: top margin + collapse strip + exit strip + bottom padding - const float fixedChrome = glassMarginY + stripH + stripH + bottomPadding; - // Section labels: 2 sections × (gap + label height + pad) - const float sectionLabelH = 2.0f * (baseSectionGap + 13.0f * dp); - // Nav items: 9 items × (height + spacing) - const float navItemsH = (float)(int)NavPage::Count_ * (baseItemHeight + baseButtonSpacing); - const float baseContentH = fixedChrome + baseNavGap + navItemsH + sectionLabelH; - - // Responsive shrink: if content would overflow, scale down flexible sizes. - // Clamp scale so buttons never shrink below what fits in the minimum - // sidebar height defined in ui.toml ("min-height"). - float sidebarMinHeight = sde("min-height", 360.0f); - float scaleFloor = 0.55f; - if (sidebarMinHeight > fixedChrome) { - float flexH = baseContentH - fixedChrome; - if (flexH > 0.0f) { - float minFlex = sidebarMinHeight - fixedChrome; - scaleFloor = std::max(0.55f, minFlex / flexH); - } - } - float sidebarScale = 1.0f; - if (baseContentH > contentHeight && contentHeight > fixedChrome) { - float flexH = baseContentH - fixedChrome; - float availFlex = contentHeight - fixedChrome; - sidebarScale = std::max(scaleFloor, availFlex / flexH); - } - - const float sbItemHeight = baseItemHeight * sidebarScale; - const float navGap = baseNavGap * sidebarScale; - const float sbSectionGap = baseSectionGap * sidebarScale; - const float buttonSpacing = baseButtonSpacing * sidebarScale; + const float baseSectionGap = sde("section-gap", 8.0f); + const float baseButtonSpacing = sde("button-spacing", 6.0f); + const float sectionLabelPadBot = sde("section-label-pad-bottom", 4.0f); // How "expanded" are we? 0.0 = fully collapsed, 1.0 = fully expanded float expandFrac = (sbWidth > sbCollapsedWidth) @@ -462,7 +429,10 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei : 1.0f; if (expandFrac < 0.0f) expandFrac = 0.0f; if (expandFrac > 1.0f) expandFrac = 1.0f; - bool showLabels = expandFrac > 0.3f; // hide labels during early part of animation + bool showLabels = expandFrac > 0.3f; + + // Font size for section labels (fixed, doesn't scale) + float olFsz = ScaledFontSize(Type().overline()); // Glass panel rounding from responsive schema float glassRounding = [&]() { @@ -470,128 +440,159 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei return (v >= 0 ? v : 8.0f) * dp; }(); + // =================================================================== + // PASS 1: Compute layout — all Y positions relative to panel top + // =================================================================== + // Separate fixed parts (don't scale) from flex parts (scale when tight). + float fixedH = stripH; // collapse strip + for (int i = 0; i < (int)NavPage::Count_; ++i) + if (kNavItems[i].section_label && showLabels) + fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below + fixedH += bottomPadding + stripH; // exit area + + float baseFlexH = baseNavGap; + for (int i = 0; i < (int)NavPage::Count_; ++i) { + if (kNavItems[i].section_label) { + if (showLabels) baseFlexH += baseSectionGap; + else baseFlexH += baseSectionGap * 0.8f; + } + baseFlexH += baseItemHeight + baseButtonSpacing; + } + + // Responsive shrink (only scales flex parts) + float sidebarMinHeight = sde("min-height", 360.0f); + float scaleFloor = (sidebarMinHeight > fixedH && baseFlexH > 0.0f) + ? std::max(0.55f, (sidebarMinHeight - fixedH) / baseFlexH) : 0.55f; + float sidebarScale = 1.0f; + if (fixedH + baseFlexH > contentHeight && baseFlexH > 0.0f) + sidebarScale = std::max(scaleFloor, (contentHeight - fixedH) / baseFlexH); + + const float itemH = baseItemHeight * sidebarScale; + const float navGap = baseNavGap * sidebarScale; + const float sectionGap = baseSectionGap * sidebarScale; + const float btnSpacing = baseButtonSpacing * sidebarScale; + + // Compute Y position for every element (relative to panel top) + float itemY[(int)NavPage::Count_]; + float sectionLabelY[4] = {}; int nSectionLabels = 0; + float separatorY[4] = {}; int nSeparators = 0; + + float curY = stripH + navGap; // after collapse strip + gap + for (int i = 0; i < (int)NavPage::Count_; ++i) { + if (kNavItems[i].section_label) { + if (showLabels) { + curY += sectionGap; + if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY; + curY += olFsz + 2.0f + sectionLabelPadBot; + } else { + curY += sectionGap * 0.4f; + if (nSeparators < 4) separatorY[nSeparators++] = curY; + curY += sectionGap * 0.4f; + } + } + itemY[i] = curY; + curY += itemH + btnSpacing; + } + float exitRelY = curY + bottomPadding; + float panelH = exitRelY + stripH; + + // Vertical centering — offset so panel is centered in the child window + float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f); + if (centerOffset + panelH > contentHeight) + centerOffset = std::max(0.0f, contentHeight - panelH); + + // =================================================================== + // PASS 2: Render using computed positions + // =================================================================== ImGui::BeginChild("##Sidebar", ImVec2(sidebarWidth, contentHeight), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBackground); ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 wp = ImGui::GetWindowPos(); - // Glass card background — inset with rounded corners, matching content cards - float sidebarMarginY = glassMarginY; // top & bottom inset - float sidebarMarginL = glassMarginL; // left inset - float sidebarMarginR = glassMarginR; // right inset (tighter on content side) - // Panel bounds - float panelLeft = wp.x + sidebarMarginL; - float panelRight = wp.x + sidebarWidth - sidebarMarginR; + float panelLeft = wp.x + glassMarginL; + float panelRight = wp.x + sidebarWidth - glassMarginR; + float panelTopY = wp.y + centerOffset; + float panelBotY = std::min(panelTopY + panelH, wp.y + contentHeight - glassMarginY); - // Defer glass panel drawing until we know content height (channel 0 = background) + // Draw list splitter: channel 0 = glass background, channel 1 = content ImDrawListSplitter splitter; splitter.Split(dl, 2); splitter.SetCurrentChannel(dl, 1); - // Top padding (just the margin — collapse button sits at panel top) - ImGui::Dummy(ImVec2(0, sidebarMarginY)); - - // ---- Collapse toggle — flush strip at top of sidebar panel ---- + // ---- Collapse toggle strip (panel top) ---- { - ImVec2 savedCursor = ImGui::GetCursorScreenPos(); - float rnd = glassRounding; - // Strip spans full panel width, sits inside top of panel float stripX = panelLeft; float stripW = panelRight - panelLeft; - float stripY = wp.y + sidebarMarginY; - ImVec2 stripMin(stripX, stripY); - ImVec2 stripMax(stripX + stripW, stripY + stripH); + ImVec2 stripMin(stripX, panelTopY); + ImVec2 stripMax(stripX + stripW, panelTopY + stripH); ImGui::SetCursorScreenPos(stripMin); - if (ImGui::InvisibleButton("##SidebarCollapse", ImVec2(stripW, stripH))) { + if (ImGui::InvisibleButton("##SidebarCollapse", ImVec2(stripW, stripH))) collapsed = !collapsed; - } bool btnHover = ImGui::IsItemHovered(); - // Draw strip background — flush with panel top (no extra rounding, blends in) - ImU32 stripBg = btnHover ? schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)) : IM_COL32(0, 0, 0, 0); if (btnHover) { - dl->AddRectFilled(stripMin, stripMax, stripBg, - rnd, ImDrawFlags_RoundCornersTop); + dl->AddRectFilled(stripMin, stripMax, + schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)), + glassRounding, ImDrawFlags_RoundCornersTop); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } - // Subtle bottom separator - dl->AddLine(ImVec2(stripMin.x + rnd * 0.5f, stripMax.y), - ImVec2(stripMax.x - rnd * 0.5f, stripMax.y), + dl->AddLine(ImVec2(stripMin.x + glassRounding * 0.5f, stripMax.y), + ImVec2(stripMax.x - glassRounding * 0.5f, stripMax.y), schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f); - if (btnHover) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - - // Chevron icon centered in strip ImU32 iconCol = btnHover ? OnSurface() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60)); float cx = stripX + stripW * 0.5f; - float cy = stripY + stripH * 0.5f; - { - ImFont* iconFont = Type().iconSmall(); - const char* chevIcon = collapsed ? ICON_MD_CHEVRON_RIGHT : ICON_MD_CHEVRON_LEFT; - float chevFsz = ScaledFontSize(iconFont); - ImVec2 chevSz = iconFont->CalcTextSizeA(chevFsz, 1000.0f, 0.0f, chevIcon); - dl->AddText(iconFont, chevFsz, - ImVec2(cx - chevSz.x * 0.5f, cy - chevSz.y * 0.5f), iconCol, chevIcon); - } - - ImGui::SetCursorScreenPos(savedCursor); // restore — toggle is an overlay + float cy = panelTopY + stripH * 0.5f; + ImFont* iconFont = Type().iconSmall(); + const char* chevIcon = collapsed ? ICON_MD_CHEVRON_RIGHT : ICON_MD_CHEVRON_LEFT; + float chevFsz = ScaledFontSize(iconFont); + ImVec2 chevSz = iconFont->CalcTextSizeA(chevFsz, 1000.0f, 0.0f, chevIcon); + dl->AddText(iconFont, chevFsz, + ImVec2(cx - chevSz.x * 0.5f, cy - chevSz.y * 0.5f), iconCol, chevIcon); } - // Gap between collapse divider and first nav item - ImGui::Dummy(ImVec2(0, navGap)); + // ---- Section labels / separators ---- + if (showLabels) { + ImFont* olFont = Type().overline(); + float olFontSz = ScaledFontSize(olFont); + int si = 0; + for (int i = 0; i < (int)NavPage::Count_; ++i) { + if (kNavItems[i].section_label && si < nSectionLabels) { + float ly = panelTopY + sectionLabelY[si++]; + ImVec4 olCol = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()); + olCol.w *= expandFrac; + dl->AddText(olFont, olFontSz, + ImVec2(wp.x + sbSectionLabelPadLeft, ly), + ImGui::ColorConvertFloat4ToU32(olCol), NavSectionLabel(kNavItems[i])); + } + } + } else { + for (int si = 0; si < nSeparators; ++si) { + float sy = panelTopY + separatorY[si]; + dl->AddLine(ImVec2(wp.x + btnPadCollapsed, sy), + ImVec2(wp.x + sidebarWidth - btnPadCollapsed, sy), + Divider(), 1.0f); + } + } // ---- Navigation items ---- + float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded; for (int i = 0; i < (int)NavPage::Count_; ++i) { const NavItem& item = kNavItems[i]; - - // Section label (only when expanded) - if (item.section_label && showLabels) { - ImGui::Dummy(ImVec2(0, sbSectionGap)); - ImFont* olFont = Type().overline(); - float labelY = ImGui::GetCursorScreenPos().y; - ImVec4 olCol = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()); - olCol.w *= expandFrac; - float olFsz = ScaledFontSize(olFont); - dl->AddText(olFont, olFsz, - ImVec2(wp.x + sbSectionLabelPadLeft, labelY), - ImGui::ColorConvertFloat4ToU32(olCol), NavSectionLabel(item)); - ImGui::Dummy(ImVec2(0, olFsz + 2.0f)); - } else if (item.section_label && !showLabels) { - // Collapsed: thin separator instead of label - ImGui::Dummy(ImVec2(0, sbSectionGap * 0.4f)); - float sepY2 = ImGui::GetCursorScreenPos().y; - dl->AddLine(ImVec2(wp.x + btnPadCollapsed, sepY2), ImVec2(wp.x + sidebarWidth - btnPadCollapsed, sepY2), - Divider(), 1.0f); - ImGui::Dummy(ImVec2(0, sbSectionGap * 0.4f)); - } - bool selected = (current == item.page); + float btnY = panelTopY + itemY[i]; + float btnRnd = itemH * 0.22f; - ImGui::PushID(i); + ImVec2 indMin(panelLeft + btnPadX, btnY); + ImVec2 indMax(panelRight - btnPadX, btnY + itemH); - float itemH = sbItemHeight; - float btnRnd = itemH * 0.22f; // moderate rounding - float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded; // tighter padding when collapsed - ImVec2 cursor = ImGui::GetCursorScreenPos(); - // Keep button height constant (itemH) so sidebar content height doesn't - // change during collapse animation, which would destabilize centering. - float btnH = itemH; - // Item bounds for icon/label placement (inset) - ImVec2 itemMin(wp.x + sbItemPadX, cursor.y); - ImVec2 itemMax(wp.x + sidebarWidth - sbItemPadX, cursor.y + btnH); - // Button bounds — inset from panel edges for spacing - ImVec2 indMin(panelLeft + btnPadX, cursor.y); - ImVec2 indMax(panelRight - btnPadX, cursor.y + btnH); - - // All buttons are embossed; hover presses halfway, selected presses fully bool hovered = material::IsRectHovered(indMin, indMax); - { - float btnDepth = 0.0f; - if (selected) btnDepth = 1.0f; - else if (hovered) btnDepth = 0.5f; - // Theme effects behind active button + // Glass bevel + theme effects + { + float btnDepth = selected ? 1.0f : (hovered ? 0.5f : 0.0f); if (selected) { auto& fx = effects::ThemeEffects::instance(); fx.drawGlowPulse(dl, indMin, indMax, btnRnd); @@ -600,17 +601,14 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei fx.drawShimmer(dl, indMin, indMax, btnRnd); fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd); } - DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f); DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18); buttonRects.push_back({indMin, indMax, btnRnd}); } - if (hovered) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - } + if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - // Click detection — block pages that require unlock when locked + // Click detection bool pageNeedsUnlock = locked && item.page != NavPage::Console && item.page != NavPage::Peers && @@ -620,76 +618,60 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei changed = true; } - // Icon + label — centered horizontally within button bounds - float iconS = iconHalfSize; // icon half-size in pixels - float iconCY = cursor.y + btnH * 0.5f; - float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f; + // Icon + label + float iconS = iconHalfSize; + float iconCY = btnY + itemH * 0.5f; + float textY = btnY + (itemH - ImGui::GetTextLineHeight()) * 0.5f; ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium()); if (showLabels) { - // Measure total width of icon + gap + label, then center. - // If the translated label is too wide, shrink the font to fit. ImFont* font = selected ? Type().subtitle2() : Type().body2(); - float gap = iconLabelGap; float lblFsz = ScaledFontSize(font); float btnW = indMax.x - indMin.x; - float maxLabelW = btnW - iconS * 2.0f - gap - Layout::spacingXs() * 2; + float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2; ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item)); if (labelSz.x > maxLabelW && maxLabelW > 0) { - float shrink = maxLabelW / labelSz.x; - lblFsz *= shrink; + lblFsz *= maxLabelW / labelSz.x; labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item)); } - float totalW = iconS * 2.0f + gap + labelSz.x; + float totalW = iconS * 2.0f + iconLabelGap + labelSz.x; float btnCX = (indMin.x + indMax.x) * 0.5f; float startX = btnCX - totalW * 0.5f; - float iconCX = startX + iconS; - DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol); + DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol); - float labelX = startX + iconS * 2.0f + gap; ImVec4 lc = ImGui::ColorConvertU32ToFloat4(textCol); lc.w *= expandFrac; - dl->AddText(font, lblFsz, ImVec2(labelX, textY), - ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item)); + dl->AddText(font, lblFsz, + ImVec2(startX + iconS * 2.0f + iconLabelGap, textY), + ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item)); } else { float iconCX = (indMin.x + indMax.x) * 0.5f; DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol); + if (hovered) ImGui::SetTooltip("%s", NavLabel(item)); } - // Tooltip when collapsed + hovered - if (!showLabels && hovered) { - ImGui::SetTooltip("%s", NavLabel(item)); - } - - // ---- Badge indicator ---- + // Badge indicator { int badgeCount = 0; - bool dotOnly = false; + bool dotOnly = false; ImU32 badgeCol = Primary(); ImU32 badgeTextCol = OnPrimary(); if (item.page == NavPage::History && status.unconfirmedTxCount > 0) { badgeCount = status.unconfirmedTxCount; - badgeCol = Warning(); - badgeTextCol = OnWarning(); + badgeCol = Warning(); badgeTextCol = OnWarning(); } else if (item.page == NavPage::Mining && status.miningActive) { - dotOnly = true; - badgeCol = Success(); + dotOnly = true; badgeCol = Success(); } else if (item.page == NavPage::Peers && status.peerCount > 0) { badgeCount = status.peerCount; } if (badgeCount > 0 || dotOnly) { float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber; - float badgeX, badgeY; - - // Upper-right corner of button, offset from edges - badgeX = indMax.x - badgeR - 6.0f; - badgeY = indMin.y + badgeR + 5.0f; - - dl->AddCircleFilled(ImVec2(badgeX, badgeY), badgeR, badgeCol); - + float bx = indMax.x - badgeR - 6.0f; + float by = indMin.y + badgeR + 5.0f; + dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol); if (!dotOnly && showLabels) { char buf[16]; snprintf(buf, sizeof(buf), "%d", badgeCount > 99 ? 99 : badgeCount); @@ -697,120 +679,81 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei float capFsz = ScaledFontSize(capFont); ImVec2 ts = capFont->CalcTextSizeA(capFsz, 1000.0f, 0.0f, buf); dl->AddText(capFont, capFsz, - ImVec2(badgeX - ts.x * 0.5f, badgeY - ts.y * 0.5f), - badgeTextCol, buf); + ImVec2(bx - ts.x * 0.5f, by - ts.y * 0.5f), badgeTextCol, buf); } } } - - ImGui::Dummy(ImVec2(sidebarWidth, btnH + buttonSpacing)); // extra vertical spacing between buttons - ImGui::PopID(); } - // (Pill indicator removed — glass bevel on active button is sufficient) - - // Reserve space for exit strip at panel bottom (drawn as overlay below) - ImGui::Dummy(ImVec2(0, stripH)); - // ---- Keyboard navigation ---- if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) { int idx = static_cast(current); bool nav = false; - - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) { + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) if (idx > 0) { idx--; nav = true; } - } - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) { + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; } - } - - // Toggle collapse with [ key - if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) { + if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) collapsed = !collapsed; - } - - if (nav) { - current = static_cast(idx); - changed = true; - } + if (nav) { current = static_cast(idx); changed = true; } } - // Bottom padding - ImGui::Dummy(ImVec2(0, bottomPadding)); - - // Measure total sidebar content height - float totalContentEndY = ImGui::GetCursorScreenPos().y; - - // Draw glass panel background — capped to sidebar bounds so corners are visible - float panelBottomY = totalContentEndY; - float panelTopY = wp.y + sidebarMarginY; - // Clamp bottom so rounded corners are never clipped by the child window - float maxPanelBottom = wp.y + contentHeight - sidebarMarginY; - if (panelBottomY > maxPanelBottom) panelBottomY = maxPanelBottom; + // ---- Glass panel background (channel 0) ---- splitter.SetCurrentChannel(dl, 0); { - float rnd = glassRounding; ImVec2 panelMin(panelLeft, panelTopY); - ImVec2 panelMax(panelRight, panelBottomY); - GlassPanelSpec sidebarGlass; - sidebarGlass.rounding = rnd; - sidebarGlass.fillAlpha = 14; - sidebarGlass.borderAlpha = 25; - sidebarGlass.borderWidth = 1.0f; - // Record vertex range before drawing the glass panel fill - int glassVtx0 = dl->VtxBuffer.Size; - DrawGlassPanel(dl, panelMin, panelMax, sidebarGlass); - int glassVtx1 = dl->VtxBuffer.Size; + ImVec2 panelMax(panelRight, panelBotY); + GlassPanelSpec glass; + glass.rounding = glassRounding; + glass.fillAlpha = 14; + glass.borderAlpha = 25; + glass.borderWidth = 1.0f; + int vtx0 = dl->VtxBuffer.Size; + DrawGlassPanel(dl, panelMin, panelMax, glass); + int vtx1 = dl->VtxBuffer.Size; - // Punch holes: zero out glass panel vertices that fall inside - // any button rect so the button area doesn't double-up opacity. - for (int vi = glassVtx0; vi < glassVtx1; vi++) { + // Punch holes in glass where buttons are to avoid double opacity + for (int vi = vtx0; vi < vtx1; vi++) { ImDrawVert& v = dl->VtxBuffer[vi]; for (int bi = 0; bi < buttonRects.Size; bi++) { const Rect& r = buttonRects[bi]; - // Inset test slightly (0.5px) to avoid killing edge pixels if (v.pos.x > r.mn.x + 0.5f && v.pos.x < r.mx.x - 0.5f && v.pos.y > r.mn.y + 0.5f && v.pos.y < r.mx.y - 0.5f) { - v.col = (v.col & 0x00FFFFFFu); // zero alpha + v.col = (v.col & 0x00FFFFFFu); break; } } } } - // ---- Exit button — flush strip at bottom of sidebar panel ---- + // ---- Exit button strip (panel bottom) ---- splitter.SetCurrentChannel(dl, 1); { - float rnd = glassRounding; - float exitStripH = stripH; - float exitStripX = panelLeft; - float exitStripW = panelRight - panelLeft; - float exitStripY = panelBottomY - exitStripH; - ImVec2 exitMin(exitStripX, exitStripY); - ImVec2 exitMax(exitStripX + exitStripW, exitStripY + exitStripH); + float exitY = panelTopY + exitRelY; + if (exitY + stripH > panelBotY) exitY = panelBotY - stripH; + float exitX = panelLeft; + float exitW = panelRight - panelLeft; + ImVec2 exitMin(exitX, exitY); + ImVec2 exitMax(exitX + exitW, exitY + stripH); ImGui::SetCursorScreenPos(exitMin); - if (ImGui::InvisibleButton("##ExitBtn", ImVec2(exitStripW, exitStripH))) { + if (ImGui::InvisibleButton("##ExitBtn", ImVec2(exitW, stripH))) status.exitClicked = true; - } bool exitHover = ImGui::IsItemHovered(); - // Hover highlight with rounded bottom corners (matching panel) if (exitHover) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - dl->AddRectFilled(exitMin, exitMax, schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)), - rnd, ImDrawFlags_RoundCornersBottom); + dl->AddRectFilled(exitMin, exitMax, + schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)), + glassRounding, ImDrawFlags_RoundCornersBottom); } - - // Subtle top separator - dl->AddLine(ImVec2(exitMin.x + rnd * 0.5f, exitMin.y), - ImVec2(exitMax.x - rnd * 0.5f, exitMin.y), + dl->AddLine(ImVec2(exitMin.x + glassRounding * 0.5f, exitMin.y), + ImVec2(exitMax.x - glassRounding * 0.5f, exitMin.y), schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f); - // Exit icon (+ label when expanded) centered in strip ImU32 exitCol = exitHover ? Error() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60)); - float cx = exitStripX + exitStripW * 0.5f; - float cy = exitStripY + exitStripH * 0.5f; + float cx = exitX + exitW * 0.5f; + float cy = exitY + stripH * 0.5f; if (showLabels) { ImFont* iconFont = Type().iconSmall(); @@ -819,31 +762,25 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei float eIconFsz = ScaledFontSize(iconFont); float eLblFsz = ScaledFontSize(font); ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon); - ImVec2 labelSz = font->CalcTextSizeA(eLblFsz, 1000.0f, 0.0f, "Exit"); - float gap = exitIconGap; - float totalW = iconSz.x + gap + labelSz.x; + const char* exitLabel = TR("exit"); + ImVec2 labelSz = font->CalcTextSizeA(eLblFsz, 1000.0f, 0.0f, exitLabel); + float totalW = iconSz.x + exitIconGap + labelSz.x; float startX = cx - totalW * 0.5f; - dl->AddText(iconFont, eIconFsz, - ImVec2(startX, cy - iconSz.y * 0.5f), exitCol, exitIcon); - + ImVec2(startX, cy - iconSz.y * 0.5f), exitCol, exitIcon); ImVec4 lc = ImGui::ColorConvertU32ToFloat4(exitCol); lc.w *= expandFrac; dl->AddText(font, eLblFsz, - ImVec2(startX + iconSz.x + gap, cy - labelSz.y * 0.5f), - ImGui::ColorConvertFloat4ToU32(lc), "Exit"); + ImVec2(startX + iconSz.x + exitIconGap, cy - labelSz.y * 0.5f), + ImGui::ColorConvertFloat4ToU32(lc), exitLabel); } else { ImFont* iconFont = Type().iconSmall(); const char* exitIcon = ICON_MD_EXIT_TO_APP; float eIconFsz = ScaledFontSize(iconFont); ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon); dl->AddText(iconFont, eIconFsz, - ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), - exitCol, exitIcon); - } - - if (!showLabels && exitHover) { - ImGui::SetTooltip("Exit"); + ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), exitCol, exitIcon); + if (exitHover) ImGui::SetTooltip("%s", TR("exit")); } }