UI/Components/Menu/MenuPresets.cs

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();
}