refactor: rewrite sidebar layout with two-pass architecture
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
This commit is contained in:
@@ -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 }
|
||||
|
||||
17
src/app.cpp
17
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;
|
||||
}
|
||||
|
||||
|
||||
435
src/ui/sidebar.h
435
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<int>(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<NavPage>(idx);
|
||||
changed = true;
|
||||
}
|
||||
if (nav) { current = static_cast<NavPage>(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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user