Editor/RigControlEditor/RigIconButton.cs
using Editor;
using Sandbox;
using System;

namespace Marionette.Tools;

/// <summary>Which drawing an icon button paints. Add as more are designed.</summary>
internal enum RigIcon
{
	/// <summary>A keyframe diamond with a plus punched through it.</summary>
	AddKeyframe,

	/// <summary>A trash can - lid, handle, tapered body, two slots.</summary>
	Delete
}

/// <summary>
/// A button whose icon is drawn rather than looked up in a font.
///
/// s&box ships CLASSIC Material Icons (addons/base/Assets/fonts/MaterialIcons-Regular.ttf), not
/// the newer Material Symbols - so a name from the Symbols set silently renders as nothing. The
/// key button was set to "diamond", which is a Symbols icon and appears nowhere in the editor's
/// own source: it had been drawing blank.
///
/// Painting it removes the guesswork entirely, and lets the icon match the tool - this diamond is
/// the same shape the timeline draws for a Linear keyframe, in the same colour, so the button and
/// the thing it makes are visibly one idea.
/// </summary>
internal sealed class RigIconButton : Widget
{
	private readonly RigIcon _icon;
	private readonly Color _color;
	private readonly string _label;
	private readonly string _hint;
	private readonly Action _clicked;

	private bool _pressed;

	public RigIconButton( Widget parent, RigIcon icon, Color color, string label, string hint, Action clicked )
		: base( parent )
	{
		_icon = icon;
		_color = color;
		_label = label;
		_hint = hint;
		_clicked = clicked;

		ToolTip = hint;
		Cursor = CursorShape.Finger;
		MouseTracking = true;

		FixedHeight = 22f;
		FixedWidth = string.IsNullOrEmpty( label ) ? 28f : 58f;
	}

	protected override void OnPaint()
	{
		Paint.Antialiasing = true;

		var hovered = IsUnderMouse;

		// Background follows the editor's own button states so it sits in a toolbar without
		// looking like a foreign object.
		Paint.ClearPen();
		Paint.SetBrush( _pressed
			? Theme.ControlBackground.Darken( 0.2f )
			: hovered ? Theme.ControlBackground.Lighten( 0.4f ) : Theme.ControlBackground );

		Paint.DrawRect( LocalRect, 3f );

		var iconCenter = string.IsNullOrEmpty( _label )
			? LocalRect.Center
			: new Vector2( 15f, LocalRect.Center.y );

		PaintIcon( iconCenter, hovered ? _color : _color.WithAlpha( 0.85f ) );

		if ( string.IsNullOrEmpty( _label ) )
			return;

		Paint.SetDefaultFont( 7, 600 );
		Paint.SetPen( _color.WithAlpha( hovered ? 1f : 0.85f ) );
		Paint.DrawText( new Rect( 26f, 0f, Width - 30f, Height ), _label, TextFlag.LeftCenter );
	}

	private void PaintIcon( Vector2 center, Color color )
	{
		switch ( _icon )
		{
			case RigIcon.AddKeyframe:
			{
				const float radius = 9f;

				Paint.ClearPen();
				Paint.SetBrush( color );
				Paint.DrawPolygon(
					center + new Vector2( 0f, -radius ),
					center + new Vector2( radius, 0f ),
					center + new Vector2( 0f, radius ),
					center + new Vector2( -radius, 0f ) );

				// The plus is PUNCHED OUT in the background colour rather than drawn on top, so it
				// reads as a hole through the diamond the way the mock does.
				Paint.SetBrush( Theme.ControlBackground );

				const float arm = 5.4f;
				const float thickness = 2.6f;

				Paint.DrawRect( new Rect( center.x - thickness * 0.5f, center.y - arm, thickness, arm * 2f ), 0.6f );
				Paint.DrawRect( new Rect( center.x - arm, center.y - thickness * 0.5f, arm * 2f, thickness ), 0.6f );
				return;
			}

			case RigIcon.Delete:
			{
				Paint.ClearPen();
				Paint.SetBrush( color );

				// Handle, then lid. Both rounded bars - at this size the roundness is most of what
				// makes it read as a drawn icon rather than a stack of rectangles.
				Paint.DrawRect( new Rect( center.x - 2.6f, center.y - 9f, 5.2f, 1.9f ), 0.9f );
				Paint.DrawRect( new Rect( center.x - 8f, center.y - 7f, 16f, 2.6f ), 1.3f );

				// Body, tapering in toward the bottom like the mock. Drawn as a filled trapezoid
				// with a smaller one punched out of it, which gives a thick even outline - simpler
				// and more predictable at 18px than trying to stroke a tapered path.
				const float topHalf = 6.6f;
				const float bottomHalf = 5.1f;
				const float top = -3.4f;
				const float bottom = 9f;

				Paint.DrawPolygon(
					center + new Vector2( -topHalf, top ),
					center + new Vector2( topHalf, top ),
					center + new Vector2( bottomHalf, bottom ),
					center + new Vector2( -bottomHalf, bottom ) );

				Paint.SetBrush( Theme.ControlBackground );

				const float inset = 2.1f;

				Paint.DrawPolygon(
					center + new Vector2( -topHalf + inset, top + inset ),
					center + new Vector2( topHalf - inset, top + inset ),
					center + new Vector2( bottomHalf - inset, bottom - inset ),
					center + new Vector2( -bottomHalf + inset, bottom - inset ) );

				// The two slots.
				Paint.SetBrush( color );
				Paint.DrawRect( new Rect( center.x - 2.9f, center.y - 0.6f, 1.9f, 5.6f ), 0.9f );
				Paint.DrawRect( new Rect( center.x + 1.0f, center.y - 0.6f, 1.9f, 5.6f ), 0.9f );
				return;
			}
		}
	}

	protected override void OnMousePress( MouseEvent e )
	{
		if ( !e.LeftMouseButton )
			return;

		_pressed = true;
		Update();
		e.Accepted = true;
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		if ( !_pressed )
			return;

		_pressed = false;
		Update();

		// Only fires if released while still over the button - dragging off to cancel is what
		// every other button does.
		if ( IsUnderMouse )
			_clicked?.Invoke();
	}

	protected override void OnMouseEnter()
	{
		base.OnMouseEnter();
		RigStatusBar.Show( _hint );
		Update();
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();
		RigStatusBar.Clear( _hint );

		_pressed = false;
		Update();
	}
}