Editor/EffigyEditor/EffigyWorkspaceBar.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;

namespace Marionette.EditorTools;

// ============================================================================
//  The workspace bar — which part of the pipeline you are working in.
//
//  WHAT WAS WRONG. Every toolset the editor has grew onto ONE bar. CAD's five
//  stages, sketch's four, sculpt's three and paint's one all took turns in the
//  same strip, and the only thing that ever said which was showing was the word
//  written small at the right of the tab row — a status readout, not a control.
//  Rig did not even get that: its tools lived in a dock panel on the right while
//  every other toolset lived in the bar, so "start rigging" was a different kind
//  of action from "start sculpting" for no reason a user could see.
//
//  Nothing was hidden and nothing was broken. It just kept growing, and a tool
//  that has grown for a while stops having a shape you can hold in your head.
//
//  WHAT THIS DOES. One row above the stage bar, four names, one selected:
//
//    ┌──────┬────────┬───────┬─────┐
//    │ CAD  │ Sculpt │ Paint │ Rig │
//    └──────┴────────┴───────┴─────┘
//      Sketch 2 | Solid 4 | Detail 5 | Repeat 4 | Finish 4        ✔ Finish
//      ▣ Primitive   ⬆ Extrude   ⟳ Revolve   ↝ Sweep   ⌂ Loft
//
//  The pipeline is now stated on screen rather than inferred from which glyphs
//  appeared, and it is a CONTROL: clicking Sculpt gets you sculpting, the way
//  clicking Extrude gets you an extrude. Picking a workspace also lays the docks
//  out for it and decides which of the viewport's eleven input handlers is live
//  — see EffigyWindow.Workspaces.cs, which owns all three of those.
//
//  TEXT ONLY, no icons, unlike every other bar in this editor. The workspaces
//  are a closed set of four that never changes and each has a name everybody
//  arriving already knows, so there is nothing for a glyph to disambiguate — it
//  would only add weight above the two rows that genuinely need it. Same
//  argument the stage TABS make against icons, one level up.
//
//  PAINTED, NOT LAID OUT, for the reason EffigyStageBar's header gives: a button
//  can only size itself to its own label inside a paint pass, so the row measures
//  while drawing, caches the rects, and hit-tests the cache.
// ============================================================================

/// <summary>
/// The four parts of the pipeline, in the order they are usually walked.
///
/// SKETCH IS NOT ONE OF THEM, deliberately. A sketch is something you open inside CAD and finish
/// again — it takes the stage bar over the way sculpt does, but you did not leave CAD to do it,
/// and putting it here would make the switcher answer "what am I doing right now" when the
/// question it exists to answer is "which part of the pipeline am I in". EffigyBarMode still
/// carries the finer distinction; this is the coarse one.
/// </summary>
internal enum EffigyWorkspace
{
	Cad,
	Sculpt,
	Paint,
	Rig,
}

/// <summary>
/// The switcher itself. Owns nothing but the selection and the painting — what a workspace MEANS
/// is entirely EffigyWindow.Workspaces.cs's business, reached through <see cref="Switched"/>.
/// </summary>
internal sealed class EffigyWorkspaceBar : Widget
{
	/// <summary>Height of the row. Shorter than the tool row and slightly taller than the tab row:
	/// it sits above both and wants to read as the outermost ring of chrome rather than as a third
	/// peer to them.</summary>
	public const float RowHeight = 32f;

	/// <summary>A workspace was clicked. Fires for the ALREADY-SELECTED one too — re-clicking Paint
	/// while painting is a reasonable way to ask for "put me back in the paint layout", and the
	/// window's entry paths are all no-ops when there is nothing to change.</summary>
	public Action<EffigyWorkspace> Switched { get; set; }

	public Color ChromeColor
	{
		get => _chrome;
		set { _chrome = value; Update(); }
	}

	/// <summary>Which workspace is lit. Set by the window from the bar mode, never by a click —
	/// the click asks, the window decides whether it happened. A sculpt that refuses to open (no
	/// cage, the feature errored) must not leave the switcher claiming you are in it.</summary>
	public EffigyWorkspace Selected
	{
		get => _selected;
		set
		{
			if ( _selected == value )
				return;

			_selected = value;
			Update();
		}
	}

	private static readonly (EffigyWorkspace Workspace, string Name, string Tip)[] Items =
	{
		(EffigyWorkspace.Cad, "CAD",
			"Sketches, solids and the feature tree — how the shape gets made"),
		(EffigyWorkspace.Sculpt, "Sculpt",
			"Brush the shape by hand. Opens the sculpt you were last in, or adds one"),
		(EffigyWorkspace.Paint, "Paint",
			"Lay colour onto a body's texture. Opens the paint you were last in, or adds one"),
		(EffigyWorkspace.Rig, "Rig",
			"Place bones and pin bodies to them, ready to export"),
	};

	private EffigyWorkspace _selected = EffigyWorkspace.Cad;
	private Color _chrome = Theme.ControlBackground;

	/// <summary>Where each item was drawn. Same cache-while-painting, hit-test-the-cache pattern as
	/// EffigyStageTabRow — a click before the first paint finds an empty list and does nothing.
	/// </summary>
	private readonly List<Rect> _rects = new();

	private int _hovered = -1;

	private const float Pad = 18f;
	private const float Gap = 3f;

	public EffigyWorkspaceBar( Widget parent ) : base( parent )
	{
		Cursor = CursorShape.Finger;
		MouseTracking = true;

		TranslucentBackground = true;
		NoSystemBackground = true;

		FixedHeight = RowHeight;
	}

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

		Paint.ClearPen();
		Paint.SetBrush( _chrome );
		Paint.DrawRect( new Rect( 0f, 0f, Width, Height ) );

		PaintItems();

		// A hairline under the row. The stage bar draws its own, so the two together give the
		// chrome a readable stack rather than one undifferentiated slab.
		Paint.ClearBrush();
		Paint.SetPen( Theme.Text.WithAlpha( 0.10f ), 1f );
		Paint.DrawLine( new Vector2( 0f, Height - 0.5f ), new Vector2( Width, Height - 0.5f ) );
	}

	private void PaintItems()
	{
		_rects.Clear();

		var x = Pad * 0.5f;

		for ( var i = 0; i < Items.Length; i++ )
		{
			var (workspace, name, _) = Items[i];
			var selected = workspace == _selected;

			Paint.SetDefaultFont( EffigyToolChrome.TabFontSize + 0.5f, selected ? 700 : 500 );

			var width = Paint.MeasureText( name ).x + Pad;
			var rect = new Rect( x, 4f, width, Height - 8f );

			_rects.Add( rect );

			// A FILLED PILL for the selected one, where the stage tabs below use a 2px underline.
			// The two are meant to look different: they are different kinds of choice — this one
			// changes what the whole window is for, that one changes which five buttons are on a
			// row — and drawing them the same way would make the outer choice look like one more
			// tab in a longer strip.
			if ( selected )
			{
				Paint.ClearPen();
				Paint.SetBrush( Theme.Blue.WithAlpha( 0.20f ) );
				Paint.DrawRect( rect, 4f );

				Paint.ClearBrush();
				Paint.SetPen( Theme.Blue.WithAlpha( 0.55f ), 1f );
				Paint.DrawRect( rect, 4f );
			}
			else if ( _hovered == i )
			{
				Paint.ClearPen();
				Paint.SetBrush( Theme.Text.WithAlpha( 0.06f ) );
				Paint.DrawRect( rect, 4f );
			}

			Paint.ClearBrush();
			Paint.SetDefaultFont( EffigyToolChrome.TabFontSize + 0.5f, selected ? 700 : 500 );
			Paint.SetPen( selected ? Theme.Blue : Theme.TextControl.WithAlpha( 0.8f ) );
			Paint.DrawText( rect, name, TextFlag.Center );

			x += width + Gap;
		}
	}

	private int ItemAt( Vector2 local )
	{
		for ( var i = 0; i < _rects.Count; i++ )
		{
			if ( _rects[i].IsInside( local ) )
				return i;
		}

		return -1;
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		var item = ItemAt( e.LocalPosition );

		if ( item == _hovered )
			return;

		_hovered = item;
		ToolTip = item >= 0 ? Items[item].Tip : "";

		Update();
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();

		_hovered = -1;
		Update();
	}

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

		e.Accepted = true;

		var item = ItemAt( e.LocalPosition );

		if ( item >= 0 )
			Switched?.Invoke( Items[item].Workspace );
	}
}