ImGui tab bar and tab item implementation ported into the S&box project. It handles creating BeginTabBar/EndTabBar, tab layout, scrolling, reordering, tab buttons, close buttons, rendering of tab backgrounds and labels, and related utilities.
// Vendored from zeljkovranjes/sbox-imgui; see LICENSE and PROVENANCE.md in this folder.
using Sandbox;
#pragma warning disable CS0649 // Upstream reserves fields for optional widgets and features.
namespace SboxSuperuser.Engine.Vendor.ImGui;
internal static partial class ImGui
{
/// <summary>Internal: tab item without a close button (p_open == null).</summary>
private const ImGuiTabItemFlags TabItemNoCloseButton = (ImGuiTabItemFlags)(1 << 20);
private const ImGuiTabItemFlags TabItemSectionMask = ImGuiTabItemFlags.Leading | ImGuiTabItemFlags.Trailing;
private struct ShrinkWidthItem
{
public int Index;
public float Width;
public float InitialWidth;
}
private struct TabBarSection
{
public int TabCount;
public float Width;
public float Spacing;
}
#region Tab bar
/// <summary>Create and append into a TabBar.</summary>
public static bool BeginTabBar( string strId, ImGuiTabBarFlags flags = ImGuiTabBarFlags.None )
{
var g = G;
var window = GetCurrentWindow();
if ( window.SkipItems )
return false;
int id = window.GetID( strId );
if ( !g.TabBars.TryGetValue( id, out var tabBar ) )
{
tabBar = new ImGuiTabBar { ID = id };
g.TabBars[id] = tabBar;
}
var tabBarBb = new ImRect( window.DC.CursorPos.x, window.DC.CursorPos.y, window.WorkRect.Max.x, window.DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2 );
tabBar.ID = id;
tabBar.SeparatorMinX = tabBarBb.Min.x - ImTrunc( window.WindowPadding.x * 0.5f );
tabBar.SeparatorMaxX = tabBarBb.Max.x + ImTrunc( window.WindowPadding.x * 0.5f );
return BeginTabBarEx( tabBar, tabBarBb, flags );
}
private static bool BeginTabBarEx( ImGuiTabBar tabBar, ImRect tabBarBb, ImGuiTabBarFlags flags )
{
var g = G;
var window = g.CurrentWindow;
g.TabBarStack.Add( tabBar );
PushOverrideID( tabBar.ID );
tabBar.BackupCursorPos = window.DC.CursorPos;
if ( tabBar.CurrFrameVisible == g.FrameCount )
{
window.DC.CursorPos = new Vector2( tabBar.BarRect.Min.x, tabBar.BarRect.Max.y + tabBar.ItemSpacingY );
tabBar.BeginCount++;
return true;
}
if ( (flags & ImGuiTabBarFlags.Reorderable) != (tabBar.Flags & ImGuiTabBarFlags.Reorderable) || (tabBar.TabsAddedNew && (flags & ImGuiTabBarFlags.Reorderable) == 0) )
tabBar.Tabs.Sort( ( a, b ) => a.BeginOrder.CompareTo( b.BeginOrder ) );
tabBar.TabsAddedNew = false;
if ( (flags & ImGuiTabBarFlags.FittingPolicyMask_) == 0 )
flags |= ImGuiTabBarFlags.FittingPolicyDefault_;
tabBar.Flags = flags;
tabBar.BarRect = tabBarBb;
tabBar.WantLayout = true;
tabBar.PrevFrameVisible = tabBar.CurrFrameVisible;
tabBar.CurrFrameVisible = g.FrameCount;
tabBar.PrevTabsContentsHeight = tabBar.CurrTabsContentsHeight;
tabBar.CurrTabsContentsHeight = 0.0f;
tabBar.ItemSpacingY = g.Style.ItemSpacing.y;
tabBar.FramePadding = g.Style.FramePadding;
tabBar.TabsActiveCount = 0;
tabBar.LastTabItemIdx = -1;
tabBar.BeginCount = 1;
window.DC.CursorPos = new Vector2( tabBar.BarRect.Min.x, tabBar.BarRect.Max.y + tabBar.ItemSpacingY );
var col = GetColorU32Internal( ImGuiCol.TabSelected );
if ( g.Style.TabBarBorderSize > 0.0f )
{
float y = tabBar.BarRect.Max.y;
window.DrawList.AddRectFilled( new Vector2( tabBar.SeparatorMinX, y - g.Style.TabBarBorderSize ), new Vector2( tabBar.SeparatorMaxX, y ), col );
}
return true;
}
/// <summary>Only call EndTabBar() if BeginTabBar() returns true!</summary>
public static void EndTabBar()
{
var g = G;
var window = g.CurrentWindow;
if ( window.SkipItems )
return;
var tabBar = g.CurrentTabBar;
if ( tabBar is null )
{
Log.Warning( "ImGui: EndTabBar() called without a matching BeginTabBar()" );
return;
}
if ( tabBar.WantLayout )
TabBarLayout( tabBar );
bool tabBarAppearing = tabBar.PrevFrameVisible + 1 < g.FrameCount;
if ( tabBar.VisibleTabWasSubmitted || tabBar.VisibleTabId == 0 || tabBarAppearing )
{
tabBar.CurrTabsContentsHeight = MathF.Max( window.DC.CursorPos.y - tabBar.BarRect.Max.y, tabBar.CurrTabsContentsHeight );
window.DC.CursorPos = new Vector2( window.DC.CursorPos.x, tabBar.BarRect.Max.y + tabBar.CurrTabsContentsHeight );
}
else
{
window.DC.CursorPos = new Vector2( window.DC.CursorPos.x, tabBar.BarRect.Max.y + tabBar.PrevTabsContentsHeight );
}
if ( tabBar.BeginCount > 1 )
window.DC.CursorPos = tabBar.BackupCursorPos;
tabBar.LastTabItemIdx = -1;
PopID();
g.TabBarStack.RemoveAt( g.TabBarStack.Count - 1 );
}
private static int TabItemGetSectionIdx( ImGuiTabItem tab )
=> (tab.Flags & ImGuiTabItemFlags.Leading) != 0 ? 0 : (tab.Flags & ImGuiTabItemFlags.Trailing) != 0 ? 2 : 1;
private static ImGuiTabItem TabBarFindTabByID( ImGuiTabBar tabBar, int tabId )
{
if ( tabId == 0 )
return null;
foreach ( var t in tabBar.Tabs )
if ( t.ID == tabId )
return t;
return null;
}
private static float TabBarCalcMaxTabWidth() => G.FontSize * 20.0f;
private static Vector2 TabItemCalcSize( string label, bool hasCloseButtonOrUnsavedMarker )
{
var g = G;
var labelSize = CalcTextSize( label, true );
var size = new Vector2( labelSize.x + g.Style.FramePadding.x, labelSize.y + g.Style.FramePadding.y * 2.0f );
if ( hasCloseButtonOrUnsavedMarker )
size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize);
else
size.x += g.Style.FramePadding.x + 1.0f;
return new Vector2( MathF.Min( size.x, TabBarCalcMaxTabWidth() ), size.y );
}
private static void ShrinkWidths( ShrinkWidthItem[] items, int offset, int count, float widthExcess )
{
if ( count <= 0 )
return;
if ( count == 1 )
{
if ( items[offset].Width >= 0.0f )
items[offset].Width = MathF.Max( items[offset].Width - widthExcess, 1.0f );
return;
}
Array.Sort( items, offset, count, Comparer<ShrinkWidthItem>.Create( ( a, b ) =>
{
int d = b.Width.CompareTo( a.Width );
return d != 0 ? d : a.Index.CompareTo( b.Index );
} ) );
int countSameWidth = 1;
while ( widthExcess > 0.0f && countSameWidth < count )
{
while ( countSameWidth < count && items[offset].Width <= items[offset + countSameWidth].Width )
countSameWidth++;
float maxWidthToRemovePerItem = (countSameWidth < count && items[offset + countSameWidth].Width >= 0.0f)
? items[offset].Width - items[offset + countSameWidth].Width
: items[offset].Width - 1.0f;
if ( maxWidthToRemovePerItem <= 0.0f )
break;
float widthToRemovePerItem = MathF.Min( widthExcess / countSameWidth, maxWidthToRemovePerItem );
for ( int n = 0; n < countSameWidth; n++ )
items[offset + n].Width -= widthToRemovePerItem;
widthExcess -= widthToRemovePerItem * countSameWidth;
}
if ( widthExcess > 0.0f && countSameWidth == count )
{
float perItem = widthExcess / count;
for ( int n = 0; n < count; n++ )
items[offset + n].Width = MathF.Max( 1.0f, items[offset + n].Width - perItem );
}
// Round width and redistribute remainder
widthExcess = 0.0f;
for ( int n = 0; n < count; n++ )
{
float rounded = ImTrunc( items[offset + n].Width );
widthExcess += items[offset + n].Width - rounded;
items[offset + n].Width = rounded;
}
for ( int guard = 0; widthExcess > 0.0f && guard < 64; guard++ )
{
bool any = false;
for ( int n = 0; n < count && widthExcess > 0.0f; n++ )
{
float add = MathF.Min( items[offset + n].InitialWidth - items[offset + n].Width, 1.0f );
if ( add <= 0f ) continue;
items[offset + n].Width += add;
widthExcess -= add;
any = true;
}
if ( !any ) break;
}
}
private static void TabBarLayout( ImGuiTabBar tabBar )
{
var g = G;
tabBar.WantLayout = false;
bool scrollToSelectedTab = tabBar.BarRectPrevWidth > tabBar.BarRect.Width;
tabBar.BarRectPrevWidth = tabBar.BarRect.Width;
// Garbage collect tabs that were not submitted
var sections = new TabBarSection[3];
bool needSortBySection = false;
for ( int i = tabBar.Tabs.Count - 1; i >= 0; i-- )
{
var tab = tabBar.Tabs[i];
if ( tab.LastFrameVisible < tabBar.PrevFrameVisible || tab.WantClose )
{
if ( tabBar.VisibleTabId == tab.ID ) tabBar.VisibleTabId = 0;
if ( tabBar.SelectedTabId == tab.ID ) tabBar.SelectedTabId = 0;
if ( tabBar.NextSelectedTabId == tab.ID ) tabBar.NextSelectedTabId = 0;
tabBar.Tabs.RemoveAt( i );
}
}
for ( int n = 0; n < tabBar.Tabs.Count; n++ )
{
var tab = tabBar.Tabs[n];
tab.IndexDuringLayout = n;
int sec = TabItemGetSectionIdx( tab );
if ( n > 0 )
{
int prevSec = TabItemGetSectionIdx( tabBar.Tabs[n - 1] );
if ( sec == 0 && prevSec != 0 ) needSortBySection = true;
if ( prevSec == 2 && sec != 2 ) needSortBySection = true;
}
sections[sec].TabCount++;
}
if ( needSortBySection )
{
var sorted = tabBar.Tabs.OrderBy( TabItemGetSectionIdx ).ThenBy( t => t.IndexDuringLayout ).ToList();
tabBar.Tabs.Clear();
tabBar.Tabs.AddRange( sorted );
}
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;
int scrollToTabId = 0;
if ( tabBar.NextSelectedTabId != 0 )
{
tabBar.SelectedTabId = tabBar.NextSelectedTabId;
tabBar.NextSelectedTabId = 0;
scrollToTabId = tabBar.SelectedTabId;
}
if ( tabBar.ReorderRequestTabId != 0 )
{
if ( TabBarProcessReorder( tabBar ) && tabBar.ReorderRequestTabId == tabBar.SelectedTabId )
scrollToTabId = tabBar.ReorderRequestTabId;
tabBar.ReorderRequestTabId = 0;
}
if ( (tabBar.Flags & ImGuiTabBarFlags.TabListPopupButton) != 0 )
{
var tabToSelect = TabBarTabListPopupButton( tabBar );
if ( tabToSelect is not null )
scrollToTabId = tabBar.SelectedTabId = tabToSelect.ID;
}
var shrinkBufferIndexes = new int[] { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount };
var shrinkBuffer = new ShrinkWidthItem[tabBar.Tabs.Count];
ImGuiTabItem mostRecentlySelectedTab = null;
int currSectionN = -1;
bool foundSelectedTabId = false;
for ( int tabN = 0; tabN < tabBar.Tabs.Count; tabN++ )
{
var tab = tabBar.Tabs[tabN];
if ( (mostRecentlySelectedTab is null || mostRecentlySelectedTab.LastFrameSelected < tab.LastFrameSelected) && (tab.Flags & ImGuiTabItemFlags.Button) == 0 )
mostRecentlySelectedTab = tab;
if ( tab.ID == tabBar.SelectedTabId )
foundSelectedTabId = true;
bool hasCloseOrMarker = (tab.Flags & TabItemNoCloseButton) == 0 || (tab.Flags & ImGuiTabItemFlags.UnsavedDocument) != 0;
tab.ContentWidth = tab.RequestedWidth >= 0.0f ? tab.RequestedWidth : TabItemCalcSize( tab.Label ?? "", hasCloseOrMarker ).x;
int sectionN = TabItemGetSectionIdx( tab );
sections[sectionN].Width += tab.ContentWidth + (sectionN == currSectionN ? g.Style.ItemInnerSpacing.x : 0.0f);
currSectionN = sectionN;
int bi = shrinkBufferIndexes[sectionN]++;
shrinkBuffer[bi] = new ShrinkWidthItem { Index = tabN, Width = tab.ContentWidth, InitialWidth = tab.ContentWidth };
tab.Width = MathF.Max( tab.ContentWidth, 1.0f );
}
tabBar.WidthAllTabsIdeal = 0.0f;
for ( int s = 0; s < 3; s++ )
tabBar.WidthAllTabsIdeal += sections[s].Width + sections[s].Spacing;
// Horizontal scrolling buttons
if ( tabBar.WidthAllTabsIdeal > tabBar.BarRect.Width && tabBar.Tabs.Count > 1 && (tabBar.Flags & ImGuiTabBarFlags.NoTabListScrollingButtons) == 0 && (tabBar.Flags & ImGuiTabBarFlags.FittingPolicyScroll) != 0 )
{
var scrollAndSelectTab = TabBarScrollingButtons( tabBar );
if ( scrollAndSelectTab is not null )
{
scrollToTabId = scrollAndSelectTab.ID;
if ( (scrollAndSelectTab.Flags & ImGuiTabItemFlags.Button) == 0 )
tabBar.SelectedTabId = scrollToTabId;
}
}
// Shrink widths if full tabs don't fit in their allocated space
float section0W = sections[0].Width + sections[0].Spacing;
float section1W = sections[1].Width + sections[1].Spacing;
float section2W = sections[2].Width + sections[2].Spacing;
bool centralSectionIsVisible = section0W + section2W < tabBar.BarRect.Width;
float widthExcess = centralSectionIsVisible
? MathF.Max( section1W - (tabBar.BarRect.Width - section0W - section2W), 0.0f )
: (section0W + section2W) - tabBar.BarRect.Width;
if ( widthExcess >= 1.0f && ((tabBar.Flags & ImGuiTabBarFlags.FittingPolicyResizeDown) != 0 || !centralSectionIsVisible) )
{
int shrinkDataCount = centralSectionIsVisible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount;
int shrinkDataOffset = centralSectionIsVisible ? sections[0].TabCount + sections[2].TabCount : 0;
ShrinkWidths( shrinkBuffer, shrinkDataOffset, shrinkDataCount, widthExcess );
for ( int n = shrinkDataOffset; n < shrinkDataOffset + shrinkDataCount; n++ )
{
var tab = tabBar.Tabs[shrinkBuffer[n].Index];
float shrinkedWidth = ImTrunc( shrinkBuffer[n].Width );
if ( shrinkedWidth < 0.0f )
continue;
shrinkedWidth = MathF.Max( 1.0f, shrinkedWidth );
int sectionN = TabItemGetSectionIdx( tab );
sections[sectionN].Width -= tab.Width - shrinkedWidth;
tab.Width = shrinkedWidth;
}
}
// Layout all active tabs
int sectionTabIndex = 0;
float tabOffset = 0.0f;
tabBar.WidthAllTabs = 0.0f;
for ( int s = 0; s < 3; s++ )
{
var section = sections[s];
if ( s == 2 )
tabOffset = MathF.Min( MathF.Max( 0.0f, tabBar.BarRect.Width - section.Width ), tabOffset );
for ( int tabN = 0; tabN < section.TabCount; tabN++ )
{
var tab = tabBar.Tabs[sectionTabIndex + tabN];
tab.Offset = tabOffset;
tabOffset += tab.Width + (tabN < section.TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f);
}
tabBar.WidthAllTabs += MathF.Max( section.Width + section.Spacing, 0.0f );
tabOffset += section.Spacing;
sectionTabIndex += section.TabCount;
}
if ( !foundSelectedTabId )
tabBar.SelectedTabId = 0;
if ( tabBar.SelectedTabId == 0 && tabBar.NextSelectedTabId == 0 && mostRecentlySelectedTab is not null )
scrollToTabId = tabBar.SelectedTabId = mostRecentlySelectedTab.ID;
tabBar.VisibleTabId = tabBar.SelectedTabId;
tabBar.VisibleTabWasSubmitted = false;
if ( scrollToTabId != 0 )
TabBarScrollToTab( tabBar, scrollToTabId, sections );
else if ( scrollToSelectedTab && tabBar.SelectedTabId != 0 )
TabBarScrollToTab( tabBar, tabBar.SelectedTabId, sections );
// Update scrolling
tabBar.ScrollingAnim = TabBarScrollClamp( tabBar, tabBar.ScrollingAnim );
tabBar.ScrollingTarget = TabBarScrollClamp( tabBar, tabBar.ScrollingTarget );
if ( tabBar.ScrollingAnim != tabBar.ScrollingTarget )
{
tabBar.ScrollingSpeed = MathF.Max( tabBar.ScrollingSpeed, 70.0f * g.FontSize );
tabBar.ScrollingSpeed = MathF.Max( tabBar.ScrollingSpeed, MathF.Abs( tabBar.ScrollingTarget - tabBar.ScrollingAnim ) / 0.3f );
bool teleport = tabBar.PrevFrameVisible + 1 < g.FrameCount || tabBar.ScrollingTargetDistToVisibility > 10.0f * g.FontSize;
tabBar.ScrollingAnim = teleport ? tabBar.ScrollingTarget : ImLinearSweep( tabBar.ScrollingAnim, tabBar.ScrollingTarget, g.IO.DeltaTime * tabBar.ScrollingSpeed );
}
else
{
tabBar.ScrollingSpeed = 0.0f;
}
tabBar.ScrollingRectMinX = tabBar.BarRect.Min.x + sections[0].Width + sections[0].Spacing;
tabBar.ScrollingRectMaxX = tabBar.BarRect.Max.x - sections[2].Width - sections[1].Spacing;
// Actual layout in host window
var window = g.CurrentWindow;
window.DC.CursorPos = tabBar.BarRect.Min;
ItemSize( new Vector2( tabBar.WidthAllTabs, tabBar.BarRect.Height ), tabBar.FramePadding.y );
window.DC.IdealMaxPos = new Vector2( MathF.Max( window.DC.IdealMaxPos.x, tabBar.BarRect.Min.x + tabBar.WidthAllTabsIdeal ), window.DC.IdealMaxPos.y );
}
private static float TabBarScrollClamp( ImGuiTabBar tabBar, float scrolling )
{
scrolling = MathF.Min( scrolling, tabBar.WidthAllTabs - tabBar.BarRect.Width );
return MathF.Max( scrolling, 0.0f );
}
private static void TabBarScrollToTab( ImGuiTabBar tabBar, int tabId, TabBarSection[] sections )
{
var g = G;
var tab = TabBarFindTabByID( tabBar, tabId );
if ( tab is null || (tab.Flags & TabItemSectionMask) != 0 )
return;
float margin = g.FontSize * 1.0f;
int order = tabBar.Tabs.IndexOf( tab );
float scrollableWidth = tabBar.BarRect.Width - sections[0].Width - sections[2].Width - sections[1].Spacing;
float tabX1 = tab.Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f);
float tabX2 = tab.Offset - sections[0].Width + tab.Width + (order + 1 < tabBar.Tabs.Count - sections[2].TabCount ? margin : 1.0f);
tabBar.ScrollingTargetDistToVisibility = 0.0f;
if ( tabBar.ScrollingTarget > tabX1 || tabX2 - tabX1 >= scrollableWidth )
{
tabBar.ScrollingTargetDistToVisibility = MathF.Max( tabBar.ScrollingAnim - tabX2, 0.0f );
tabBar.ScrollingTarget = tabX1;
}
else if ( tabBar.ScrollingTarget < tabX2 - scrollableWidth )
{
tabBar.ScrollingTargetDistToVisibility = MathF.Max( (tabX1 - scrollableWidth) - tabBar.ScrollingAnim, 0.0f );
tabBar.ScrollingTarget = tabX2 - scrollableWidth;
}
}
private static void TabBarQueueReorderFromMousePos( ImGuiTabBar tabBar, ImGuiTabItem srcTab, Vector2 mousePos )
{
var g = G;
if ( (tabBar.Flags & ImGuiTabBarFlags.Reorderable) == 0 )
return;
bool isCentralSection = (srcTab.Flags & TabItemSectionMask) == 0;
float barOffset = tabBar.BarRect.Min.x - (isCentralSection ? tabBar.ScrollingTarget : 0);
int dir = barOffset + srcTab.Offset > mousePos.x ? -1 : +1;
int srcIdx = tabBar.Tabs.IndexOf( srcTab );
int dstIdx = srcIdx;
for ( int i = srcIdx; i >= 0 && i < tabBar.Tabs.Count; i += dir )
{
var dstTab = tabBar.Tabs[i];
if ( (dstTab.Flags & ImGuiTabItemFlags.NoReorder) != 0 )
break;
if ( (dstTab.Flags & TabItemSectionMask) != (srcTab.Flags & TabItemSectionMask) )
break;
dstIdx = i;
float x1 = barOffset + dstTab.Offset - g.Style.ItemInnerSpacing.x;
float x2 = barOffset + dstTab.Offset + dstTab.Width + g.Style.ItemInnerSpacing.x;
if ( (dir < 0 && mousePos.x > x1) || (dir > 0 && mousePos.x < x2) )
break;
}
if ( dstIdx != srcIdx )
{
tabBar.ReorderRequestTabId = srcTab.ID;
tabBar.ReorderRequestOffset = dstIdx - srcIdx;
}
}
private static bool TabBarProcessReorder( ImGuiTabBar tabBar )
{
var tab1 = TabBarFindTabByID( tabBar, tabBar.ReorderRequestTabId );
if ( tab1 is null || (tab1.Flags & ImGuiTabItemFlags.NoReorder) != 0 )
return false;
int idx1 = tabBar.Tabs.IndexOf( tab1 );
int tab2Order = idx1 + tabBar.ReorderRequestOffset;
if ( tab2Order < 0 || tab2Order >= tabBar.Tabs.Count )
return false;
var tab2 = tabBar.Tabs[tab2Order];
if ( (tab2.Flags & ImGuiTabItemFlags.NoReorder) != 0 )
return false;
if ( (tab1.Flags & TabItemSectionMask) != (tab2.Flags & TabItemSectionMask) )
return false;
tabBar.Tabs.RemoveAt( idx1 );
tabBar.Tabs.Insert( tab2Order, tab1 );
return true;
}
private static ImGuiTabItem TabBarScrollingButtons( ImGuiTabBar tabBar )
{
var g = G;
var window = g.CurrentWindow;
var arrowButtonSize = new Vector2( g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f );
float scrollingButtonsWidth = arrowButtonSize.x * 2.0f;
var backupCursorPos = window.DC.CursorPos;
int selectDir = 0;
var arrowCol = g.Style.Colors[(int)ImGuiCol.Text];
arrowCol.w *= 0.5f;
PushStyleColor( ImGuiCol.Text, arrowCol );
PushStyleColor( ImGuiCol.Button, new Vector4( 0, 0, 0, 0 ) );
PushItemFlag( ImGuiItemFlags.ButtonRepeat | ImGuiItemFlags.NoNav, true );
float backupRepeatDelay = g.IO.KeyRepeatDelay;
float backupRepeatRate = g.IO.KeyRepeatRate;
g.IO.KeyRepeatDelay = 0.250f;
g.IO.KeyRepeatRate = 0.200f;
float x = MathF.Max( tabBar.BarRect.Min.x, tabBar.BarRect.Max.x - scrollingButtonsWidth );
window.DC.CursorPos = new Vector2( x, tabBar.BarRect.Min.y );
if ( ArrowButtonEx( "##<", ImGuiDir.Left, arrowButtonSize, ImGuiButtonFlags.PressedOnClick ) )
selectDir = -1;
window.DC.CursorPos = new Vector2( x + arrowButtonSize.x, tabBar.BarRect.Min.y );
if ( ArrowButtonEx( "##>", ImGuiDir.Right, arrowButtonSize, ImGuiButtonFlags.PressedOnClick ) )
selectDir = +1;
PopItemFlag();
PopStyleColor( 2 );
g.IO.KeyRepeatRate = backupRepeatRate;
g.IO.KeyRepeatDelay = backupRepeatDelay;
ImGuiTabItem tabToScrollTo = null;
if ( selectDir != 0 )
{
var tabItem = TabBarFindTabByID( tabBar, tabBar.SelectedTabId );
if ( tabItem is not null )
{
int selectedOrder = tabBar.Tabs.IndexOf( tabItem );
int targetOrder = selectedOrder + selectDir;
for ( int guard = 0; tabToScrollTo is null && guard < tabBar.Tabs.Count + 2; guard++ )
{
tabToScrollTo = tabBar.Tabs[(targetOrder >= 0 && targetOrder < tabBar.Tabs.Count) ? targetOrder : Math.Clamp( selectedOrder, 0, tabBar.Tabs.Count - 1 )];
if ( (tabToScrollTo.Flags & ImGuiTabItemFlags.Button) != 0 )
{
targetOrder += selectDir;
selectedOrder += selectDir;
tabToScrollTo = (targetOrder < 0 || targetOrder >= tabBar.Tabs.Count) ? tabToScrollTo : null;
}
}
}
}
window.DC.CursorPos = backupCursorPos;
tabBar.BarRect.Max.x -= scrollingButtonsWidth + 1.0f;
return tabToScrollTo;
}
private static ImGuiTabItem TabBarTabListPopupButton( ImGuiTabBar tabBar )
{
var g = G;
var window = g.CurrentWindow;
float tabListPopupButtonWidth = g.FontSize + g.Style.FramePadding.y;
var backupCursorPos = window.DC.CursorPos;
window.DC.CursorPos = new Vector2( tabBar.BarRect.Min.x - g.Style.FramePadding.y, tabBar.BarRect.Min.y );
tabBar.BarRect.Min.x += tabListPopupButtonWidth;
var arrowCol = g.Style.Colors[(int)ImGuiCol.Text];
arrowCol.w *= 0.5f;
PushStyleColor( ImGuiCol.Text, arrowCol );
PushStyleColor( ImGuiCol.Button, new Vector4( 0, 0, 0, 0 ) );
bool open = BeginCombo( "##v", null, ImGuiComboFlags.NoPreview | ImGuiComboFlags.HeightLargest );
PopStyleColor( 2 );
ImGuiTabItem tabToSelect = null;
if ( open )
{
foreach ( var tab in tabBar.Tabs )
{
if ( (tab.Flags & ImGuiTabItemFlags.Button) != 0 )
continue;
PushID( tab.ID );
if ( Selectable( LabelText( tab.Label ?? "" ), tabBar.SelectedTabId == tab.ID ) )
tabToSelect = tab;
PopID();
}
EndCombo();
}
window.DC.CursorPos = backupCursorPos;
return tabToSelect;
}
#endregion
#region Tab items
/// <summary>Create a Tab. Returns true if the Tab is selected.</summary>
public static bool BeginTabItem( string label, ImGuiTabItemFlags flags = ImGuiTabItemFlags.None )
{
bool dummy = true;
return BeginTabItemImpl( label, false, ref dummy, flags );
}
/// <summary>Create a Tab with a close button. When closed, <paramref name="open"/> is set to false.</summary>
public static bool BeginTabItem( string label, ref bool open, ImGuiTabItemFlags flags = ImGuiTabItemFlags.None )
=> BeginTabItemImpl( label, true, ref open, flags );
private static bool BeginTabItemImpl( string label, bool hasOpen, ref bool open, ImGuiTabItemFlags flags )
{
var g = G;
var window = g.CurrentWindow;
if ( window.SkipItems )
return false;
var tabBar = g.CurrentTabBar;
if ( tabBar is null )
{
Log.Warning( "ImGui: BeginTabItem() needs to be called between BeginTabBar() and EndTabBar()" );
return false;
}
if ( (flags & ImGuiTabItemFlags.Button) != 0 )
flags &= ~ImGuiTabItemFlags.Button;
bool ret = TabItemEx( tabBar, label, hasOpen, ref open, flags );
if ( ret && (flags & ImGuiTabItemFlags.NoPushId) == 0 )
{
var tab = tabBar.Tabs[tabBar.LastTabItemIdx];
PushOverrideID( tab.ID );
}
return ret;
}
/// <summary>Only call EndTabItem() if BeginTabItem() returns true!</summary>
public static void EndTabItem()
{
var g = G;
var window = g.CurrentWindow;
if ( window.SkipItems )
return;
var tabBar = g.CurrentTabBar;
if ( tabBar is null || tabBar.LastTabItemIdx < 0 || tabBar.LastTabItemIdx >= tabBar.Tabs.Count )
{
Log.Warning( "ImGui: EndTabItem() needs to be called between BeginTabBar() and EndTabBar()" );
return;
}
var tab = tabBar.Tabs[tabBar.LastTabItemIdx];
if ( (tab.Flags & ImGuiTabItemFlags.NoPushId) == 0 )
PopID();
}
/// <summary>Create a Tab behaving like a button. Return true when clicked. Cannot be selected in the tab bar.</summary>
public static bool TabItemButton( string label, ImGuiTabItemFlags flags = ImGuiTabItemFlags.None )
{
var g = G;
var window = g.CurrentWindow;
if ( window.SkipItems )
return false;
var tabBar = g.CurrentTabBar;
if ( tabBar is null )
{
Log.Warning( "ImGui: TabItemButton() needs to be called between BeginTabBar() and EndTabBar()" );
return false;
}
bool dummy = true;
return TabItemEx( tabBar, label, false, ref dummy, flags | ImGuiTabItemFlags.Button | ImGuiTabItemFlags.NoReorder );
}
/// <summary>Notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars).</summary>
public static void SetTabItemClosed( string tabOrDockedWindowLabel )
{
var g = G;
var tabBar = g.CurrentTabBar;
if ( tabBar is null )
return;
int tabId = g.CurrentWindow.GetID( tabOrDockedWindowLabel );
var tab = TabBarFindTabByID( tabBar, tabId );
if ( tab is not null )
tab.WantClose = true;
}
private static void TabBarCloseTab( ImGuiTabBar tabBar, ImGuiTabItem tab )
{
if ( (tab.Flags & ImGuiTabItemFlags.Button) != 0 )
return;
if ( (tab.Flags & (ImGuiTabItemFlags.UnsavedDocument | ImGuiTabItemFlags.NoAssumedClosure)) == 0 )
{
tab.WantClose = true;
if ( tabBar.VisibleTabId == tab.ID )
{
tab.LastFrameVisible = -1;
tabBar.SelectedTabId = tabBar.NextSelectedTabId = 0;
}
}
else
{
if ( tabBar.VisibleTabId != tab.ID )
tabBar.NextSelectedTabId = tab.ID;
}
}
private static bool TabItemEx( ImGuiTabBar tabBar, string label, bool hasOpen, ref bool open, ImGuiTabItemFlags flags )
{
var g = G;
var window = g.CurrentWindow;
if ( tabBar.WantLayout )
{
var backupNextItemData = g.NextItemData;
TabBarLayout( tabBar );
g.NextItemData = backupNextItemData;
}
if ( window.SkipItems )
return false;
var style = g.Style;
int id = window.GetID( label );
if ( hasOpen && !open )
{
ItemAdd( new ImRect(), id, null, ImGuiItemFlags.NoNav );
return false;
}
if ( !hasOpen )
flags |= TabItemNoCloseButton;
var tab = TabBarFindTabByID( tabBar, id );
bool tabIsNew = false;
if ( tab is null )
{
tab = new ImGuiTabItem { ID = id };
tabBar.Tabs.Add( tab );
tabBar.TabsAddedNew = tabIsNew = true;
}
tabBar.LastTabItemIdx = tabBar.Tabs.IndexOf( tab );
var size = TabItemCalcSize( label, hasOpen || (flags & ImGuiTabItemFlags.UnsavedDocument) != 0 );
tab.RequestedWidth = -1.0f;
if ( (g.NextItemData.Flags & ImGuiNextItemDataFlags.HasWidth) != 0 )
size.x = tab.RequestedWidth = g.NextItemData.Width;
if ( tabIsNew )
tab.Width = MathF.Max( 1.0f, size.x );
tab.ContentWidth = size.x;
tab.BeginOrder = tabBar.TabsActiveCount++;
bool tabBarAppearing = tabBar.PrevFrameVisible + 1 < g.FrameCount;
bool tabAppearing = tab.LastFrameVisible + 1 < g.FrameCount;
bool tabJustUnsaved = (flags & ImGuiTabItemFlags.UnsavedDocument) != 0 && (tab.Flags & ImGuiTabItemFlags.UnsavedDocument) == 0;
bool isTabButton = (flags & ImGuiTabItemFlags.Button) != 0;
tab.LastFrameVisible = g.FrameCount;
tab.Flags = flags;
tab.Label = label;
if ( !isTabButton )
{
if ( tabAppearing && (tabBar.Flags & ImGuiTabBarFlags.AutoSelectNewTabs) != 0 && tabBar.NextSelectedTabId == 0 )
if ( !tabBarAppearing || tabBar.SelectedTabId == 0 )
tabBar.NextSelectedTabId = id;
if ( (flags & ImGuiTabItemFlags.SetSelected) != 0 && tabBar.SelectedTabId != id )
tabBar.NextSelectedTabId = id;
}
bool tabContentsVisible = tabBar.VisibleTabId == id;
if ( tabContentsVisible )
tabBar.VisibleTabWasSubmitted = true;
if ( !tabContentsVisible && tabBar.SelectedTabId == 0 && tabBarAppearing )
if ( tabBar.Tabs.Count == 1 && (tabBar.Flags & ImGuiTabBarFlags.AutoSelectNewTabs) == 0 )
tabContentsVisible = true;
if ( tabAppearing && (!tabBarAppearing || tabIsNew) )
{
ItemAdd( new ImRect(), id, null, ImGuiItemFlags.NoNav );
if ( isTabButton )
return false;
return tabContentsVisible;
}
if ( tabBar.SelectedTabId == id )
tab.LastFrameSelected = g.FrameCount;
var backupMainCursorPos = window.DC.CursorPos;
bool isCentralSection = (tab.Flags & TabItemSectionMask) == 0;
size.x = tab.Width;
if ( isCentralSection )
window.DC.CursorPos = tabBar.BarRect.Min + new Vector2( ImTrunc( tab.Offset - tabBar.ScrollingAnim ), 0.0f );
else
window.DC.CursorPos = tabBar.BarRect.Min + new Vector2( tab.Offset, 0.0f );
var pos = window.DC.CursorPos;
var bb = new ImRect( pos, pos + size );
bool wantClipRect = isCentralSection && (bb.Min.x < tabBar.ScrollingRectMinX || bb.Max.x > tabBar.ScrollingRectMaxX);
if ( wantClipRect )
PushClipRect( new Vector2( MathF.Max( bb.Min.x, tabBar.ScrollingRectMinX ), bb.Min.y - 1 ), new Vector2( tabBar.ScrollingRectMaxX, bb.Max.y ), true );
var backupCursorMaxPos = window.DC.CursorMaxPos;
ItemSize( bb.Size, style.FramePadding.y );
window.DC.CursorMaxPos = backupCursorMaxPos;
if ( !ItemAdd( bb, id ) )
{
if ( wantClipRect )
PopClipRect();
window.DC.CursorPos = backupMainCursorPos;
return tabContentsVisible;
}
int closeButtonId = hasOpen ? ImHashStr( "#CLOSE", id ) : 0;
var buttonFlags = (isTabButton ? ImGuiButtonFlags.PressedOnClickRelease : ImGuiButtonFlags.PressedOnClick) | ImGuiButtonFlags.AllowOverlap;
if ( g.DragDropActive )
buttonFlags |= ImGuiButtonFlags.PressedOnDragDropHold;
bool hovered = false, held = false, pressed = false;
// Let the close button (submitted after us) win the hover when it was hovered last frame.
bool closeButtonOnTop = closeButtonId != 0 && (g.HoveredIdPreviousFrame == closeButtonId || g.ActiveId == closeButtonId) && g.ActiveId != id;
if ( !closeButtonOnTop )
pressed = ButtonBehavior( bb, id, out hovered, out held, buttonFlags );
if ( pressed && !isTabButton )
tabBar.NextSelectedTabId = id;
// Drag to reorder
if ( held && !tabAppearing && IsMouseDragging( ImGuiMouseButton.Left ) )
{
if ( !g.DragDropActive && (tabBar.Flags & ImGuiTabBarFlags.Reorderable) != 0 )
{
if ( g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x )
TabBarQueueReorderFromMousePos( tabBar, tab, g.IO.MousePos );
else if ( g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x )
TabBarQueueReorderFromMousePos( tabBar, tab, g.IO.MousePos );
}
}
// Render tab shape
var displayDrawList = window.DrawList;
bool isHoveredAny = hovered || held || g.HoveredId == closeButtonId && closeButtonId != 0;
var tabCol = GetColorU32Internal( (held || isHoveredAny) ? ImGuiCol.TabHovered : tabContentsVisible ? ImGuiCol.TabSelected : ImGuiCol.Tab );
TabItemBackground( displayDrawList, bb, flags, tabCol );
if ( tabContentsVisible && (tabBar.Flags & ImGuiTabBarFlags.DrawSelectedOverline) != 0 && style.TabBarOverlineSize > 0.0f )
{
var overlineCol = GetColorU32Internal( ImGuiCol.TabSelectedOverline );
displayDrawList.AddRectFilled( bb.TL, new Vector2( bb.Max.x, bb.Min.y + style.TabBarOverlineSize ), overlineCol, style.TabRounding, ImDrawFlags.RoundCornersTop );
}
// Select with right mouse button (common idiom for context menus)
bool hoveredUnblocked = IsItemHovered( ImGuiHoveredFlags.AllowWhenBlockedByPopup );
if ( tabBar.SelectedTabId != tab.ID && hoveredUnblocked && (IsMouseClicked( ImGuiMouseButton.Right ) || IsMouseReleased( ImGuiMouseButton.Right )) && !isTabButton )
tabBar.NextSelectedTabId = id;
if ( (tabBar.Flags & ImGuiTabBarFlags.NoCloseWithMiddleMouseButton) != 0 )
flags |= ImGuiTabItemFlags.NoCloseWithMiddleMouseButton;
TabItemLabelAndCloseButton( displayDrawList, bb, tabJustUnsaved ? (flags & ~ImGuiTabItemFlags.UnsavedDocument) : flags, tabBar.FramePadding, label, id, closeButtonId, tabContentsVisible, out bool justClosed, out bool textClipped );
if ( justClosed && hasOpen )
{
open = false;
TabBarCloseTab( tabBar, tab );
}
if ( wantClipRect )
PopClipRect();
window.DC.CursorPos = backupMainCursorPos;
// Tooltip with full label when truncated
if ( textClipped && g.HoveredId == id && !held )
if ( (tabBar.Flags & ImGuiTabBarFlags.NoTooltip) == 0 && (tab.Flags & ImGuiTabItemFlags.NoTooltip) == 0 )
SetItemTooltip( "{0}", LabelText( label ) );
if ( isTabButton )
return pressed;
return tabContentsVisible;
}
private static void TabItemBackground( ImDrawList drawList, ImRect bb, ImGuiTabItemFlags flags, Color32 col )
{
var g = G;
float width = bb.Width;
float rounding = MathF.Max( 0.0f, MathF.Min( (flags & ImGuiTabItemFlags.Button) != 0 ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f ) );
float y1 = bb.Min.y + 1.0f;
float y2 = bb.Max.y - g.Style.TabBarBorderSize;
drawList.AddRectFilled( new Vector2( bb.Min.x, y1 ), new Vector2( bb.Max.x, y2 ), col, rounding, ImDrawFlags.RoundCornersTop );
if ( g.Style.TabBorderSize > 0.0f )
drawList.AddRect( new Vector2( bb.Min.x, y1 ), new Vector2( bb.Max.x, y2 ), GetColorU32Internal( ImGuiCol.Border ), rounding, ImDrawFlags.RoundCornersTop, g.Style.TabBorderSize );
}
private static void TabItemLabelAndCloseButton( ImDrawList drawList, ImRect bb, ImGuiTabItemFlags flags, Vector2 framePadding, string label, int tabId, int closeButtonId, bool isContentsVisible, out bool outJustClosed, out bool outTextClipped )
{
var g = G;
var labelSize = CalcTextSize( label, true );
outJustClosed = false;
outTextClipped = false;
if ( bb.Width <= 1.0f )
return;
var textEllipsisClipBb = new ImRect( bb.Min.x + framePadding.x, bb.Min.y + framePadding.y, bb.Max.x - framePadding.x, bb.Max.y );
outTextClipped = textEllipsisClipBb.Min.x + labelSize.x > textEllipsisClipBb.Max.x;
float buttonSz = g.FontSize;
var buttonPos = new Vector2( MathF.Max( bb.Min.x, bb.Max.x - framePadding.x - buttonSz ), bb.Min.y + framePadding.y );
bool closeButtonPressed = false;
bool closeButtonVisible = false;
bool isHovered = g.HoveredId == tabId || (closeButtonId != 0 && g.HoveredId == closeButtonId) || g.ActiveId == tabId || (closeButtonId != 0 && g.ActiveId == closeButtonId);
if ( closeButtonId != 0 )
{
if ( isContentsVisible )
closeButtonVisible = g.Style.TabCloseButtonMinWidthSelected < 0.0f || (isHovered && bb.Width >= MathF.Max( buttonSz, g.Style.TabCloseButtonMinWidthSelected ));
else
closeButtonVisible = g.Style.TabCloseButtonMinWidthUnselected < 0.0f || (isHovered && bb.Width >= MathF.Max( buttonSz, g.Style.TabCloseButtonMinWidthUnselected ));
}
bool unsavedMarkerVisible = (flags & ImGuiTabItemFlags.UnsavedDocument) != 0 && buttonPos.x + buttonSz <= bb.Max.x && (!closeButtonVisible || !isHovered);
if ( unsavedMarkerVisible )
{
var bulletBb = new ImRect( buttonPos, buttonPos + new Vector2( buttonSz, buttonSz ) );
RenderBullet( drawList, bulletBb.Center, GetColorU32Internal( ImGuiCol.Text ) );
}
else if ( closeButtonVisible )
{
var lastItemBackup = g.LastItemData;
if ( CloseButton( closeButtonId, buttonPos ) )
closeButtonPressed = true;
g.LastItemData = lastItemBackup;
if ( isHovered && (flags & ImGuiTabItemFlags.NoCloseWithMiddleMouseButton) == 0 && IsMouseClicked( ImGuiMouseButton.Middle ) )
closeButtonPressed = true;
}
if ( closeButtonVisible || unsavedMarkerVisible )
textEllipsisClipBb.Max.x -= buttonSz * (unsavedMarkerVisible ? 0.80f : 1.0f);
float ellipsisMaxX = textEllipsisClipBb.Max.x;
RenderTextEllipsis( drawList, textEllipsisClipBb.Min, textEllipsisClipBb.Max, textEllipsisClipBb.Max.x, ellipsisMaxX, label, labelSize );
outJustClosed = closeButtonPressed;
}
#endregion
}