An editor UI dialog for browsing architecture presets. It displays categories, search, a grid of preset tiles with previews or glyphs, lets the user pick or design a preset, and watches preview updates to repaint.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
// The expanded half of the preset chooser, reached from every inline browser's header so browsing,
// comparing and designing take one route whatever the tool.
public sealed class ArchPresetBrowserDialog<T> : Dialog, IArchPreviewWatcher
{
const string Everything = "All";
const string Preferred = "Recommended";
const float Strip = 34f;
readonly ArchKit kit;
readonly IReadOnlyList<ArchPresetItem<T>> items;
readonly Func<T, bool> chosen;
readonly Action<T> use;
readonly LineEdit search;
readonly ArchTileGrid grid;
readonly Label counted;
readonly List<Widget> cards = new();
readonly List<Button> categories = new();
string category = Preferred;
ArchPresetItem<T> pending;
public ArchPresetBrowserDialog(
Widget parent,
ArchKit kit,
string title,
IReadOnlyList<ArchPresetItem<T>> items,
Func<T, bool> chosen,
Action<T> use ) : base( parent )
{
this.kit = kit;
this.items = items;
this.chosen = chosen;
this.use = use;
pending = items.FirstOrDefault( item => chosen?.Invoke( item.Value ) == true );
Window.Title = title;
Window.SetWindowIcon( "grid_view" );
Window.SetModal( true, true );
Window.Size = new Vector2( 1000, 680 );
Window.MinimumWidth = 780;
Window.MinimumHeight = 520;
Layout = Layout.Column();
Layout.Margin = 0;
Layout.Spacing = 0;
var split = Layout.AddRow( 1 );
split.Add( Navigation() );
split.AddSeparator();
var right = split.AddColumn( 1 );
right.Margin = 10;
right.Spacing = 8;
var top = right.AddRow();
top.Spacing = 8;
search = new LineEdit( "" ) { PlaceholderText = $"Search {title.ToLowerInvariant()}" };
search.TextEdited += _ => Fill();
top.Add( search, 1 );
counted = new Label( "" );
counted.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.4);" );
top.Add( counted );
grid = new ArchTileGrid( this, ArchTileSize.Browsed );
right.Add( grid, 1 );
Layout.AddSeparator();
var footer = Layout.AddRow();
footer.Margin = 10;
footer.Spacing = 6;
footer.AddStretchCell();
DesignButton = new Button( "Design new preset", "tune" ) { Clicked = () => Design?.Invoke() };
footer.Add( DesignButton );
footer.Add( new Button( "Cancel", "close" ) { Clicked = Close } );
footer.Add( new Button.Primary( "Use preset", "check" ) { Clicked = Confirm } );
Fill();
ArchPreviewPump.Watch( this );
search.Focus();
}
public Button DesignButton { get; }
public Action Design { get; set; }
Widget Navigation()
{
var column = new Widget( this ) { Layout = Layout.Column() };
column.Layout.Margin = 10;
column.Layout.Spacing = 3;
column.FixedWidth = 168;
foreach ( var name in Names() )
{
var captured = name;
var button = new Button( name ) { IsToggle = true, Clicked = () => Navigate( captured ) };
categories.Add( button );
column.Layout.Add( button );
}
column.Layout.AddStretchCell();
Lit();
return column;
}
List<string> Names()
{
var names = new List<string>();
if ( items.Any( item => item.Recommended ) )
{
names.Add( Preferred );
}
names.Add( Everything );
names.AddRange( items
.Select( item => item.Category )
.Where( name => !string.IsNullOrWhiteSpace( name ) )
.Distinct()
.OrderBy( name => name ) );
category = names[0];
return names;
}
void Navigate( string name )
{
category = name;
Lit();
Fill();
}
void Lit()
{
foreach ( var button in categories )
{
button.IsChecked = button.Text == category;
}
}
void Fill()
{
var showing = items
.Where( Included )
.Where( item => item.Matches( search.Text ) )
.OrderByDescending( item => item.Recommended )
.ToList();
counted.Text = $"{showing.Count} of {items.Count}";
cards.Clear();
var section = new ArchTileSection();
foreach ( var item in showing )
{
var captured = item;
section.Tiles.Add( canvas => Card( canvas, captured ) );
}
grid.Set( new[] { section } );
}
bool Included( ArchPresetItem<T> item )
{
return category switch
{
Everything => true,
Preferred => item.Recommended,
_ => item.Category == category
};
}
Widget Card( Widget canvas, ArchPresetItem<T> item )
{
var tile = new ArchTile( canvas, null, item.Name, item.Detail, false, ArchTileSize.Browsed )
{
Badge = item.Badge,
ToolTip = string.IsNullOrWhiteSpace( item.Detail ) ? item.Name : $"{item.Name} — {item.Detail}",
Current = () => pending == item,
Clicked = () => { pending = item; Repaint(); },
Opened = Confirm
};
if ( item.Recipe is not null )
{
var art = new Vector2( ArchTileSize.Browsed.x, ArchTileSize.Browsed.y - Strip ) * 1.75f;
tile.Source = () => ArchPresetPreview.For( item.Identity, item.Shot( art ), kit, item.Recipe );
}
else if ( !string.IsNullOrWhiteSpace( item.Glyph ) )
{
tile.Glyph = rect => Paint.DrawIcon( rect, item.Glyph, rect.Height );
}
cards.Add( tile );
return tile;
}
void Repaint()
{
foreach ( var card in cards.Where( card => card.IsValid() ) )
{
card.Update();
}
}
void Confirm()
{
if ( pending is null )
{
return;
}
use( pending.Value );
Close();
}
public bool Alive => this.IsValid();
public void PreviewTick( bool advanced )
{
if ( advanced )
{
Repaint();
}
}
protected override void OnKeyPress( KeyEvent e )
{
if ( e.Key == KeyCode.Enter || e.Key == KeyCode.Return )
{
Confirm();
e.Accepted = true;
return;
}
base.OnKeyPress( e );
}
}