From 325b7f9f430fb8605d401d1fe0b236d7530d1e82 Mon Sep 17 00:00:00 2001 From: Noah Date: Mon, 9 Mar 2026 14:20:32 -0400 Subject: [PATCH] Custom UI styling: hamburger menu, tab spacing, centered labels - Replace dock menu arrow with hamburger icon - Set tab spacing to 0, center tab labels - Taller tab bar with 8px padding for docked windows - Simplified close/collapse button hover (no background rect) - Tab border: simple left-side vertical line - Opaque dock window menu popup Co-Authored-By: Claude Opus 4.6 --- imgui.cpp | 19 +++++++++++++------ imgui_draw.cpp | 11 +++++++++-- imgui_widgets.cpp | 44 ++++++++++++++++++++++---------------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index 71f1937b68a1..a7b0a8309672 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -7736,7 +7736,8 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Lock menu offset so size calculation can use it as menu-bar windows need a minimum size. window->DC.MenuBarOffset.x = ImMax(ImMax(window->WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x); window->DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y; - window->TitleBarHeight = (flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : g.FontSize + g.Style.FramePadding.y * 2.0f; + const float title_bar_padding = (window->DockIsActive) ? 8.0f : g.Style.FramePadding.y; + window->TitleBarHeight = (flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : g.FontSize + title_bar_padding * 2.0f; window->MenuBarHeight = (flags & ImGuiWindowFlags_MenuBar) ? window->DC.MenuBarOffset.y + g.FontSize + g.Style.FramePadding.y * 2.0f : 0.0f; window->FontRefSize = g.FontSize; // Lock this to discourage calling window->CalcFontSize() outside of current window. @@ -18452,12 +18453,16 @@ static void ImGui::DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* ta SetNextWindowPos(ImVec2(node->Pos.x, node->Pos.y + GetFrameHeight()), ImGuiCond_Always, ImVec2(0.0f, 0.0f)); else SetNextWindowPos(ImVec2(node->Pos.x + node->Size.x, node->Pos.y + GetFrameHeight()), ImGuiCond_Always, ImVec2(1.0f, 0.0f)); + ImVec4 popupBg = g.Style.Colors[ImGuiCol_PopupBg]; + popupBg.w = 1.0f; + PushStyleColor(ImGuiCol_PopupBg, popupBg); if (BeginPopup("#WindowMenu")) { node->IsFocused = true; g.DockNodeWindowMenuHandler(&g, node, tab_bar); EndPopup(); } + PopStyleColor(); } // User helper to append/amend into a dock node tab bar. Most commonly used to add e.g. a "+" button. @@ -18857,28 +18862,30 @@ static void ImGui::DockNodeCalcTabBarLayout(const ImGuiDockNode* node, ImRect* o ImGuiContext& g = *GImGui; ImGuiStyle& style = g.Style; - ImRect r = ImRect(node->Pos.x, node->Pos.y, node->Pos.x + node->Size.x, node->Pos.y + g.FontSize + g.Style.FramePadding.y * 2.0f); + const float tab_padding_y = 8.0f; + ImRect r = ImRect(node->Pos.x, node->Pos.y, node->Pos.x + node->Size.x, node->Pos.y + g.FontSize + tab_padding_y * 2.0f); if (out_title_rect) { *out_title_rect = r; } r.Min.x += style.WindowBorderSize; r.Max.x -= style.WindowBorderSize; float button_sz = g.FontSize; + float button_y = r.Min.y + (r.GetHeight() - button_sz) * 0.5f; r.Min.x += style.FramePadding.x; r.Max.x -= style.FramePadding.x; - ImVec2 window_menu_button_pos = ImVec2(r.Min.x, r.Min.y + style.FramePadding.y); + ImVec2 window_menu_button_pos = ImVec2(r.Min.x, button_y); if (node->HasCloseButton) { - if (out_close_button_pos) *out_close_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y); + if (out_close_button_pos) *out_close_button_pos = ImVec2(r.Max.x - button_sz, button_y); r.Max.x -= button_sz + style.ItemInnerSpacing.x; } if (node->HasWindowMenuButton && style.WindowMenuButtonPosition == ImGuiDir_Left) { - r.Min.x += button_sz + style.ItemInnerSpacing.x; + r.Min.x += button_sz + style.FramePadding.x; } else if (node->HasWindowMenuButton && style.WindowMenuButtonPosition == ImGuiDir_Right) { - window_menu_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y); + window_menu_button_pos = ImVec2(r.Max.x - button_sz, button_y); r.Max.x -= button_sz + style.ItemInnerSpacing.x; } if (out_tab_bar_rect) { *out_tab_bar_rect = r; } diff --git a/imgui_draw.cpp b/imgui_draw.cpp index e6f9d9465014..df5ea5ec4bd6 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -4430,8 +4430,15 @@ void ImGui::RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half // and because the saved space means that the left-most tab label can stay at exactly the same position as the label of a loose window. void ImGui::RenderArrowDockMenu(ImDrawList* draw_list, ImVec2 p_min, float sz, ImU32 col) { - draw_list->AddRectFilled(p_min + ImVec2(sz * 0.20f, sz * 0.15f), p_min + ImVec2(sz * 0.80f, sz * 0.30f), col); - RenderArrowPointingAt(draw_list, p_min + ImVec2(sz * 0.50f, sz * 0.85f), ImVec2(sz * 0.30f, sz * 0.40f), ImGuiDir_Down, col); + // Draw a hamburger menu icon (three horizontal lines) + float cx = p_min.x + sz * 0.5f; + float cy = p_min.y + sz * 0.5f; + float half = sz * 0.30f; + float spacing = sz * 0.22f; + float thickness = ImMax(sz * 0.12f, 1.0f); + draw_list->AddLine(ImVec2(cx - half, cy - spacing), ImVec2(cx + half, cy - spacing), col, thickness); + draw_list->AddLine(ImVec2(cx - half, cy), ImVec2(cx + half, cy), col, thickness); + draw_list->AddLine(ImVec2(cx - half, cy + spacing), ImVec2(cx + half, cy + spacing), col, thickness); } static inline float ImAcos01(float x) diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 153b6bc7dc59..9b356fac6a2b 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -903,13 +903,10 @@ bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) return pressed; // Render - ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); - if (hovered) - window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact); - const ImU32 cross_col = GetColorU32(ImGuiCol_Text); + const ImU32 cross_col = (hovered || held) ? GetColorU32(ImGuiCol_Text) : GetColorU32(ImGuiCol_TextDisabled); const ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f); - const float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f; + const float cross_extent = g.FontSize * 0.5f * 0.5f - 1.0f; const float cross_thickness = 1.0f; // FIXME-DPI window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, cross_thickness); window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, cross_thickness); @@ -932,10 +929,7 @@ bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_no // Render //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed); - ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); ImU32 text_col = GetColorU32(ImGuiCol_Text); - if (hovered || held) - window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact); if (dock_node) @@ -9593,8 +9587,9 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection); // Calculate spacing between sections - sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; - sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + const float tab_spacing = 0.0f; + sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? tab_spacing : 0.0f; + sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? tab_spacing : 0.0f; // Setup next selected tab ImGuiID scroll_to_tab_id = 0; @@ -9724,7 +9719,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n]; tab->Offset = tab_offset; tab->NameOffset = -1; - tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f); + tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? tab_spacing : 0.0f); } tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f); tab_offset += section->Spacing; @@ -9985,6 +9980,7 @@ void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* s if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0) return; + const float tab_spacing = 0.0f; const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0); @@ -10475,7 +10471,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, if (is_visible) { ImDrawList* display_draw_list = window->DrawList; - const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed)); + const ImU32 tab_col = GetColorU32(tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed)); TabItemBackground(display_draw_list, bb, flags, tab_col); if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f) { @@ -10572,11 +10568,12 @@ ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsave { ImGuiContext& g = *GImGui; ImVec2 label_size = CalcTextSize(label, NULL, true); - ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f); + const float tab_padding_y = 8.0f; + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x * 2.0f, label_size.y + tab_padding_y * 2.0f); if (has_close_button_or_unsaved_marker) - size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle. + size.x += g.Style.FramePadding.x * 2.0f + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle. else - size.x += g.Style.FramePadding.x + 1.0f; + size.x += g.Style.FramePadding.x * 2.0f + 1.0f; return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y); } @@ -10602,11 +10599,7 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI draw_list->PathFillConvex(col); if (g.Style.TabBorderSize > 0.0f) { - draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2)); - draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9); - draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12); - draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2)); - draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize); + draw_list->AddLine(ImVec2(bb.Min.x + 0.5f, y1), ImVec2(bb.Min.x + 0.5f, y2), IM_COL32(0, 0, 0, 255), g.Style.TabBorderSize); } } @@ -10634,7 +10627,14 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, #endif // Render text label (with clipping + alpha gradient) + unsaved marker - ImRect text_ellipsis_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y); + // Center text vertically within visible tab area (background starts at bb.Min.y + 1.0f) + float visible_top = bb.Min.y + 1.0f; + float visible_height = bb.Max.y - visible_top; + float text_y = visible_top + (visible_height - label_size.y) * 0.5f; + // Center text horizontally within the tab + float avail_width = bb.GetWidth() - frame_padding.x * 2.0f; + float text_offset_x = (avail_width > label_size.x) ? (avail_width - label_size.x) * 0.5f : 0.0f; + ImRect text_ellipsis_clip_bb(bb.Min.x + frame_padding.x + text_offset_x, text_y, bb.Max.x - frame_padding.x, bb.Max.y); // Return clipped state ignoring the close button if (out_text_clipped) @@ -10644,7 +10644,7 @@ void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, } const float button_sz = g.FontSize; - const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y); + const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + (bb.GetHeight() - button_sz) * 0.5f); // Close Button & Unsaved Marker // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap()