Editor/Widgets/SnapButton.cs
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace AltCurves.Widgets;

/// <summary>
/// Button that controls snapping of time/values, consists of a main toggle button and a dropdown that customizes the snap value
/// </summary>
internal abstract class SnapButton<T> : Widget
{
	/// <summary>
	/// Overall snap toggle state
	/// </summary>
	public bool SnapEnabled
	{
		get { return _snapEnabled; }
		set { _snapEnabled = value; BuildButtonLayout(); }
	}
	private bool _snapEnabled = true;

	/// <summary>
	/// True if this snapping is forcefully disabled via alt-key
	/// </summary>
	public bool ForcefullyDisabled
	{
		get { return _forcefullyDisabled; }
		set { _forcefullyDisabled = value; BuildButtonLayout(); }
	}
	private bool _forcefullyDisabled = false;

	/// <summary>
	/// Selected snap mode
	/// </summary>
	public T CurrentSnapMode { get; set; }

	/// <summary>
	/// The user-provided snap value if we're in custom mode.
	/// </summary>
	public float CustomSnapValue { get; set; }

	private T _customSnapMode;

	/// <summary>
	/// Custom values must match this regex pattern
	/// </summary>
	protected abstract string CustomValueRegexValidation { get; }

	/// <summary>
	/// Placeholder string for custom value input
	/// </summary>
	protected abstract string CustomValuePlaceholderString { get; }

	private Button _snapToggle;
	private Button _showMore;
	private readonly string _text;
	private readonly string _shortcut;

	public SnapButton( Widget parent, string text, string shortcut, T customValue, T defaultValue ) : base( parent )
	{
		MinimumWidth = 80;
		Cursor = CursorShape.Finger;

		CurrentSnapMode = defaultValue;
		CustomSnapValue = 1.0f;
		_customSnapMode = customValue;

		_text = text;
		_shortcut = shortcut;

		Enabled = true;

		Layout = Layout.Row();
		BuildButtonLayout();
	}

	protected void BuildButtonLayout()
	{
		// Not pretty, but Button doesn't respond to changes in styles, and I don't like the default togglebutton, so I'll just toggle between primary/unstyled for now.
		Layout.Clear( true );
		Layout.Add( _snapToggle = new Button( _text )
		{
			VerticalSizeMode = SizeMode.CanGrow,
			Tint = SnapEnabled && !ForcefullyDisabled ? Theme.Primary : "#48494c", // Hard coded for now
			ToolTip = $"Toggle {_text} (Shortcut: {_shortcut})"
		} );
		_snapToggle.MouseClick += () => SnapEnabled = !SnapEnabled;

		Layout.Add( _showMore = new Button( "", "more_vert" )
		{
			FixedWidth = 30.0f,
			VerticalSizeMode = SizeMode.CanGrow,
			ToolTip = $"Choose {_text} precision"
		} );
		_showMore.MouseClick += ClickedShowMore;
	}

	/// <summary>
	/// Create the dropdown menu for selecting snap amounts
	/// </summary>
	protected void ClickedShowMore()
	{
		var m = new Menu()
		{
			DeleteOnClose = true
		};

		m.AddHeading( $"{_text} Precision:" );

		var enumValues = typeof( T ).GetEnumValues();
		var enumInfo = DisplayInfo.ForEnumValues( typeof( T ) );
		for ( int i = 0; i < enumValues.Length; i++ )
		{
			var value = enumValues.GetValue( i );
			if ( value.Equals( _customSnapMode ) )
				continue;

			var option = m.AddOption( new Option( enumInfo[i].Name )
			{
				Checkable = true,
				Checked = value.Equals( CurrentSnapMode )
			} );
			option.Triggered += () =>
			{
				CurrentSnapMode = (T)value;
				SnapEnabled = true; // Re-enable disabled snap on option selection
			};
		}

		// Separate custom button logic
		m.AddSeparator();
		var customHeader = m.AddOption( new Option( "Custom:" )
		{
			Checkable = true,
			Checked = EqualityComparer<T>.Default.Equals( CurrentSnapMode, _customSnapMode )
		} );
		customHeader.Triggered += () =>
		{
			CurrentSnapMode = _customSnapMode;
			SnapEnabled = true; // Re-enable disabled snap on option selection
		};
		var snapLine = m.AddWidget( new ContextMenuLineEdit( this )
		{
			PlaceholderText = CustomValuePlaceholderString,
			RegexValidator = CustomValueRegexValidation
		} );
		snapLine.ReturnPressed += () =>
		{
			var match = Regex.Match( snapLine.Text, CustomValueRegexValidation );
			if ( match.Success )
			{
				CustomSnapValue = ParseCustomValue( match.Groups );
				CurrentSnapMode = _customSnapMode;
				SnapEnabled = true; // Re-enable disabled snap on option selection
				m.Close();
			}
			else
			{
				Log.Warning( $"Ignoring '{snapLine.Text}' not matching {CustomValueRegexValidation} regex" );
			}
		};

		if ( EqualityComparer<T>.Default.Equals( CurrentSnapMode, _customSnapMode ) )
			snapLine.Text = CustomValueString();

		m.OpenAt( Editor.Application.CursorPosition );
	}

	protected abstract float ParseCustomValue( GroupCollection groupCollection );
	protected virtual string CustomValueString() => $"{CustomSnapValue}";
}