using Sandbox.Engine; namespace Sandbox.UI; /// /// A root panel. Serves as a container for other panels, handles things such as rendering. /// public partial class RootPanel : Panel { /// /// Bounds of the panel, i.e. its size and position on the screen. /// public Rect PanelBounds { get; set; } = new Rect( 0, 0, 512, 512 ); /// /// If any of our panels are visible and want mouse input (pointer-events != none) then /// this will be set to true. /// internal bool ChildrenWantMouseInput { get; set; } /// /// The scale of this panel and its children. /// public float Scale { get; protected set; } = 1.0f; /// /// 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). /// public bool RenderedManually { get; set; } /// /// True if this is a world panel, so should be skipped when determining cursor visibility etc /// public virtual bool IsWorldPanel { get; set; } /// /// If this panel belongs to a VR overlay /// public bool IsVR { get; internal set; } /// /// If this panel should be rendered with ~4K resolution. /// public bool IsHighQualityVR { get; internal set; } /// /// Current global mouse position, projected onto plane for world panels. /// 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 ); } /// /// This is called from tests to emulate the regular root panel simulate loop /// internal void Layout() { TickInternal(); PreLayout(); CalculateLayout(); PostLayout(); } int layoutHash; /// /// 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. /// protected virtual void UpdateBounds( Rect rect ) { PanelBounds = rect; if ( IsVR && IsHighQualityVR ) { PanelBounds = new Rect( 0, 0, 3840, 2400 ); return; } } /// /// Work out scaling here. Default is to scale relative to the screen being /// 1920 wide. ie - scale = screensize.Width / 1920.0f; /// 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 ); } /// /// Render this panel manually. This gives more flexibility to where UI is rendered, to texture for example. /// must be set to true. /// 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(); } /// /// A list of panels that are waiting to have their styles re-evaluated /// readonly HashSet styleRuleUpdates = new(); /// /// 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. /// internal void AddToBuildStyleRulesList( Panel panel ) { styleRuleUpdates.Add( panel ); } /// /// Run through all panels that are pending a re-check on their style rules. /// Only properly invalidate them if their rules actually change. /// 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" ); } } }