Code/SboxSuperuser/Engine/Vendor/ImGui/ImGui.Columns.cs

ImGui columns implementation vendored into the project. It provides the legacy ImGui columns API: creating, beginning, switching, resizing, and ending column sets; managing column offsets, widths, clipping, and background drawing; and handling mouse drag resizing and channel splitting for multi-column draw lists.

// 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
{
	private const float COLUMNS_HIT_RECT_HALF_THICKNESS = 4.0f;

	private static int GetColumnsID( string strId, int columnsCount )
	{
		var window = G.CurrentWindow;
		PushID( 0x11223347 + (strId is not null ? 0 : columnsCount) );
		int id = window.GetID( strId ?? "columns" );
		PopID();
		return id;
	}

	private static ImGuiOldColumns FindOrCreateColumns( ImGuiWindow window, int id )
	{
		foreach ( var c in window.ColumnsStorage )
			if ( c.ID == id )
				return c;
		var columns = new ImGuiOldColumns { ID = id };
		window.ColumnsStorage.Add( columns );
		return columns;
	}

	private static float GetColumnOffsetFromNorm( ImGuiOldColumns columns, float offsetNorm ) => offsetNorm * (columns.OffMaxX - columns.OffMinX);
	private static float GetColumnNormFromOffset( ImGuiOldColumns columns, float offset ) => offset / (columns.OffMaxX - columns.OffMinX);

	private static float GetColumnWidthEx( ImGuiOldColumns columns, int columnIndex, bool beforeResize = false )
	{
		if ( columnIndex < 0 )
			columnIndex = columns.Current;
		float offsetNorm = beforeResize
			? columns.Columns[columnIndex + 1].OffsetNormBeforeResize - columns.Columns[columnIndex].OffsetNormBeforeResize
			: columns.Columns[columnIndex + 1].OffsetNorm - columns.Columns[columnIndex].OffsetNorm;
		return GetColumnOffsetFromNorm( columns, offsetNorm );
	}

	/// <summary>Legacy columns API (2020: prefer using Tables!). Columns(1) ends the current column set.</summary>
	public static void Columns( int count = 1, string id = null, bool borders = true )
	{
		var window = GetCurrentWindow();
		var flags = borders ? ImGuiOldColumnFlags.None : ImGuiOldColumnFlags.NoBorder;
		var columns = window.DC.CurrentColumns;
		if ( columns is not null && columns.Count == count && columns.Flags == flags )
			return;
		if ( columns is not null )
			EndColumns();
		if ( count != 1 )
			BeginColumns( id, count, flags );
	}

	public static void BeginColumns( string strId, int columnsCount, ImGuiOldColumnFlags flags = ImGuiOldColumnFlags.None )
	{
		var g = G;
		var window = GetCurrentWindow();
		if ( columnsCount < 1 || window.DC.CurrentColumns is not null )
			return;

		int id = GetColumnsID( strId, columnsCount );
		var columns = FindOrCreateColumns( window, id );
		columns.Current = 0;
		columns.Count = columnsCount;
		columns.Flags = flags;
		window.DC.CurrentColumns = columns;

		columns.HostCursorPosY = window.DC.CursorPos.y;
		columns.HostCursorMaxPosX = window.DC.CursorMaxPos.x;
		columns.HostInitialClipRect = window.ClipRect;
		columns.HostBackupParentWorkRect = window.ParentWorkRect;
		window.ParentWorkRect = window.WorkRect;

		float columnPadding = g.Style.ItemSpacing.x;
		float halfClipExtendX = ImTrunc( MathF.Max( window.WindowPadding.x * 0.5f, window.WindowBorderSize ) );
		float max1 = window.WorkRect.Max.x + columnPadding - MathF.Max( columnPadding - window.WindowPadding.x, 0.0f );
		float max2 = window.WorkRect.Max.x + halfClipExtendX;
		columns.OffMinX = window.DC.Indent - columnPadding + MathF.Max( columnPadding - window.WindowPadding.x, 0.0f );
		columns.OffMaxX = MathF.Max( MathF.Min( max1, max2 ) - window.Pos.x, columns.OffMinX + 1.0f );
		columns.LineMinY = columns.LineMaxY = window.DC.CursorPos.y;

		if ( columns.Columns.Count != 0 && columns.Columns.Count != columnsCount + 1 )
			columns.Columns.Clear();

		columns.IsFirstFrame = columns.Columns.Count == 0;
		if ( columns.Columns.Count == 0 )
		{
			for ( int n = 0; n < columnsCount + 1; n++ )
				columns.Columns.Add( new ImGuiOldColumnData { OffsetNorm = n / (float)columnsCount } );
		}

		for ( int n = 0; n < columnsCount; n++ )
		{
			var column = columns.Columns[n];
			float clipX1 = MathF.Round( window.Pos.x + GetColumnOffset( n ) );
			float clipX2 = MathF.Round( window.Pos.x + GetColumnOffset( n + 1 ) - 1.0f );
			column.ClipRect = new ImRect( clipX1, -float.MaxValue, clipX2, float.MaxValue );
			column.ClipRect.ClipWithFull( window.ClipRect );
		}

		columns.UsesChannels = columns.Count > 1 && !g.SplitDrawLists.Contains( window.DrawList );
		if ( columns.Count > 1 )
		{
			if ( columns.UsesChannels )
			{
				g.SplitDrawLists.Add( window.DrawList );
				window.DrawList.ChannelsSplit( 1 + columns.Count );
				window.DrawList.ChannelsSetCurrent( 1 );
			}
			PushColumnClipRect( 0 );
		}

		float offset0 = GetColumnOffset( columns.Current );
		float offset1 = GetColumnOffset( columns.Current + 1 );
		float width = offset1 - offset0;
		PushItemWidth( width * 0.65f );
		window.DC.ColumnsOffset = MathF.Max( columnPadding - window.WindowPadding.x, 0.0f );
		window.DC.CursorPos = new Vector2( ImTrunc( window.Pos.x + window.DC.Indent + window.DC.ColumnsOffset ), window.DC.CursorPos.y );
		window.WorkRect.Max.x = window.Pos.x + offset1 - columnPadding;
		window.WorkRect.Max.y = window.ContentRegionRect.Max.y;
	}

	private static void PushColumnClipRect( int columnIndex )
	{
		var window = G.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columnIndex < 0 )
			columnIndex = columns.Current;
		var column = columns.Columns[columnIndex];
		PushClipRect( column.ClipRect.Min, column.ClipRect.Max, false );
	}

	public static void NextColumn()
	{
		var g = G;
		var window = GetCurrentWindow();
		if ( window.SkipItems || window.DC.CurrentColumns is null )
			return;

		var columns = window.DC.CurrentColumns;
		if ( columns.Count == 1 )
		{
			window.DC.CursorPos = new Vector2( ImTrunc( window.Pos.x + window.DC.Indent + window.DC.ColumnsOffset ), window.DC.CursorPos.y );
			return;
		}

		if ( ++columns.Current == columns.Count )
			columns.Current = 0;

		PopItemWidth();
		PopClipRect();
		if ( columns.UsesChannels )
			window.DrawList.ChannelsSetCurrent( columns.Current + 1 );
		PushColumnClipRect( columns.Current );

		float columnPadding = g.Style.ItemSpacing.x;
		columns.LineMaxY = MathF.Max( columns.LineMaxY, window.DC.CursorPos.y );
		if ( columns.Current > 0 )
		{
			window.DC.ColumnsOffset = GetColumnOffset( columns.Current ) - window.DC.Indent + columnPadding;
		}
		else
		{
			window.DC.ColumnsOffset = MathF.Max( columnPadding - window.WindowPadding.x, 0.0f );
			window.DC.IsSameLine = false;
			columns.LineMinY = columns.LineMaxY;
		}
		window.DC.CursorPos = new Vector2( ImTrunc( window.Pos.x + window.DC.Indent + window.DC.ColumnsOffset ), columns.LineMinY );
		window.DC.CurrLineSize = Vector2.Zero;
		window.DC.CurrLineTextBaseOffset = 0.0f;

		float offset0 = GetColumnOffset( columns.Current );
		float offset1 = GetColumnOffset( columns.Current + 1 );
		float width = offset1 - offset0;
		PushItemWidth( width * 0.65f );
		window.WorkRect.Max.x = window.Pos.x + offset1 - columnPadding;
	}

	public static void EndColumns()
	{
		var g = G;
		var window = G.CurrentWindow;
		var columns = window?.DC.CurrentColumns;
		if ( columns is null )
			return;

		PopItemWidth();
		if ( columns.Count > 1 )
		{
			PopClipRect();
			if ( columns.UsesChannels )
			{
				window.DrawList.ChannelsMerge();
				g.SplitDrawLists.Remove( window.DrawList );
			}
		}

		var flags = columns.Flags;
		columns.LineMaxY = MathF.Max( columns.LineMaxY, window.DC.CursorPos.y );
		window.DC.CursorPos = new Vector2( window.DC.CursorPos.x, columns.LineMaxY );
		if ( (flags & ImGuiOldColumnFlags.GrowParentContentsSize) == 0 )
			window.DC.CursorMaxPos = new Vector2( columns.HostCursorMaxPosX, window.DC.CursorMaxPos.y );

		bool isBeingResized = false;
		if ( (flags & ImGuiOldColumnFlags.NoBorder) == 0 && !window.SkipItems )
		{
			float y1 = MathF.Max( columns.HostCursorPosY, window.ClipRect.Min.y );
			float y2 = MathF.Min( window.DC.CursorPos.y, window.ClipRect.Max.y );
			int draggingColumn = -1;
			for ( int n = 1; n < columns.Count; n++ )
			{
				var column = columns.Columns[n];
				float x = window.Pos.x + GetColumnOffset( n );
				int columnId = unchecked(columns.ID + n);
				float hitHw = ImTrunc( COLUMNS_HIT_RECT_HALF_THICKNESS * g.AppliedStyleScale );
				var hitRect = new ImRect( new Vector2( x - hitHw, y1 ), new Vector2( x + hitHw, y2 ) );
				if ( !ItemAdd( hitRect, columnId, null, ImGuiItemFlags.NoNav ) )
					continue;

				bool hovered = false, held = false;
				if ( (flags & ImGuiOldColumnFlags.NoResize) == 0 )
				{
					ButtonBehavior( hitRect, columnId, out hovered, out held );
					if ( hovered || held )
						g.MouseCursor = ImGuiMouseCursor.ResizeEW;
					if ( held && (column.Flags & ImGuiOldColumnFlags.NoResize) == 0 )
						draggingColumn = n;
				}

				var col = GetColorU32Internal( held ? ImGuiCol.SeparatorActive : hovered ? ImGuiCol.SeparatorHovered : ImGuiCol.Separator );
				float xi = ImTrunc( x );
				window.DrawList.AddLine( new Vector2( xi, y1 + 1.0f ), new Vector2( xi, y2 ), col );
			}

			if ( draggingColumn != -1 )
			{
				if ( !columns.IsBeingResized )
					foreach ( var c in columns.Columns )
						c.OffsetNormBeforeResize = c.OffsetNorm;
				columns.IsBeingResized = isBeingResized = true;
				float x = GetDraggedColumnOffset( columns, draggingColumn );
				SetColumnOffset( draggingColumn, x );
			}
		}
		columns.IsBeingResized = isBeingResized;

		window.WorkRect = window.ParentWorkRect;
		window.ParentWorkRect = columns.HostBackupParentWorkRect;
		window.DC.CurrentColumns = null;
		window.DC.ColumnsOffset = 0.0f;
		window.DC.CursorPos = new Vector2( ImTrunc( window.Pos.x + window.DC.Indent + window.DC.ColumnsOffset ), window.DC.CursorPos.y );
	}

	private static float GetDraggedColumnOffset( ImGuiOldColumns columns, int columnIndex )
	{
		var g = G;
		var window = g.CurrentWindow;
		float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc( COLUMNS_HIT_RECT_HALF_THICKNESS * g.AppliedStyleScale ) - window.Pos.x;
		x = MathF.Max( x, GetColumnOffset( columnIndex - 1 ) + g.Style.ColumnsMinSpacing );
		if ( (columns.Flags & ImGuiOldColumnFlags.NoPreserveWidths) != 0 )
			x = MathF.Min( x, GetColumnOffset( columnIndex + 1 ) - g.Style.ColumnsMinSpacing );
		return x;
	}

	public static int GetColumnIndex() => G.CurrentWindow.DC.CurrentColumns?.Current ?? 0;
	public static int GetColumnsCount() => G.CurrentWindow.DC.CurrentColumns?.Count ?? 1;

	/// <summary>Get position of column line (in pixels, from the left side of the contents region).</summary>
	public static float GetColumnOffset( int columnIndex = -1 )
	{
		var window = G.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columns is null )
			return 0.0f;
		if ( columnIndex < 0 )
			columnIndex = columns.Current;
		columnIndex = Math.Clamp( columnIndex, 0, columns.Columns.Count - 1 );
		float t = columns.Columns[columnIndex].OffsetNorm;
		return ImLerp( columns.OffMinX, columns.OffMaxX, t );
	}

	public static void SetColumnOffset( int columnIndex, float offset )
	{
		var g = G;
		var window = g.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columns is null )
			return;
		if ( columnIndex < 0 )
			columnIndex = columns.Current;
		if ( columnIndex >= columns.Columns.Count )
			return;

		bool preserveWidth = (columns.Flags & ImGuiOldColumnFlags.NoPreserveWidths) == 0 && columnIndex < columns.Count - 1;
		float width = preserveWidth ? GetColumnWidthEx( columns, columnIndex, columns.IsBeingResized ) : 0.0f;

		if ( (columns.Flags & ImGuiOldColumnFlags.NoForceWithinWindow) == 0 )
			offset = MathF.Min( offset, columns.OffMaxX - g.Style.ColumnsMinSpacing * (columns.Count - columnIndex) );
		columns.Columns[columnIndex].OffsetNorm = GetColumnNormFromOffset( columns, offset - columns.OffMinX );

		if ( preserveWidth )
			SetColumnOffset( columnIndex + 1, offset + MathF.Max( g.Style.ColumnsMinSpacing, width ) );
	}

	public static float GetColumnWidth( int columnIndex = -1 )
	{
		var window = G.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columns is null )
			return GetContentRegionAvail().x;
		if ( columnIndex < 0 )
			columnIndex = columns.Current;
		return GetColumnOffsetFromNorm( columns, columns.Columns[columnIndex + 1].OffsetNorm - columns.Columns[columnIndex].OffsetNorm );
	}

	public static void SetColumnWidth( int columnIndex, float width )
	{
		var window = G.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columns is null )
			return;
		if ( columnIndex < 0 )
			columnIndex = columns.Current;
		SetColumnOffset( columnIndex + 1, GetColumnOffset( columnIndex ) + width );
	}

	/// <summary>Draw into the background channel spanning all columns (used e.g. by Selectable with SpanAllColumns).</summary>
	internal static void PushColumnsBackground()
	{
		var window = G.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columns is null || columns.Count == 1 )
			return;
		columns.HostBackupClipRect = window.ClipRect;
		if ( columns.UsesChannels )
			window.DrawList.ChannelsSetCurrent( 0 );
		PushClipRect( columns.HostInitialClipRect.Min, columns.HostInitialClipRect.Max, false );
	}

	internal static void PopColumnsBackground()
	{
		var window = G.CurrentWindow;
		var columns = window.DC.CurrentColumns;
		if ( columns is null || columns.Count == 1 )
			return;
		PopClipRect();
		if ( columns.UsesChannels )
			window.DrawList.ChannelsSetCurrent( columns.Current + 1 );
	}
}