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.
@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 );
}