mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 08:18:20 -04:00
354 lines
7.6 KiB
C#
354 lines
7.6 KiB
C#
using System;
|
|
|
|
namespace Editor;
|
|
|
|
/// <summary>
|
|
/// The editor's central widget. Every open scene and prefab session lives here
|
|
/// as a browser style tab - sessions are never dock widgets, so they can't be
|
|
/// floated, split or lost by layout changes.
|
|
/// </summary>
|
|
sealed class SceneTabWidget : Widget
|
|
{
|
|
readonly Widget _tabBar;
|
|
readonly ContentHost _content;
|
|
readonly IconButton _newTab;
|
|
readonly List<SceneTab> _tabs = new();
|
|
|
|
SceneTab _currentTab;
|
|
|
|
public SceneTabWidget( Widget parent ) : base( parent )
|
|
{
|
|
Name = "SceneTabs";
|
|
Layout = Layout.Column();
|
|
|
|
_tabBar = Layout.Add( new TabBarWidget( this ) );
|
|
_content = Layout.Add( new ContentHost( this ), 1 );
|
|
|
|
_newTab = new IconButton( "add", EditorScene.NewScene, _tabBar )
|
|
{
|
|
ToolTip = "New scene",
|
|
Background = Color.Transparent,
|
|
FixedWidth = 20,
|
|
FixedHeight = 20
|
|
};
|
|
|
|
RebuildTabBar();
|
|
}
|
|
|
|
public void Open( SceneEditorSession session )
|
|
{
|
|
if ( _tabs.Any( x => x.Session == session ) ) return;
|
|
|
|
var tab = new SceneTab( this, session );
|
|
_tabs.Add( tab );
|
|
|
|
session.SceneDock.Visible = false;
|
|
session.SceneDock.Parent = _content;
|
|
|
|
RebuildTabBar();
|
|
MakeCurrent( tab );
|
|
}
|
|
|
|
public void Remove( SceneEditorSession session )
|
|
{
|
|
var tab = _tabs.FirstOrDefault( x => x.Session == session );
|
|
if ( tab is null ) return;
|
|
|
|
CloseTab( tab );
|
|
}
|
|
|
|
void CloseTab( SceneTab tab )
|
|
{
|
|
var index = _tabs.IndexOf( tab );
|
|
if ( index < 0 ) return;
|
|
|
|
_tabs.Remove( tab );
|
|
tab.Destroy();
|
|
|
|
if ( !_tabBar.IsValid() )
|
|
return;
|
|
|
|
if ( _tabs.Count == 0 )
|
|
{
|
|
_currentTab = null;
|
|
_content.Clear();
|
|
RebuildTabBar();
|
|
return;
|
|
}
|
|
|
|
if ( _currentTab == tab )
|
|
{
|
|
_currentTab = null;
|
|
|
|
var neighbour = _tabs[Math.Min( index, _tabs.Count - 1 )];
|
|
neighbour.Session.MakeActive();
|
|
}
|
|
|
|
RebuildTabBar();
|
|
}
|
|
|
|
public void MakeCurrent( SceneEditorSession session )
|
|
{
|
|
if ( _tabs.FirstOrDefault( x => x.Session == session ) is { } tab )
|
|
MakeCurrent( tab );
|
|
}
|
|
|
|
void MakeCurrent( SceneTab tab )
|
|
{
|
|
if ( _currentTab == tab ) return;
|
|
|
|
_currentTab = tab;
|
|
_content.Show( tab.Session.SceneDock );
|
|
|
|
_tabBar.Update();
|
|
}
|
|
|
|
public void UpdateTitle( SceneEditorSession session )
|
|
{
|
|
if ( _tabs.FirstOrDefault( x => x.Session == session ) is not { } tab ) return;
|
|
|
|
tab.UpdateGeometry();
|
|
tab.Update();
|
|
}
|
|
|
|
[Event( "scene.play" )]
|
|
[Event( "scene.stop" )]
|
|
void OnPlayStateChanged()
|
|
{
|
|
foreach ( var tab in _tabs )
|
|
tab.Update();
|
|
}
|
|
|
|
internal bool IsCurrent( SceneTab tab ) => _currentTab == tab;
|
|
|
|
internal List<SceneTab> Tabs => _tabs;
|
|
|
|
void RebuildTabBar()
|
|
{
|
|
_tabBar.Layout.Clear( false );
|
|
|
|
foreach ( var tab in _tabs )
|
|
{
|
|
_tabBar.Layout.Add( tab );
|
|
}
|
|
|
|
_tabBar.Layout.AddSpacingCell( 2 );
|
|
_tabBar.Layout.Add( _newTab );
|
|
_tabBar.Layout.AddStretchCell();
|
|
_tabBar.Layout.AddSpacingCell( 4 );
|
|
|
|
_tabBar.Update();
|
|
}
|
|
|
|
sealed class TabBarWidget : Widget
|
|
{
|
|
public TabBarWidget( Widget parent ) : base( parent )
|
|
{
|
|
Layout = Layout.Row();
|
|
FixedHeight = 26;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( Theme.TabBarBackground );
|
|
Paint.DrawRect( LocalRect );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hosts the scene views stacked on top of each other, so switching doesn't
|
|
/// relayout anything. The outgoing widget stays on top until the end of the
|
|
/// frame, by which point the incoming viewport has presented.
|
|
/// </summary>
|
|
sealed class ContentHost : Widget
|
|
{
|
|
Widget _current;
|
|
|
|
public ContentHost( Widget parent ) : base( parent )
|
|
{
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
if ( _current.IsValid() )
|
|
_current.Visible = false;
|
|
|
|
_current = null;
|
|
Update();
|
|
}
|
|
|
|
public void Show( Widget widget )
|
|
{
|
|
if ( _current == widget ) return;
|
|
|
|
var outgoing = _current;
|
|
_current = widget;
|
|
|
|
widget.Position = 0;
|
|
widget.Size = Size;
|
|
widget.Visible = true;
|
|
|
|
if ( !outgoing.IsValid() || !outgoing.Visible )
|
|
return;
|
|
|
|
widget.Lower();
|
|
|
|
EngineLoop.DisposeAtFrameEnd( new Sandbox.Utility.DisposeAction( () =>
|
|
{
|
|
if ( outgoing.IsValid() && outgoing != _current )
|
|
outgoing.Visible = false;
|
|
|
|
if ( _current.IsValid() )
|
|
_current.Raise();
|
|
} ) );
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( Theme.TabBackground );
|
|
Paint.DrawRect( LocalRect );
|
|
}
|
|
|
|
protected override void OnResize()
|
|
{
|
|
foreach ( var child in Children )
|
|
{
|
|
if ( !child.Visible ) continue;
|
|
|
|
child.Position = 0;
|
|
child.Size = Size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A single scene/prefab tab in the <see cref="SceneTabWidget"/> tab bar.
|
|
/// </summary>
|
|
sealed class SceneTab : Widget
|
|
{
|
|
public SceneEditorSession Session { get; }
|
|
|
|
readonly SceneTabWidget _owner;
|
|
readonly IconButton _close;
|
|
|
|
bool IsCurrent => _owner.IsCurrent( this );
|
|
bool IsPlaying => Session.IsPlaying;
|
|
string Icon => IsPlaying ? "play_arrow" : Session.IsPrefabSession ? "home_repair_service" : "grid_4x4";
|
|
string Title => Session.SceneDock?.WindowTitle ?? "Untitled";
|
|
|
|
public SceneTab( SceneTabWidget owner, SceneEditorSession session ) : base( owner )
|
|
{
|
|
_owner = owner;
|
|
Session = session;
|
|
Cursor = CursorShape.Finger;
|
|
|
|
_close = new IconButton( "close", CloseSession, this )
|
|
{
|
|
ToolTip = "Close",
|
|
Background = Color.Transparent,
|
|
Foreground = Theme.TextLight,
|
|
IconSize = 8,
|
|
FixedWidth = 16,
|
|
FixedHeight = 16
|
|
};
|
|
}
|
|
|
|
protected override Vector2 SizeHint()
|
|
{
|
|
Paint.SetDefaultFont( 8, 500 );
|
|
return new Vector2( Paint.MeasureText( Title ).x + 58, 26 );
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
var hovered = Paint.HasMouseOver;
|
|
var background = IsCurrent ? Theme.TabBackground : hovered ? Theme.SurfaceBackground : Theme.TabInactiveBackground;
|
|
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( background );
|
|
Paint.DrawRect( new Rect( 0, 0, Width, Height + Theme.ControlRadius ), Theme.ControlRadius );
|
|
|
|
if ( IsCurrent || IsPlaying )
|
|
{
|
|
Paint.SetBrush( IsPlaying ? Theme.Green : Theme.Primary );
|
|
Paint.DrawRect( new Rect( 0, 0, Width, 2 ) );
|
|
}
|
|
|
|
var color = IsCurrent || hovered ? Theme.Text : Theme.TextLight;
|
|
|
|
Paint.SetPen( IsPlaying ? Theme.Green : color );
|
|
Paint.DrawIcon( new Rect( 8, 0, 16, Height ), Icon, 13 );
|
|
|
|
Paint.SetPen( color );
|
|
Paint.SetDefaultFont( 8, 500 );
|
|
Paint.DrawText( new Rect( 29, 0, Width - 52, Height ), Title, TextFlag.LeftCenter );
|
|
}
|
|
|
|
protected override void OnResize()
|
|
{
|
|
_close.Position = new Vector2( Width - 20, (Height - _close.Height) * 0.5f + 1 );
|
|
}
|
|
|
|
protected override void OnMouseEnter()
|
|
{
|
|
base.OnMouseEnter();
|
|
Update();
|
|
}
|
|
|
|
protected override void OnMouseLeave()
|
|
{
|
|
base.OnMouseLeave();
|
|
Update();
|
|
}
|
|
|
|
protected override void OnMousePress( MouseEvent e )
|
|
{
|
|
base.OnMousePress( e );
|
|
|
|
if ( e.MiddleMouseButton )
|
|
{
|
|
e.Accepted = true;
|
|
CloseSession();
|
|
return;
|
|
}
|
|
|
|
if ( e.LeftMouseButton ) Session.MakeActive();
|
|
}
|
|
|
|
protected override void OnContextMenu( ContextMenuEvent e )
|
|
{
|
|
var menu = new ContextMenu( this );
|
|
var tabs = _owner.Tabs;
|
|
var index = tabs.IndexOf( this );
|
|
|
|
menu.AddOption( "Close", null, CloseSession );
|
|
AddCloseOption( menu, "Close Others", tabs.Where( x => x != this ) );
|
|
AddCloseOption( menu, "Close Tabs to the Left", tabs.Take( index ) );
|
|
AddCloseOption( menu, "Close Tabs to the Right", tabs.Skip( index + 1 ) );
|
|
|
|
menu.OpenAtCursor();
|
|
e.Accepted = true;
|
|
}
|
|
|
|
static void AddCloseOption( ContextMenu menu, string title, IEnumerable<SceneTab> tabs )
|
|
{
|
|
var closable = tabs.ToArray();
|
|
if ( closable.Length == 0 ) return;
|
|
|
|
menu.AddOption( title, null, () =>
|
|
{
|
|
foreach ( var tab in closable )
|
|
tab.CloseSession();
|
|
} );
|
|
}
|
|
|
|
void CloseSession()
|
|
{
|
|
// routes through SceneDock.OnClose so the unsaved changes prompt still applies
|
|
Session.SceneDock?.Close();
|
|
}
|
|
}
|