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

232 lines
5.3 KiB
C#

using Sandbox.Engine;
namespace Sandbox.UI;
public class StyleSheet
{
public static List<StyleSheet> Loaded { get; internal set; } = new List<StyleSheet>();
/// <summary>
/// Between sessions we clear the stylesheets, so one gamemode can't accidentally
/// use cached values from another.
/// </summary>
internal static void InitStyleSheets()
{
foreach ( var sheet in Loaded )
{
sheet?.Release();
}
Loaded.Clear();
}
public List<StyleBlock> Nodes { get; set; } = new List<StyleBlock>();
public string FileName { get; internal set; }
internal FileWatch Watcher { get; private set; }
public List<string> IncludedFiles { get; set; } = new List<string>();
public Dictionary<string, string> Variables;
public Dictionary<string, KeyFrames> KeyFrames = new Dictionary<string, KeyFrames>( StringComparer.OrdinalIgnoreCase );
/// <summary>
/// Releases the filesystem watcher so we won't get file changed events.
/// </summary>
public void Release()
{
Watcher?.Dispose();
Watcher = null;
}
public static StyleSheet FromFile( string filename, IEnumerable<(string key, string value)> variables = null, bool failSilently = false )
{
filename = BaseFileSystem.NormalizeFilename( filename );
var alreadyLoaded = Loaded.FirstOrDefault( x => x.FileName == filename );
if ( alreadyLoaded != null )
return alreadyLoaded;
var sheet = new StyleSheet();
sheet.UpdateFromFile( filename, failSilently );
sheet.AddVariables( variables );
sheet.FileName = filename;
sheet.AddWatcher( filename );
Loaded.Add( sheet );
return sheet;
}
internal void AddFilename( string filename )
{
IncludedFiles.Add( filename );
Watcher?.AddFile( filename );
}
public static StyleSheet FromString( string styles, string filename = "none", IEnumerable<(string key, string value)> variables = null )
{
try
{
return StyleParser.ParseSheet( styles, filename, variables );
}
catch ( Exception e )
{
Log.Warning( e, $"Error parsing stylesheet: {e.Message}\n{e.StackTrace}" );
return new StyleSheet();
}
}
internal bool UpdateFromFile( string name, bool failSilently = false, GlobalContext ctx = null )
{
ctx ??= GlobalContext.Current;
if ( ctx.FileMount is null )
{
return false;
}
if ( failSilently && !ctx.FileMount.FileExists( name ) )
{
Nodes = new();
return true;
}
try
{
var text = ctx.FileMount.ReadAllText( name );
if ( text is null ) throw new System.IO.FileNotFoundException( "File not found", name );
return UpdateFromString( text, name, failSilently );
}
catch ( Exception e )
{
if ( !failSilently )
{
Log.Warning( e, $"Error opening stylesheet: {name} ({e.Message})" );
}
Nodes = new();
}
return false;
}
internal bool UpdateFromString( string text, string filename = "none", bool failSilently = false )
{
try
{
var sheet = FromString( text, filename, null );
Nodes = sheet.Nodes;
Variables = sheet.Variables;
KeyFrames = sheet.KeyFrames;
// Don't overwrite the included files if the stylesheet
// failed to load, because it won't be able to hotload
if ( sheet.IncludedFiles.Any() )
{
IncludedFiles = sheet.IncludedFiles;
}
sheet.Release();
return true;
}
catch ( Exception e )
{
if ( !failSilently )
{
Log.Warning( e, $"Error opening stylesheet: {filename} ({e.Message})" );
}
Nodes = new();
}
return false;
}
void AddWatcher( string name )
{
Watcher?.Dispose();
Watcher = null;
if ( GlobalContext.Current.FileMount is null )
return;
//
// Store the current context to pass through to the watcher because
// we might be in a different scope later, and won't be able to find the files
//
var context = GlobalContext.Current;
Watcher = context.FileMount.Watch();
Watcher.OnChanges += x =>
{
UpdateFromFile( name, true, context );
context.UISystem.DirtyAllStyles();
};
foreach ( var file in IncludedFiles )
{
Watcher.AddFile( file );
}
}
internal void SetVariable( string key, string value, bool isdefault = false )
{
Variables ??= new Dictionary<string, string>( StringComparer.OrdinalIgnoreCase );
if ( isdefault && Variables.ContainsKey( key ) ) return;
// If it's another variable, straight swap it
value = ReplaceVariables( value );
Variables[key] = value;
}
public string GetVariable( string name, string defaultValue = default )
{
if ( Variables == null ) return defaultValue;
if ( Variables.TryGetValue( name, out var val ) ) return val;
return null;
}
public string ReplaceVariables( string str )
{
if ( !str.Contains( '$' ) ) return str; // fast exit
if ( Variables == null )
throw new Exception( "Couldn't replace variables -- none set?" );
var pairs = Variables.Where( x => str.Contains( x.Key ) ).ToArray();
bool replaced = false;
foreach ( var var in pairs.OrderByDescending( x => x.Key.Length ) ) // replace the longest first so $button won't stomp $button-bright
{
str = str.Replace( var.Key, var.Value );
replaced = true;
}
if ( !replaced )
{
throw new Exception( $"Unknown variable '{str}'" );
}
return str;
}
internal void AddVariables( IEnumerable<(string key, string value)> variables )
{
if ( variables == null ) return;
foreach ( var var in variables )
{
SetVariable( var.key, var.value );
}
}
public void AddKeyFrames( KeyFrames frames )
{
KeyFrames[frames.Name] = frames;
}
}