UI/Components/MenuPanel.razor
@using System
@using Sandbox.UI
@namespace Sandbox
@inherits Panel
<root>
@if ( !_isSubmenu )
{
<div class="background" @onmousedown=@Close @onclick=@Close></div>
}
<div class="inner @( _submenuOpensLeft && _options.Any( o => o.SubmenuBuilder != null ) ? "opens-left" : "" )">
@foreach ( var option in _options )
{
if ( option.IsSpacer )
{
<div class="spacer"></div>
continue;
}
var opt = option;
<div class="option @( opt.SubmenuBuilder != null ? "has-submenu" : "" )"
@onclick="@(() => OnOptionClick( opt ))"
@onmouseover="@(() => OnOptionHover( opt ))">
<div class="icon">@opt.Icon</div>
<div class="text">@opt.Text</div>
@if ( opt.SubmenuBuilder != null )
{
<div class="submenu-arrow">@( _submenuOpensLeft ? "◀" : "▶" )</div>
}
</div>
}
</div>
</root>
@code
{
record MenuOption( string Icon, string Text, Action Action, Action<MenuPanel> SubmenuBuilder = null )
{
public bool IsSpacer => Icon == "__spacer__";
}
readonly List<MenuOption> _options = new();
MenuPanel _openSubmenu;
MenuOption? _activeSubmenuOption;
bool _isSubmenu;
bool _openLeft;
float _parentLeft;
bool _submenuOpensLeft;
Rect _layoutRect;
Panel _source;
public static MenuPanel Open( Panel source )
{
var root = source.FindRootPanel();
var menu = root.AddChild<MenuPanel>();
menu._source = source;
menu.Style.Left = root.MousePosition.x * root.ScaleFromScreen;
menu.Style.Top = root.MousePosition.y * root.ScaleFromScreen;
menu.PlaySound( "sounds/kenney/ui/ui.navigate.forward.sound" );
return menu;
}
public override void Tick()
{
base.Tick();
if ( _isSubmenu ) return;
if ( !_source.IsValid() ) { Close(); return; }
// Walk the source ancestor chain — if any panel becomes invisible, close the menu
if ( _source.AncestorsAndSelf.Any( p => !p.IsVisible ) )
Close();
}
public void AddOption( string icon, string text, Action action )
{
_options.Add( new MenuOption( icon, text, action ) );
}
public void AddSubmenu( string icon, string text, Action<MenuPanel> builder )
{
_options.Add( new MenuOption( icon, text, null, builder ) );
}
public void AddSpacer()
{
_options.Add( new MenuOption( "__spacer__", "", null ) );
}
void OnOptionClick( MenuOption option )
{
if ( option.SubmenuBuilder != null )
OnOptionHover( option );
else
SelectOption( option );
}
void OnOptionHover( MenuOption option )
{
if ( _activeSubmenuOption == option ) return;
_activeSubmenuOption = option;
CloseSubmenu();
if ( option.SubmenuBuilder == null ) return;
const int minWidth = 200;
const int gap = 4;
const int padding = 10;
bool hasRoomRight = Box.Right + gap + minWidth < Screen.Width - padding;
var root = FindRootPanel();
var submenu = root.AddChild<MenuPanel>();
submenu._isSubmenu = true;
submenu._openLeft = !hasRoomRight;
submenu._parentLeft = Box.Left;
submenu.Style.Left = hasRoomRight ? Box.Right + gap : Box.Left - gap - minWidth;
submenu.Style.Top = root.MousePosition.y * root.ScaleFromScreen - gap;
option.SubmenuBuilder( submenu );
submenu.StateHasChanged();
_openSubmenu = submenu;
}
void CloseSubmenu()
{
if ( _openSubmenu == null ) return;
_openSubmenu.Delete( false );
_openSubmenu = null;
}
void SelectOption( MenuOption option )
{
CloseAll( this );
option.Action?.Invoke();
}
public void Close()
{
CloseAll( this );
}
static void CloseAll( Panel source )
{
var root = source.FindRootPanel();
foreach ( var menu in root.Children.OfType<MenuPanel>().ToList() )
menu.Delete( false );
}
public override void OnLayout( ref Rect layoutRect )
{
const int padding = 10;
const int gap = 4;
if ( _openLeft )
{
var w = layoutRect.Width;
layoutRect.Right = _parentLeft - gap;
layoutRect.Left = layoutRect.Right - w;
}
if ( layoutRect.Right > Screen.Width - padding )
{
layoutRect.Left -= layoutRect.Right - ( Screen.Width - padding );
layoutRect.Right = Screen.Width - padding;
}
if ( layoutRect.Bottom > Screen.Height - padding )
{
layoutRect.Top -= layoutRect.Bottom - ( Screen.Height - padding );
layoutRect.Bottom = Screen.Height - padding;
}
_layoutRect = layoutRect;
_submenuOpensLeft = !_isSubmenu && !( layoutRect.Right + 4 + 200 < Screen.Width - 10 );
}
}