using Microsoft.AspNetCore.Components; using System.Text; namespace Sandbox.UI; /// /// 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. /// 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; } /// /// Called once before building the tree /// internal virtual void Start() { stack.Clear(); CurrentScope = default; RootBlock.Reset(); RootBlock.WasSeen = true; } /// /// Called once after building the tree /// 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; /// /// Add the current source location. Used to record in which file the element was created. /// 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 ); /// /// Start working on this element /// 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 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; } } /// /// Handles "style" and "class" attributes.. /// 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}" ); } /// /// Handles "style" and "class" attributes.. /// 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 ); } /// /// Styles from a style block /// 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 ); } /// /// ]]> /// public override void AddAttribute( int sequence, Action 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 ); } /// /// Finish working on this element /// public override void CloseElement() { FlushContent(); var panel = CurrentScope.Element; //Log.Info( $"CloseElement {CurrentScope.Sequence}" ); PopScope(); } /// /// Handles text content within an element /// public override void AddContent( 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}]" ); } /// /// Delete all of the elements created by this render tree /// 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*" ); } /// /// Implements @ref /// public override void AddReferenceCapture( int sequence, T current, Action 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( Action 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( Func> getter, Action> 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 ); } } }