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 );
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 );
editorPanel.Parent = _windowStack;
_editors.Add( new EditorEntry( editor ) );
}
}
void SetActive( EditorEntry entry )
{
_active = entry;
ApplyVisibility();
}
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 );
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();
}
}
protected override int BuildHash() => HashCode.Combine( hovered, _selected.Count, _active );
public override void Tick()
{
_selected.RemoveAll( x => !x.IsValid() );
InitEditors();
UpdateEditors();
UpdateHighlights();
UpdateCursor();
}
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 == null ) 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;
List<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 (!_canvas.HasHovered) go = default;
if (!CanSelect(go)) go = null;
UpdateHovered(go);
}
bool CanSelect( GameObject o )
{
if (o == null) 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 isPlayer = target.Tags.Has( "player" );
var prop = target.GetComponent<Prop>();
var isGibbable = prop.IsValid() && prop.Health > 0;
var menu = MenuPanel.Open( this );
if ( !isPlayer )
{
menu.AddOption( "🗑️", Game.Language.GetPhrase( "spawnmenu.inspect.delete" ), () => GameManager.DeleteInspectedObject( target ) );
}
if ( isGibbable )
{
menu.AddOption( "💥", Game.Language.GetPhrase( "spawnmenu.inspect.break" ), () => GameManager.BreakInspectedProp( prop ) );
}
}
public void WorldMouseUp(MousePanelEvent e)
{
// nothing.
}
public void SelectObject( GameObject o )
{
if ( !o.IsValid() )
{
_selected.Clear();
}
else
{
if (!Input.Down("run"))
_selected.Clear();
_selected.Remove(o);
_selected.Add(o);
}
_selected = _selected.Distinct().ToList();
}
void WindowPress( PanelEvent panelEvent )
{
panelEvent.StopPropagation();
}
}