Elements/Widgets/Slider.cs
using Duccsoft.ImGui.Rendering;
using System.Numerics;

namespace Duccsoft.ImGui.Elements;

public class Slider<T> : Element where T : INumber<T>
{
	public Slider( Element parent, string label, ref T[] components, T min, T max, string format ) 
		: base( parent )
	{
		Label = label;
		Id = ImGui.GetID( Label );
		ComponentCount = components.Length;

		OnBegin();
		ImGui.PushID( Id );
		var sliderWidth = BarAreaWidth / (components.Length);
		for ( int i = 0; i < components.Length; i++ )
		{
			ImGui.PushID( i );
			_ = new SliderBar( this, ref components[i], sliderWidth, min, max, format );
			ImGui.PopID();
			ImGui.SameLine( sliderWidth, Style.ItemInnerSpacing.x );
		}
		ImGui.PopID();
		OnEnd();
	}

	public string Label { get; set; }
	public int ComponentCount { get; set; }
	private float BarAreaWidth => ImGui.GetFontSize() * 15f;
	public override Vector2 Size => new Vector2( BarAreaWidth, ImGui.GetFrameHeightWithSpacing() )
		+ ComponentCount * new Vector2( Style.ItemInnerSpacing.x, 0f );

	private class SliderBar : Element
	{
		public SliderBar( Element parent, ref T value, float width, T min, T max, string format )
			: base( parent )
		{
			Value = value;
			Min = min;
			Max = max;
			Format = format;

			Size = new Vector2( width, ImGui.GetFrameHeightWithSpacing() );

			OnBegin();
			OnEnd();

			value = Value;
		}

		public T Value { get; set; }
		public T Min { get; init; }
		public T Max { get; init; }
		public string Format { get; init; }

		protected float ValueProgress
		{
			get => LerpInverse( Value, Min, Max );
			set
			{
				Value = Lerp( Min, Max, value );
			}
		}

		private static T Lerp( T from, T to, float frac, bool clamp = true )
		{
			if ( clamp )
			{
				frac = frac.Clamp( 0f, 1f );
			}

			var fromF = float.CreateTruncating( from );
			var toF = float.CreateTruncating( to );
			return T.CreateTruncating( fromF + frac * (toF - fromF) );
		}

		private static float LerpInverse( T value, T from, T to )
		{
			var valueF = float.CreateTruncating( value );
			var fromF = float.CreateTruncating( from );
			var toF = float.CreateTruncating( to );
			valueF -= fromF;
			toF -= fromF;
			return valueF / toF;
		}

		protected Color32 GrabColor
		{
			get
			{
				return IsActive
					? ImGui.GetColorU32( ImGuiCol.SliderGrabActive )
					: ImGui.GetColorU32( ImGuiCol.SliderGrab );
			}
		}

		public override void OnUpdateInput()
		{
			base.OnUpdateInput();

			if ( IsActive )
			{
				var xPosMin = ScreenPosition.x;
				var xPosMax = ScreenPosition.x + Size.x;
				ValueProgress = MathX.LerpInverse( ImGui.GetMousePos().x, xPosMin, xPosMax );
			}
		}

		protected override void OnDrawSelf( ImDrawList drawList )
		{
			var bgRect = new Rect( ScreenPosition, Size );
			drawList.AddRectFilled( ScreenPosition, ScreenPosition + Size, FrameColor );

			var grabSize = new Vector2( Style.GrabMinSize, ImGui.GetFrameHeight() );
			var xGrabPos = MathX.Lerp( 0f, bgRect.Size.x - grabSize.x, ValueProgress );
			var grabPos = ScreenPosition + new Vector2( xGrabPos, Style.FramePadding.y * 0.5f );
			drawList.AddRectFilled( grabPos, grabPos + grabSize, GrabColor );

			var text = string.Format( "{0:" + Format + "}", Value );
			var xOffsetText = bgRect.Size.x * 0.5f;
			var textPos = ScreenPosition + new Vector2( xOffsetText, Style.FramePadding.y );
			drawList.AddText( textPos, ImGui.GetColorU32( ImGuiCol.Text ), text, TextFlag.CenterTop );
		}
	}
}