Editor/Designers/ArchDesignerDialog.cs
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; }
// A designer whose subject is sometimes ART rather than geometry stands a second viewport in the same slot and
// swaps to it, so aiming a prefab is a step of one design rather than a second window over the top of it.
Widget aiming;
Label caption;
Label advice;
Widget views;
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 static Widget Note( string text ) {
var label = new Label( text );
label.WordWrap = true;
label.FixedWidth = 200f;
label.MinimumHeight = MathF.Ceiling( text.Length / 34f ) * 15f + 4f;
label.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.55);" );
return label;
}
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;
}
// A designer that never aims art leaves this null and keeps the one generated stage it always had.
protected virtual Widget Aiming( Widget parent ) => null;
// Which viewport the right-hand side is showing. The generated stage is the resting state, so a step that
// stops aiming does not have to remember to say so.
protected void Aim( bool art, string title, string note ) {
if ( aiming is not null ) {
aiming.Visible = art;
}
Stage.Visible = !art || aiming is null;
views.Visible = Stage.Visible;
caption.Text = title;
advice.Text = note;
}
Widget Viewport( ArchKit kit, string title, string note ) {
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;
caption = new Label( title );
caption.SetStyles( "font-size: 15px; font-weight: 700;" );
heading.Add( caption );
advice = new Label( note ) { WordWrap = true };
heading.Add( advice );
Stage = new ArchDesignerStage( column, kit, Staged );
column.Layout.Add( Stage, 1 );
aiming = Aiming( column );
if ( aiming is not null ) {
aiming.Visible = false;
column.Layout.Add( aiming, 1 );
}
views = new Widget( column ) { Layout = Layout.Row() };
views.Layout.Margin = 8;
views.Layout.Spacing = 6;
views.Layout.Add( new Button( "Front", "crop_landscape" ) { Clicked = Stage.Front } );
views.Layout.Add( new Button( "Inside", "flip" ) { Clicked = Stage.Inside } );
views.Layout.Add( new Button( "Perspective", "view_in_ar" ) { Clicked = Stage.Perspective } );
views.Layout.AddStretchCell();
column.Layout.Add( views );
return column;
}
}