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.
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;
}
}