Editor/WeaponImporter/Tool/UI/Timeline/TimelineCard.cs
using Editor;
using Sandbox;
using WeaponImporter.Core.Analysis;

namespace WeaponImporter.Tool.UI;

/// <summary>Transport for the selected action (play, step, speed, loop, scrub) above its contact and event lanes.</summary>
public sealed class TimelineCard : Card
{
	private readonly ImporterController _c;
	private readonly IconButton _play;
	private readonly IconButton _loop;
	private readonly FloatSlider _scrub;
	private readonly Label _clock;
	private readonly Label _role;
	private readonly TimelineLanes _lanes;
	private bool _scrubbing;

	public TimelineCard( Widget parent, ImporterController controller ) : base( parent )
	{
		_c = controller;
		Layout.Margin = new Sandbox.UI.Margin( 10, 6, 10, 8 );
		Layout.Spacing = 4;

		var transport = Layout.AddRow();
		transport.Spacing = 6;
		_role = transport.Add( UiStyle.Bold( new Label( "Idle", this ) { FixedWidth = TimelineLanes.LabelWidth - 6, FixedHeight = UiStyle.ControlHeight, ToolTip = "The action being previewed; pick another in the preview header or in Animations" } ) );
		_play = transport.Add( UiStyle.Icon( this, "pause", controller.TogglePlay, "Play / pause (Space)" ) );
		transport.Add( UiStyle.Icon( this, "chevron_left", () => controller.StepFrame( -1 ), "Previous frame (Left arrow)" ) );
		transport.Add( UiStyle.Icon( this, "chevron_right", () => controller.StepFrame( 1 ), "Next frame (Right arrow)" ) );
		_scrub = transport.Add( new FloatSlider( this ) { Minimum = 0, Maximum = 1, Value = 0, ToolTip = "Scrub through the action", FixedHeight = UiStyle.ControlHeight }, 1 );
		_scrub.OnValueEdited = () =>
		{
			_scrubbing = true;
			controller.Seek( _scrub.Value );
			_scrubbing = false;
		};
		_clock = transport.Add( UiStyle.Muted( new Label( "0.00 s", this ) { FixedWidth = 92, FixedHeight = UiStyle.ControlHeight, Alignment = TextFlag.RightCenter, ToolTip = "Time in the action / its length" } ) );
		var speed = transport.Add( UiStyle.Framed( new ComboBox( this ) { FixedWidth = 70, ToolTip = "Playback speed" } ) );
		foreach ( var s in new[] { 0.25f, 0.5f, 1f, 2f } )
		{
			var v = s;
			speed.AddItem( $"{s:0.##}×", null, () => controller.Speed = v, selected: s == 1f );
		}
		_loop = transport.Add( UiStyle.Toggle( this, "repeat", true, null, "Loop the action" ) );
		_loop.OnToggled = on => controller.Loop = on;

		_lanes = Layout.Add( new TimelineLanes( this, controller ) );

		controller.RoleChanged += Refresh;
		controller.Changed += Refresh;
	}

	private void Refresh()
	{
		_role.Text = AnimationRoles.Label( _c.Role );
		_lanes.Update();
	}

	/// <summary>Per frame: playhead, clock and play icon.</summary>
	public void Tick()
	{
		var seconds = _c.RoleSeconds( _c.Role );
		if ( !_scrubbing )
			_scrub.Value = _c.Time;
		_clock.Text = $"{_c.Time * seconds:0.00} / {seconds:0.00} s";
		var icon = _c.Playing ? "pause" : "play_arrow";
		if ( _play.Icon != icon )
		{
			_play.Icon = icon;
			_play.Update();
		}
		if ( _role.Text != AnimationRoles.Label( _c.Role ) )
			_role.Text = AnimationRoles.Label( _c.Role );
		_lanes.SetPlayhead( _c.Time );
	}

	public override void OnDestroyed()
	{
		_c.RoleChanged -= Refresh;
		_c.Changed -= Refresh;
		base.OnDestroyed();
	}
}