UI/ContextMenu/Inspector.razor
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel
<root>
<div class="canvas" @ref=_canvas></div>
<div class="inspector-panel @(_editors?.Any( e => e.WasVisible ) == true ? "" : "hidden")" @onmousedown="@WindowPress">
<div class="tab-bar">
@foreach ( var entry in _editors?.Where( e => e.WasVisible ) ?? [] )
{
var e = entry;
<div class="tab @( _active == e ? "active" : "" )" @onclick="@(() => SetActive( e ))">
@e.Editor.Title
</div>
}
</div>
<div class="window-stack" @ref=_windowStack></div>
</div>
</root>
@code
{
public GameObject Hovered => hovered;
Panel _canvas = default;
Panel _windowStack = default;
record EditorEntry( IInspectorEditor Editor )
{
public bool WasVisible { get; set; }
}
List<EditorEntry> _editors;
EditorEntry _active;
void InitEditors()
{
if ( _editors != null || _windowStack == null ) return;
_editors = new();
var types = TypeLibrary.GetTypesWithAttribute<InspectorEditorAttribute>()
.OrderByDescending( x => x.Type.GetAttribute<OrderAttribute>()?.Value ?? 0 )
.ThenBy( t => t.Attribute.Type is null ? 1 : 0 )
.ToList();
foreach ( var (typeDesc, attr) in types )
{
var editor = typeDesc.Create<IInspectorEditor>();
if ( editor is not Panel editorPanel ) continue;
editorPanel.AddClass( "window" );
editorPanel.SetClass( "hidden", true );
_windowStack.AddChild( editorPanel );
_editors.Add( new EditorEntry( editor ) );
}
StateHasChanged();
}
void AttachEditors()
{
if ( _windowStack == null ) return;
foreach ( var entry in _editors ?? [] )
{
if ( entry.Editor is Panel editorPanel )
_windowStack.AddChild( editorPanel );
}
}
void SetActive( EditorEntry entry )
{
_active = entry;
ApplyVisibility();
StateHasChanged();
}
void ApplyVisibility()
{
foreach ( var e in _editors )
(e.Editor as Panel)?.SetClass( "hidden", !( e.WasVisible && e == _active ) );
}
HashSet<GameObject> _lastSelected = new();
void UpdateEditors()
{
if ( _editors == null ) return;
bool changed = false;
foreach ( var entry in _editors )
{
bool visible = entry.Editor.TrySetTarget( _selected.ToList() );
if ( visible != entry.WasVisible ) changed = true;
entry.WasVisible = visible;
}
if ( !_lastSelected.SetEquals( _selected ) )
{
changed = true;
_lastSelected = _selected.ToHashSet();
}
if ( changed )
{
var visible = _editors.Where( e => e.WasVisible ).ToList();
if ( _active == null || !_active.WasVisible )
_active = visible.LastOrDefault();
ApplyVisibility();
StateHasChanged();
}
}
protected override int BuildHash() => HashCode.Combine( hovered, _selected.Count, _active );
public override void Tick()
{
_selected.RemoveWhere( x => !x.IsValid() );
InitEditors();
UpdateEditors();
UpdateHighlights();
UpdateCursor();
}
protected override void OnAfterTreeRender( bool firstTime )
{
base.OnAfterTreeRender( firstTime );
if ( _editors == null ) return;
AttachEditors();
ApplyVisibility();
}
protected override void OnVisibilityChanged()
{
UpdateHighlights();
}
void UpdateHighlights()
{
var host = Ancestors.OfType<ContextMenuHost>().FirstOrDefault();
if ( host is null ) return;
if ( host.SelectedOutline is null || host.HoveredOutline is null )
return;
host.SelectedOutline.Targets ??= new();
host.SelectedOutline.Targets.Clear();
host.SelectedOutline.Color = new Color( 4.7f, 10.1f, 30.6f, 1 );
host.SelectedOutline.ObscuredColor = new Color( 2.2f, 2.3f, 2.9f, 0.1f );
host.SelectedOutline.Width = 0.2f;
host.HoveredOutline.Targets ??= new();
host.HoveredOutline.Targets.Clear();
host.HoveredOutline.Color = new Color( 2.6f, 2.0f, 0.2f, 1 );
host.HoveredOutline.ObscuredColor = new Color( 2.6f, 2.0f, 0.2f, 0.1f );
host.HoveredOutline.Width = 0.2f;
if ( !IsVisible ) return;
host.SelectedOutline.Targets.AddRange( _selected.SelectMany( x => GetRenderers( x ) ) ?? [] );
if ( !_selected.Contains( hovered ) )
{
host.HoveredOutline.Targets = GetRenderers( hovered ).ToList();
}
else
{
host.HoveredOutline.Targets = default;
}
}
IEnumerable<Renderer> GetRenderers( GameObject o )
{
if ( !o.IsValid() ) yield break;
foreach ( var r in o.GetComponents<Renderer>() )
{
yield return r;
}
foreach ( var c in o.Children )
{
if ( c.NetworkMode == NetworkMode.Object ) continue;
foreach ( var rr in GetRenderers( c ) )
{
yield return rr;
}
}
}
GameObject hovered;
HashSet<GameObject> _selected = new();
void UpdateCursor()
{
var cursorPos = Mouse.Position;
var screenRay = Scene.Camera.ScreenPixelToRay( cursorPos );
var tr = Scene.Trace.Ray( screenRay, 4096 )
.IgnoreGameObjectHierarchy( Player.FindLocalPlayer()?.GameObject )
.Run();
var go = tr.Collider?.GameObject ?? tr.GameObject;
go = go.FindNetworkRoot();
if ( !CanSelect( go ) ) go = null;
UpdateHovered( go );
}
bool CanSelect( GameObject o )
{
if ( !o.IsValid() ) return false;
if ( o.Tags.Has( "world" ) ) return false;
if ( o.NetworkMode == NetworkMode.Never ) return false;
o = o?.FindNetworkRoot();
return true;
}
void UpdateHovered( GameObject o )
{
o = o?.FindNetworkRoot();
if ( hovered == o ) return;
hovered = o;
PlaySound( "ui.button.over" );
}
public void WorldMouseDown(MousePanelEvent e)
{
SelectObject( hovered );
}
public void WorldMouseRightDown(MousePanelEvent e)
{
if ( !hovered.IsValid() ) return;
SelectObject( hovered );
var target = hovered;
var ev = new IContextMenuEvent.Event( target );
Scene.RunEvent<IContextMenuEvent>( listener => listener.PopulateContextMenu( ev ) );
if ( !ev.Options.Any() ) return;
var menu = new Sandbox.UI.Menu();
ev.Populate( menu );
menu.Open( this, Popup.PositionMode.UnderMouse );
}
public void SelectObject( GameObject o )
{
if ( !o.IsValid() )
{
_selected.Clear();
}
else
{
if ( !Input.Down( "run" ) )
{
_selected.Clear();
}
_selected.Add( o );
}
}
void WindowPress( PanelEvent panelEvent )
{
panelEvent.StopPropagation();
}
}