Tab Bar: Added TabItemButton(), ImGuiTabItemFlags_Leading, ImGuiTabItemFlags_Trailing + demo. (#3291)

(squashed various commits by 2 authors)
This commit is contained in:
Louis Schnellbach 2020-08-03 18:55:51 +02:00 committed by ocornut
parent 29836412e1
commit 4a57a982be
5 changed files with 264 additions and 50 deletions

View File

@ -86,6 +86,11 @@ Other Changes:
generate an unnecessary extra draw call. generate an unnecessary extra draw call.
- Tab Bar: Fixed a small bug where toggling a tab bar from Reorderable to not Reorderable would leave - 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] 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 - Columns: Fix inverted ClipRect being passed to renderer when using certain primitives inside of
a fully clipped column. (#3475) [@szreder] a fully clipped column. (#3475) [@szreder]
- Popups, Tooltips: Fix edge cases issues with positionning popups and tooltips when they are larger than - Popups, Tooltips: Fix edge cases issues with positionning popups and tooltips when they are larger than

View File

@ -655,6 +655,7 @@ namespace ImGui
IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! 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 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. 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 // Logging/Capture
@ -954,7 +955,10 @@ enum ImGuiTabItemFlags_
ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() 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_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_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() // Flags for ImGui::IsWindowFocused()

View File

@ -2354,6 +2354,91 @@ static void ShowDemoWindowLayout()
ImGui::Separator(); ImGui::Separator();
ImGui::TreePop(); ImGui::TreePop();
} }
if (ImGui::TreeNode("TabItem Button"))
{
static int next_id = 0;
static ImVector<int> 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(); ImGui::TreePop();
} }

View File

@ -1703,7 +1703,8 @@ enum ImGuiTabBarFlagsPrivate_
// Extend ImGuiTabItemFlags_ // Extend ImGuiTabItemFlags_
enum ImGuiTabItemFlagsPrivate_ 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) // 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 LastTabContentHeight; // Record the height of contents submitted below the tab bar
float WidthAllTabs; // Actual width of all tabs (locked during layout) float WidthAllTabs; // Actual width of all tabs (locked during layout)
float WidthAllTabsIdeal; // Ideal width if all tabs were visible and not clipped 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 ScrollingAnim;
float ScrollingTarget; float ScrollingTarget;
float ScrollingTargetDistToVisibility; float ScrollingTargetDistToVisibility;

View File

@ -6814,7 +6814,8 @@ ImGuiTabBar::ImGuiTabBar()
SelectedTabId = NextSelectedTabId = VisibleTabId = 0; SelectedTabId = NextSelectedTabId = VisibleTabId = 0;
CurrFrameVisible = PrevFrameVisible = -1; CurrFrameVisible = PrevFrameVisible = -1;
LastTabContentHeight = 0.0f; LastTabContentHeight = 0.0f;
WidthAllTabs = WidthAllTabsIdeal = OffsetNextTab = 0.0f; WidthAllTabs = WidthAllTabsIdeal = 0.0f;
LeadingWidth = TrailingWidth = 0.0f;
ScrollingAnim = ScrollingTarget = ScrollingTargetDistToVisibility = ScrollingSpeed = 0.0f; ScrollingAnim = ScrollingTarget = ScrollingTargetDistToVisibility = ScrollingSpeed = 0.0f;
Flags = ImGuiTabBarFlags_None; Flags = ImGuiTabBarFlags_None;
ReorderRequestTabId = 0; ReorderRequestTabId = 0;
@ -6824,6 +6825,17 @@ ImGuiTabBar::ImGuiTabBar()
LastTabItemIdx = -1; 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) static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs)
{ {
const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; const ImGuiTabItem* a = (const ImGuiTabItem*)lhs;
@ -6950,6 +6962,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
// Garbage collect by compacting list // Garbage collect by compacting list
int tab_dst_n = 0; 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++) for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++)
{ {
ImGuiTabItem* tab = &tab_bar->Tabs[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) 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] = 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++; tab_dst_n++;
} }
if (tab_bar->Tabs.Size != tab_dst_n) if (tab_bar->Tabs.Size != tab_dst_n)
tab_bar->Tabs.resize(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 // Setup next selected tab
ImGuiID scroll_track_selected_tab_id = 0; ImGuiID scroll_track_selected_tab_id = 0;
if (tab_bar->NextSelectedTabId) 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!) // 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; const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0;
if (tab_list_popup_button) 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; scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID;
// Compute ideal widths // 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; float width_total_contents = 0.0f;
ImGuiTabItem* most_recently_selected_tab = NULL; ImGuiTabItem* most_recently_selected_tab = NULL;
bool found_selected_tab_id = false; bool found_selected_tab_id = false;
@ -7003,6 +7028,7 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible); IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible);
if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected)
if (!(tab->Flags & ImGuiTabItemFlags_Button))
most_recently_selected_tab = tab; most_recently_selected_tab = tab;
if (tab->ID == tab_bar->SelectedTabId) if (tab->ID == tab_bar->SelectedTabId)
found_selected_tab_id = true; 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; const bool has_close_button = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) ? false : true;
tab->ContentWidth = TabItemCalcSize(tab_name, has_close_button).x; tab->ContentWidth = TabItemCalcSize(tab_name, has_close_button).x;
width_total_contents += (tab_n > 0 ? g.Style.ItemInnerSpacing.x : 0.0f) + 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 // 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_shrinkable].Index = tab_n;
g.ShrinkWidthBuffer[tab_n].Width = tab->ContentWidth; g.ShrinkWidthBuffer[tab_n_shrinkable].Width = tab->ContentWidth;
tab_n_shrinkable++;
}
} }
// Compute width // Compute width
const float initial_offset_x = 0.0f; // g.Style.ItemInnerSpacing.x; const float width_avail = ImMax(tab_bar->BarRect.GetWidth() - tab_bar->LeadingWidth - tab_bar->TrailingWidth, 0.0f);
const float width_avail = ImMax(tab_bar->BarRect.GetWidth() - initial_offset_x, 0.0f);
float width_excess = (width_avail < width_total_contents) ? (width_total_contents - width_avail) : 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 // If we don't have enough room, resize down the largest tabs first
ShrinkWidths(g.ShrinkWidthBuffer.Data, g.ShrinkWidthBuffer.Size, width_excess); ShrinkWidths(g.ShrinkWidthBuffer.Data, tab_n_shrinkable, width_excess);
for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) 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); tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index].Width = IM_FLOOR(g.ShrinkWidthBuffer[tab_n].Width);
} }
else else
@ -7044,26 +7077,44 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
} }
// Layout all active tabs // Layout all active tabs
float offset_x = initial_offset_x; float offset_x_button_leading = 0.0f;
float offset_x_ideal = offset_x; float offset_x_button_trailing = tab_bar->BarRect.GetWidth() - tab_bar->TrailingWidth + g.Style.ItemInnerSpacing.x;
tab_bar->OffsetNextTab = offset_x; // This is used by non-reorderable tab bar where the submission order is always honored. 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++) for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
{ {
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
tab->Offset = offset_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) if (scroll_track_selected_tab_id == 0 && g.NavJustMovedToId == tab->ID)
scroll_track_selected_tab_id = tab->ID; scroll_track_selected_tab_id = tab->ID;
offset_x += tab->Width + g.Style.ItemInnerSpacing.x; offset_x_regular += width;
offset_x_ideal += tab->ContentWidth + g.Style.ItemInnerSpacing.x; 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 // 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); 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 (scrolling_buttons)
if (ImGuiTabItem* tab_to_select = TabBarScrollingButtons(tab_bar)) // NB: Will alter BarRect.Max.x! if (ImGuiTabItem* scroll_track_selected_tab = TabBarScrollingButtons(tab_bar)) // NB: Will alter BarRect.Max.x and each trailing tab->Offset
scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; 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 we have lost the selected tab, select the next most recently active one
if (found_selected_tab_id == false) if (found_selected_tab_id == false)
@ -7103,6 +7154,9 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar)
ImGuiWindow* window = g.CurrentWindow; ImGuiWindow* window = g.CurrentWindow;
window->DC.CursorPos = tab_bar->BarRect.Min; window->DC.CursorPos = tab_bar->BarRect.Min;
ItemSize(ImVec2(tab_bar->WidthAllTabsIdeal, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y); 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. // 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 // Called on manual closure attempt
void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
{ {
IM_ASSERT(!(tab->Flags & ImGuiTabItemFlags_Button));
if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument)) if (!(tab->Flags & ImGuiTabItemFlags_UnsavedDocument))
{ {
// This will remove a frame of lag for selecting another tab on closure. // 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) static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab)
{ {
if (tab->Flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing))
return;
ImGuiContext& g = *GImGui; 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) 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); int order = tab_bar->GetTabOrder(tab);
float tab_x1 = tab->Offset + (order > 0 ? -margin : 0.0f); 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); float tab_x2 = tab->Offset + tab->Width + (order + 1 < tab_bar->Tabs.Size ? margin : 1.0f);
tab_bar->ScrollingTargetDistToVisibility = 0.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) bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar)
{ {
ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId); ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId);
if (!tab1) if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder))
return false; return false;
//IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools //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) if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size)
return false; return false;
// Reordered TabItem must share the same position flags than target
ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; 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; ImGuiTabItem item_tmp = *tab1;
*tab1 = *tab2; *tab1 = *tab2;
*tab2 = item_tmp; *tab2 = item_tmp;
tab1 = tab2 = NULL;
if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings)
MarkIniSettingsDirty(); MarkIniSettingsDirty();
@ -7262,10 +7326,32 @@ static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar)
{ {
int selected_order = tab_bar->GetTabOrder(tab_item); int selected_order = tab_bar->GetTabOrder(tab_item);
int target_order = selected_order + select_dir; 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; window->DC.CursorPos = backup_cursor_pos;
tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f; 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; 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++) for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++)
{ {
ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; ImGuiTabItem* tab = &tab_bar->Tabs[tab_n];
if (tab->Flags & ImGuiTabItemFlags_Button)
continue;
const char* tab_name = tab_bar->GetTabName(tab); const char* tab_name = tab_bar->GetTabName(tab);
if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID)) if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID))
tab_to_select = tab; tab_to_select = tab;
@ -7310,6 +7399,7 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar)
//------------------------------------------------------------------------- //-------------------------------------------------------------------------
// - BeginTabItem() // - BeginTabItem()
// - EndTabItem() // - EndTabItem()
// - TabItemButton()
// - TabItemEx() [Internal] // - TabItemEx() [Internal]
// - SetTabItemClosed() // - SetTabItemClosed()
// - TabItemCalcSize() [Internal] // - 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()!"); IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!");
return false; 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); bool ret = TabItemEx(tab_bar, label, p_open, flags);
if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) if (ret && !(flags & ImGuiTabItemFlags_NoPushId))
{ {
@ -7358,6 +7450,22 @@ void ImGui::EndTabItem()
window->IDStack.pop_back(); 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) bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags)
{ {
// Layout whole tab bar if not already done // 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; 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) // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented)
if (flags & ImGuiTabItemFlags_NoCloseButton) if (flags & ImGuiTabItemFlags_NoCloseButton)
p_open = NULL; 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_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount);
const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0; const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0;
const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount); const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount);
const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0;
tab->LastFrameVisible = g.FrameCount; tab->LastFrameVisible = g.FrameCount;
tab->Flags = flags; tab->Flags = flags;
@ -7417,19 +7528,13 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
tab->NameOffset = (ImS16)tab_bar->TabsNames.size(); tab->NameOffset = (ImS16)tab_bar->TabsNames.size();
tab_bar->TabsNames.append(label, label + strlen(label) + 1); 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 // Update selected tab
if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0)
if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) if (!tab_bar_appearing || tab_bar->SelectedTabId == 0)
if (!is_tab_button)
tab_bar->NextSelectedTabId = id; // New tabs gets activated 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 if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // SetSelected can only be passed on explicit tab bar
if (!is_tab_button)
tab_bar->NextSelectedTabId = id; tab_bar->NextSelectedTabId = id;
// Lock visibility // Lock visibility
@ -7450,6 +7555,8 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true); PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true);
ItemAdd(ImRect(), id); ItemAdd(ImRect(), id);
PopItemFlag(); PopItemFlag();
if (is_tab_button)
return false;
return tab_contents_visible; return tab_contents_visible;
} }
@ -7461,14 +7568,21 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open,
// Layout // Layout
size.x = tab->Width; size.x = tab->Width;
//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); window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_FLOOR(tab->Offset - tab_bar->ScrollingAnim), 0.0f);
ImVec2 pos = window->DC.CursorPos; ImVec2 pos = window->DC.CursorPos;
ImRect bb(pos, pos + size); 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) // 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) 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; ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos;
ItemSize(bb.GetSize(), style.FramePadding.y); 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 // 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) if (g.DragDropActive)
button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; button_flags |= ImGuiButtonFlags_PressedOnDragDropHold;
bool hovered, held; bool hovered, held;
bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags);
if (pressed) if (pressed && !is_tab_button)
tab_bar->NextSelectedTabId = id; tab_bar->NextSelectedTabId = id;
hovered |= (g.HoveredId == id); hovered |= (g.HoveredId == id);
@ -7534,6 +7648,7 @@ 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. // 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); const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup);
if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1))) if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)))
if (!is_tab_button)
tab_bar->NextSelectedTabId = id; tab_bar->NextSelectedTabId = id;
if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) if (tab_bar->Flags & ImGuiTabBarFlags_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)) if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip))
SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); 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; return tab_contents_visible;
} }
@ -7597,7 +7715,7 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI
const float width = bb.GetWidth(); const float width = bb.GetWidth();
IM_UNUSED(flags); IM_UNUSED(flags);
IM_ASSERT(width > 0.0f); 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 y1 = bb.Min.y + 1.0f;
const float y2 = bb.Max.y - 1.0f; const float y2 = bb.Max.y - 1.0f;
draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); draw_list->PathLineTo(ImVec2(bb.Min.x, y2));