SboxSuperuser/Engine/Vendor/ImGui/ImGui.TabBar.cs

ImGui tab bar and tab item implementation vendored into the project. It lays out tab bars, handles tab creation, selection, reordering, scrolling, close buttons, drawing tab backgrounds and labels, and managing tab visibility and interactions.

Native InteropObfuscated Code
// 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
}