terminalCode Example
s&box context menu with submenus and auto-positioning
s&box Context Menu with Submenus and Auto-Positioning
This example shows a context menu system using s&box's Panel, Screen, and layout system with submenu builder pattern.
CSHARP
@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;
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; }
// Close menu if source panel becomes invisible
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 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 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;
}
// Keep menu on screen using Screen.Width and Screen.Height
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);
}
}s&box-Specific Features
- Panel.FindRootPanel(): Gets the root panel for adding children
- ScaleFromScreen: Converts screen coordinates to UI scale
- Screen.Width/Screen.Height: Screen dimensions for layout clamping
- OnLayout(ref Rect): s&box layout system override for positioning
- Panel.AddChild<T>(): Adds child panels to the hierarchy
- Panel.Delete(false): Removes panel without destroying children
- Panel.PlaySound(): Plays UI sounds from asset paths
- Panel.AncestorsAndSelf: Traverses panel hierarchy
Was this helpful?