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

299 lines
7.2 KiB
C#

using Microsoft.AspNetCore.Components;
using System.Text;
namespace Sandbox.UI;
/// <summary>
/// This is a tree renderer for panels. If we ever use razor on other ui we'll want to make a copy of
/// this class and do the specific things to that.
/// </summary>
public partial class PanelRenderTreeBuilder : Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder
{
Panel Parent;
int contentHash = 0;
StringBuilder contentBuilder = new();
void FlushContent()
{
if ( contentBuilder.Length == 0 ) return;
var block = GetBlock( contentHash );
var label = CurrentScope.Element as Label;
if ( label == null && (contentBuilder.Length > 0 || block.ElementPanel.IsValid()) )
label = block.FindOrCreateElement( "label", CurrentScope.Element ?? Parent ) as Label;
if ( label != null )
{
CurrentScope.ChildIndex++;
label.SetContent( contentBuilder.ToString() );
}
contentBuilder.Clear();
}
internal PanelRenderTreeBuilder( Panel panel )
{
Parent = panel;
RootBlock = new Block();
RootBlock.ElementPanel = panel;
RootBlock.IsRootElement = true;
}
/// <summary>
/// Called once before building the tree
/// </summary>
internal virtual void Start()
{
stack.Clear();
CurrentScope = default;
RootBlock.Reset();
RootBlock.WasSeen = true;
}
/// <summary>
/// Called once after building the tree
/// </summary>
internal virtual void Finish()
{
RootBlock.DestroyUnseen();
}
Block GetBlock( int hash )
{
var currentBlock = CurrentScope.Block ?? RootBlock;
return currentBlock.GetChild( hash );
}
string sourceFile;
int sourceLine;
int sourceColumn;
/// <summary>
/// Add the current source location. Used to record in which file the element was created.
/// </summary>
public override void AddLocation( string filename, int line, int column )
{
sourceFile = filename;
sourceLine = line;
sourceColumn = column;
}
public override void OpenElement( int sequence, string elementName ) => OpenElement( sequence, elementName, null );
/// <summary>
/// Start working on this element
/// </summary>
public override void OpenElement( int sequence, string elementName, object key = null )
{
FlushContent();
var parentElement = CurrentScope.Element ?? Parent;
var childIndex = CurrentScope.ChildIndex;
CurrentScope.ChildIndex++;
PushScope( sequence, key );
//Log.Info( $"OpenElement {CurrentScope.Sequence}.{CurrentScope.Loop} [{CurrentScope.Element}] [{elementName}]" );
var block = GetBlock( CurrentScope.Hash );
//
// If we're a <root> and we are actually at the root, then we act like it's the root panel.
//
if ( string.Equals( "root", elementName, StringComparison.OrdinalIgnoreCase ) && CurrentScope.Element == null )
{
block.IsRootElement = true;
block.ElementPanel = Parent;
CurrentScope.Block = block;
}
else
{
var element = block.FindOrCreateElement( elementName, parentElement );
element.Parent.SetChildIndex( element, childIndex );
element.SourceFile = sourceFile;
element.SourceLine = sourceLine;
//element.SourecColumn = column;
CurrentScope.Block = block;
}
}
/// <summary>
/// Handles "style" and "class" attributes..
/// </summary>
public void AddAttributeObject( int sequence, string attrName, object value )
{
var scope = CurrentScope;
scope.Sequence = sequence;
// Only set the value if it changed
if ( CurrentBlock.CheckCacheValue( HashCode.Combine( scope.Element, attrName ), value?.GetHashCode() ?? 0 ) )
return;
scope.Element?.SetProperty( attrName, $"{value}" );
}
/// <summary>
/// Handles "style" and "class" attributes..
/// </summary>
public void AddAttributeString( int sequence, string attrName, string value )
{
var scope = CurrentScope;
scope.Sequence = sequence;
// Only set the value if it changed
if ( CurrentBlock.CheckCacheValue( HashCode.Combine( scope.Element, attrName ), value?.GetHashCode() ?? 0 ) )
return;
scope.Element?.SetProperty( attrName, value );
}
/// <summary>
/// Styles from a style block
/// </summary>
public override void AddStyleDefinitions( int sequence, string styles )
{
var scope = CurrentScope;
scope.Sequence = sequence;
// only if it changed
if ( CurrentBlock.CheckCacheValue( HashCode.Combine( scope.Element, sequence ), styles?.GetHashCode() ?? 0 ) )
return;
var sheet = StyleSheet.FromString( styles, sourceFile, null );
var fileName = $"__Razor.{scope.Hash}";
sheet.FileName = fileName;
Parent.StyleSheet.Remove( sheet.FileName );
Parent.StyleSheet.Add( sheet );
}
/// <summary>
/// <![CDATA[ <Icon OnSomething=@Function></Icon> ]]>
/// </summary>
public override void AddAttribute<T>( int sequence, Action<T> value )
{
var scope = CurrentScope;
scope.Sequence = sequence;
// Only cache if it changed - the value is a delegate so we can't just use the hashcode
// plus it doesn't matter - because it shouldn't really change unless they're doing something weird
if ( CurrentBlock.CheckCacheValue( HashCode.Combine( scope.Element, sequence ), 1 ) )
return;
var e = (T)(object)scope.Element;
if ( e == null ) return;
value?.Invoke( e );
}
/// <summary>
/// Finish working on this element
/// </summary>
public override void CloseElement()
{
FlushContent();
var panel = CurrentScope.Element;
//Log.Info( $"CloseElement {CurrentScope.Sequence}" );
PopScope();
}
/// <summary>
/// Handles text content within an element
/// </summary>
public override void AddContent<T>( int sequence, T content )
{
if ( content is RenderFragment frag )
{
frag( this );
return;
}
var scope = CurrentScope;
scope.Sequence = sequence;
scope.Loop = CurrentBlock.Increment( sequence );
scope.Hash = HashCode.Combine( scope.Loop, scope.Sequence );
contentHash = scope.Hash;
contentBuilder.Append( content );
//Log.Info( $" AddContentT {scope.Sequence} [{content}]" );
}
/// <summary>
/// Delete all of the elements created by this render tree
/// </summary>
internal void Clear()
{
RootBlock?.Destroy();
RootBlock = new Block();
RootBlock.ElementPanel = Parent;
RootBlock.IsRootElement = true;
// if we applied any stylesheets to the root, we should remove them now
Parent.StyleSheet.Remove( "*__Razor*" );
}
/// <summary>
/// Implements @ref
/// </summary>
public override void AddReferenceCapture<T>( int sequence, T current, Action<T> value )
{
var block = CurrentBlock;
// we should have a parent block for this to be happening!
Assert.NotNull( block );
if ( block.ReferenceClearer != null )
return;
if ( block.ElementPanel is T t && t != null )
{
value?.Invoke( t );
block.ReferenceClearer = () => value( default );
}
}
public override void SetRenderFragment<T>( Action<T, RenderFragment> setter, RenderFragment builder )
{
var block = CurrentBlock;
if ( block.ElementPanel is not T t ) return;
setter( t, builder );
if ( t is Panel p )
{
p.OnRenderFragmentChanged( Parent );
}
}
public override void SetRenderFragmentWithContext<T, U>( Func<T, RenderFragment<U>> getter, Action<T, RenderFragment<U>> setter, RenderFragment<U> builder )
{
var block = CurrentBlock;
if ( block.ElementPanel is not T t ) return;
setter( t, builder );
if ( t is Panel p )
{
p.OnRenderFragmentChanged( Parent );
}
}
}