Files
sbox-public/engine/Sandbox.Engine/Systems/UI/PanelStyle.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

320 lines
7.1 KiB
C#

namespace Sandbox.UI;
public sealed class PanelStyle : Styles
{
Panel panel;
internal Styles Cached = new Styles();
internal Styles Final = new Styles();
/// <summary>
/// This could be a local variable if we wanted to create a new class every time
/// </summary>
List<StyleSelector> activeRules;
/// <summary>
/// Store the last active rules so we can compare them when they change and trigger sounds etc on new styles
/// </summary>
internal List<StyleSelector> LastActiveRules;
/// <summary>
/// Cache of the active rules that are applied, that way we can trigger stuff only if they actually changed
/// </summary>
int ActiveRulesGuid;
private bool isDirty = true;
internal bool skipTransitions = true;
bool rulesChanged = true;
public override void Dirty() => isDirty = true;
internal bool IsDirty => isDirty;
internal PanelStyle( Panel panel )
{
this.panel = panel;
}
/// <summary>
/// Should be called when a stylesheet in our bundle has changed. This can happen as a result of
/// editing it in the style editor.
/// </summary>
internal void UnderlyingStyleHasChanged()
{
ActiveRulesGuid = -1;
}
/// <summary>
/// All these styles could possibly apply to us. To get this list we get the stylesheets from
/// ourself and our anscestors and then filter them by the broadphase. The broadphase is a check
/// against classes, element names and ids, things that don't change in a recursive way.
/// </summary>
StyleBlock[] StyleBlocks;
/// <summary>
/// A hash of the things that are checked in the broadphase.
/// </summary>
int broadPhaseHash = 0;
bool _hasBeforeElement;
bool _hasAfterElement;
/// <summary>
/// This style has a ::before element available. This is signalling to the panel system that if we
/// apply this style, we should also create a ::before element.
/// </summary>
public bool HasBeforeElement => _hasBeforeElement;
/// <summary>
/// This style has a ::after element available. This is signalling to the panel system that if we
/// apply this style, we should also create a ::after element.
/// </summary>
public bool HasAfterElement => _hasAfterElement;
/// <summary>
/// Called when a stylesheet has been added or removed from ourselves or one of
/// our ancestor panels - because under that condition we need to rebuild our
/// broadphase.
/// </summary>
internal void InvalidateBroadphase()
{
if ( StyleBlocks == null )
return;
StyleBlocks = null;
foreach ( var child in panel.Children )
{
child.Style.InvalidateBroadphase();
}
}
void BuildApplicableBlocks()
{
StyleBlocks = panel.AllStyleSheets
.SelectMany( x => x.Nodes )
.Where( x => x.TestBroadphase( panel ) )
.ToArray();
}
/// <summary>
/// Called from the root panel in a thread. We replace activeRules with all of the rules that
/// we want applied and return true if the rules changed.
/// </summary>
internal bool BuildRulesInThread()
{
activeRules?.Clear();
var hash = HashCode.Combine( panel.Id, panel.ElementName, panel.Classes );
if ( StyleBlocks == null || hash != broadPhaseHash )
{
BuildApplicableBlocks();
}
broadPhaseHash = hash;
_hasBeforeElement = false;
_hasAfterElement = false;
bool isBeforeOrAfter = (panel as IStyleTarget).IsBeforeOrAfter;
foreach ( var c in StyleBlocks )
{
//
// If we're not a ::before or ::after element, see if we have any styles with ::before or ::after elements.
//
if ( !isBeforeOrAfter )
{
_hasBeforeElement = _hasBeforeElement || c.Test( panel, PseudoClass.Before ) != null;
_hasAfterElement = _hasAfterElement || c.Test( panel, PseudoClass.After ) != null;
}
var winningSelector = c.Test( panel );
if ( winningSelector == null ) continue;
activeRules ??= new();
activeRules.Add( winningSelector );
}
int ruleguid = 0;
if ( activeRules != null )
{
activeRules.Sort( StyleOrderer.Instance );
foreach ( var entry in activeRules )
{
ruleguid = HashCode.Combine( ruleguid, entry );
}
}
rulesChanged = rulesChanged || ruleguid != ActiveRulesGuid;
ActiveRulesGuid = ruleguid;
return rulesChanged;
}
internal bool BuildCached( ref LayoutCascade cascade )
{
if ( !isDirty && !cascade.SelectorChanged && !rulesChanged )
return false;
isDirty = false;
Cached.From( Styles.Default );
if ( activeRules != null )
{
foreach ( var entry in activeRules )
{
Cached.Add( entry.Block.Styles );
}
}
//
// Rules changed
//
if ( rulesChanged )
{
rulesChanged = false;
cascade.SelectorChanged = true;
LastActiveRules ??= new();
activeRules ??= new();
foreach ( var rule in activeRules.Except( LastActiveRules ) )
{
OnRuleAdded( rule );
}
foreach ( var rule in LastActiveRules.Except( activeRules ) )
{
OnRuleRemoved( rule );
}
LastActiveRules.Clear();
LastActiveRules.AddRange( activeRules );
}
Cached.Add( this );
Cached.ApplyScale( cascade.Scale );
return true;
}
internal Styles BuildFinal( ref LayoutCascade cascade, out bool changed )
{
var time = panel.TimeNow;
var timeDelta = panel.TimeDelta;
cascade.SkipTransitions = skipTransitions || cascade.SkipTransitions;
changed = BuildCached( ref cascade );
if ( !cascade.SkipTransitions && !HasTransitions && !HasAnimation )
{
Final.CopyShadows( Cached );
}
if ( cascade.SkipTransitions )
{
Final.CopyShadows( Cached );
panel.Transitions.Kill();
skipTransitions = false;
}
else if ( changed && Final != null )
{
panel.Transitions.Kill( Final );
panel.Transitions.Add( Final, Cached, time - timeDelta );
}
Final.From( Cached );
cascade.ApplyCascading( Final );
Final.FillDefaults();
if ( panel.Transitions.Run( Final, time ) )
{
changed = true;
}
if ( Final.ApplyAnimation( panel ) )
{
changed = true;
}
return Final;
}
public override bool Set( string property, string value )
{
isDirty = true;
return base.Set( property, value );
}
internal void OnRuleAdded( StyleSelector selector )
{
if ( selector.Block.Styles.SoundIn != null )
{
panel.PlaySound( selector.Block.Styles.SoundIn );
}
}
internal void OnRuleRemoved( StyleSelector selector )
{
if ( selector.Block.Styles.SoundOut != null )
{
panel.PlaySound( selector.Block.Styles.SoundOut );
}
}
//
// Helpers
//
public void SetBackgroundImage( Texture texture )
{
BackgroundImage = texture;
}
public void SetBackgroundImage( string image )
{
SetBackgroundImage( Texture.Load( image ) );
}
public async Task SetBackgroundImageAsync( string image )
{
SetBackgroundImage( await Texture.LoadAsync( image ) );
}
public void SetRect( Rect rect )
{
Left = rect.Left;
Top = rect.Top;
Width = rect.Width;
Height = rect.Height;
Dirty();
}
/// <summary>
/// Returns true if we have the style
/// </summary>
internal bool ContainsStyle( Styles style )
{
if ( LastActiveRules == null ) return false;
return LastActiveRules.Any( x => x.Block.Styles == style );
}
}
internal class StyleOrderer : IComparer<StyleSelector>
{
internal static StyleOrderer Instance = new StyleOrderer();
public int Compare( StyleSelector x, StyleSelector y )
{
return x.Score - y.Score;
}
}