Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Fazor.UI/FazorApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ public static void Run(RootPanel rootPanel, int width = 1280, int height = 720,

nativeWindow.RootPanel = rootPanel;

#if DEBUG
// Set up hotloading: when stylesheets change, dirty all styles on the root panel
StyleSheet.OnStyleSheetsHotloaded += () =>
{
Sandbox.UI.Log.Info("[Hotload] Stylesheets changed - refreshing all panel styles");
rootPanel.DirtyAllStyles();
};
Comment on lines +250 to +254

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This static event subscription creates a potential memory leak. Each time RunPanel is called, a new lambda is subscribed to the static event, but there's no corresponding unsubscription. The lambda captures rootPanel, which prevents the RootPanel from being garbage collected even after the window closes.

Consider either:

  1. Unsubscribing when the window is disposed/closed
  2. Using weak event patterns
  3. Making this subscription at a different lifecycle point where it can be properly cleaned up
Suggested change
StyleSheet.OnStyleSheetsHotloaded += () =>
{
Sandbox.UI.Log.Info("[Hotload] Stylesheets changed - refreshing all panel styles");
rootPanel.DirtyAllStyles();
};
// Use a weak reference so the RootPanel can be collected, and unsubscribe when it's gone
var rootPanelRef = new WeakReference<RootPanel>(rootPanel);
void OnHotload()
{
if (rootPanelRef.TryGetTarget(out var rp))
{
Sandbox.UI.Log.Info("[Hotload] Stylesheets changed - refreshing all panel styles");
rp.DirtyAllStyles();
}
else
{
// Root panel has been collected; remove this handler to avoid leaking it
StyleSheet.OnStyleSheetsHotloaded -= OnHotload;
}
}
StyleSheet.OnStyleSheetsHotloaded += OnHotload;

Copilot uses AI. Check for mistakes.
#endif

// Initialize window manager for multi-window support (create once globally)
if (_windowManager == null)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Fazor.UI/NativeWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ private void OnRender(double delta)
{
if (RootPanel == null) return;

// Process file watcher notifications (DEBUG builds only - handles stylesheet/Razor hotloading)
FileWatcher.Tick((float)delta);

// Update panel time for transitions and animations
PanelRealTime.Update(delta);
RealTime.Update(delta);
Expand Down
18 changes: 18 additions & 0 deletions src/Sandbox.UI/Panel/RootPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,22 @@ internal void BuildStyleRules()

styleRuleUpdates.Clear();
}

/// <summary>
/// Dirty all styles on this root panel and its children.
/// Called when stylesheets are hotloaded to force a complete style rebuild.
/// </summary>
internal void DirtyAllStyles()
{
DirtyStylesRecursive();
}

/// <summary>
/// Call OnHotloaded on this panel and all children.
/// Called when razor files or stylesheets are hotloaded.
/// </summary>
internal void TriggerHotload()
{
OnHotloaded();
}
Comment on lines +358 to +361

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TriggerHotload method is defined but never called anywhere in the codebase. If this is intended for future Razor hotloading support, consider adding a comment explaining this. If it should be used for stylesheet hotloading, it should be called alongside or instead of DirtyAllStyles().

Copilot uses AI. Check for mistakes.
}
63 changes: 62 additions & 1 deletion src/Sandbox.UI/Styles/StyleSheet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ public class StyleSheet
{
public static List<StyleSheet> Loaded { get; internal set; } = new List<StyleSheet>();

/// <summary>
/// Event raised when any stylesheet is hotloaded.
/// Used by the UI system to invalidate all panel styles.
/// </summary>
public static event Action? OnStyleSheetsHotloaded;

/// <summary>
/// Between sessions we clear the stylesheets, so one gamemode can't accidentally
/// use cached values from another.
Expand All @@ -20,6 +26,7 @@ internal static void InitStyleSheets()

public List<StyleBlock> Nodes { get; set; } = new List<StyleBlock>();
public string FileName { get; internal set; }
internal FileWatcher? 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 );
Expand All @@ -29,7 +36,8 @@ internal static void InitStyleSheets()
/// </summary>
public void Release()
{
// File watching not implemented in this port
Watcher?.Dispose();
Watcher = null;
}

public static StyleSheet FromFile( string filename, IEnumerable<(string key, string value)> variables = null, bool failSilently = false )
Expand All @@ -46,6 +54,7 @@ public static StyleSheet FromFile( string filename, IEnumerable<(string key, str

sheet.AddVariables( variables );
sheet.FileName = filename;
sheet.AddWatcher( filename );

Loaded.Add( sheet );

Expand All @@ -55,6 +64,7 @@ public static StyleSheet FromFile( string filename, IEnumerable<(string key, str
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 )
Expand Down Expand Up @@ -211,4 +221,55 @@ public void AddKeyFrames( KeyFrames frames )
{
KeyFrames[frames.Name] = frames;
}

/// <summary>
/// Set up file watching for this stylesheet (DEBUG builds only).
/// </summary>
private void AddWatcher( string name )
{
#if DEBUG
Watcher?.Dispose();
Watcher = null;

var fullPath = name;
if ( !System.IO.Path.IsPathRooted( fullPath ) )
{
fullPath = System.IO.Path.GetFullPath( name );
}

if ( !System.IO.File.Exists( fullPath ) )
return;

// Store the filename for reloading
var watchFileName = fullPath;

Watcher = new FileWatcher();
Watcher.AddFile( fullPath );

// Add any included files to the watcher
foreach ( var file in IncludedFiles )
{
var includePath = file;
if ( !System.IO.Path.IsPathRooted( includePath ) )
{
// Try relative to the main stylesheet
var dir = System.IO.Path.GetDirectoryName( fullPath );
if ( !string.IsNullOrEmpty( dir ) )
{
includePath = System.IO.Path.Combine( dir, file );
}
}
Watcher.AddFile( includePath );
}

Watcher.OnChanges += ( watcher ) =>
{
Log.Info( $"[StyleSheet] Hotloading: {watchFileName}" );
UpdateFromFile( watchFileName, true );

// Notify the UI system that stylesheets have changed
OnStyleSheetsHotloaded?.Invoke();
};
#endif
}
}
Loading
Loading