Editor/Designers/ArchDesignerDialog.cs

Editor dialog class for architecture designers. It builds a UI with a sidebar form and a viewport stage, provides helpers for common form controls, handles assembling and repopulating the UI, and copies debug reports to the clipboard.

File AccessNetworking
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// A designer decides three things: its form, what the stage shows, what Apply hands back.
public abstract class ArchDesignerDialog : Dialog
{
	protected Widget Form { get; private set; }
	protected ArchDesignerStage Stage { get; private set; }

	protected ArchDesignerDialog( Widget parent, string title, string icon ) : base( parent )
	{
		Window.Title = title;
		Window.SetWindowIcon( icon );
		Window.SetModal( true, true );
		Window.Size = new Vector2( 1080, 720 );
		Window.MinimumWidth = 900;
		Window.MinimumHeight = 620;
	}

	protected abstract void Populate();

	protected abstract ArchStaged Staged();

	protected abstract void Apply();

	protected abstract void SaveTemplate();

	protected abstract string SaveLabel { get; }

	// Called by the subclass once its draft exists: the base constructor would read unassigned fields.
	protected void Assemble( ArchKit kit, string caption, string advice, string footnote )
	{
		Layout = Layout.Column();
		Layout.Margin = 0;
		Layout.Spacing = 0;

		var split = Layout.AddRow( 1 );

		split.Add( Sidebar() );
		split.AddSeparator();
		split.Add( Viewport( kit, caption, advice ), 1 );

		Layout.AddSeparator();

		var footer = Layout.AddRow();
		footer.Margin = 10;
		footer.Spacing = 6;
		footer.Add( new Label( footnote ) );
		footer.Add( new Button( "Copy Debug", "content_copy" ) { Clicked = CopyDebug } );
		footer.AddStretchCell();
		footer.Add( new Button( "Cancel", "close" ) { Clicked = Close } );
		footer.Add( new Button( SaveLabel, "save" ) { Clicked = SaveTemplate } );
		footer.Add( new Button.Primary( "Apply", "check" ) { Clicked = Apply } );

		Repopulate();
	}

	// Resolved vs generated vs plan - one paste to settle data-or-geometry.
	protected virtual string Diagnose() => null;

	void CopyDebug()
	{
		var report = string.Join( "\n",
			$"# {Window.Title}",
			"",
			"## resolved",
			Diagnose() ?? "nothing reported",
			"",
			"## generated",
			Stage.Describe(),
			"",
			"## plan",
			ArchStorage.Snapshot( Staged().Plan ) );

		EditorUtility.Clipboard.Copy( report );
		Log.Info( $"Architecture: {Window.Title} debug copied to the clipboard ({report.Length} characters)." );
	}

	// Relaid out, not greyed: options that don't apply are not there at all.
	protected void Repopulate()
	{
		Form.Layout.Clear( true );

		Populate();

		Form.Layout.AddStretchCell();
		Stage.Restage();
	}

	protected Widget Heading( string text )
	{
		var label = new Label( text );
		label.SetStyles( "font-size: 13px; font-weight: 700; margin-top: 8px;" );

		return label;
	}

	protected Widget Row( string label, Widget control )
	{
		var row = new Widget( this ) { Layout = Layout.Row() };
		row.Layout.Spacing = 6;
		row.Layout.Add( new Label( label ) { MinimumWidth = 112 } );
		row.Layout.Add( control, 1 );

		return row;
	}

	protected Widget Text( string label, string value, Action<string> assign, bool restages = false )
	{
		var edit = new LineEdit( value ?? "" );
		edit.TextEdited += text =>
		{
			assign( text ?? "" );

			if ( restages )
			{
				Stage.Restage();
			}
		};

		return Row( label, edit );
	}

	protected Widget Number( string label, float value, Action<float> assign )
	{
		void Take( string text )
		{
			if ( !float.TryParse( text, out var parsed ) )
			{
				return;
			}

			assign( ArchGridService.Fine( parsed ) );
			Stage.Restage();
		}

		var edit = new ArchNumberEdit( value.ToString( "0.##" ), ArchGridService.FinestSize, 1f, "0.##",
			pulled => Take( pulled.ToString( "0.##" ) ) );

		edit.TextEdited += Take;

		return Row( label, edit );
	}

	protected Widget Integer( string label, int value, Action<int> assign )
	{
		void Take( string text )
		{
			if ( !int.TryParse( text, out var parsed ) )
			{
				return;
			}

			assign( Math.Max( 1, parsed ) );
			Stage.Restage();
		}

		var edit = new ArchNumberEdit( value.ToString(), 1f, 0.25f, "0", pulled => Take( pulled.ToString( "0" ) ) );

		edit.TextEdited += Take;

		return Row( label, edit );
	}

	protected Widget Check( string label, bool value, Action<bool> assign )
	{
		var check = new Checkbox( label ) { Value = value };
		check.Toggled += () =>
		{
			assign( check.Value );
			Stage.Restage();
		};

		return check;
	}

	// A toggle that decides whether other rows exist, so it repopulates.
	protected Widget Gate( string label, bool value, Action<bool> assign )
	{
		var check = new Checkbox( label ) { Value = value };
		check.Toggled += () =>
		{
			assign( check.Value );
			Repopulate();
		};

		return check;
	}

	Widget Sidebar()
	{
		var sidebar = new Widget( this ) { Layout = Layout.Column() };
		sidebar.MinimumWidth = 330;
		sidebar.MaximumWidth = 330;
		sidebar.Layout.Margin = 8;

		var scroll = new ScrollArea( sidebar );
		scroll.HorizontalScrollbarMode = ScrollbarMode.Off;

		Form = new Widget( scroll ) { Layout = Layout.Column() };
		Form.Layout.Margin = 4;
		Form.Layout.Spacing = 6;
		scroll.Canvas = Form;
		sidebar.Layout.Add( scroll, 1 );

		return sidebar;
	}

	Widget Viewport( ArchKit kit, string caption, string advice )
	{
		var column = new Widget( this ) { Layout = Layout.Column() };
		column.Layout.Margin = 0;
		column.Layout.Spacing = 0;

		var heading = column.Layout.AddColumn();
		heading.Margin = 12;
		heading.Spacing = 2;

		var title = new Label( caption );
		title.SetStyles( "font-size: 15px; font-weight: 700;" );
		heading.Add( title );
		heading.Add( new Label( advice ) { WordWrap = true } );

		Stage = new ArchDesignerStage( column, kit, Staged );
		column.Layout.Add( Stage, 1 );

		var views = column.Layout.AddRow();
		views.Margin = 8;
		views.Spacing = 6;
		views.Add( new Button( "Front", "crop_landscape" ) { Clicked = Stage.Front } );
		views.Add( new Button( "Inside", "flip" ) { Clicked = Stage.Inside } );
		views.Add( new Button( "Perspective", "view_in_ar" ) { Clicked = Stage.Perspective } );
		views.AddStretchCell();

		return column;
	}
}