Editor UI code for the architecture creation sidebar. It defines header, advice label, collapsible sections, a persistent run bar, and related widgets used to layout tool sidebars and stateful disclosures.
using System;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// The stable landmarks every creation sidebar opens with: what tool this is, what gesture it wants, and
// the sections that come and go beneath them.
public static class ArchSidebarLayout
{
public static void Header( ToolSidebarWidget panel, string title, string icon, string shortcut, string advice )
{
panel.Layout.Add( new ArchSidebarHeader( title, icon, shortcut ) );
if ( string.IsNullOrWhiteSpace( advice ) )
{
return;
}
panel.Layout.Add( Advice( advice ) );
}
// The shared *Ui forms lay themselves out through AddGroup, which needs a sidebar rather than a layout, so a
// disclosure holds one by nesting it. Here once, or every caller folding a form away repeats the dance.
public static void Form( Layout group, Action<ToolSidebarWidget> build )
{
var form = new ToolSidebarWidget();
build( form );
group.Add( form );
}
// Qt puts a wrappable label's minimum at one line however much text it holds, so the line count is estimated.
public static Widget Advice( string text )
{
var label = new Label( text );
label.WordWrap = true;
label.FixedWidth = ArchPartUi.PanelWidth;
label.MinimumHeight = MathF.Ceiling( text.Length / 36f ) * 14f + 4f;
label.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.48);" );
return label;
}
}
public sealed class ArchSidebarHeader : Widget
{
readonly string title;
readonly string icon;
readonly string shortcut;
public ArchSidebarHeader( string title, string icon, string shortcut )
{
this.title = title;
this.icon = icon;
this.shortcut = shortcut;
FixedHeight = 36f;
}
protected override void OnPaint()
{
Paint.Antialiasing = true;
Paint.TextAntialiasing = true;
var body = LocalRect;
Paint.SetPen( Theme.Blue );
Paint.DrawIcon( new Rect( body.Left, body.Top + 9f, 18f, 18f ), icon, 18f );
Paint.SetDefaultFont( 10f, 600 );
Paint.SetPen( Theme.Text );
Paint.DrawText( body.Shrink( 25f, 0f, 0f, 0f ), title, TextFlag.LeftCenter );
if ( !string.IsNullOrWhiteSpace( shortcut ) )
{
Paint.SetDefaultFont( 7f, 500 );
Paint.SetPen( Theme.TextControl.WithAlpha( 0.4f ) );
Paint.DrawText( body, shortcut, TextFlag.RightCenter );
}
Paint.ClearBrush();
Paint.SetPen( Theme.Text.WithAlpha( 0.09f ) );
Paint.DrawLine( new Vector2( body.Left, body.Bottom - 1f ), new Vector2( body.Right, body.Bottom - 1f ) );
}
}
// A disclosure that keeps its own open state per subtool, and that is simply absent when the choice above
// it cannot use it - a greyed control still asks the reader to work out why it cannot be used.
public sealed class ArchSidebarSection : Widget
{
public static void Show( ToolSidebarWidget panel, string key, string title, Action<Layout> build )
{
Show( panel, key, title, true, true, false, build );
}
public static void Show( ToolSidebarWidget panel, string key, string title, bool visible, Action<Layout> build )
{
Show( panel, key, title, visible, true, false, build );
}
public static void Show( ToolSidebarWidget panel, string key, string title, bool visible, bool expanded, bool collapsible, Action<Layout> build )
{
if ( !visible )
{
return;
}
var section = new ArchSidebarSection( key, title, expanded, collapsible );
build( section.content.Layout );
panel.Layout.Add( section );
}
public static void Disclosure( ToolSidebarWidget panel, string key, string title, bool visible, Action<Layout> build )
{
Show( panel, key, title, visible, false, true, build );
}
readonly string key;
readonly string title;
readonly bool collapsible;
readonly Widget content;
bool open;
bool hovered;
ArchSidebarSection( string key, string title, bool expanded, bool collapsible )
{
this.key = key;
this.title = title;
this.collapsible = collapsible;
open = !collapsible || ArchSidebarState.Expanded( key, expanded );
Layout = Layout.Column();
Layout.Spacing = 2;
content = new Widget( this ) { Layout = Layout.Column() };
content.Layout.Spacing = 3;
content.Visible = open;
Layout.Add( content );
Margins();
HorizontalSizeMode = SizeMode.Expand | SizeMode.CanGrow;
VerticalSizeMode = SizeMode.CanShrink;
MouseTracking = true;
}
// A zero hint takes the height from the content's minimum, which is what keeps a collapsed section thin.
protected override Vector2 SizeHint() => 0;
void Margins()
{
Layout.Margin = new Sandbox.UI.Margin( 6, 15, 6, open ? 6 : 0 );
}
void Toggle()
{
open = !open;
content.Visible = open;
Margins();
ArchSidebarState.Expand( key, open );
Update();
}
protected override void OnMouseMove( MouseEvent e )
{
var was = hovered;
hovered = collapsible && e.LocalPosition.y < 14f;
if ( was != hovered )
{
Update();
}
}
protected override void OnMouseLeave()
{
if ( !hovered )
{
return;
}
hovered = false;
Update();
}
protected override void OnMouseClick( MouseEvent e )
{
if ( !collapsible || e.LocalPosition.y > 14f )
{
return;
}
Toggle();
e.Accepted = true;
}
protected override void OnPaint()
{
Paint.Antialiasing = true;
Paint.TextAntialiasing = true;
if ( open )
{
var frame = Paint.LocalRect;
frame.Top += 6f;
frame = frame.Shrink( 0, 0, 1, 1 );
Paint.SetBrushAndPen( Theme.Text.WithAlpha( 0.012f ), Theme.Text.WithAlpha( 0.09f ) );
Paint.DrawRect( frame, 4 );
}
Paint.SetPen( Theme.TextControl.WithAlpha( hovered ? 1f : 0.55f ) );
Paint.SetDefaultFont( 7f, 600 );
if ( collapsible )
{
Paint.DrawIcon( new Rect( 1, -2, 12, 14 ), open ? "expand_more" : "chevron_right", 10 );
Paint.DrawText( new Vector2( 15, 0 ), title.ToUpperInvariant() );
Cursor = CursorShape.Finger;
}
else
{
Paint.DrawText( new Vector2( 10, 0 ), title.ToUpperInvariant() );
}
}
}
public readonly record struct ArchRunState( bool Active, string Progress );
// Finish and Cancel exist only while a chained operation is live. It rides the tool FOOTER, outside the
// sidebar's scroll, so a long form cannot push the way out of a run off the bottom.
public sealed class ArchRunBar : Widget
{
readonly Func<ArchRunState> read;
readonly Label progress;
public ArchRunBar( Func<ArchRunState> read, Action finish, Action cancel )
{
this.read = read;
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 8, 6, 8, 6 );
Layout.Spacing = 6;
progress = new Label( "" );
progress.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.7);" );
Layout.Add( progress );
Layout.AddStretchCell();
Layout.Add( new Button( "Cancel", "close" ) { Clicked = () => cancel?.Invoke() } );
Layout.Add( new Button.Primary( "Finish", "check" ) { Clicked = () => finish?.Invoke() } );
Visible = false;
}
[EditorEvent.Frame]
public void Poll()
{
var state = read();
if ( Visible != state.Active )
{
Visible = state.Active;
}
if ( !state.Active )
{
return;
}
var text = state.Progress ?? "";
if ( progress.Text != text )
{
progress.Text = text;
}
}
protected override void OnPaint()
{
Paint.ClearPen();
Paint.SetBrush( Theme.TabBackground );
Paint.DrawRect( LocalRect );
Paint.SetPen( Theme.Blue.WithAlpha( 0.5f ) );
Paint.DrawLine( new Vector2( 0, 0 ), new Vector2( Width, 0 ) );
}
}