Extension methods and typed builder wrappers for constructing common menu screens. Provides presets for Settings, Host Game, Map Selector and Lobby screens, each wrapping a generic MenuScreenBuilder and exposing fluent helper methods to add standard controls and then return to the underlying builder.
namespace Sunless.Libraries.UI;
using System;
using System.Collections.Generic;
using System.Linq;
// Layer-2 presets. Each WithXxxScreen(...) on MenuBuilder gives back a typed
// builder with sensible defaults and named setters, so consumers don't hand-
// chain WithRange / WithToggle for the same standard screen every time.
//
// Every preset still composes the generic MenuScreenBuilder underneath, so
// you can keep adding generic widgets after calling the preset's specialised
// setters — or call .Generic() to drop down to the underlying builder.
public static class MenuPresetExtensions
{
public static SettingsScreenBuilder WithSettingsScreen( this MenuBuilder b, string id = "settings", string label = "Options", string icon = "settings" )
=> new( b.WithScreen( id, label, icon ) );
public static HostGameScreenBuilder WithHostGameScreen( this MenuBuilder b, string id = "host", string label = "Host Game", string icon = "groups" )
=> new( b.WithScreen( id, label, icon ) );
public static MapSelectorScreenBuilder WithMapSelectorScreen( this MenuBuilder b, string id = "maps", string label = "Select Map", string icon = "map" )
=> new( b.WithScreen( id, label, icon ) );
public static LobbyScreenBuilder WithLobbyScreen( this MenuBuilder b, string id = "lobby", string label = "Lobby", string icon = "groups" )
=> new( b.WithScreen( id, label, icon ) );
}
// === Settings ========================================================
public sealed class SettingsScreenBuilder
{
readonly MenuScreenBuilder _s;
internal SettingsScreenBuilder( MenuScreenBuilder s )
{
_s = s;
_s.WithSubtitle( "Tweak audio, video and feel." );
}
public MenuScreenBuilder Generic() => _s;
public SettingsScreenBuilder WithSubtitle( string subtitle )
{
_s.WithSubtitle( subtitle );
return this;
}
public SettingsScreenBuilder WithFullscreenToggle( Func<bool> get, Action<bool> set )
{
_s.WithToggle( "Fullscreen" ).Bind( get, set ).EndToggle();
return this;
}
public SettingsScreenBuilder WithVSyncToggle( Func<bool> get, Action<bool> set )
{
_s.WithToggle( "V-Sync" ).Bind( get, set ).EndToggle();
return this;
}
public SettingsScreenBuilder WithQualityPicker( string[] choices, Func<int> get, Action<int> set )
{
_s.WithSegmented( "Graphics Quality", choices ).Bind( get, set ).EndSegmented();
return this;
}
public SettingsScreenBuilder WithVolumeSlider( string label, Func<float> get, Action<float> set )
{
_s.WithRange( label ).WithRange( 0f, 1f ).WithFormat( "0%" ).Bind( get, set ).EndRange();
return this;
}
public SettingsScreenBuilder WithKeyBindings( Action<MenuKeyBindingsBuilder> configure )
{
var b = _s.WithKeyBindings();
configure?.Invoke( b );
b.EndKeyBindings();
return this;
}
public SettingsScreenBuilder WithBack( string routeId, string label = "Back" )
{
_s.WithActionRow()
.WithLink( label, "arrow_back" ).WithGoTo( routeId ).EndLink()
.EndActionRow();
return this;
}
public MenuBuilder EndScreen() => _s.EndScreen();
}
// === Host Game =======================================================
public sealed class HostGameScreenBuilder
{
readonly MenuScreenBuilder _s;
internal HostGameScreenBuilder( MenuScreenBuilder s )
{
_s = s;
}
public MenuScreenBuilder Generic() => _s;
public HostGameScreenBuilder WithSubtitle( string subtitle )
{
_s.WithSubtitle( subtitle );
return this;
}
public HostGameScreenBuilder WithServerName( Func<string> get, Action<string> set )
{
_s.WithTextField( "Server Name" ).Bind( get, set ).EndTextField();
return this;
}
public HostGameScreenBuilder WithMaxPlayers( int min, int max, Func<float> get, Action<float> set )
{
_s.WithRange( "Max Players" ).WithRange( min, max ).WithStep( 1f ).WithFormat( "0" ).Bind( get, set ).EndRange();
return this;
}
public HostGameScreenBuilder WithVisibility( Func<int> get, Action<int> set, params string[] choices )
{
if ( choices is null || choices.Length == 0 )
choices = new[] { "Friends", "Public", "Private" };
_s.WithSegmented( "Visibility", choices ).Bind( get, set ).EndSegmented();
return this;
}
public HostGameScreenBuilder WithActions( string backRoute, string createLabel, Action createAction, string lobbyRoute = null )
{
var row = _s.WithActionRow()
.WithLink( "Back", "arrow_back" ).WithGoTo( backRoute ).EndLink()
.WithLink( createLabel, "play_arrow" ).WithTone( MenuTone.Confirm ).WithAction( createAction ).Grows();
if ( !string.IsNullOrEmpty( lobbyRoute ) )
row.WithGoTo( lobbyRoute );
row.EndLink().EndActionRow();
return this;
}
public MenuBuilder EndScreen() => _s.EndScreen();
}
// === Map Selector ====================================================
public sealed class MapSelectorScreenBuilder
{
readonly MenuScreenBuilder _s;
readonly MenuMapGridBuilder _grid;
internal MapSelectorScreenBuilder( MenuScreenBuilder s )
{
_s = s;
_grid = s.WithMapGrid();
}
public MenuScreenBuilder Generic() => _s;
public MapSelectorScreenBuilder WithSubtitle( string subtitle )
{
_s.WithSubtitle( subtitle );
return this;
}
public MapSelectorScreenBuilder WithMap( string title, Action<MenuMapCardCardBuilder> configure = null )
{
var c = _grid.WithCard( title );
configure?.Invoke( c );
c.EndCard();
return this;
}
public MapSelectorScreenBuilder Bind( Func<int> get, Action<int> set )
{
_grid.WithActive( get ).OnPick( set );
return this;
}
public MapSelectorScreenBuilder WithBack( string routeId, string label = "Back to Lobby" )
{
_grid.EndMapGrid();
_s.WithActionRow()
.WithLink( label, "arrow_back" ).WithGoTo( routeId ).Grows().EndLink()
.EndActionRow();
return this;
}
public MenuBuilder EndScreen()
{
// EndMapGrid is idempotent; safe to call even if WithBack already did.
_grid.EndMapGrid();
return _s.EndScreen();
}
}
// === Lobby ===========================================================
public sealed class LobbyScreenBuilder
{
readonly MenuScreenBuilder _s;
internal LobbyScreenBuilder( MenuScreenBuilder s )
{
_s = s;
_s.WithSubtitle( "Configure the session before you go." );
}
public MenuScreenBuilder Generic() => _s;
public LobbyScreenBuilder WithSubtitle( string subtitle )
{
_s.WithSubtitle( subtitle );
return this;
}
public LobbyScreenBuilder WithMapCard( Action<MenuMapCardBuilder> configure )
{
var b = _s.WithMapCard();
configure?.Invoke( b );
b.EndMapCard();
return this;
}
public LobbyScreenBuilder WithSurvivors( string sectionLabel, Action<MenuPlayerListBuilder> configure )
{
_s.WithSection( sectionLabel );
var b = _s.WithPlayerList();
configure?.Invoke( b );
b.EndPlayerList();
return this;
}
public LobbyScreenBuilder WithPrivacy( Func<int> get, Action<int> set, params string[] choices )
{
if ( choices is null || choices.Length == 0 )
choices = new[] { "Public", "Friends", "Private" };
_s.WithSegmented( "Privacy", choices ).Bind( get, set ).EndSegmented();
return this;
}
public LobbyScreenBuilder WithActions( string leaveRoute, Func<bool> ready, Action toggleReady, Action start, Func<bool> canStart = null )
{
_s.WithActionRow()
.WithLink( "Leave", "arrow_back" ).WithGoTo( leaveRoute ).EndLink()
.WithLink( ready() ? "Unready" : "Ready Up", ready() ? "check_circle" : "radio_button_unchecked" )
.WithTone( MenuTone.Confirm ).WithAction( toggleReady ).Grows().EndLink()
.WithLink( "Start", "play_arrow" ).WithTone( MenuTone.Primary ).WithAction( start )
.WhenEnabled( canStart ?? ready ).Grows().EndLink()
.EndActionRow();
return this;
}
public MenuBuilder EndScreen() => _s.EndScreen();
}