UI/Components/Menu/Menu.cs

UI menu system and builder. Defines global Menu state (brand, version, list of screens, active screen, navigation), enums for tone and link style, and builder types (MenuBuilder, MenuScreen, MenuScreenBuilder entry points) to construct and commit menu screens.

File AccessNetworking
namespace Sunless.Libraries.UI;

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components;
using Sandbox;

// Mount <MenuHost /> once. Describe screens via Menu.New().WithScreen(...).Apply().
// Screens keyed by id; re-applying replaces in place.
public static class Menu
{
	public static string BrandTop { get; private set; }
	public static string BrandBottom { get; private set; }
	public static string Version { get; private set; }
	public static IReadOnlyList<MenuScreen> Screens => _screens;
	public static string ActiveId { get; private set; }
	public static int Revision { get; private set; }

	public static Action OnNavigateSound { get; set; }

	static readonly List<MenuScreen> _screens = new();

	public static MenuBuilder New() => new();

	public static MenuScreen Active
		=> string.IsNullOrEmpty( ActiveId )
			? null
			: _screens.FirstOrDefault( s => s.Id == ActiveId && s.IsVisible );

	public static IEnumerable<MenuScreen> NavScreens
		=> _screens.Where( s => s.IsVisible && s.ShowInNav ).OrderBy( s => s.Order );

	public static void Navigate( string id )
	{
		if ( ActiveId == id ) return;
		ActiveId = id;
		Revision++;
		try { OnNavigateSound?.Invoke(); }
		catch ( Exception e ) { Log.Warning( $"[Menu] navigate sound threw: {e.Message}" ); }
	}

	public static void Clear()
	{
		_screens.Clear();
		ActiveId = null;
		BrandTop = null;
		BrandBottom = null;
		Version = null;
		Revision++;
	}

	internal static void Commit( MenuBuilder b )
	{
		BrandTop = b._brandTop;
		BrandBottom = b._brandBottom;
		Version = b._version;

		foreach ( var entry in b._screens )
		{
			var existing = _screens.FirstOrDefault( s => s.Id == entry.Id );
			if ( existing is not null ) _screens.Remove( existing );
			_screens.Add( entry );
		}

		if ( Active is null )
		{
			ActiveId = NavScreens.FirstOrDefault()?.Id ?? _screens.FirstOrDefault()?.Id;
		}

		Revision++;
	}
}

public enum MenuTone { Default, Primary, Confirm, Danger, Ghost }

public enum MenuLinkStyle
{
	Row,
	Button,
	Text,
}

public sealed class MenuBuilder
{
	internal string _brandTop;
	internal string _brandBottom;
	internal string _version;
	internal readonly List<MenuScreen> _screens = new();

	public MenuBuilder WithBrand( string top, string bottom = null )
	{
		_brandTop = top;
		_brandBottom = bottom;
		return this;
	}

	public MenuBuilder WithVersion( string version ) { _version = version; return this; }

	public MenuScreenBuilder WithScreen( string id, string label, string icon )
		=> new( this, id, label, icon );

	internal void AddScreen( MenuScreen screen ) => _screens.Add( screen );

	public void Apply() => Menu.Commit( this );
}

public sealed class MenuScreen
{
	public string Id { get; init; }
	public string Label { get; init; }
	public string Icon { get; init; }
	public string Subtitle { get; init; }
	public int Order { get; init; }
	public bool ShowInNav { get; init; } = true;
	public IReadOnlyList<MenuWidget> Widgets { get; init; }

	internal Func<bool> VisibleWhen { get; init; }
	public bool IsVisible => VisibleWhen?.Invoke() ?? true;
}