Editor/TextToAnimation/UI/TaFold.cs

An editor UI widget that implements a collapsible fold section with a header and body. It remembers open/closed state per key during the editor session, renders header icons/title/note, and toggles body visibility on click.

File Access
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;

namespace TextToAnimation.Editor.UI;

/// <summary>
/// A collapsible section: a slim header (chevron, blue icon, title, optional note on the right) over a body
/// that only shows when open. Open/closed is remembered per section for the editor session, so a panel
/// keeps the shape the user left it in.
/// </summary>
public sealed class TaFold : Widget
{
	static readonly Dictionary<string, bool> Remembered = new();

	readonly string _key;
	readonly FoldHeader _header;
	bool _open;

	public Widget Body { get; }
	public Layout Content => Body.Layout;

	public TaFold( Widget parent, string icon, string title, bool open = false, string key = null ) : base( parent )
	{
		_key = key ?? title;
		_open = Remembered.TryGetValue( _key, out var remembered ) ? remembered : open;
		Layout = Layout.Column();
		_header = Layout.Add( new FoldHeader( this, icon, title ) );
		Body = Layout.Add( new Widget( this ) { Layout = Layout.Column() }, 1 );
		Body.Layout.Margin = new Sandbox.UI.Margin( 12, 2, 12, 12 );
		Body.Layout.Spacing = 6;
		Body.Visible = _open;
	}

	public bool Open
	{
		get => _open;
		set
		{
			if ( _open == value ) return;
			_open = value;
			Remembered[_key] = value;
			Body.Visible = value;
			_header.Update();
		}
	}

	/// <summary>Small text on the right of the header (a count, a warning).</summary>
	public string Note
	{
		get => _header.Note;
		set { _header.Note = value; _header.Update(); }
	}

	public Color NoteColor { get => _header.NoteColor; set { _header.NoteColor = value; _header.Update(); } }

	protected override void OnPaint()
	{
		Paint.Antialiasing = true;
		Paint.SetPen( Theme.ControlBackground.Lighten( .25f ), 1 );
		Paint.SetBrush( Theme.ControlBackground );
		Paint.DrawRect( LocalRect.Shrink( 1 ), 6 );
	}

	sealed class FoldHeader : Widget
	{
		readonly TaFold _fold;
		readonly string _icon, _title;
		public string Note = "";
		public Color NoteColor = Theme.TextLight;

		public FoldHeader( TaFold fold, string icon, string title ) : base( fold )
		{
			_fold = fold;
			_icon = icon;
			_title = title;
			FixedHeight = 34;
			Cursor = CursorShape.Finger;
			MouseTracking = true;
		}

		protected override void OnMouseEnter() => Update();
		protected override void OnMouseLeave() => Update();

		protected override void OnMousePress( MouseEvent e )
		{
			if ( e.LeftMouseButton ) e.Accepted = true;
		}

		protected override void OnMouseReleased( MouseEvent e )
		{
			base.OnMouseReleased( e );
			if ( e.LeftMouseButton && LocalRect.IsInside( e.LocalPosition ) ) _fold.Open = !_fold.Open;
		}

		protected override void OnPaint()
		{
			Paint.Antialiasing = true;
			if ( Paint.HasMouseOver )
			{
				Paint.ClearPen();
				Paint.SetBrush( Color.White.WithAlpha( .03f ) );
				Paint.DrawRect( LocalRect.Shrink( 1 ), 6 );
			}
			Paint.SetPen( Theme.TextLight );
			Paint.DrawIcon( new Rect( 8, 0, 16, Height ), _fold.Open ? "expand_more" : "chevron_right", 16, TextFlag.Center );
			Paint.SetPen( TaStyle.Accent );
			Paint.DrawIcon( new Rect( 28, 0, 18, Height ), _icon, 16, TextFlag.Center );
			Paint.SetDefaultFont( 9, 600 );
			Paint.SetPen( _fold.Open || Paint.HasMouseOver ? Theme.Text : Theme.Text.WithAlpha( .8f ) );
			Paint.DrawText( new Rect( 52, 0, Width - 140, Height ), _title, TextFlag.LeftCenter );
			if ( string.IsNullOrEmpty( Note ) ) return;
			Paint.SetDefaultFont( 8 );
			Paint.SetPen( NoteColor );
			Paint.DrawText( new Rect( Width - 150, 0, 140, Height ), Note, TextFlag.RightCenter );
		}
	}
}