SboxSuperuser/Engine/Vendor/ImGui/ImDrawList.cs

A vendored ImGui-style draw list implementation for s&box. Records high-level draw commands (lines, shapes, text, images) into Cmd entries, manages clipping and channels, and replays them through a Painter to perform actual rendering.

Native InteropFile Access
// 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;

/// <summary>
/// A list of draw commands in screen space. Mirrors Dear ImGui's ImDrawList API. Commands are recorded
/// while widgets are submitted, then replayed through <see cref="Painter"/> at the end of the frame.
/// </summary>
internal class ImDrawList
{
	internal enum CmdType
	{
		RectFilled,
		Rect,
		Line,
		Polyline,
		PolygonFilled,
		Circle,
		CircleFilled,
		Ellipse,
		EllipseFilled,
		Text,
		Image,
		BezierCubic,
		BezierQuadratic,
		RectGradient,
	}

	internal struct Cmd
	{
		public CmdType Type;
		public int ClipIdx;
		public Vector2 P1, P2, P3, P4;
		public Color C1, C2;
		public float Thickness;
		public float Rounding;
		public ImDrawFlags Flags;
		public string Text;
		public float FontSize;
		public Texture Texture;
		public Vector2[] Points;
		public bool Closed;
	}

	public ImDrawList( string name )
	{
		Name = name;
		ResetForNewFrame();
	}

	public string Name { get; }

	private readonly List<Cmd> _cmds = new();
	private List<Cmd>[] _channelStorage = new List<Cmd>[1];
	private int _currentChannel;
	private int _channelCount = 1;

	internal readonly List<ImRect> ClipRects = new();
	private readonly List<int> _clipStack = new();
	private readonly List<Vector2> _path = new();

	internal int CommandCount => _cmds.Count;

	internal static ImRect FullscreenRect => new( new Vector2( -8192f, -8192f ), new Vector2( 8192f, 8192f ) );

	internal void ResetForNewFrame()
	{
		_cmds.Clear();
		for ( int i = 1; i < _channelStorage.Length; i++ )
			_channelStorage[i]?.Clear();
		_currentChannel = 0;
		_channelCount = 1;
		ClipRects.Clear();
		_clipStack.Clear();
		_path.Clear();
		ClipRects.Add( FullscreenRect );
		_clipStack.Add( 0 );
	}

	private int CurrentClipIdx => _clipStack.Count > 0 ? _clipStack[^1] : 0;

	#region Clipping
	/// <summary>
	/// Render-level scissoring. Use ImGui.PushClipRect() instead to affect logic (hit-testing and widget culling).
	/// </summary>
	public void PushClipRect( Vector2 clipRectMin, Vector2 clipRectMax, bool intersectWithCurrentClipRect = false )
	{
		var cr = new ImRect( clipRectMin, clipRectMax );
		if ( intersectWithCurrentClipRect && ClipRects.Count > 0 )
			cr.ClipWithFull( ClipRects[CurrentClipIdx] );
		cr.Max = ImGui.ImMax( cr.Min, cr.Max );
		ClipRects.Add( cr );
		_clipStack.Add( ClipRects.Count - 1 );
	}

	public void PushClipRectFullScreen() => PushClipRect( FullscreenRect.Min, FullscreenRect.Max );

	public void PopClipRect()
	{
		if ( _clipStack.Count > 1 )
			_clipStack.RemoveAt( _clipStack.Count - 1 );
	}

	public Vector2 GetClipRectMin() => CurrentClipRect.Min;
	public Vector2 GetClipRectMax() => CurrentClipRect.Max;
	internal ImRect CurrentClipRect => ClipRects.Count > 0 ? ClipRects[CurrentClipIdx] : FullscreenRect;
	#endregion

	#region Channels
	/// <summary>
	/// Split the draw list into layers, so that items can be submitted out of order. Channel 0 holds the existing contents.
	/// </summary>
	public void ChannelsSplit( int count )
	{
		if ( count < 1 ) count = 1;
		if ( _channelStorage.Length < count )
			Array.Resize( ref _channelStorage, count );
		for ( int i = 1; i < count; i++ )
		{
			_channelStorage[i] ??= new List<Cmd>();
			_channelStorage[i].Clear();
		}
		_channelCount = count;
		_currentChannel = 0;
	}

	public void ChannelsSetCurrent( int idx )
	{
		if ( idx < 0 || idx >= _channelCount ) return;
		_currentChannel = idx;
	}

	public void ChannelsMerge()
	{
		if ( _channelCount <= 1 ) return;
		for ( int i = 1; i < _channelCount; i++ )
		{
			_cmds.AddRange( _channelStorage[i] );
			_channelStorage[i].Clear();
		}
		_channelCount = 1;
		_currentChannel = 0;
	}

	private void Add( in Cmd cmd )
	{
		var c = cmd;
		c.ClipIdx = CurrentClipIdx;
		if ( _currentChannel == 0 )
			_cmds.Add( c );
		else
			_channelStorage[_currentChannel].Add( c );
	}
	#endregion

	private static Color ToColor( Color32 c ) => new( c.r / 255f, c.g / 255f, c.b / 255f, c.a / 255f );

	#region Primitives
	public void AddLine( Vector2 p1, Vector2 p2, Color32 col, float thickness = 1.0f )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.Line, P1 = p1, P2 = p2, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddRect( Vector2 pMin, Vector2 pMax, Color32 col, float rounding = 0.0f, ImDrawFlags flags = ImDrawFlags.None, float thickness = 1.0f )
	{
		if ( col.a == 0 || thickness <= 0f ) return;
		Add( new Cmd { Type = CmdType.Rect, P1 = pMin, P2 = pMax, C1 = ToColor( col ), Rounding = rounding, Flags = flags, Thickness = thickness } );
	}

	public void AddRectFilled( Vector2 pMin, Vector2 pMax, Color32 col, float rounding = 0.0f, ImDrawFlags flags = ImDrawFlags.None )
	{
		if ( col.a == 0 ) return;
		if ( pMax.x <= pMin.x || pMax.y <= pMin.y ) return;
		Add( new Cmd { Type = CmdType.RectFilled, P1 = pMin, P2 = pMax, C1 = ToColor( col ), Rounding = rounding, Flags = flags } );
	}

	public void AddRectFilledMultiColor( Vector2 pMin, Vector2 pMax, Color32 colUprLeft, Color32 colUprRight, Color32 colBotRight, Color32 colBotLeft )
	{
		if ( pMax.x <= pMin.x || pMax.y <= pMin.y ) return;
		if ( colUprLeft.a == 0 && colUprRight.a == 0 && colBotRight.a == 0 && colBotLeft.a == 0 ) return;

		bool vertical = colUprLeft == colUprRight && colBotLeft == colBotRight;
		bool horizontal = colUprLeft == colBotLeft && colUprRight == colBotRight;
		if ( vertical )
		{
			Add( new Cmd { Type = CmdType.RectGradient, P1 = pMin, P2 = pMax, P3 = pMin, P4 = new Vector2( pMin.x, pMax.y ), C1 = ToColor( colUprLeft ), C2 = ToColor( colBotLeft ) } );
			return;
		}
		if ( horizontal )
		{
			Add( new Cmd { Type = CmdType.RectGradient, P1 = pMin, P2 = pMax, P3 = pMin, P4 = new Vector2( pMax.x, pMin.y ), C1 = ToColor( colUprLeft ), C2 = ToColor( colUprRight ) } );
			return;
		}

		// General bilinear case: approximate with horizontal-gradient strips.
		const int strips = 12;
		var ul = ToColor( colUprLeft ); var ur = ToColor( colUprRight );
		var bl = ToColor( colBotLeft ); var br = ToColor( colBotRight );
		float h = (pMax.y - pMin.y) / strips;
		for ( int i = 0; i < strips; i++ )
		{
			float t = (i + 0.5f) / strips;
			var left = Color.Lerp( ul, bl, t );
			var right = Color.Lerp( ur, br, t );
			var a = new Vector2( pMin.x, pMin.y + h * i );
			var b = new Vector2( pMax.x, i == strips - 1 ? pMax.y : pMin.y + h * (i + 1) + 0.5f );
			Add( new Cmd { Type = CmdType.RectGradient, P1 = a, P2 = b, P3 = a, P4 = new Vector2( pMax.x, a.y ), C1 = left, C2 = right } );
		}
	}

	public void AddQuad( Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, Color32 col, float thickness = 1.0f )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.Polyline, Points = new[] { p1, p2, p3, p4 }, Closed = true, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddQuadFilled( Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, Color32 col )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.PolygonFilled, Points = new[] { p1, p2, p3, p4 }, C1 = ToColor( col ) } );
	}

	public void AddTriangle( Vector2 p1, Vector2 p2, Vector2 p3, Color32 col, float thickness = 1.0f )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.Polyline, Points = new[] { p1, p2, p3 }, Closed = true, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddTriangleFilled( Vector2 p1, Vector2 p2, Vector2 p3, Color32 col )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.PolygonFilled, Points = new[] { p1, p2, p3 }, C1 = ToColor( col ) } );
	}

	public void AddCircle( Vector2 center, float radius, Color32 col, int numSegments = 0, float thickness = 1.0f )
	{
		if ( col.a == 0 || radius < 0.5f ) return;
		if ( numSegments > 2 )
		{
			AddNgon( center, radius, col, numSegments, thickness );
			return;
		}
		Add( new Cmd { Type = CmdType.Circle, P1 = center, Rounding = radius, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddCircleFilled( Vector2 center, float radius, Color32 col, int numSegments = 0 )
	{
		if ( col.a == 0 || radius < 0.5f ) return;
		if ( numSegments > 2 )
		{
			AddNgonFilled( center, radius, col, numSegments );
			return;
		}
		Add( new Cmd { Type = CmdType.CircleFilled, P1 = center, Rounding = radius, C1 = ToColor( col ) } );
	}

	public void AddNgon( Vector2 center, float radius, Color32 col, int numSegments, float thickness = 1.0f )
	{
		if ( col.a == 0 || numSegments <= 2 ) return;
		var pts = NgonPoints( center, radius - 0.5f, numSegments );
		Add( new Cmd { Type = CmdType.Polyline, Points = pts, Closed = true, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddNgonFilled( Vector2 center, float radius, Color32 col, int numSegments )
	{
		if ( col.a == 0 || numSegments <= 2 ) return;
		var pts = NgonPoints( center, radius, numSegments );
		Add( new Cmd { Type = CmdType.PolygonFilled, Points = pts, C1 = ToColor( col ) } );
	}

	private static Vector2[] NgonPoints( Vector2 center, float radius, int numSegments )
	{
		var pts = new Vector2[numSegments];
		for ( int i = 0; i < numSegments; i++ )
		{
			float a = MathF.PI * 2.0f * i / numSegments;
			pts[i] = new Vector2( center.x + MathF.Cos( a ) * radius, center.y + MathF.Sin( a ) * radius );
		}
		return pts;
	}

	public void AddEllipse( Vector2 center, Vector2 radius, Color32 col, float rot = 0.0f, int numSegments = 0, float thickness = 1.0f )
	{
		if ( col.a == 0 ) return;
		if ( rot == 0f && numSegments <= 0 )
		{
			Add( new Cmd { Type = CmdType.Ellipse, P1 = center, P2 = radius, C1 = ToColor( col ), Thickness = thickness } );
			return;
		}
		Add( new Cmd { Type = CmdType.Polyline, Points = EllipsePoints( center, radius, rot, numSegments ), Closed = true, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddEllipseFilled( Vector2 center, Vector2 radius, Color32 col, float rot = 0.0f, int numSegments = 0 )
	{
		if ( col.a == 0 ) return;
		if ( rot == 0f && numSegments <= 0 )
		{
			Add( new Cmd { Type = CmdType.EllipseFilled, P1 = center, P2 = radius, C1 = ToColor( col ) } );
			return;
		}
		Add( new Cmd { Type = CmdType.PolygonFilled, Points = EllipsePoints( center, radius, rot, numSegments ), C1 = ToColor( col ) } );
	}

	private static Vector2[] EllipsePoints( Vector2 center, Vector2 radius, float rot, int numSegments )
	{
		if ( numSegments <= 0 ) numSegments = 48;
		var pts = new Vector2[numSegments];
		float cos = MathF.Cos( rot ), sin = MathF.Sin( rot );
		for ( int i = 0; i < numSegments; i++ )
		{
			float a = MathF.PI * 2f * i / numSegments;
			float x = MathF.Cos( a ) * radius.x;
			float y = MathF.Sin( a ) * radius.y;
			pts[i] = new Vector2( center.x + x * cos - y * sin, center.y + x * sin + y * cos );
		}
		return pts;
	}

	public void AddBezierCubic( Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, Color32 col, float thickness, int numSegments = 0 )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.BezierCubic, P1 = p1, P2 = p2, P3 = p3, P4 = p4, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddBezierQuadratic( Vector2 p1, Vector2 p2, Vector2 p3, Color32 col, float thickness, int numSegments = 0 )
	{
		if ( col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.BezierQuadratic, P1 = p1, P2 = p2, P3 = p3, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddPolyline( ReadOnlySpan<Vector2> points, Color32 col, ImDrawFlags flags, float thickness )
	{
		if ( col.a == 0 || points.Length < 2 ) return;
		Add( new Cmd { Type = CmdType.Polyline, Points = points.ToArray(), Closed = (flags & ImDrawFlags.Closed) != 0, C1 = ToColor( col ), Thickness = thickness } );
	}

	public void AddPolyline( Vector2[] points, int numPoints, Color32 col, ImDrawFlags flags, float thickness )
		=> AddPolyline( points.AsSpan( 0, numPoints ), col, flags, thickness );

	public void AddConvexPolyFilled( ReadOnlySpan<Vector2> points, Color32 col )
	{
		if ( col.a == 0 || points.Length < 3 ) return;
		Add( new Cmd { Type = CmdType.PolygonFilled, Points = points.ToArray(), C1 = ToColor( col ) } );
	}

	public void AddConvexPolyFilled( Vector2[] points, int numPoints, Color32 col )
		=> AddConvexPolyFilled( points.AsSpan( 0, numPoints ), col );

	/// <summary>Concave polygons are supported by the backend (even-odd fill rule).</summary>
	public void AddConcavePolyFilled( ReadOnlySpan<Vector2> points, Color32 col ) => AddConvexPolyFilled( points, col );
	#endregion

	#region Text
	public void AddText( Vector2 pos, Color32 col, string text )
		=> AddText( 0f, pos, col, text );

	/// <summary>
	/// Add text with an explicit font size (0 = current font size). Supports '\n' and optional word wrapping.
	/// </summary>
	public void AddText( float fontSize, Vector2 pos, Color32 col, string text, float wrapWidth = 0.0f, ImRect? cpuFineClipRect = null )
	{
		if ( col.a == 0 || string.IsNullOrEmpty( text ) ) return;

		var g = ImGuiContext.Current;
		float baseLine = g?.FontSize ?? 16f;
		float basePoint = g?.FontPointSize ?? 16f;
		float lineHeight = fontSize > 0f ? fontSize : baseLine;
		float pointSize = basePoint * (lineHeight / baseLine);

		if ( cpuFineClipRect.HasValue )
		{
			var r = cpuFineClipRect.Value;
			PushClipRect( r.Min, r.Max, true );
		}

		var color = ToColor( col );
		float y = pos.y;
		if ( wrapWidth > 0f )
		{
			foreach ( var line in ImGui.WrapText( text, wrapWidth, pointSize ) )
			{
				AddTextLine( new Vector2( pos.x, y ), color, line, pointSize, lineHeight );
				y += lineHeight;
			}
		}
		else
		{
			int start = 0;
			while ( start <= text.Length )
			{
				int nl = text.IndexOf( '\n', start );
				int end = nl < 0 ? text.Length : nl;
				if ( end > start )
					AddTextLine( new Vector2( pos.x, y ), color, text.Substring( start, end - start ), pointSize, lineHeight );
				if ( nl < 0 ) break;
				start = nl + 1;
				y += lineHeight;
			}
		}

		if ( cpuFineClipRect.HasValue )
			PopClipRect();
	}

	private void AddTextLine( Vector2 pos, Color color, string line, float pointSize, float lineHeight )
	{
		if ( string.IsNullOrEmpty( line ) ) return;
		// Cull lines that are entirely outside of the clip rect.
		var clip = CurrentClipRect;
		if ( pos.y > clip.Max.y || pos.y + lineHeight < clip.Min.y ) return;
		Add( new Cmd { Type = CmdType.Text, P1 = pos, C1 = color, Text = line, FontSize = pointSize } );
	}
	#endregion

	#region Image
	public void AddImage( Texture texture, Vector2 pMin, Vector2 pMax )
		=> AddImage( texture, pMin, pMax, Vector2.Zero, Vector2.One, new Color32( 255, 255, 255, 255 ) );

	public void AddImage( Texture texture, Vector2 pMin, Vector2 pMax, Vector2 uvMin, Vector2 uvMax, Color32 col )
	{
		if ( texture is null || col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.Image, Texture = texture, P1 = pMin, P2 = pMax, P3 = uvMin, P4 = uvMax, C1 = ToColor( col ) } );
	}

	public void AddImageRounded( Texture texture, Vector2 pMin, Vector2 pMax, Vector2 uvMin, Vector2 uvMax, Color32 col, float rounding, ImDrawFlags flags = ImDrawFlags.None )
	{
		if ( texture is null || col.a == 0 ) return;
		Add( new Cmd { Type = CmdType.Image, Texture = texture, P1 = pMin, P2 = pMax, P3 = uvMin, P4 = uvMax, C1 = ToColor( col ), Rounding = rounding, Flags = flags } );
	}
	#endregion

	#region Path API
	public void PathClear() => _path.Clear();
	public void PathLineTo( Vector2 pos ) => _path.Add( pos );

	public void PathLineToMergeDuplicate( Vector2 pos )
	{
		if ( _path.Count == 0 || _path[^1] != pos )
			_path.Add( pos );
	}

	public void PathFillConvex( Color32 col )
	{
		AddConvexPolyFilled( _path.ToArray(), col );
		_path.Clear();
	}

	public void PathFillConcave( Color32 col ) => PathFillConvex( col );

	public void PathStroke( Color32 col, ImDrawFlags flags = ImDrawFlags.None, float thickness = 1.0f )
	{
		AddPolyline( _path.ToArray(), col, flags, thickness );
		_path.Clear();
	}

	public void PathArcTo( Vector2 center, float radius, float aMin, float aMax, int numSegments = 0 )
	{
		if ( radius < 0.5f )
		{
			_path.Add( center );
			return;
		}
		if ( numSegments <= 0 )
			numSegments = Math.Clamp( (int)(MathF.Abs( aMax - aMin ) * radius / 4f), 4, 64 );
		for ( int i = 0; i <= numSegments; i++ )
		{
			float a = aMin + (i / (float)numSegments) * (aMax - aMin);
			_path.Add( new Vector2( center.x + MathF.Cos( a ) * radius, center.y + MathF.Sin( a ) * radius ) );
		}
	}

	/// <summary>Use precomputed angles for a 12 steps circle. aMinOf12/aMaxOf12 are in [0,12].</summary>
	public void PathArcToFast( Vector2 center, float radius, int aMinOf12, int aMaxOf12 )
	{
		PathArcTo( center, radius, aMinOf12 * MathF.PI * 2f / 12f, aMaxOf12 * MathF.PI * 2f / 12f, Math.Max( 1, aMaxOf12 - aMinOf12 ) * 2 );
	}

	public void PathEllipticalArcTo( Vector2 center, Vector2 radius, float rot, float aMin, float aMax, int numSegments = 0 )
	{
		if ( numSegments <= 0 ) numSegments = 24;
		float cos = MathF.Cos( rot ), sin = MathF.Sin( rot );
		for ( int i = 0; i <= numSegments; i++ )
		{
			float a = aMin + (i / (float)numSegments) * (aMax - aMin);
			float x = MathF.Cos( a ) * radius.x, y = MathF.Sin( a ) * radius.y;
			_path.Add( new Vector2( center.x + x * cos - y * sin, center.y + x * sin + y * cos ) );
		}
	}

	public void PathBezierCubicCurveTo( Vector2 p2, Vector2 p3, Vector2 p4, int numSegments = 0 )
	{
		var p1 = _path.Count > 0 ? _path[^1] : p2;
		if ( numSegments <= 0 ) numSegments = 24;
		for ( int i = 1; i <= numSegments; i++ )
		{
			float t = i / (float)numSegments;
			float u = 1 - t;
			float w1 = u * u * u, w2 = 3 * u * u * t, w3 = 3 * u * t * t, w4 = t * t * t;
			_path.Add( p1 * w1 + p2 * w2 + p3 * w3 + p4 * w4 );
		}
	}

	public void PathBezierQuadraticCurveTo( Vector2 p2, Vector2 p3, int numSegments = 0 )
	{
		var p1 = _path.Count > 0 ? _path[^1] : p2;
		if ( numSegments <= 0 ) numSegments = 16;
		for ( int i = 1; i <= numSegments; i++ )
		{
			float t = i / (float)numSegments;
			float u = 1 - t;
			_path.Add( p1 * (u * u) + p2 * (2 * u * t) + p3 * (t * t) );
		}
	}

	public void PathRect( Vector2 rectMin, Vector2 rectMax, float rounding = 0.0f, ImDrawFlags flags = ImDrawFlags.None )
	{
		if ( rounding < 0.5f )
		{
			_path.Add( rectMin );
			_path.Add( new Vector2( rectMax.x, rectMin.y ) );
			_path.Add( rectMax );
			_path.Add( new Vector2( rectMin.x, rectMax.y ) );
			return;
		}
		var r = Radii( flags, rounding, rectMax - rectMin );
		PathArcToFast( new Vector2( rectMin.x + r.x, rectMin.y + r.x ), r.x, 6, 9 );
		PathArcToFast( new Vector2( rectMax.x - r.y, rectMin.y + r.y ), r.y, 9, 12 );
		PathArcToFast( new Vector2( rectMax.x - r.z, rectMax.y - r.z ), r.z, 0, 3 );
		PathArcToFast( new Vector2( rectMin.x + r.w, rectMax.y - r.w ), r.w, 3, 6 );
	}
	#endregion

	#region Rendering
	/// <summary>Returns the corner radii (TL, TR, BR, BL) for a rounding amount and draw flags.</summary>
	private static Vector4 Radii( ImDrawFlags flags, float rounding, Vector2 size )
	{
		rounding = MathF.Min( rounding, MathF.Min( MathF.Abs( size.x ), MathF.Abs( size.y ) ) * 0.5f );
		if ( rounding <= 0f || (flags & ImDrawFlags.RoundCornersMask_) == ImDrawFlags.RoundCornersNone )
			return Vector4.Zero;
		if ( (flags & ImDrawFlags.RoundCornersMask_) == 0 )
			flags |= ImDrawFlags.RoundCornersAll;
		return new Vector4(
			(flags & ImDrawFlags.RoundCornersTopLeft) != 0 ? rounding : 0f,
			(flags & ImDrawFlags.RoundCornersTopRight) != 0 ? rounding : 0f,
			(flags & ImDrawFlags.RoundCornersBottomRight) != 0 ? rounding : 0f,
			(flags & ImDrawFlags.RoundCornersBottomLeft) != 0 ? rounding : 0f );
	}

	private static Painter.CornerRadii CornerRadii( ImDrawFlags flags, float rounding, Vector2 size )
	{
		var r = Radii( flags, rounding, size );
		return new Painter.CornerRadii( new Vector2( r.x ), new Vector2( r.y ), new Vector2( r.z ), new Vector2( r.w ) );
	}

	/// <summary>
	/// Replay all recorded commands into a painter.
	/// </summary>
	internal void Render( Painter painter )
	{
		ChannelsMerge();
		if ( _cmds.Count == 0 )
			return;

		int currentClip = -1;
		Painter.StateScope scope = default;
		bool hasScope = false;

		try
		{
			foreach ( var cmd in _cmds )
			{
				if ( cmd.ClipIdx != currentClip )
				{
					if ( hasScope ) scope.Dispose();
					scope = painter.Scope();
					hasScope = true;
					currentClip = cmd.ClipIdx;
					if ( currentClip != 0 )
						painter.Clip( ClipRects[currentClip].ToRect() );
				}

				var cr = ClipRects[currentClip];
				if ( cr.Width <= 0f || cr.Height <= 0f )
					continue;

				RenderCmd( painter, cmd );
			}
		}
		finally
		{
			if ( hasScope ) scope.Dispose();
		}
	}

	private static void RenderCmd( Painter painter, in Cmd cmd )
	{
		switch ( cmd.Type )
		{
			case CmdType.RectFilled:
				painter.Stroke = Stroke.None;
				painter.Fill = cmd.C1;
				painter.Rect( new Rect( cmd.P1, cmd.P2 - cmd.P1 ), CornerRadii( cmd.Flags, cmd.Rounding, cmd.P2 - cmd.P1 ) );
				break;
			case CmdType.Rect:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness ).WithAlignment( Stroke.StrokeAlignment.Inside );
				painter.Rect( new Rect( cmd.P1, cmd.P2 - cmd.P1 ), CornerRadii( cmd.Flags, cmd.Rounding, cmd.P2 - cmd.P1 ) );
				break;
			case CmdType.RectGradient:
				painter.Stroke = Stroke.None;
				painter.Fill = Fill.LinearGradient( cmd.P3, cmd.P4, cmd.C1, cmd.C2 );
				painter.Rect( new Rect( cmd.P1, cmd.P2 - cmd.P1 ), 0f );
				break;
			case CmdType.Line:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness );
				painter.Line( cmd.P1, cmd.P2 );
				break;
			case CmdType.Polyline:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness );
				if ( cmd.Closed && cmd.Points.Length >= 3 )
					painter.Polygon( cmd.Points );
				else
					painter.Line( cmd.Points );
				break;
			case CmdType.PolygonFilled:
				painter.Stroke = Stroke.None;
				painter.Fill = cmd.C1;
				painter.Polygon( cmd.Points );
				break;
			case CmdType.Circle:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness );
				painter.Circle( cmd.P1, cmd.Rounding - cmd.Thickness * 0.5f );
				break;
			case CmdType.CircleFilled:
				painter.Stroke = Stroke.None;
				painter.Fill = cmd.C1;
				painter.Circle( cmd.P1, cmd.Rounding );
				break;
			case CmdType.Ellipse:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness );
				painter.Circle( cmd.P1, cmd.P2 * 2f );
				break;
			case CmdType.EllipseFilled:
				painter.Stroke = Stroke.None;
				painter.Fill = cmd.C1;
				painter.Circle( cmd.P1, cmd.P2 * 2f );
				break;
			case CmdType.BezierCubic:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness );
				painter.Bezier( cmd.P1, cmd.P2, cmd.P3, cmd.P4 );
				break;
			case CmdType.BezierQuadratic:
				painter.Fill = Fill.None;
				painter.Stroke = Stroke.Solid( cmd.C1, cmd.Thickness );
				painter.Bezier( cmd.P1, cmd.P2, cmd.P3 );
				break;
			case CmdType.Text:
				{
					var g = ImGuiContext.Current;
					var font = g?.FontName ?? "Roboto Mono";
					var weight = g?.FontWeight ?? 400;
					painter.TextStyle = new TextStyle( font, cmd.FontSize, cmd.C1 ) { FontWeight = weight };
					painter.Text( cmd.Text, new Rect( cmd.P1, new Vector2( 8096f, 8096f ) ) );
					break;
				}
			case CmdType.Image:
				RenderImage( painter, cmd );
				break;
		}
	}

	private static void RenderImage( Painter painter, in Cmd cmd )
	{
		if ( !cmd.Texture.IsValid() )
			return;

		var pMin = cmd.P1;
		var pMax = cmd.P2;
		var uvMin = cmd.P3;
		var uvMax = cmd.P4;
		var target = new Rect( pMin, pMax - pMin );
		var uvSize = uvMax - uvMin;
		bool fullUv = uvMin == Vector2.Zero && uvMax == Vector2.One;
		bool rounded = cmd.Rounding > 0f;

		if ( fullUv && !rounded )
		{
			painter.Texture( cmd.Texture, target, cmd.C1 );
			return;
		}

		// Sub-rectangle of the texture: draw the whole texture scaled up and clip to the target.
		using var scope = painter.Scope();
		painter.Clip( target, rounded ? CornerRadii( cmd.Flags, cmd.Rounding, pMax - pMin ) : default );
		if ( MathF.Abs( uvSize.x ) < 1e-6f || MathF.Abs( uvSize.y ) < 1e-6f )
			return;
		var fullSize = new Vector2( (pMax.x - pMin.x) / uvSize.x, (pMax.y - pMin.y) / uvSize.y );
		var fullMin = new Vector2( pMin.x - uvMin.x * fullSize.x, pMin.y - uvMin.y * fullSize.y );
		painter.Texture( cmd.Texture, new Rect( fullMin, fullSize ), cmd.C1 );
	}
	#endregion
}