diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 1c81c724..d1f5fb2a 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -86,6 +86,11 @@ Other Changes: generate an unnecessary extra draw call. - Tab Bar: Fixed a small bug where toggling a tab bar from Reorderable to not Reorderable would leave tabs reordered in the tab list popup. [@Xipiryon] +- Tab Bar: Added TabItemButton() to submit tab that behave like a button. (#3291) [@Xipiryon] +- Tab Bar: Added ImGuiTabItemFlags_Leading and ImGuiTabItemFlags_Trailing flags to position tabs or button at + either end of the tab bar. Those tabs won't be part of the scrolling region, won't shrink down, and when + reordering cannot be moving outside of their section. Most often used with TabItemButton(). (#3291) [@Xipiryon] +- Tab Bar: Added ImGuiTabItemFlags_NoReorder flag to disable reordering a given tab. - Columns: Fix inverted ClipRect being passed to renderer when using certain primitives inside of a fully clipped column. (#3475) [@szreder] - Popups, Tooltips: Fix edge cases issues with positionning popups and tooltips when they are larger than diff --git a/imgui.h b/imgui.h index 3b003a2b..91a0b162 100644 --- a/imgui.h +++ b/imgui.h @@ -653,8 +653,9 @@ namespace ImGui // Tab Bars, Tabs IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! - IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0);// create a Tab. Returns true if the Tab is selected. + IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0); // create a Tab. Returns true if the Tab is selected. IMGUI_API void EndTabItem(); // only call EndTabItem() if BeginTabItem() returns true! + IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. // Logging/Capture @@ -954,7 +955,10 @@ enum ImGuiTabItemFlags_ ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID(tab->ID)/PopID() on BeginTabItem()/EndTabItem() - ImGuiTabItemFlags_NoTooltip = 1 << 4 // Disable tooltip for the given tab + ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab + ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab + ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) and disable resizing down + ImGuiTabItemFlags_Trailing = 1 << 7 // Enforce the tab position to the right of the tab bar (before the scrolling buttons) and disable resizing down }; // Flags for ImGui::IsWindowFocused() diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 34914e0d..42a3bf31 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -2354,6 +2354,91 @@ static void ShowDemoWindowLayout() ImGui::Separator(); ImGui::TreePop(); } + + if (ImGui::TreeNode("TabItem Button")) + { + static int next_id = 0; + static ImVector tabs_list; + if (next_id == 0) // Initialize with a default tab + for (int i = 0; i < 9; i++) + tabs_list.push_back(next_id++); + + static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown; + ImGui::CheckboxFlags("ImGuiTabBarFlags_Reorderable", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll); + + static bool show_leading_button = true; + static bool show_trailing_button = true; + ImGui::Checkbox("Show Leading TabItemButton()", &show_leading_button); + ImGui::Checkbox("Show Trailing TabItemButton()", &show_trailing_button); + static bool enable_position = false; + ImGui::Checkbox("Enable Leading/Trailing TabItem()", &enable_position); + + if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) + { + if (show_leading_button) + { + if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_NoTooltip)) + tabs_list.push_back(next_id++); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Add a new TabItem() in the tab bar"); + + // Popup Context also works with TabItemButton + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("I'm a popup!"); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + } + + bool close_current_tab = false; + if (show_trailing_button) + { + if (ImGui::TabItemButton("X", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + close_current_tab = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Close the currently selected TabItem()"); + } + + for (int n = 0; n < tabs_list.Size; ) + { + int mod = tabs_list[n] % 3; + ImGuiTabItemFlags flags = mod == 0 ? 0 : mod == 1 ? ImGuiTabItemFlags_Leading : ImGuiTabItemFlags_Trailing; + if (!enable_position) + flags = 0; + + char name[16]; + snprintf(name, IM_ARRAYSIZE(name), "%04d", tabs_list[n]); + bool open = true; + if (ImGui::BeginTabItem(name, &open, flags)) + { + ImGui::Text("This is the %s tab!", name); + ImGui::EndTabItem(); + + if (close_current_tab) + { + open = false; + ImGui::SetTabItemClosed(name); + } + } + + if (!open) + tabs_list.erase(tabs_list.Data + n); + else + ++n; + } + + ImGui::EndTabBar(); + } + ImGui::Separator(); + ImGui::TreePop(); + } ImGui::TreePop(); } diff --git a/imgui_internal.h b/imgui_internal.h index c5b3028b..0563e7c5 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1703,7 +1703,8 @@ enum ImGuiTabBarFlagsPrivate_ // Extend ImGuiTabItemFlags_ enum ImGuiTabItemFlagsPrivate_ { - ImGuiTabItemFlags_NoCloseButton = 1 << 20 // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) + ImGuiTabItemFlags_NoCloseButton = 1 << 20, // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) + ImGuiTabItemFlags_Button = 1 << 21 // [Internal] Used by TabItemButton, change the tab item behavior to mimic a button }; // Storage for one active tab item (sizeof() 28~32 bytes) @@ -1737,7 +1738,8 @@ struct ImGuiTabBar float LastTabContentHeight; // Record the height of contents submitted below the tab bar float WidthAllTabs; // Actual width of all tabs (locked during layout) float WidthAllTabsIdeal; // Ideal width if all tabs were visible and not clipped - float OffsetNextTab; // Distance from BarRect.Min.x, incremented with each BeginTabItem() call, not used if ImGuiTabBarFlags_Reorderable if set. + float LeadingWidth; // Total width used by leading button + float TrailingWidth; // Total width used by trailing button float ScrollingAnim; float ScrollingTarget; float ScrollingTargetDistToVisibility; diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index abc3f16c..79031027 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -6814,7 +6814,8 @@ ImGuiTabBar::ImGuiTabBar() SelectedTabId = NextSelectedTabId = VisibleTabId = 0; CurrFrameVisible = PrevFrameVisible = -1; LastTabContentHeight = 0.0f; - WidthAllTabs = WidthAllTabsIdeal = OffsetNextTab = 0.0f; + WidthAllTabs = WidthAllTabsIdeal = 0.0f; + LeadingWidth = TrailingWidth = 0.0f; ScrollingAnim = ScrollingTarget = ScrollingTargetDistToVisibility = ScrollingSpeed = 0.0f; Flags = ImGuiTabBarFlags_None; ReorderRequestTabId = 0; @@ -6824,6 +6825,17 @@ ImGuiTabBar::ImGuiTabBar() LastTabItemIdx = -1; } +static int IMGUI_CDECL TabItemComparerByPositionAndIndex(const void* lhs, const void* rhs) +{ + const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; + const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; + const int a_position = (a->Flags & ImGuiTabItemFlags_Leading) ? 0 : (a->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1; + const int b_position = (b->Flags & ImGuiTabItemFlags_Leading) ? 0 : (b->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1; + if (a_position != b_position) + return a_position - b_position; + return (int)(a->IndexDuringLayout - b->IndexDuringLayout); +} + static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs) { const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; @@ -6950,6 +6962,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) // Garbage collect by compacting list int tab_dst_n = 0; + bool need_sort_trailing_or_leading = false; for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n]; @@ -6963,11 +6976,21 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) } if (tab_dst_n != tab_src_n) tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n]; + + tab_bar->Tabs[tab_dst_n].IndexDuringLayout = (ImS8)tab_dst_n; + if (tab_dst_n > 0 && (tab_bar->Tabs[tab_dst_n].Flags & ImGuiTabItemFlags_Leading) && !(tab_bar->Tabs[tab_dst_n - 1].Flags & ImGuiTabItemFlags_Leading)) + need_sort_trailing_or_leading = true; + if (tab_dst_n > 0 && (tab_bar->Tabs[tab_dst_n - 1].Flags & ImGuiTabItemFlags_Trailing) && !(tab_bar->Tabs[tab_dst_n].Flags & ImGuiTabItemFlags_Trailing)) + need_sort_trailing_or_leading = true; + tab_dst_n++; } if (tab_bar->Tabs.Size != tab_dst_n) tab_bar->Tabs.resize(tab_dst_n); + if (need_sort_trailing_or_leading) + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByPositionAndIndex); + // Setup next selected tab ImGuiID scroll_track_selected_tab_id = 0; if (tab_bar->NextSelectedTabId) @@ -6989,11 +7012,13 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!) const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0; if (tab_list_popup_button) - if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Max.x! + if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x! scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; // Compute ideal widths - g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size); + g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size); // Reserve for maximum possible number of tabs, + int tab_n_shrinkable = 0; // ..but buttons won't shrink + tab_bar->LeadingWidth = tab_bar->TrailingWidth = 0.0f; float width_total_contents = 0.0f; ImGuiTabItem* most_recently_selected_tab = NULL; bool found_selected_tab_id = false; @@ -7003,7 +7028,8 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible); if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) - most_recently_selected_tab = tab; + if (!(tab->Flags & ImGuiTabItemFlags_Button)) + most_recently_selected_tab = tab; if (tab->ID == tab_bar->SelectedTabId) found_selected_tab_id = true; @@ -7014,22 +7040,29 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) const bool has_close_button = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) ? false : true; tab->ContentWidth = TabItemCalcSize(tab_name, has_close_button).x; - width_total_contents += (tab_n > 0 ? g.Style.ItemInnerSpacing.x : 0.0f) + tab->ContentWidth; - - // Store data so we can build an array sorted by width if we need to shrink tabs down - g.ShrinkWidthBuffer[tab_n].Index = tab_n; - g.ShrinkWidthBuffer[tab_n].Width = tab->ContentWidth; + float width = tab->ContentWidth; + if (tab->Flags & ImGuiTabItemFlags_Leading) + tab_bar->LeadingWidth += width + g.Style.ItemInnerSpacing.x; + else if (tab->Flags & ImGuiTabItemFlags_Trailing) + tab_bar->TrailingWidth += width + g.Style.ItemInnerSpacing.x; + else + { + width_total_contents += width + (tab_n_shrinkable > 0 ? g.Style.ItemInnerSpacing.x : 0.0f); + // Store data so we can build an array sorted by width if we need to shrink tabs down + g.ShrinkWidthBuffer[tab_n_shrinkable].Index = tab_n; + g.ShrinkWidthBuffer[tab_n_shrinkable].Width = tab->ContentWidth; + tab_n_shrinkable++; + } } // Compute width - const float initial_offset_x = 0.0f; // g.Style.ItemInnerSpacing.x; - const float width_avail = ImMax(tab_bar->BarRect.GetWidth() - initial_offset_x, 0.0f); + const float width_avail = ImMax(tab_bar->BarRect.GetWidth() - tab_bar->LeadingWidth - tab_bar->TrailingWidth, 0.0f); float width_excess = (width_avail < width_total_contents) ? (width_total_contents - width_avail) : 0.0f; - if (width_excess > 0.0f && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown)) + if (width_excess > 0.0f && !(tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll)) { // If we don't have enough room, resize down the largest tabs first - ShrinkWidths(g.ShrinkWidthBuffer.Data, g.ShrinkWidthBuffer.Size, width_excess); - for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + ShrinkWidths(g.ShrinkWidthBuffer.Data, tab_n_shrinkable, width_excess); + for (int tab_n = 0; tab_n < tab_n_shrinkable; tab_n++) tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index].Width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width); } else @@ -7044,26 +7077,44 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) } // Layout all active tabs - float offset_x = initial_offset_x; - float offset_x_ideal = offset_x; - tab_bar->OffsetNextTab = offset_x; // This is used by non-reorderable tab bar where the submission order is always honored. + float offset_x_button_leading = 0.0f; + float offset_x_button_trailing = tab_bar->BarRect.GetWidth() - tab_bar->TrailingWidth + g.Style.ItemInnerSpacing.x; + float offset_x_regular = tab_bar->LeadingWidth; + float offset_x_ideal = offset_x_regular; for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; - tab->Offset = offset_x; - if (scroll_track_selected_tab_id == 0 && g.NavJustMovedToId == tab->ID) - scroll_track_selected_tab_id = tab->ID; - offset_x += tab->Width + g.Style.ItemInnerSpacing.x; - offset_x_ideal += tab->ContentWidth + g.Style.ItemInnerSpacing.x; + float width = tab->Width + g.Style.ItemInnerSpacing.x; + if (tab->Flags & ImGuiTabItemFlags_Leading) + { + tab->Offset = offset_x_button_leading; + offset_x_button_leading += width; + } + else if (tab->Flags & ImGuiTabItemFlags_Trailing) + { + tab->Offset = offset_x_button_trailing; + offset_x_button_trailing += width; + } + else + { + tab->Offset = offset_x_regular; + if (scroll_track_selected_tab_id == 0 && g.NavJustMovedToId == tab->ID) + scroll_track_selected_tab_id = tab->ID; + offset_x_regular += width; + offset_x_ideal += tab->ContentWidth + g.Style.ItemInnerSpacing.x; + } } - tab_bar->WidthAllTabs = ImMax(offset_x - g.Style.ItemInnerSpacing.x, 0.0f); - tab_bar->WidthAllTabsIdeal = ImMax(offset_x_ideal - g.Style.ItemInnerSpacing.x, 0.0f); + tab_bar->WidthAllTabs = ImMax(offset_x_regular - g.Style.ItemInnerSpacing.x + tab_bar->TrailingWidth, 0.0f); + tab_bar->WidthAllTabsIdeal = ImMax(offset_x_ideal - g.Style.ItemInnerSpacing.x + tab_bar->TrailingWidth, 0.0f); // Horizontal scrolling buttons const bool scrolling_buttons = (tab_bar->WidthAllTabs > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll); if (scrolling_buttons) - if (ImGuiTabItem* tab_to_select = TabBarScrollingButtons(tab_bar)) // NB: Will alter BarRect.Max.x! - scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; + if (ImGuiTabItem* scroll_track_selected_tab = TabBarScrollingButtons(tab_bar)) // NB: Will alter BarRect.Max.x and each trailing tab->Offset + if (scroll_track_selected_tab->Flags & ImGuiTabItemFlags_Button) + scroll_track_selected_tab_id = scroll_track_selected_tab->ID; + else + scroll_track_selected_tab_id = tab_bar->SelectedTabId = scroll_track_selected_tab->ID; // If we have lost the selected tab, select the next most recently active one if (found_selected_tab_id == false) @@ -7103,6 +7154,9 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) ImGuiWindow* window = g.CurrentWindow; window->DC.CursorPos = tab_bar->BarRect.Min; ItemSize(ImVec2(tab_bar->WidthAllTabsIdeal, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y); + + window->DrawList->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Min + ImVec2(tab_bar->LeadingWidth, tab_bar->BarRect.GetHeight()), IM_COL32(255, 0, 0, 255)); + window->DrawList->AddRect(tab_bar->BarRect.Max - ImVec2(tab_bar->TrailingWidth, tab_bar->BarRect.GetHeight()), tab_bar->BarRect.Max, IM_COL32(0, 255, 0, 255)); } // Dockables uses Name/ID in the global namespace. Non-dockable items use the ID stack. @@ -7149,6 +7203,7 @@ void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) // Called on manual closure attempt void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { + IM_ASSERT(!(tab->Flags & ImGuiTabItemFlags_Button)); if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument)) { // This will remove a frame of lag for selecting another tab on closure. @@ -7176,9 +7231,13 @@ static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling) static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { + if (tab->Flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) + return; + ImGuiContext& g = *GImGui; float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar) int order = tab_bar->GetTabOrder(tab); + float tab_x1 = tab->Offset + (order > 0 ? -margin : 0.0f); float tab_x2 = tab->Offset + tab->Width + (order + 1 < tab_bar->Tabs.Size ? margin : 1.0f); tab_bar->ScrollingTargetDistToVisibility = 0.0f; @@ -7205,7 +7264,7 @@ void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, in bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar) { ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId); - if (!tab1) + if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder)) return false; //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools @@ -7213,11 +7272,16 @@ bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar) if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size) return false; + // Reordered TabItem must share the same position flags than target ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; + if (tab2->Flags & ImGuiTabItemFlags_NoReorder) + return false; + if ((tab1->Flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (tab2->Flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing))) + return false; + ImGuiTabItem item_tmp = *tab1; *tab1 = *tab2; *tab2 = item_tmp; - tab1 = tab2 = NULL; if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) MarkIniSettingsDirty(); @@ -7262,10 +7326,32 @@ static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar) { int selected_order = tab_bar->GetTabOrder(tab_item); int target_order = selected_order + select_dir; - tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; // If we are at the end of the list, still scroll to make our tab visible + + // Skip tab item buttons until another tab item is found or end is reached + while (tab_to_scroll_to == NULL) + { + // If we are at the end of the list, still scroll to make our tab visible + tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; + + // Cross through buttons + // (even if first/last item is a button, return it so we can update the scroll) + if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button) + { + target_order += select_dir; + selected_order += select_dir; + tab_to_scroll_to = (target_order <= 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL; + } + } } window->DC.CursorPos = backup_cursor_pos; + tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f; + for (int i = 0; i < tab_bar->Tabs.Size; ++i) + { + ImGuiTabItem* tab = &tab_bar->Tabs[i]; + if (tab->Flags & ImGuiTabItemFlags_Trailing) + tab->Offset -= scrolling_buttons_width + 1.0f; + } return tab_to_scroll_to; } @@ -7294,6 +7380,9 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (tab->Flags & ImGuiTabItemFlags_Button) + continue; + const char* tab_name = tab_bar->GetTabName(tab); if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID)) tab_to_select = tab; @@ -7310,6 +7399,7 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) //------------------------------------------------------------------------- // - BeginTabItem() // - EndTabItem() +// - TabItemButton() // - TabItemEx() [Internal] // - SetTabItemClosed() // - TabItemCalcSize() [Internal] @@ -7330,6 +7420,8 @@ bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags f IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!"); return false; } + IM_ASSERT_USER_ERROR(!(flags & ImGuiTabItemFlags_Button), "BeginTabItem() Can't be used with button flags, use TabItemButton() instead!"); + bool ret = TabItemEx(tab_bar, label, p_open, flags); if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) { @@ -7358,6 +7450,22 @@ void ImGui::EndTabItem() window->IDStack.pop_back(); } +bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar, "TabItemButton() needs to be called between BeginTabBar() and EndTabBar()!"); + return false; + } + return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder); +} + bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags) { // Layout whole tab bar if not already done @@ -7383,6 +7491,8 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, return false; } + IM_ASSERT(!p_open || (p_open && !(flags & ImGuiTabItemFlags_Button))); // TabItemButton should not have visible content + // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented) if (flags & ImGuiTabItemFlags_NoCloseButton) p_open = NULL; @@ -7410,6 +7520,7 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0; const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount); + const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0; tab->LastFrameVisible = g.FrameCount; tab->Flags = flags; @@ -7417,20 +7528,14 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, tab->NameOffset = (ImS16)tab_bar->TabsNames.size(); tab_bar->TabsNames.append(label, label + strlen(label) + 1); - // If we are not reorderable, always reset offset based on submission order. - // (We already handled layout and sizing using the previous known order, but sizing is not affected by order!) - if (!tab_appearing && !(tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) - { - tab->Offset = tab_bar->OffsetNextTab; - tab_bar->OffsetNextTab += tab->Width + g.Style.ItemInnerSpacing.x; - } - // Update selected tab if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) - tab_bar->NextSelectedTabId = id; // New tabs gets activated + if (!is_tab_button) + tab_bar->NextSelectedTabId = id; // New tabs gets activated if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // SetSelected can only be passed on explicit tab bar - tab_bar->NextSelectedTabId = id; + if (!is_tab_button) + tab_bar->NextSelectedTabId = id; // Lock visibility // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!) @@ -7450,6 +7555,8 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true); ItemAdd(ImRect(), id); PopItemFlag(); + if (is_tab_button) + return false; return tab_contents_visible; } @@ -7461,14 +7568,21 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, // Layout size.x = tab->Width; - window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f); + //if (is_tab_button && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown)) + if (tab->Flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); + else + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f); ImVec2 pos = window->DC.CursorPos; ImRect bb(pos, pos + size); // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation) - bool want_clip_rect = (bb.Min.x < tab_bar->BarRect.Min.x) || (bb.Max.x > tab_bar->BarRect.Max.x); + // If TabBar fitting policy is scroll, then just clip through the BarRect + float offset_leading = (flags & ImGuiTabItemFlags_Leading) ? 0.0f : tab_bar->LeadingWidth; + float offset_trailing = (flags & (ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_Leading)) ? 0.0f : tab_bar->TrailingWidth; // Leading buttons will be clipped by BarRect.Max.x, trailing buttons will be clipped at BarRect.Minx + LeadingsWidth + bool want_clip_rect = (bb.Min.x < tab_bar->BarRect.Min.x + offset_leading) || (bb.Max.x + offset_trailing > tab_bar->BarRect.Max.x); if (want_clip_rect) - PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->BarRect.Min.x), bb.Min.y - 1), ImVec2(tab_bar->BarRect.Max.x, bb.Max.y), true); + PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->BarRect.Min.x + offset_leading), bb.Min.y - 1), ImVec2(tab_bar->BarRect.Max.x - offset_trailing, bb.Max.y), true); ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; ItemSize(bb.GetSize(), style.FramePadding.y); @@ -7483,12 +7597,12 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, } // Click to Select a tab - ImGuiButtonFlags button_flags = (ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowItemOverlap); + ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowItemOverlap); if (g.DragDropActive) button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); - if (pressed) + if (pressed && !is_tab_button) tab_bar->NextSelectedTabId = id; hovered |= (g.HoveredId == id); @@ -7534,7 +7648,8 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget. const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1))) - tab_bar->NextSelectedTabId = id; + if (!is_tab_button) + tab_bar->NextSelectedTabId = id; if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; @@ -7559,6 +7674,9 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip)) SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); + IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected + if (is_tab_button) + return pressed; return tab_contents_visible; } @@ -7597,7 +7715,7 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI const float width = bb.GetWidth(); IM_UNUSED(flags); IM_ASSERT(width > 0.0f); - const float rounding = ImMax(0.0f, ImMin(g.Style.TabRounding, width * 0.5f - 1.0f)); + const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); const float y1 = bb.Min.y + 1.0f; const float y2 = bb.Max.y - 1.0f; draw_list->PathLineTo(ImVec2(bb.Min.x, y2));