Editor/UI/ArchPresetBrowserDialog.cs
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; }
// Forwarded from the inline browser, so the expanded wall of cards offers the same right-click it does.
public Action<ArchPresetItem<T>> Remove { get; set; }
public Func<ArchPresetItem<T>, string> Removes { get; set; }
public Action<ArchPresetItem<T>, Menu> Options { get; set; }
public Action<ArchPresetItem<T>> Export { get; set; }
// The cards are laid in the CONSTRUCTOR, so anything assigned after it has to say so - a tile built while
// Remove and Options were still null carries no menu at all, and nothing anywhere reports that.
public void Refill() => Fill();
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
};
var exports = Export is not null && item.Recipe is not null;
if ( Remove is not null || Options is not null || exports ) {
tile.Menu = menu => {
Options?.Invoke( item, menu );
if ( exports ) {
menu.AddOption( $"Export {item.Name} to OBJ…", "download", () => Export( item ) );
}
if ( Remove is not null ) {
menu.AddOption( Removes?.Invoke( item ) ?? $"Remove {item.Name}", "delete", () => Remove( item ) );
}
};
}
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 );
}
}