UI/Components/Menu/MenuHost.razor

A Blazor-like Razor UI component (MenuHost) that renders a menu shell and various menu widgets into HTML-like markup for the game's UI system. It maps different MenuWidget types to RenderFragments and renders navigation, branding, active screen header and a scrolling body with many widget renderers.

Native Interop
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@inherits Panel
@attribute [StyleSheet]

<root>
	<div class="menu-shell">

		<div class="menu-side">
			@if ( !string.IsNullOrEmpty( Menu.BrandTop ) || !string.IsNullOrEmpty( Menu.BrandBottom ) )
			{
				<div class="menu-brand">
					@if ( !string.IsNullOrEmpty( Menu.BrandTop ) )
					{
						<div class="menu-brand-top">@Menu.BrandTop</div>
					}
					@if ( !string.IsNullOrEmpty( Menu.BrandBottom ) )
					{
						<div class="menu-brand-bottom">@Menu.BrandBottom</div>
					}
					@if ( !string.IsNullOrEmpty( Menu.Version ) )
					{
						<div class="menu-brand-version">@Menu.Version</div>
					}
				</div>
			}

			<div class="menu-nav">
				@foreach ( var screen in Menu.NavScreens )
				{
					var id = screen.Id;
					var isActive = id == Menu.ActiveId;
					<div class="menu-nav-item @(isActive ? "active" : "")" @onclick=@(() => Menu.Navigate( id ))>
						<Icon [email protected] Size=@(IconSize.Medium) />
						<div class="menu-nav-label">@screen.Label</div>
					</div>
				}
			</div>
		</div>

		<div class="menu-body">
			@if ( Menu.Active is { } active )
			{
				<div class="menu-body-head">
					<div class="menu-body-title">@active.Label</div>
					@if ( !string.IsNullOrEmpty( active.Subtitle ) )
					{
						<div class="menu-body-sub">@active.Subtitle</div>
					}
				</div>

				<div class="menu-body-scroll">
					@if ( active.Widgets is not null )
					{
						foreach ( var widget in active.Widgets )
						{
							@RenderWidget( widget )
						}
					}
				</div>
			}
		</div>
	</div>
</root>

@code
{
	Microsoft.AspNetCore.Components.RenderFragment RenderWidget( MenuWidget w ) => w switch
	{
		MenuSectionWidget s      => RenderSection( s ),
		MenuTextWidget t         => RenderText( t ),
		MenuLinkWidget l         => RenderLink( l ),
		MenuToggleWidget tg      => RenderToggle( tg ),
		MenuRangeWidget r        => RenderRange( r ),
		MenuSegmentedWidget sg   => RenderSegmented( sg ),
		MenuRadioWidget rd       => RenderRadio( rd ),
		MenuInfoWidget i         => RenderInfo( i ),
		MenuTextFieldWidget tf   => RenderTextField( tf ),
		MenuComponentWidget c    => RenderComponent( c ),
		MenuActionRowWidget ar   => RenderActionRow( ar ),
		MenuHeroCardWidget h     => RenderHeroCard( h ),
		MenuMapCardWidget m      => RenderMapCard( m ),
		MenuMapGridWidget mg     => RenderMapGrid( mg ),
		MenuTileGridWidget tile  => RenderTileGrid( tile ),
		MenuPlayerListWidget p   => RenderPlayerList( p ),
		MenuKeyBindingsWidget k  => RenderKeyBindings( k ),
		MenuPromptCardWidget pc  => RenderPromptCard( pc ),
		_ => null,
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderSection( MenuSectionWidget w ) => __builder =>
	{
		<div class="section-label">@w.Label</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderText( MenuTextWidget w ) => __builder =>
	{
		<div class="menu-row menu-row--text">
			<div class="menu-row-hint menu-row-hint--standalone">@w.Text</div>
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderLink( MenuLinkWidget w ) => __builder =>
	{
		var disabled = !w.IsEnabled;
		var styleClass = w.Style switch
		{
			MenuLinkStyle.Button => "menu-row--button",
			MenuLinkStyle.Text   => "menu-row--text-link",
			_                    => "menu-row--link",
		};
		<div class="menu-row @styleClass @ToneClass(w.Tone) @(disabled ? "is-disabled" : "") @(w.Grow ? "grow" : "")"
			 @onclick=@(() => { if ( !disabled ) w.Activate(); })>
			@if ( !string.IsNullOrEmpty( w.Icon ) )
			{
				<Icon [email protected] Size=@(IconSize.Medium) />
			}
			<div class="menu-row-text">
				<div class="menu-row-title">@w.Label</div>
				@if ( !string.IsNullOrEmpty( w.Hint ) )
				{
					<div class="menu-row-hint">@w.Hint</div>
				}
			</div>
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderToggle( MenuToggleWidget w ) => __builder =>
	{
		var on = w.Get?.Invoke() ?? false;
		<div class="menu-row menu-row--toggle @(on ? "is-on" : "")" @onclick=@(() => w.Set?.Invoke( !on ))>
			<div class="menu-row-text">
				<div class="menu-row-title">@w.Label</div>
				@if ( !string.IsNullOrEmpty( w.Hint ) )
				{
					<div class="menu-row-hint">@w.Hint</div>
				}
			</div>
			<div class="menu-toggle">
				<div class="menu-toggle-knob"></div>
			</div>
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderRange( MenuRangeWidget w ) => __builder =>
	{
		<div class="menu-row menu-row--range">
			<RangeField [email protected]
						Value=@(w.Get?.Invoke() ?? 0f)
						[email protected] [email protected] [email protected] [email protected]
						Setter=@((float v) => w.Set?.Invoke( v )) />
			@if ( !string.IsNullOrEmpty( w.Hint ) )
			{
				<div class="menu-row-hint menu-row-hint--standalone">@w.Hint</div>
			}
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderSegmented( MenuSegmentedWidget w ) => __builder =>
	{
		var cur = w.Get?.Invoke() ?? 0;
		<div class="menu-row menu-row--segmented">
			<div class="menu-row-text">
				<div class="menu-row-title">@w.Label</div>
				@if ( !string.IsNullOrEmpty( w.Hint ) )
				{
					<div class="menu-row-hint">@w.Hint</div>
				}
			</div>
			<div class="seg solid">
				@for ( int i = 0; i < w.Choices.Length; i++ )
				{
					var idx = i;
					<div class="seg-btn @(cur == idx ? "active" : "")" @onclick=@(() => w.Set?.Invoke( idx ))>
						@w.Choices[idx]
					</div>
				}
			</div>
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderRadio( MenuRadioWidget w ) => __builder =>
	{
		<div class="menu-row menu-row--radio">
			@if ( !string.IsNullOrEmpty( w.Label ) )
			{
				<div class="menu-row-text">
					<div class="menu-row-title">@w.Label</div>
					@if ( !string.IsNullOrEmpty( w.Hint ) )
					{
						<div class="menu-row-hint">@w.Hint</div>
					}
				</div>
			}
			<MenuRadio [email protected] [email protected] [email protected] />
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderInfo( MenuInfoWidget w ) => __builder =>
	{
		<div class="menu-row menu-row--info">
			@if ( !string.IsNullOrEmpty( w.Icon ) )
			{
				<Icon [email protected] Size=@(IconSize.Medium) />
			}
			<div class="menu-row-text">
				<div class="menu-row-title">@w.Label</div>
			</div>
			<div class="menu-row-value">@(w.Get?.Invoke() ?? "")</div>
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderTextField( MenuTextFieldWidget w ) => __builder =>
	{
		<div class="menu-row menu-row--field">
			<div class="field">
				<div class="field-label"><label>@w.Label</label></div>
				<TextEntry Text=@(w.Get?.Invoke() ?? "") OnTextEdited=@((string v) => w.Set?.Invoke( v )) />
			</div>
			@if ( !string.IsNullOrEmpty( w.Hint ) )
			{
				<div class="menu-row-hint menu-row-hint--standalone">@w.Hint</div>
			}
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderComponent( MenuComponentWidget w ) => __builder =>
	{
		<div class="menu-row menu-row--component">
			@if ( !string.IsNullOrEmpty( w.Label ) )
			{
				<div class="menu-row-text">
					<div class="menu-row-title">@w.Label</div>
					@if ( !string.IsNullOrEmpty( w.Hint ) )
					{
						<div class="menu-row-hint">@w.Hint</div>
					}
				</div>
			}
			<div class="menu-row-slot">
				@w.Content
			</div>
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderActionRow( MenuActionRowWidget w ) => __builder =>
	{
		<div class="menu-action-row">
			@if ( w.Links is not null )
			{
				foreach ( var link in w.Links )
				{
					var local = link;
					var disabled = !local.IsEnabled;
					<button class="menu-action @ToneClass(local.Tone) @(local.Grow ? "grow" : "") @(disabled ? "is-disabled" : "")"
							@onclick=@(() => { if ( !disabled ) local.Activate(); })>
						@if ( !string.IsNullOrEmpty( local.Icon ) )
						{
							<Icon [email protected] Size=@(IconSize.Small) />
						}
						<div class="menu-action-label">@local.Label</div>
					</button>
				}
			}
		</div>
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderHeroCard( MenuHeroCardWidget w ) => __builder =>
	{
		<MenuHeroCard [email protected] [email protected] [email protected] [email protected] [email protected] />
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderMapCard( MenuMapCardWidget w ) => __builder =>
	{
		<MenuMapCard [email protected] [email protected] [email protected]
					 [email protected] [email protected] [email protected]
					 [email protected] [email protected]
					 OnAction=@(() => w.TriggerAction()) />
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderMapGrid( MenuMapGridWidget w ) => __builder =>
	{
		<MenuMapGrid [email protected] [email protected] [email protected] />
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderTileGrid( MenuTileGridWidget w ) => __builder =>
	{
		<MenuTileGrid [email protected] [email protected] [email protected] />
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderPlayerList( MenuPlayerListWidget w ) => __builder =>
	{
		<MenuPlayerList [email protected] />
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderKeyBindings( MenuKeyBindingsWidget w ) => __builder =>
	{
		<MenuKeyBindings [email protected] />
	};

	Microsoft.AspNetCore.Components.RenderFragment RenderPromptCard( MenuPromptCardWidget w ) => __builder =>
	{
		<PromptCard [email protected] [email protected] [email protected] [email protected]
					[email protected] [email protected] />
	};

	static string ToneClass( MenuTone tone ) => tone switch
	{
		MenuTone.Primary => "tone-primary",
		MenuTone.Confirm => "tone-confirm",
		MenuTone.Danger  => "tone-danger",
		MenuTone.Ghost   => "tone-ghost",
		_ => "",
	};

	protected override int BuildHash() => HashCode.Combine( Menu.Revision, Menu.ActiveId, Menu.Screens.Count );
}