Files
sbox-public/engine/Sandbox.Engine/Systems/UI/RootPanel.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

322 lines
7.0 KiB
C#

using Sandbox.Engine;
namespace Sandbox.UI;
/// <summary>
/// A root panel. Serves as a container for other panels, handles things such as rendering.
/// </summary>
public partial class RootPanel : Panel
{
/// <summary>
/// Bounds of the panel, i.e. its size and position on the screen.
/// </summary>
public Rect PanelBounds { get; set; } = new Rect( 0, 0, 512, 512 );
/// <summary>
/// If any of our panels are visible and want mouse input (pointer-events != none) then
/// this will be set to true.
/// </summary>
internal bool ChildrenWantMouseInput { get; set; }
/// <summary>
/// The scale of this panel and its children.
/// </summary>
public float Scale { get; protected set; } = 1.0f;
/// <summary>
/// If set to true this panel won't be rendered to the screen like a normal panel.
/// This is true when the panel is drawn via other means (like as a world panel).
/// </summary>
public bool RenderedManually { get; set; }
/// <summary>
/// True if this is a world panel, so should be skipped when determining cursor visibility etc
/// </summary>
public virtual bool IsWorldPanel { get; set; }
/// <summary>
/// If this panel belongs to a VR overlay
/// </summary>
public bool IsVR { get; internal set; }
/// <summary>
/// If this panel should be rendered with ~4K resolution.
/// </summary>
public bool IsHighQualityVR { get; internal set; }
/// <summary>
/// Current global mouse position, projected onto plane for world panels.
/// </summary>
internal Vector2 MousePos;
public RootPanel()
{
Style.Width = Length.Percent( 100 );
Style.Height = Length.Percent( 100 );
GlobalContext.Current.UISystem.AddRoot( this );
AddToLists();
StyleSheet.Load( "/styles/rootpanel.scss" );
}
public override void Delete( bool immediate = true )
{
base.Delete( immediate );
}
public override void OnDeleted()
{
base.OnDeleted();
GlobalContext.Current.UISystem.RemoveRoot( this );
}
internal override void AddToLists()
{
base.AddToLists();
Sandbox.Internal.IPanel.InspectablePanels.Add( this );
}
internal override void RemoveFromLists()
{
base.RemoveFromLists();
Sandbox.Internal.IPanel.InspectablePanels.Remove( this );
}
/// <summary>
/// This is called from tests to emulate the regular root panel simulate loop
/// </summary>
internal void Layout()
{
TickInternal();
PreLayout();
CalculateLayout();
PostLayout();
}
int layoutHash;
/// <summary>
/// Called before layout to lock the bounds of this root panel to the screen size (which is passed).
/// Internally this sets PanelBounds to rect and calls UpdateScale.
/// </summary>
protected virtual void UpdateBounds( Rect rect )
{
PanelBounds = rect;
if ( IsVR && IsHighQualityVR )
{
PanelBounds = new Rect( 0, 0, 3840, 2400 );
return;
}
}
/// <summary>
/// Work out scaling here. Default is to scale relative to the screen being
/// 1920 wide. ie - scale = screensize.Width / 1920.0f;
/// </summary>
protected virtual void UpdateScale( Rect screenSize )
{
Scale = screenSize.Height / 1080.0f;
if ( Game.IsRunningOnHandheld )
{
Scale = Scale * 1.333f;
}
if ( IsVR && IsHighQualityVR )
{
Scale = 2.33f;
}
}
internal void TickInputInternal()
{
ChildrenWantMouseInput = WantsMouseInput();
}
internal void PreLayout( Rect screenSize )
{
UpdateBounds( screenSize );
UpdateScale( PanelBounds );
Scale = MathX.Clamp( Scale, 0.1f, 10.0f );
PreLayout();
}
internal void PreLayout()
{
var cascade = new LayoutCascade
{
Scale = Scale,
Root = this,
};
Style.Left = 0.0f;
Style.Top = 0.0f;
Style.Width = PanelBounds.Width * (1 / Scale);
Style.Height = PanelBounds.Height * (1 / Scale);
var hash = HashCode.Combine( PanelBounds.Width, PanelBounds.Height, Scale );
if ( hash != layoutHash )
{
layoutHash = hash;
StyleSelectorsChanged( true, true );
SkipAllTransitions();
cascade.SelectorChanged = true;
}
BuildStyleRules();
PushRootValues();
PreLayout( cascade );
}
internal void CalculateLayout()
{
if ( YogaNode == null )
return;
using var perfScope = Performance.Scope( "CalculateLayout" );
PushRootValues();
YogaNode.CalculateLayout();
}
internal void PostLayout()
{
PushRootValues();
FinalLayout( Vector2.Zero );
}
internal void PushRootValues()
{
Length.RootSize = new Vector2( PanelBounds.Width, PanelBounds.Height );
Length.RootFontSize = ComputedStyle?.FontSize ?? Length.Pixels( 13 ).Value;
Length.RootScale = ScaleToScreen;
}
public override void OnLayout( ref Rect layoutRect )
{
layoutRect = PanelBounds;
}
internal void Render( float opacity = 1.0f )
{
ThreadSafe.AssertIsMainThread();
GlobalContext.Current.UISystem.Renderer.Render( this, opacity );
}
/// <summary>
/// Render this panel manually. This gives more flexibility to where UI is rendered, to texture for example.
/// <see cref="RenderedManually"/> must be set to true.
/// </summary>
public void RenderManual( float opacity = 1.0f )
{
Graphics.AssertRenderBlock();
if ( !RenderedManually && !IsWorldPanel )
throw new Exception( $"{nameof( RenderedManually )} must be set to true to render this panel manually." );
Render( opacity );
}
[Event( "ui.skiptransitions" )]
internal void SkipAllTransitions()
{
SkipTransitions();
}
[Event( "language.changed" )]
internal void OnLanguageChanged()
{
LanguageChanged();
}
/// <summary>
/// A list of panels that are waiting to have their styles re-evaluated
/// </summary>
readonly HashSet<Panel> styleRuleUpdates = new();
/// <summary>
/// Add this panel to a list to have their styles re-evaluated. This should be done any
/// time the panel changes in a way that could affect its style selector.. like if its child
/// index changed, or classes added or removed, or became hovered etc.
/// </summary>
internal void AddToBuildStyleRulesList( Panel panel )
{
styleRuleUpdates.Add( panel );
}
/// <summary>
/// Run through all panels that are pending a re-check on their style rules.
/// Only properly invalidate them if their rules actually change.
/// </summary>
internal void BuildStyleRules()
{
if ( styleRuleUpdates.Count == 0 )
return;
var timer = FastTimer.StartNew();
int count = styleRuleUpdates.Count;
int locks = 0;
var l = new object();
//
// Anything in BuildRules should be thread safe
//
#if true
{
Parallel.ForEach( styleRuleUpdates, panel =>
{
if ( !panel.IsValid )
return;
if ( panel.Style.BuildRulesInThread() )
{
lock ( l )
{
locks++;
panel.SetNeedsPreLayout();
}
}
panel.MarkStylesRebuilt();
} );
}
#else
{
foreach ( var panel in styleRuleUpdates )
{
if ( !panel.IsValid )
return;
if ( panel.Style.BuildRulesInThread() )
{
lock ( l )
{
locks++;
panel.SetNeedsPreLayout();
}
}
};
}
#endif
styleRuleUpdates.Clear();
if ( timer.ElapsedMilliSeconds > 0.5 )
{
Log.Trace( $"BuildStyleRules {count:n0} ({locks}) took {timer.ElapsedMilliSeconds}ms" );
}
}
}