Editor/Common/CollapsibleSection.cs
using System;
using Editor;
using Sandbox;

namespace Grains.RazorDesigner.Common;

public class CollapsibleSection : Widget
{
	public string Title { get; set; } = "";
	public string Icon { get; set; } = "";
	public Color HeaderColor { get; set; } = "#8E9199";
	public bool IsCollapsible { get; set; } = true;
	public Action<bool> ExpandedChanged;

	public Layout HeaderRightLayout { get; private set; }

	// Width reserved for the right-justified widgets; the title text is right-clipped by this much.
	public float HeaderRightReservedWidth { get; set; } = 0f;

	private readonly Header _header;
	private readonly Widget _body;
	private bool _expanded;

	public bool Expanded
	{
		get => _expanded;
		set
		{
			if ( _expanded == value ) return;
			_expanded = value;
			_body.Visible = _expanded;
			_header.Update();
			ExpandedChanged?.Invoke( _expanded );
		}
	}

	public Layout BodyLayout => _body.Layout;

	public CollapsibleSection( Widget parent, string title, string icon ) : base( parent )
	{
		Title = title;
		Icon = icon;

		Layout = Layout.Column();
		Layout.Margin = 0;
		Layout.Spacing = 0;

		_header = new Header( this );
		_header.OnToggled = () => { if ( IsCollapsible ) Expanded = !Expanded; };
		Layout.Add( _header );

		_body = new Widget( this );
		_body.Layout = Layout.Column();
		_body.Layout.Margin = new Sandbox.UI.Margin( 0 );
		Layout.Add( _body );

		_expanded = true;
	}

	private sealed class Header : Widget
	{
		public Action OnToggled;
		private readonly CollapsibleSection _owner;

		public Header( CollapsibleSection owner ) : base( owner )
		{
			_owner = owner;
			MouseTracking = true;
			FixedHeight = Theme.RowHeight + 4;

			Layout = Layout.Row();
			Layout.Margin = new Sandbox.UI.Margin( 0 );
			Layout.AddStretchCell();
			_owner.HeaderRightLayout = Layout;
		}

		protected override void OnPaint()
		{
			var rect = LocalRect;
			Paint.Antialiasing = true;
			Paint.TextAntialiasing = true;

			// Top hairline + subtle shadow line, like InspectorHeader.
			{
				var top = rect;
				top.Bottom = top.Top + 1;
				Paint.SetBrushAndPen( Theme.ControlBackground );
				Paint.DrawRect( top );

				top.Position += new Vector2( 0, 1 );
				Paint.SetBrushAndPen( Theme.BorderLight );
				Paint.DrawRect( top );
			}

			// Hover highlight (only when interactive).
			if ( Paint.HasMouseOver && _owner.IsCollapsible )
			{
				Paint.ClearPen();
				Paint.SetBrush( Theme.Blue.WithAlpha( 0.10f ) );
				Paint.DrawRect( rect, 0 );
			}

			var color = _owner.HeaderColor;
			var opacity = _owner.Expanded ? 1f : 0.8f;

			float left = rect.Left + 4;
			if ( _owner.IsCollapsible )
			{
				var chevronRect = new Rect( left, rect.Top, 18, rect.Height );
				Paint.SetPen( color );
				Paint.DrawIcon(
					chevronRect,
					_owner.Expanded ? "arrow_drop_down" : "arrow_right",
					18,
					TextFlag.Center );
				left = chevronRect.Right + 4;
			}
			var iconText = string.IsNullOrEmpty( _owner.Icon ) ? "category" : _owner.Icon;
			var iconRect = new Rect( left, rect.Top, 20, rect.Height );
			Paint.SetPen( color.WithAlpha( opacity ) );
			Paint.DrawIcon( iconRect, iconText, 16, TextFlag.Center );

			// Title.
			var textRect = rect;
			textRect.Left = iconRect.Right + 6;
			textRect.Right -= 8 + _owner.HeaderRightReservedWidth;  // reserve space for inline header widgets
			Paint.SetPen( Theme.Text.WithAlphaMultiplied( opacity ) );
			Paint.SetHeadingFont( 11, 440, sizeInPixels: true );
			Paint.DrawText( textRect, _owner.Title, TextFlag.LeftCenter );
		}

		protected override void OnMouseClick( MouseEvent e )
		{
			base.OnMouseClick( e );
			if ( e.LeftMouseButton )
				OnToggled?.Invoke();
		}
	}
}