🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (184 total matches found)
sklmr.razordesigner / Editor/Document/Payloads/CheckboxPayload.cs
Editor library
using System.Text.Json.Serialization;

namespace Grains.RazorDesigner.Document;

public sealed record CheckboxPayload : Payload
{
    [JsonIgnore]
    public override ControlType Kind => ControlType.Checkbox;

    // Checkbox label text. Overrides Payload.Content (neutral default "").
    public override string Content { get; init; } = "";

    public override Length CheckboxSize { get; init; } = Length.Px( 16 );
}
sklmr.razordesigner / Editor/Inspector/EdgesControlWidget.cs
Editor library
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;

namespace Grains.RazorDesigner.Inspector;

[CustomEditor( typeof( Edges ) )]
public sealed class EdgesControlWidget : ControlWidget
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	public override bool SupportsMultiEdit => true;

	private readonly EdgesProxy _proxy;
	private readonly SerializedObject _proxySerialized;
	// Synchronous change events on both sides; without this guard SetValue would loop.
	private bool _syncing;

	private sealed class EdgesProxy
	{
		public Length Top    { get; set; } = Length.Px( 0 );
		public Length Right  { get; set; } = Length.Px( 0 );
		public Length Bottom { get; set; } = Length.Px( 0 );
		public Length Left   { get; set; } = Length.Px( 0 );
	}

	public EdgesControlWidget( SerializedProperty property ) : base( property )
	{
		Log.Info( $"{LogPrefix} EdgesControlWidget ctor for {property.Name}" );

		Layout = Layout.Column();
		Layout.Spacing = 2;

		_proxy = new EdgesProxy();
		_proxySerialized = EditorTypeLibrary.GetSerializedObject( _proxy );

		var topRow = Layout.Add( Layout.Row() );
		topRow.Spacing = 2;
		AddSide( topRow, nameof( EdgesProxy.Top    ), "border_top"    );
		AddSide( topRow, nameof( EdgesProxy.Right  ), "border_right"  );

		var bottomRow = Layout.Add( Layout.Row() );
		bottomRow.Spacing = 2;
		AddSide( bottomRow, nameof( EdgesProxy.Bottom ), "border_bottom" );
		AddSide( bottomRow, nameof( EdgesProxy.Left   ), "border_left"   );

		_proxySerialized.OnPropertyChanged += OnProxyChanged;

		SyncFromProperty();
	}

	private void AddSide( Layout row, string propName, string icon )
	{
		var prop = _proxySerialized.GetProperty( propName );
		var lengthWidget = new LengthControlWidget( prop, icon );
		row.Add( lengthWidget, 1 );
	}

	protected override void PaintControl()
	{
		// nothing
	}

	private void SyncFromProperty()
	{
		if ( _syncing ) return;
		_syncing = true;

		try
		{
			var e = SerializedProperty.GetValue<Edges>( Edges.Zero );

			_proxySerialized.GetProperty( nameof( EdgesProxy.Top    ) ).SetValue( e.Top );
			_proxySerialized.GetProperty( nameof( EdgesProxy.Right  ) ).SetValue( e.Right );
			_proxySerialized.GetProperty( nameof( EdgesProxy.Bottom ) ).SetValue( e.Bottom );
			_proxySerialized.GetProperty( nameof( EdgesProxy.Left   ) ).SetValue( e.Left );
		}
		finally
		{
			_syncing = false;
		}
	}

	private void OnProxyChanged( SerializedProperty property )
	{
		if ( _syncing ) return;
		if ( ReadOnly || !SerializedProperty.IsEditable )
			return;

		_syncing = true;
		try
		{
			var newValue = new Edges( _proxy.Top, _proxy.Right, _proxy.Bottom, _proxy.Left );

			Log.Info( $"{LogPrefix} EdgesControlWidget OnProxyChanged {newValue}" );

			PropertyStartEdit();
			SerializedProperty.SetValue( newValue );
			SignalValuesChanged();
			PropertyFinishEdit();
		}
		finally
		{
			_syncing = false;
		}
	}

	protected override void OnValueChanged()
	{
		base.OnValueChanged();
		SyncFromProperty();
	}
}
sklmr.razordesigner / Editor/Palette/PalettePanel.cs
Editor library
using System;
using System.Collections.Generic;
using Editor;
using Grains.RazorDesigner.Common;
using Grains.RazorDesigner.Contracts;
using Grains.RazorDesigner.Document;
using Grains.RazorDesigner.Templates;
using Sandbox;

namespace Grains.RazorDesigner.Palette;

public class PalettePanel : Widget
{
	private const string LogPrefix = "[Grains.RazorDesigner]";
	private const string CookiePrefix = "razordesigner.palette.";

	// Click-to-add target. Window decides where the new record goes (typically active selection or root).
	public event Action<ControlType> TypeAddRequested;

	// Click-to-add a saved template. Window decides where to insert.
	public event Action<PaletteTemplate> TemplateAddRequested;

	private readonly PaletteTemplateStore _templateStore = new();
	private CollapsibleSection _templatesSection;
	private WrapPanel _templatesWrap;
	public PaletteTemplateStore TemplateStore => _templateStore;

	public PalettePanel( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 0;
		Layout.Spacing = 0;
		MinimumWidth = 180;
		VerticalSizeMode = SizeMode.CanGrow;

		var byCategory = new Dictionary<ControlCategory, List<ControlType>>();
		foreach ( ControlType type in Enum.GetValues( typeof( ControlType ) ) )
		{
			var cat = ControlDefaults.For( type ).Category;
			if ( !byCategory.TryGetValue( cat, out var list ) )
			{
				list = new List<ControlType>();
				byCategory[cat] = list;
			}
			list.Add( type );
		}

		// Templates section (top of palette). Hidden when store is empty; rebuilt on Changed.
		_templatesSection = new CollapsibleSection( this, "Templates", "bookmark" );
		_templatesWrap = new WrapPanel( null )
		{
			MinItemWidth = 92,
			ItemHeight = (int)( Theme.RowHeight + 4 ),
			HSpacing = 4,
			VSpacing = 4,
			PaddingLeft = 4,
			PaddingTop = 4,
			PaddingRight = 14,
			PaddingBottom = 4,
		};
		_templatesSection.BodyLayout.Add( _templatesWrap );

		var templatesCookie = $"{CookiePrefix}templates.expanded";
		_templatesSection.Expanded = EditorCookie.Get<bool>( templatesCookie, true );
		_templatesSection.ExpandedChanged += expanded =>
		{
			EditorCookie.Set( templatesCookie, expanded );
			Log.Info( $"{LogPrefix} Palette Templates {(expanded ? "expanded" : "collapsed")}" );
		};

		Layout.Add( _templatesSection );

		_templateStore.Changed += RebuildTemplatesSection;
		_templateStore.Scan(); // initial fill (also fires Changed and rebuilds the section)

		foreach ( ControlCategory cat in Enum.GetValues( typeof( ControlCategory ) ) )
		{
			if ( !byCategory.TryGetValue( cat, out var list ) ) continue;

			var section = new CollapsibleSection(
				this,
				ControlDefaults.CategoryDisplayName( cat ),
				CategoryIcon( cat ) );

			var wrap = new WrapPanel( null )
			{
				MinItemWidth = 92,
				ItemHeight = (int)( Theme.RowHeight + 4 ),
				HSpacing = 4,
				VSpacing = 4,
				PaddingLeft = 4,
				PaddingTop = 4,
				PaddingRight = 14,    // clear the ScrollArea's vertical scrollbar
				PaddingBottom = 4,
			};
			section.BodyLayout.Add( wrap );

			foreach ( var t in list )
				new PaletteTypeButton( wrap, this, t );

			var cookieKey = $"{CookiePrefix}{cat}.expanded";
			section.Expanded = EditorCookie.Get<bool>( cookieKey, DefaultExpanded( cat ) );
			section.ExpandedChanged += expanded =>
			{
				EditorCookie.Set( cookieKey, expanded );
				Log.Info( $"{LogPrefix} Palette category {cat} {(expanded ? "expanded" : "collapsed")}" );
			};

			Layout.Add( section );
		}

		Layout.AddStretchCell();

		Log.Info( $"{LogPrefix} PalettePanel ctor (icon grid, {byCategory.Count} categories)" );
	}

	internal void NotifyTypeClicked( ControlType type )
	{
		Log.Info( $"{LogPrefix} PalettePanel.NotifyTypeClicked: {type}" );
		TypeAddRequested?.Invoke( type );
	}

	internal void NotifyTemplateClicked( PaletteTemplate template )
	{
		Log.Info( $"{LogPrefix} PalettePanel.NotifyTemplateClicked: \"{template.Name}\"" );
		TemplateAddRequested?.Invoke( template );
	}

	internal void RequestTemplateDelete( PaletteTemplate template )
	{
		var dialog = new Editor.Dialog( this );
		dialog.Window.WindowTitle = "Delete template";
		dialog.Window.SetWindowIcon( "delete" );
		dialog.Window.SetModal( true, true );
		dialog.Window.MinimumWidth = 320;

		dialog.Layout = Layout.Column();
		dialog.Layout.Margin = 16;
		dialog.Layout.Spacing = 10;

		dialog.Layout.Add( new Editor.Label( dialog )
		{
			Text = $"Delete template \"{template.Name}\"?",
		} );

		var hint = new Editor.Label( dialog )
		{
			Text = "Already-instantiated copies in open documents are unaffected.",
		};
		hint.SetStyles( "color: #888; font-size: 11px;" );
		dialog.Layout.Add( hint );

		var buttonRow = dialog.Layout.Add( Layout.Row() );
		buttonRow.Spacing = 6;
		buttonRow.AddStretchCell();

		var cancel = new Editor.Button( dialog ) { Text = "Cancel", MinimumWidth = 72 };
		cancel.MouseLeftPress += () => dialog.Close();
		buttonRow.Add( cancel );

		var del = new Editor.Button( dialog ) { Text = "Delete", MinimumWidth = 72 };
		del.SetStyles( "color: #e07070;" );
		del.MouseLeftPress += () =>
		{
			Log.Info( $"{LogPrefix} Palette delete confirmed: \"{template.Name}\"" );
			_templateStore.Delete( template );
			dialog.Close();
		};
		buttonRow.Add( del );

		dialog.Window.AdjustSize();
		dialog.Show();
	}

	private void RebuildTemplatesSection()
	{
		var templates = _templateStore.All;

		// Hide the entire section (header + body) when there are no templates.
		_templatesSection.Visible = templates.Count > 0;

		using ( Editor.SuspendUpdates.For( _templatesWrap ) )
		{
			_templatesWrap.DestroyChildren();
			foreach ( var t in templates )
				new PaletteTemplateButton( _templatesWrap, this, t );
		}

		_templatesWrap.Relayout();
		_templatesWrap.UpdateGeometry();
		_templatesSection.UpdateGeometry();
		UpdateGeometry();

		Log.Info( $"{LogPrefix} PalettePanel.RebuildTemplatesSection: {templates.Count} tile(s), section.Visible={_templatesSection.Visible}" );
	}

	private static bool DefaultExpanded( ControlCategory cat ) =>
		cat is ControlCategory.Layout or ControlCategory.Display or ControlCategory.Input;

	private static string CategoryIcon( ControlCategory cat ) => cat switch
	{
		ControlCategory.Layout   => "view_quilt",
		ControlCategory.Display  => "visibility",
		ControlCategory.Input    => "edit",
		ControlCategory.Form     => "list_alt",
		_ => "category",
	};

	private sealed class PaletteTypeButton : Widget
	{
		private readonly PalettePanel _owner;
		private readonly ControlType _type;
		// InspectorIcon comes from the contract (engine-fidelity); drag defaults from ControlDefaults.
		private readonly string _icon;

		public PaletteTypeButton( Widget parent, PalettePanel owner, ControlType type ) : base( parent )
		{
			_owner = owner;
			_type = type;
			_icon = ContractScanner.Table.Get( type ).InspectorIcon;

			ToolTip = type.ToString();
			Cursor = CursorShape.Finger;
			MouseTracking = true;
			IsDraggable = true;
		}

		protected override void OnPaint()
		{
			var rect = LocalRect.Shrink( 1 );
			Paint.Antialiasing = true;
			Paint.TextAntialiasing = true;

			var tint = ControlPresentation.IconTint( _type );
			var fillAlpha = Paint.HasMouseOver ? 0.35f : 0.15f;
			var borderAlpha = Paint.HasMouseOver ? 0.55f : 0.25f;
			Paint.SetBrush( tint.WithAlpha( fillAlpha ) );
			Paint.SetPen( tint.WithAlpha( borderAlpha ) );
			Paint.DrawRect( rect, 3 );

			var hoverOpacity = Paint.HasMouseOver ? 1f : 0.85f;
			var iconRect = new Rect( rect.Left + 4, rect.Top, 20, rect.Height );
			Paint.SetPen( tint.WithAlphaMultiplied( hoverOpacity ) );
			Paint.DrawIcon( iconRect, _icon, 16, TextFlag.Center );

			var textRect = rect;
			textRect.Left = iconRect.Right + 2;
			textRect.Right -= 4;
			Paint.SetPen( Theme.Text.WithAlphaMultiplied( hoverOpacity ) );
			Paint.SetDefaultFont();
			Paint.DrawText( textRect, _type.ToString(), TextFlag.LeftCenter );
		}

		protected override void OnMouseClick( MouseEvent e )
		{
			base.OnMouseClick( e );
			if ( e.LeftMouseButton )
				_owner.NotifyTypeClicked( _type );
		}

		protected override void OnDragStart()
		{
			base.OnDragStart();

			var drag = new Drag( this );
			drag.Data.Object = _type;
			drag.Data.Text = $"palette:{_type}";
			drag.Execute();

			Log.Info( $"{LogPrefix} PaletteTypeButton.OnDragStart: {_type}" );
		}
	}

	private sealed class PaletteTemplateButton : Widget
	{
		private readonly PalettePanel _owner;
		private readonly PaletteTemplate _template;

		public PaletteTemplateButton( Widget parent, PalettePanel owner, PaletteTemplate template ) : base( parent )
		{
			_owner = owner;
			_template = template;

			ToolTip = template.Name;
			Cursor = CursorShape.Finger;
			MouseTracking = true;
			IsDraggable = true;
		}

		protected override void OnPaint()
		{
			var rect = LocalRect.Shrink( 1 );
			Paint.Antialiasing = true;
			Paint.TextAntialiasing = true;

			var tint = ControlPresentation.TemplateTint;
			var fillAlpha = Paint.HasMouseOver ? 0.18f : 0.08f;
			var borderAlpha = Paint.HasMouseOver ? 0.55f : 0.25f;
			Paint.SetBrush( tint.WithAlpha( fillAlpha ) );
			Paint.SetPen( tint.WithAlpha( borderAlpha ) );
			Paint.DrawRect( rect, 3 );

			var hoverOpacity = Paint.HasMouseOver ? 1f : 0.85f;
			var iconRect = new Rect( rect.Left + 4, rect.Top, 20, rect.Height );
			var icon = string.IsNullOrEmpty( _template.IconName ) ? "bookmark" : _template.IconName;
			Paint.SetPen( tint.WithAlphaMultiplied( hoverOpacity ) );
			Paint.DrawIcon( iconRect, icon, 16, TextFlag.Center );

			var textRect = rect;
			textRect.Left = iconRect.Right + 2;
			textRect.Right -= 4;
			Paint.SetPen( Theme.Text.WithAlphaMultiplied( hoverOpacity ) );
			Paint.SetDefaultFont();
			Paint.DrawText( textRect, _template.Name, TextFlag.LeftCenter );
		}

		protected override void OnMouseClick( MouseEvent e )
		{
			base.OnMouseClick( e );
			if ( e.LeftMouseButton )
				_owner.NotifyTemplateClicked( _template );
		}

		protected override void OnContextMenu( ContextMenuEvent e )
		{
			base.OnContextMenu( e );
			var menu = new Menu( this );
			menu.AddOption( "Delete…", "delete", () => _owner.RequestTemplateDelete( _template ) );
			menu.OpenAtCursor();
			e.Accepted = true;
		}

		protected override void OnDragStart()
		{
			base.OnDragStart();
			var drag = new Drag( this );
			drag.Data.Object = _template;
			drag.Data.Text = $"template:{_template.Name}";
			drag.Execute();
			Log.Info( $"{LogPrefix} PaletteTemplateButton.OnDragStart: \"{_template.Name}\"" );
		}
	}
}
sklmr.razordesigner / Editor/Projection/CSharp/CSharpOp.cs
Editor library
namespace Grains.RazorDesigner.Projection.CSharp;

public abstract record CSharpOp;

// File-level scaffold
public sealed record HeaderBanner( string ClassName, string Namespace ) : CSharpOp;
public sealed record UsingDirective( string Namespace ) : CSharpOp;
public sealed record NamespaceOpen( string Namespace ) : CSharpOp;
public sealed record ClassOpen( string ClassName, string BaseClass ) : CSharpOp;
public sealed record ClassClose() : CSharpOp;

public sealed record FieldDecl(
    string Visibility, string Type, string Name, string InitialExpr,
    bool IsParameter, bool IsProperty = false ) : CSharpOp;

public sealed record MethodOpen(
    string Visibility, bool IsOverride, bool IsAsync,
    string ReturnType, string Name, string ParameterList ) : CSharpOp;

public sealed record MethodClose() : CSharpOp;

// Body-level
public sealed record Statement( string Code ) : CSharpOp;          // single `;`-terminated line
public sealed record BlockOpen( string Header ) : CSharpOp;        // e.g. `if ( <cond> )` — applier writes "<header> {\n" and indents
public sealed record BlockClose() : CSharpOp;
public sealed record BlankLine() : CSharpOp;
public sealed record Comment( string Text ) : CSharpOp;            // `// <text>`
sklmr.razordesigner / Editor/Projection/CSharp/CSharpProjector.cs
Editor library
using System.Collections.Generic;
using Grains.RazorDesigner.Projection.CSharp.Projectors;

namespace Grains.RazorDesigner.Projection.CSharp;

public static class CSharpProjector
{
    public static CSharpResult Project(
        IReadOnlyWiring wiring,
        bool documentHasAnyBindings )
    {
        if ( wiring.Symbols.Count == 0 && !documentHasAnyBindings )
            return new CSharpResult( System.Array.Empty<CSharpOp>(), null );

        var ctx = new CSharpProjectorContext( wiring );
        var ops = new List<CSharpOp>( 64 );

        ops.Add( new HeaderBanner( wiring.ClassName, wiring.Namespace ) );
        ops.Add( new UsingDirective( "Sandbox" ) );
        ops.Add( new UsingDirective( "Sandbox.UI" ) );
        foreach ( var u in wiring.Usings )
            ops.Add( new UsingDirective( u ) );
        ops.Add( new NamespaceOpen( wiring.Namespace ) );
        ops.Add( new ClassOpen( wiring.ClassName, wiring.BaseClass ) );

        // Step 3: body. SymbolProjector handles grouping + sorting + per-kind dispatch.
        SymbolProjector.EmitAll( wiring, ops, ctx );

        ops.Add( new ClassClose() );

        var source = CSharpApplier.Apply( ops ).Replace( "\r\n", "\n" );

        return new CSharpResult( ops, source );
    }
}
sklmr.razordesigner / Editor/Projection/CSharp/Projectors/ParameterSymbolProjector.cs
Editor library
using System.Collections.Generic;
using Grains.RazorDesigner.Wiring;

namespace Grains.RazorDesigner.Projection.CSharp.Projectors;

public static class ParameterSymbolProjector
{
    public static void Emit( ParameterSymbol s, List<CSharpOp> ops, CSharpProjectorContext ctx )
    {
        var initial = s.Initial is null ? "default" : ExpressionEmitter.Emit( s.Initial, ctx );
        ops.Add( new FieldDecl(
            Visibility: "public", Type: s.Type, Name: s.Name,
            InitialExpr: initial, IsParameter: true ) );
    }
}
sklmr.razordesigner / Editor/Projection/Escape.cs
Editor library
namespace Grains.RazorDesigner.Projection;

public static class Escape
{
    public static string Html( string s )
    {
        if ( string.IsNullOrEmpty( s ) ) return "";
        return s
            .Replace( "&", "&amp;" )
            .Replace( "<", "&lt;" )
            .Replace( ">", "&gt;" )
            .Replace( "\"", "&quot;" );
    }
}
sklmr.razordesigner / Editor/Projection/IReadOnlyNode.cs
Editor library
using System;
using System.Collections.Generic;
using Sandbox; // Color

namespace Grains.RazorDesigner.Projection;

public interface IReadOnlyStateRule
{
    Document.PseudoKind State { get; }
    Document.NthChildMode NthChildMode { get; }
    int NthChildArg { get; }
    IAppearance Delta { get; }

    public static int CompareCanonical( IReadOnlyStateRule a, IReadOnlyStateRule b )
    {
        int c = ((int)a.State).CompareTo( (int)b.State );
        if ( c != 0 ) return c;
        c = ((int)a.NthChildMode).CompareTo( (int)b.NthChildMode );
        if ( c != 0 ) return c;
        return a.NthChildArg.CompareTo( b.NthChildArg );
    }
}

public interface IReadOnlyNode
{
    Guid Id { get; }
    string Kind { get; }         // == ControlType.ToString()
    string ClassName { get; }
    IAppearance Appearance { get; }
    IPayload Payload { get; }
    IReadOnlyList<IReadOnlyNode> Children { get; }                              // non-slot children
    IReadOnlyDictionary<string, IReadOnlyList<IReadOnlyNode>> Slots { get; }    // slot-name -> slot children (only SplitContainer populates)
    IReadOnlyList<IReadOnlyStateRule> StateRules { get; }                       // per-state style deltas; canonical order not guaranteed here (the Applier sorts)
}

public interface IAppearance
{
    // Layout
    Document.Length Width { get; }
    Document.Length Height { get; }

    // Flex container
    Document.FlexDirection Direction { get; }
    Document.JustifyContent Justify { get; }
    Document.AlignItems Align { get; }
    float Gap { get; }
    Document.Edges Padding { get; }
    Document.FlexWrap Wrap { get; }

    // Positioning (grd-7t2z)
    Document.PositionKind Position { get; }
    Document.Length Top { get; }
    Document.Length Left { get; }
    Document.Length Right { get; }
    Document.Length Bottom { get; }

    // Flex self
    float FlexGrow { get; }
    float FlexShrink { get; }
    Document.Length FlexBasis { get; }
    Document.AlignSelfKind AlignSelf { get; }

    // Typography + OverrideTypography
    bool OverrideTypography { get; }
    string FontFamily { get; }
    Document.Length FontSize { get; }
    int FontWeight { get; }
    Color Color { get; }
    Document.TextAlignment TextAlign { get; }
    bool FontStyleItalic { get; }
    Document.TextTransformKind TextTransform { get; }
    Document.Length LetterSpacing { get; }
    Document.Length LineHeight { get; }

    // Background + OverrideBackground
    bool OverrideBackground { get; }
    Color BackgroundColor { get; }
    string BackgroundImage { get; }
    string BackgroundSize { get; }
    string BackgroundPosition { get; }
    string BackgroundRepeat { get; }

    // Border + OverrideBorder
    bool OverrideBorder { get; }
    Document.Length BorderRadius { get; }
    Color BorderColor { get; }
    Document.Length BorderWidth { get; }

    // Effects + OverrideEffects
    bool OverrideEffects { get; }
    Document.Length BoxShadowX { get; }
    Document.Length BoxShadowY { get; }
    Document.Length BoxShadowBlur { get; }
    Color BoxShadowColor { get; }
    bool BoxShadowInset { get; }
    float Opacity { get; }

    // Constraints + OverrideConstraints
    bool OverrideConstraints { get; }
    Document.Edges Margin { get; }
    Document.Length MinWidth { get; }
    Document.Length MaxWidth { get; }
    Document.Length MinHeight { get; }
    Document.Length MaxHeight { get; }

    // Interaction + OverrideInteraction
    bool OverrideInteraction { get; }
    Document.CursorKind Cursor { get; }
    Document.OverflowKind Overflow { get; }
    int ZIndex { get; }
    bool PointerEvents { get; }
}

public interface IPayload
{
    string Content { get; }       // Label/Button text; Checkbox label
    string Placeholder { get; }   // TextEntry
    string Source { get; }        // Image src
    string IconName { get; }      // IconPanel glyph
    Document.Length CheckboxSize { get; }  // Checkbox box size
}
sklmr.razordesigner / Editor/Projection/Projectors/ButtonProjector.cs
Editor library
using System.Collections.Generic;
using Grains.RazorDesigner.Projection.Appearance;
using Grains.RazorDesigner.Projection.Razor;

namespace Grains.RazorDesigner.Projection.Projectors;

[Projector( "Button" )]
public sealed class ButtonProjector : IControlProjector
{
    public string Kind => "Button";

    public ProjectionResult Project( IReadOnlyNode node, IAppearance a, IPayload p, ProjectionContext ctx )
    {
        var scss = AppearanceScss.Emit(
            a,
            isRoot:       node.ClassName == Document.DesignerDocument.RootClassName,
            isContainer:  false,
            childCount:   0,
            isLabel:      false,
            isCheckbox:   false,
            checkboxSize: default );

        var nodeId = node.Id.ToString();
        var ops = new PanelOp[]
        {
            new SetAttribute( "data-grd-node-id", nodeId ),
            new SetInnerText( p.Content ?? "" ),
        };

        var razorAttrs = new[] { RazorEmit.Attr( "data-grd-node-id", nodeId ) };

        return new ProjectionResult(
            PanelOps:        ops,
            ScssLines:       scss,
            RazorAttributes: razorAttrs,
            RazorInnerText:  Escape.Html( p.Content ?? "" ) );
    }
}
sklmr.razordesigner / Editor/Projection/Projectors/FieldProjector.cs
Editor library
using System.Collections.Generic;
using Grains.RazorDesigner.Projection.Appearance;
using Grains.RazorDesigner.Projection.Razor;

namespace Grains.RazorDesigner.Projection.Projectors;

[Projector( "Field" )]
public sealed class FieldProjector : IControlProjector
{
    public string Kind => "Field";

    public ProjectionResult Project( IReadOnlyNode node, IAppearance a, IPayload p, ProjectionContext ctx )
    {
        var scss = AppearanceScss.Emit(
            a,
            isRoot:       node.ClassName == Document.DesignerDocument.RootClassName,
            isContainer:  true,
            childCount:   node.Children.Count,
            isLabel:      false,
            isCheckbox:   false,
            checkboxSize: default );

        var nodeId = node.Id.ToString();
        var ops = new PanelOp[]
        {
            new SetAttribute( "data-grd-node-id", nodeId ),
        };

        var razorAttrs = new[] { RazorEmit.Attr( "data-grd-node-id", nodeId ) };

        return new ProjectionResult(
            PanelOps:        ops,
            ScssLines:       scss,
            RazorAttributes: razorAttrs,
            RazorInnerText:  null );
    }
}
sklmr.razordesigner / Editor/Projection/Razor/RazorEmit.cs
Editor library
namespace Grains.RazorDesigner.Projection.Razor;

public static class RazorEmit
{
    public static string Attr( string name, string value ) => $"{name}=\"{Escape.Html( value )}\"";
}
sklmr.razordesigner / Editor/Projection/Tests/PanelOpExhaustivenessTest.cs
Editor library
using System;

namespace Grains.RazorDesigner.Projection.Tests;

public static class PanelOpExhaustivenessTest
{
    public static (bool pass, string message) Run()
    {
        var ops = new PanelOp[]
        {
            new SetClass( "" ),
            new SetStyle( "", "" ),
            new SetAttribute( "", "" ),
            new SetInnerText( "" ),
        };
        try
        {
            foreach ( var op in ops )
                Applier.ApplyOpToScratch( op );
            return (true, $"PanelOpExhaustivenessTest: {ops.Length} variants OK");
        }
        catch ( Exception e )
        {
            return (false, $"PanelOpExhaustivenessTest FAILED: {e.GetType().Name}: {e.Message}");
        }
    }
}
sklmr.razordesigner / Editor/Serialization/IR/IRWriter.cs
Editor library
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Grains.RazorDesigner.Document;

namespace Grains.RazorDesigner.Serialization.IR;

public static class IRWriter
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	// Empty collections reused for nodes that have no slots/children/metadata.
	private static readonly IReadOnlyDictionary<string, object> _emptyMetadata = new Dictionary<string, object>();
	private static readonly IReadOnlyList<IRNodeEnvelope>      _emptyChildren = System.Array.Empty<IRNodeEnvelope>();
	private static readonly IReadOnlyDictionary<string, IRNodeEnvelope> _emptySlots = new Dictionary<string, IRNodeEnvelope>();

	public static string WriteDocument( DesignerDocument doc )
	{
		if ( doc is null )
			throw new ArgumentNullException( nameof( doc ) );

		Log.Info( $"{LogPrefix} IRWriter.WriteDocument: serialising document (root children: {doc.RootRecord.Children.Count})" );

		var envelope = new IRDocumentEnvelope
		{
			Root   = ToNode( doc.RootRecord ),
			Wiring = doc.Wiring ?? Grains.RazorDesigner.Wiring.WiringEnvelope.Empty,
		};

		var json = JsonSerializer.Serialize( envelope, DesignerIRJson.Options );

		// Normalise CRLF → LF (canonical form; .gitattributes also pins LF as a backstop).
		if ( json.Contains( '\r' ) )
			json = json.Replace( "\r\n", "\n" ).Replace( "\r", "\n" );

		Log.Info( $"{LogPrefix} IRWriter.WriteDocument: OK ({json.Length} chars)" );
		return json;
	}

	public static string CanonicalHash( string json )
	{
		if ( json is null )
			throw new ArgumentNullException( nameof( json ) );

		var bytes = Encoding.UTF8.GetBytes( json );
		var hash  = SHA256.HashData( bytes );
		return Convert.ToHexString( hash ).ToLowerInvariant();
	}

	// Recursively converts a ControlRecord to its IRNodeEnvelope representation.
	private static IRNodeEnvelope ToNode( ControlRecord r )
	{
		Dictionary<string, IRNodeEnvelope> slotDict  = null;
		List<IRNodeEnvelope>               childList = null;

		foreach ( var child in r.Children )
		{
			if ( child.IsSlot )
			{
				slotDict ??= new Dictionary<string, IRNodeEnvelope>();
				slotDict[child.SlotName] = ToNode( child );
			}
			else
			{
				childList ??= new List<IRNodeEnvelope>();
				childList.Add( ToNode( child ) );
			}
		}

		return new IRNodeEnvelope
		{
			Id         = r.Id,
			Kind       = r.Type,
			ClassName  = r.ClassName,
			Appearance = r.Appearance,
			Payload    = r.Payload,
			Slots      = slotDict  is not null
				? (IReadOnlyDictionary<string, IRNodeEnvelope>)slotDict
				: _emptySlots,
			Children   = childList is not null
				? (IReadOnlyList<IRNodeEnvelope>)childList
				: _emptyChildren,
			States = r.StateRules.Count == 0
				? null
				: r.StateRules
					.OrderBy( rule => rule, Comparer<StateRule>.Create( StateRule.CompareCanonical ) )
					.Select( rule => new IRStateEnvelope
					{
						State       = rule.State,
						NthChildMode = rule.NthChildMode,
						NthChildArg  = rule.NthChildArg,
						Delta        = rule.Delta,
					} )
					.ToList(),
			Bindings   = r.Bindings.Count == 0
				? System.Array.Empty<Grains.RazorDesigner.Wiring.Binding>()
				: r.Bindings.ToArray(),

				CustomStyles = r.CustomStyles.Count == 0 
				? null 
				: new Dictionary<string, string>( r.CustomStyles ),
		};
	}
}
sklmr.razordesigner / Editor/Wiring/Actions/Action.cs
Editor library
using System;
using System.Text.Json.Serialization;

namespace Grains.RazorDesigner.Wiring;

[JsonPolymorphic( TypeDiscriminatorPropertyName = "$type" )]
[JsonDerivedType( typeof( SetAction ),              "Set" )]
[JsonDerivedType( typeof( CallAction ),             "Call" )]
[JsonDerivedType( typeof( IfAction ),               "If" )]
[JsonDerivedType( typeof( StateHasChangedAction ),  "StateHasChanged" )]
[JsonDerivedType( typeof( LogAction ),              "Log" )]
[JsonDerivedType( typeof( ReturnAction ),           "Return" )]
[JsonDerivedType( typeof( InlineAction ),           "Inline" )]
public abstract record Action
{
    public Guid Id { get; init; } = Guid.NewGuid();
}
sklmr.razordesigner / Editor/Wiring/Actions/InlineAction.cs
Editor library
namespace Grains.RazorDesigner.Wiring;

public sealed record InlineAction : Action
{
    public string Code { get; init; } = "";
}
sklmr.razordesigner / Editor/Wiring/Actions/SetAction.cs
Editor library
namespace Grains.RazorDesigner.Wiring;

// `Target = Value;` — assignment to a Symbol field.
public sealed record SetAction : Action
{
    public TargetRef Target { get; init; }
    public Expression Value { get; init; }
}
sklmr.razordesigner / Editor/Wiring/Bindings/EventBinding.cs
Editor library
using System.Collections.Generic;

namespace Grains.RazorDesigner.Wiring;

public sealed record EventBinding : Binding
{
    public string Event { get; init; } = "";
    public IReadOnlyList<Action> Body { get; init; } = System.Array.Empty<Action>();
}
sklmr.razordesigner / Editor/Wiring/Bindings/VisibleBinding.cs
Editor library
namespace Grains.RazorDesigner.Wiring;

public sealed record VisibleBinding : Binding
{
    public Expression Condition { get; init; }
}
sklmr.razordesigner / Editor/Wiring/Symbols/SymbolVisibility.cs
Editor library
namespace Grains.RazorDesigner.Wiring;

public enum SymbolVisibility
{
    Private,
    Public,
    Internal,
    Protected,
}
sklmr.razordesigner / Editor/Canvas/DesignerCanvas.cs
Editor library
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;

namespace Grains.RazorDesigner.Canvas;

// Same shape as ShaderGraph PreviewPanel: override PreFrame to advance the scene.
public class DesignerCanvas : SceneRenderingWidget
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	private readonly DesignerScene _designerScene;

	public DesignerCanvas( Widget parent ) : base( parent )
	{
		Log.Info( $"{LogPrefix} DesignerCanvas ctor" );

		_designerScene = new DesignerScene();

		Scene = _designerScene.Scene;
		Camera = _designerScene.Camera;

		HorizontalSizeMode = SizeMode.Default | SizeMode.Expand;
		VerticalSizeMode = SizeMode.Default | SizeMode.Expand;

		// Without AcceptDrops, OnDragHover/OnDragDrop are never invoked.
		AcceptDrops = true;

		MouseTracking = true;
	}

	public DesignerScene DesignerScene => _designerScene;

	// Assigned by DesignerWindow after construction (mirrors how _viewportFrame.Canvas is wired).
	public OverlayController Overlay { get; set; }

	public CanvasViewportFrame ViewportFrame { get; set; }

	public event System.Action<Vector2, bool, Sandbox.KeyboardModifiers> CanvasClicked;
	public event System.Action<Vector2, Sandbox.KeyboardModifiers> CanvasMoved;
	public event System.Action<Vector2> CanvasReleased;

	public event System.Action<Vector2> CanvasPanDragged; // delta, screen px

	// Fires when the cursor leaves the canvas widget. Used to clear hover-pick state.
	public event System.Action CanvasHoverEnded;

	public event System.Action<ControlType, Vector2> RecordDropped;
	// Mirrors RecordDropped but for saved palette templates.
	public event System.Action<Grains.RazorDesigner.Templates.PaletteTemplate, Vector2> TemplateDropped;

	private bool _middlePanning;
	private Vector2 _lastPanScreen;

	private const bool ProbeFrameCost = false;
	private const int ProbeWindow = 120;
	private readonly System.Diagnostics.Stopwatch _probeSw = new();
	private double _probeTickMs;
	private double _probeUpdateMs;
	private int _probeFrames;

	public override void OnDragHover( DragEvent ev )
	{
		base.OnDragHover( ev );
		if ( ev.Data.Object is ControlType || ev.Data.Object is Grains.RazorDesigner.Templates.PaletteTemplate )
		{
			ev.Action = DropAction.Copy;
		}
	}

	public override void OnDragDrop( DragEvent ev )
	{
		base.OnDragDrop( ev );
		if ( ev.Data.Object is ControlType type )
		{
			Log.Info( $"{LogPrefix} DesignerCanvas drop: {type} at widget ({ev.LocalPosition.x:F0}, {ev.LocalPosition.y:F0})" );
			RecordDropped?.Invoke( type, ev.LocalPosition );
		}
		else if ( ev.Data.Object is Grains.RazorDesigner.Templates.PaletteTemplate template )
		{
			Log.Info( $"{LogPrefix} DesignerCanvas drop: template \"{template.Name}\" at widget ({ev.LocalPosition.x:F0}, {ev.LocalPosition.y:F0})" );
			TemplateDropped?.Invoke( template, ev.LocalPosition );
		}
	}

	protected override void OnMousePress( MouseEvent e )
	{
		if ( e.MiddleMouseButton )
		{
			// Swallow entirely — don't let base (SceneRenderingWidget) see the middle drag.
			_middlePanning = true;
			_lastPanScreen = e.ScreenPosition;
			e.Accepted = true;
			return;
		}
		base.OnMousePress( e );
		if ( e.LeftMouseButton || e.RightMouseButton )
		{
			var pos = e.LocalPosition;
			Log.Info( $"{LogPrefix} DesignerCanvas click at widget ({pos.x:F0}, {pos.y:F0}) right={e.RightMouseButton}" );
			CanvasClicked?.Invoke( pos, e.RightMouseButton, e.KeyboardModifiers );
		}
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		if ( _middlePanning )
		{
			var s = e.ScreenPosition;
			CanvasPanDragged?.Invoke( s - _lastPanScreen );
			_lastPanScreen = s;
			e.Accepted = true;
			return;
		}
		base.OnMouseMove( e );
		CanvasMoved?.Invoke( e.LocalPosition, e.KeyboardModifiers );
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		if ( _middlePanning )
		{
			_middlePanning = false;
			e.Accepted = true;
			return;
		}
		base.OnMouseReleased( e );
		CanvasReleased?.Invoke( e.LocalPosition );
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();
		CanvasHoverEnded?.Invoke();
	}

	protected override void PreFrame()
	{
		base.PreFrame();

		if ( !_designerScene.Scene.IsValid() )
			return;

		if ( ProbeFrameCost ) _probeSw.Restart();

		using ( _designerScene.Scene.Push() )
		{
			// EditorTick (not GameTick): preview scene, no SceneNetworkUpdate / fixed physics.
			_designerScene.Scene.EditorTick( RealTime.Now, RealTime.Delta );
		}

		if ( ProbeFrameCost ) { _probeTickMs += _probeSw.Elapsed.TotalMilliseconds; _probeSw.Restart(); }

		// Layout in framebuffer space (Size * DpiScale); DesignerScene sets Scale = DpiScale so authoring stays in logical px.
		_designerScene.Update( Size.x, Size.y, DpiScale );

		Overlay?.Tick( _designerScene, DpiScale );

		if ( ProbeFrameCost )
		{
			_probeUpdateMs += _probeSw.Elapsed.TotalMilliseconds;
			if ( ++_probeFrames >= ProbeWindow )
			{
				Log.Info( $"{LogPrefix} probe: EditorTick avg {_probeTickMs / _probeFrames:F3}ms | Update avg {_probeUpdateMs / _probeFrames:F3}ms (over {_probeFrames} frames, canvas {Size.x:F0}x{Size.y:F0})" );
				_probeTickMs = 0;
				_probeUpdateMs = 0;
				_probeFrames = 0;
			}
		}
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		base.OnKeyPress( e );

		if ( ViewportFrame is not { CanPanZoom: true } frame ) return;

		var mods  = e.KeyboardModifiers;
		var ctrl  = (mods & Sandbox.KeyboardModifiers.Ctrl)  != 0;
		var shift = (mods & Sandbox.KeyboardModifiers.Shift) != 0;
		var alt   = (mods & Sandbox.KeyboardModifiers.Alt)   != 0;

		if ( !ctrl || alt ) return; // every shortcut is Ctrl-based; Alt clears it.

		switch ( e.Key )
		{
			case KeyCode.Num0:
				if ( shift )
				{
					Log.Info( $"{LogPrefix} OnKeyPress Ctrl+Shift+0 → ZoomToSelection" );
					ZoomToSelectionViaFrame( frame );
				}
				else
				{
					Log.Info( $"{LogPrefix} OnKeyPress Ctrl+0 → ApplyFit" );
					frame.ApplyFit();
				}
				e.Accepted = true;
				break;

			case KeyCode.Num1:
				if ( shift ) return;
				Log.Info( $"{LogPrefix} OnKeyPress Ctrl+1 → ResetZoomOnly" );
				frame.ResetZoomOnly();
				e.Accepted = true;
				break;

			case KeyCode.Equal:
				if ( shift ) return;
				frame.ZoomBy( 1.25f );
				e.Accepted = true;
				break;

			case KeyCode.Minus:
				if ( shift ) return;
				frame.ZoomBy( 1f / 1.25f );
				e.Accepted = true;
				break;
		}
	}

	private void ZoomToSelectionViaFrame( CanvasViewportFrame frame )
	{
		var record = frame.Selection?.Selected;
		var live = record?.LivePanel;
		if ( live is null || !live.IsValid )
		{
			Log.Info( $"{LogPrefix} Ctrl+Shift+0: no valid selection" );
			return;
		}
		frame.ApplyZoomToRect( live.Box.Rect, /* maxZoom */ 4f, /* padding */ 0.10f );
	}

	public override void OnDestroyed()
	{
		Log.Info( $"{LogPrefix} DesignerCanvas.OnDestroyed" );
		base.OnDestroyed();
		_designerScene.Dispose();
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 184,
    "Files": [
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Document/Payloads/CheckboxPayload.cs",
            "FileName": "CheckboxPayload.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System.Text.Json.Serialization;\r\n\r\nnamespace Grains.RazorDesigner.Document;\r\n\r\npublic sealed record CheckboxPayload : Payload\r\n{\r\n    [JsonIgnore]\r\n    public override ControlType Kind => ControlType.Checkbox;\r\n\r\n    // Checkbox label text. Overrides Payload.Content (neutral default \"\").\r\n    public override string Content { get; init; } = \"\";\r\n\r\n    public override Length CheckboxSize { get; init; } = Length.Px( 16 );\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Inspector/EdgesControlWidget.cs",
            "FileName": "EdgesControlWidget.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using Editor;\r\nusing Grains.RazorDesigner.Document;\r\nusing Sandbox;\r\n\r\nnamespace Grains.RazorDesigner.Inspector;\r\n\r\n[CustomEditor( typeof( Edges ) )]\r\npublic sealed class EdgesControlWidget : ControlWidget\r\n{\r\n\tprivate const string LogPrefix = \"[Grains.RazorDesigner]\";\r\n\r\n\tpublic override bool SupportsMultiEdit => true;\r\n\r\n\tprivate readonly EdgesProxy _proxy;\r\n\tprivate readonly SerializedObject _proxySerialized;\r\n\t// Synchronous change events on both sides; without this guard SetValue would loop.\r\n\tprivate bool _syncing;\r\n\r\n\tprivate sealed class EdgesProxy\r\n\t{\r\n\t\tpublic Length Top    { get; set; } = Length.Px( 0 );\r\n\t\tpublic Length Right  { get; set; } = Length.Px( 0 );\r\n\t\tpublic Length Bottom { get; set; } = Length.Px( 0 );\r\n\t\tpublic Length Left   { get; set; } = Length.Px( 0 );\r\n\t}\r\n\r\n\tpublic EdgesControlWidget( SerializedProperty property ) : base( property )\r\n\t{\r\n\t\tLog.Info( $\"{LogPrefix} EdgesControlWidget ctor for {property.Name}\" );\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Spacing = 2;\r\n\r\n\t\t_proxy = new EdgesProxy();\r\n\t\t_proxySerialized = EditorTypeLibrary.GetSerializedObject( _proxy );\r\n\r\n\t\tvar topRow = Layout.Add( Layout.Row() );\r\n\t\ttopRow.Spacing = 2;\r\n\t\tAddSide( topRow, nameof( EdgesProxy.Top    ), \"border_top\"    );\r\n\t\tAddSide( topRow, nameof( EdgesProxy.Right  ), \"border_right\"  );\r\n\r\n\t\tvar bottomRow = Layout.Add( Layout.Row() );\r\n\t\tbottomRow.Spacing = 2;\r\n\t\tAddSide( bottomRow, nameof( EdgesProxy.Bottom ), \"border_bottom\" );\r\n\t\tAddSide( bottomRow, nameof( EdgesProxy.Left   ), \"border_left\"   );\r\n\r\n\t\t_proxySerialized.OnPropertyChanged += OnProxyChanged;\r\n\r\n\t\tSyncFromProperty();\r\n\t}\r\n\r\n\tprivate void AddSide( Layout row, string propName, string icon )\r\n\t{\r\n\t\tvar prop = _proxySerialized.GetProperty( propName );\r\n\t\tvar lengthWidget = new LengthControlWidget( prop, icon );\r\n\t\trow.Add( lengthWidget, 1 );\r\n\t}\r\n\r\n\tprotected override void PaintControl()\r\n\t{\r\n\t\t// nothing\r\n\t}\r\n\r\n\tprivate void SyncFromProperty()\r\n\t{\r\n\t\tif ( _syncing ) return;\r\n\t\t_syncing = true;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar e = SerializedProperty.GetValue<Edges>( Edges.Zero );\r\n\r\n\t\t\t_proxySerialized.GetProperty( nameof( EdgesProxy.Top    ) ).SetValue( e.Top );\r\n\t\t\t_proxySerialized.GetProperty( nameof( EdgesProxy.Right  ) ).SetValue( e.Right );\r\n\t\t\t_proxySerialized.GetProperty( nameof( EdgesProxy.Bottom ) ).SetValue( e.Bottom );\r\n\t\t\t_proxySerialized.GetProperty( nameof( EdgesProxy.Left   ) ).SetValue( e.Left );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_syncing = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void OnProxyChanged( SerializedProperty property )\r\n\t{\r\n\t\tif ( _syncing ) return;\r\n\t\tif ( ReadOnly || !SerializedProperty.IsEditable )\r\n\t\t\treturn;\r\n\r\n\t\t_syncing = true;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar newValue = new Edges( _proxy.Top, _proxy.Right, _proxy.Bottom, _proxy.Left );\r\n\r\n\t\t\tLog.Info( $\"{LogPrefix} EdgesControlWidget OnProxyChanged {newValue}\" );\r\n\r\n\t\t\tPropertyStartEdit();\r\n\t\t\tSerializedProperty.SetValue( newValue );\r\n\t\t\tSignalValuesChanged();\r\n\t\t\tPropertyFinishEdit();\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_syncing = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnValueChanged()\r\n\t{\r\n\t\tbase.OnValueChanged();\r\n\t\tSyncFromProperty();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Palette/PalettePanel.cs",
            "FileName": "PalettePanel.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing Editor;\r\nusing Grains.RazorDesigner.Common;\r\nusing Grains.RazorDesigner.Contracts;\r\nusing Grains.RazorDesigner.Document;\r\nusing Grains.RazorDesigner.Templates;\r\nusing Sandbox;\r\n\r\nnamespace Grains.RazorDesigner.Palette;\r\n\r\npublic class PalettePanel : Widget\r\n{\r\n\tprivate const string LogPrefix = \"[Grains.RazorDesigner]\";\r\n\tprivate const string CookiePrefix = \"razordesigner.palette.\";\r\n\r\n\t// Click-to-add target. Window decides where the new record goes (typically active selection or root).\r\n\tpublic event Action<ControlType> TypeAddRequested;\r\n\r\n\t// Click-to-add a saved template. Window decides where to insert.\r\n\tpublic event Action<PaletteTemplate> TemplateAddRequested;\r\n\r\n\tprivate readonly PaletteTemplateStore _templateStore = new();\r\n\tprivate CollapsibleSection _templatesSection;\r\n\tprivate WrapPanel _templatesWrap;\r\n\tpublic PaletteTemplateStore TemplateStore => _templateStore;\r\n\r\n\tpublic PalettePanel( Widget parent ) : base( parent )\r\n\t{\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 0;\r\n\t\tLayout.Spacing = 0;\r\n\t\tMinimumWidth = 180;\r\n\t\tVerticalSizeMode = SizeMode.CanGrow;\r\n\r\n\t\tvar byCategory = new Dictionary<ControlCategory, List<ControlType>>();\r\n\t\tforeach ( ControlType type in Enum.GetValues( typeof( ControlType ) ) )\r\n\t\t{\r\n\t\t\tvar cat = ControlDefaults.For( type ).Category;\r\n\t\t\tif ( !byCategory.TryGetValue( cat, out var list ) )\r\n\t\t\t{\r\n\t\t\t\tlist = new List<ControlType>();\r\n\t\t\t\tbyCategory[cat] = list;\r\n\t\t\t}\r\n\t\t\tlist.Add( type );\r\n\t\t}\r\n\r\n\t\t// Templates section (top of palette). Hidden when store is empty; rebuilt on Changed.\r\n\t\t_templatesSection = new CollapsibleSection( this, \"Templates\", \"bookmark\" );\r\n\t\t_templatesWrap = new WrapPanel( null )\r\n\t\t{\r\n\t\t\tMinItemWidth = 92,\r\n\t\t\tItemHeight = (int)( Theme.RowHeight + 4 ),\r\n\t\t\tHSpacing = 4,\r\n\t\t\tVSpacing = 4,\r\n\t\t\tPaddingLeft = 4,\r\n\t\t\tPaddingTop = 4,\r\n\t\t\tPaddingRight = 14,\r\n\t\t\tPaddingBottom = 4,\r\n\t\t};\r\n\t\t_templatesSection.BodyLayout.Add( _templatesWrap );\r\n\r\n\t\tvar templatesCookie = $\"{CookiePrefix}templates.expanded\";\r\n\t\t_templatesSection.Expanded = EditorCookie.Get<bool>( templatesCookie, true );\r\n\t\t_templatesSection.ExpandedChanged += expanded =>\r\n\t\t{\r\n\t\t\tEditorCookie.Set( templatesCookie, expanded );\r\n\t\t\tLog.Info( $\"{LogPrefix} Palette Templates {(expanded ? \"expanded\" : \"collapsed\")}\" );\r\n\t\t};\r\n\r\n\t\tLayout.Add( _templatesSection );\r\n\r\n\t\t_templateStore.Changed += RebuildTemplatesSection;\r\n\t\t_templateStore.Scan(); // initial fill (also fires Changed and rebuilds the section)\r\n\r\n\t\tforeach ( ControlCategory cat in Enum.GetValues( typeof( ControlCategory ) ) )\r\n\t\t{\r\n\t\t\tif ( !byCategory.TryGetValue( cat, out var list ) ) continue;\r\n\r\n\t\t\tvar section = new CollapsibleSection(\r\n\t\t\t\tthis,\r\n\t\t\t\tControlDefaults.CategoryDisplayName( cat ),\r\n\t\t\t\tCategoryIcon( cat ) );\r\n\r\n\t\t\tvar wrap = new WrapPanel( null )\r\n\t\t\t{\r\n\t\t\t\tMinItemWidth = 92,\r\n\t\t\t\tItemHeight = (int)( Theme.RowHeight + 4 ),\r\n\t\t\t\tHSpacing = 4,\r\n\t\t\t\tVSpacing = 4,\r\n\t\t\t\tPaddingLeft = 4,\r\n\t\t\t\tPaddingTop = 4,\r\n\t\t\t\tPaddingRight = 14,    // clear the ScrollArea's vertical scrollbar\r\n\t\t\t\tPaddingBottom = 4,\r\n\t\t\t};\r\n\t\t\tsection.BodyLayout.Add( wrap );\r\n\r\n\t\t\tforeach ( var t in list )\r\n\t\t\t\tnew PaletteTypeButton( wrap, this, t );\r\n\r\n\t\t\tvar cookieKey = $\"{CookiePrefix}{cat}.expanded\";\r\n\t\t\tsection.Expanded = EditorCookie.Get<bool>( cookieKey, DefaultExpanded( cat ) );\r\n\t\t\tsection.ExpandedChanged += expanded =>\r\n\t\t\t{\r\n\t\t\t\tEditorCookie.Set( cookieKey, expanded );\r\n\t\t\t\tLog.Info( $\"{LogPrefix} Palette category {cat} {(expanded ? \"expanded\" : \"collapsed\")}\" );\r\n\t\t\t};\r\n\r\n\t\t\tLayout.Add( section );\r\n\t\t}\r\n\r\n\t\tLayout.AddStretchCell();\r\n\r\n\t\tLog.Info( $\"{LogPrefix} PalettePanel ctor (icon grid, {byCategory.Count} categories)\" );\r\n\t}\r\n\r\n\tinternal void NotifyTypeClicked( ControlType type )\r\n\t{\r\n\t\tLog.Info( $\"{LogPrefix} PalettePanel.NotifyTypeClicked: {type}\" );\r\n\t\tTypeAddRequested?.Invoke( type );\r\n\t}\r\n\r\n\tinternal void NotifyTemplateClicked( PaletteTemplate template )\r\n\t{\r\n\t\tLog.Info( $\"{LogPrefix} PalettePanel.NotifyTemplateClicked: \\\"{template.Name}\\\"\" );\r\n\t\tTemplateAddRequested?.Invoke( template );\r\n\t}\r\n\r\n\tinternal void RequestTemplateDelete( PaletteTemplate template )\r\n\t{\r\n\t\tvar dialog = new Editor.Dialog( this );\r\n\t\tdialog.Window.WindowTitle = \"Delete template\";\r\n\t\tdialog.Window.SetWindowIcon( \"delete\" );\r\n\t\tdialog.Window.SetModal( true, true );\r\n\t\tdialog.Window.MinimumWidth = 320;\r\n\r\n\t\tdialog.Layout = Layout.Column();\r\n\t\tdialog.Layout.Margin = 16;\r\n\t\tdialog.Layout.Spacing = 10;\r\n\r\n\t\tdialog.Layout.Add( new Editor.Label( dialog )\r\n\t\t{\r\n\t\t\tText = $\"Delete template \\\"{template.Name}\\\"?\",\r\n\t\t} );\r\n\r\n\t\tvar hint = new Editor.Label( dialog )\r\n\t\t{\r\n\t\t\tText = \"Already-instantiated copies in open documents are unaffected.\",\r\n\t\t};\r\n\t\thint.SetStyles( \"color: #888; font-size: 11px;\" );\r\n\t\tdialog.Layout.Add( hint );\r\n\r\n\t\tvar buttonRow = dialog.Layout.Add( Layout.Row() );\r\n\t\tbuttonRow.Spacing = 6;\r\n\t\tbuttonRow.AddStretchCell();\r\n\r\n\t\tvar cancel = new Editor.Button( dialog ) { Text = \"Cancel\", MinimumWidth = 72 };\r\n\t\tcancel.MouseLeftPress += () => dialog.Close();\r\n\t\tbuttonRow.Add( cancel );\r\n\r\n\t\tvar del = new Editor.Button( dialog ) { Text = \"Delete\", MinimumWidth = 72 };\r\n\t\tdel.SetStyles( \"color: #e07070;\" );\r\n\t\tdel.MouseLeftPress += () =>\r\n\t\t{\r\n\t\t\tLog.Info( $\"{LogPrefix} Palette delete confirmed: \\\"{template.Name}\\\"\" );\r\n\t\t\t_templateStore.Delete( template );\r\n\t\t\tdialog.Close();\r\n\t\t};\r\n\t\tbuttonRow.Add( del );\r\n\r\n\t\tdialog.Window.AdjustSize();\r\n\t\tdialog.Show();\r\n\t}\r\n\r\n\tprivate void RebuildTemplatesSection()\r\n\t{\r\n\t\tvar templates = _templateStore.All;\r\n\r\n\t\t// Hide the entire section (header + body) when there are no templates.\r\n\t\t_templatesSection.Visible = templates.Count > 0;\r\n\r\n\t\tusing ( Editor.SuspendUpdates.For( _templatesWrap ) )\r\n\t\t{\r\n\t\t\t_templatesWrap.DestroyChildren();\r\n\t\t\tforeach ( var t in templates )\r\n\t\t\t\tnew PaletteTemplateButton( _templatesWrap, this, t );\r\n\t\t}\r\n\r\n\t\t_templatesWrap.Relayout();\r\n\t\t_templatesWrap.UpdateGeometry();\r\n\t\t_templatesSection.UpdateGeometry();\r\n\t\tUpdateGeometry();\r\n\r\n\t\tLog.Info( $\"{LogPrefix} PalettePanel.RebuildTemplatesSection: {templates.Count} tile(s), section.Visible={_templatesSection.Visible}\" );\r\n\t}\r\n\r\n\tprivate static bool DefaultExpanded( ControlCategory cat ) =>\r\n\t\tcat is ControlCategory.Layout or ControlCategory.Display or ControlCategory.Input;\r\n\r\n\tprivate static string CategoryIcon( ControlCategory cat ) => cat switch\r\n\t{\r\n\t\tControlCategory.Layout   => \"view_quilt\",\r\n\t\tControlCategory.Display  => \"visibility\",\r\n\t\tControlCategory.Input    => \"edit\",\r\n\t\tControlCategory.Form     => \"list_alt\",\r\n\t\t_ => \"category\",\r\n\t};\r\n\r\n\tprivate sealed class PaletteTypeButton : Widget\r\n\t{\r\n\t\tprivate readonly PalettePanel _owner;\r\n\t\tprivate readonly ControlType _type;\r\n\t\t// InspectorIcon comes from the contract (engine-fidelity); drag defaults from ControlDefaults.\r\n\t\tprivate readonly string _icon;\r\n\r\n\t\tpublic PaletteTypeButton( Widget parent, PalettePanel owner, ControlType type ) : base( parent )\r\n\t\t{\r\n\t\t\t_owner = owner;\r\n\t\t\t_type = type;\r\n\t\t\t_icon = ContractScanner.Table.Get( type ).InspectorIcon;\r\n\r\n\t\t\tToolTip = type.ToString();\r\n\t\t\tCursor = CursorShape.Finger;\r\n\t\t\tMouseTracking = true;\r\n\t\t\tIsDraggable = true;\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tvar rect = LocalRect.Shrink( 1 );\r\n\t\t\tPaint.Antialiasing = true;\r\n\t\t\tPaint.TextAntialiasing = true;\r\n\r\n\t\t\tvar tint = ControlPresentation.IconTint( _type );\r\n\t\t\tvar fillAlpha = Paint.HasMouseOver ? 0.35f : 0.15f;\r\n\t\t\tvar borderAlpha = Paint.HasMouseOver ? 0.55f : 0.25f;\r\n\t\t\tPaint.SetBrush( tint.WithAlpha( fillAlpha ) );\r\n\t\t\tPaint.SetPen( tint.WithAlpha( borderAlpha ) );\r\n\t\t\tPaint.DrawRect( rect, 3 );\r\n\r\n\t\t\tvar hoverOpacity = Paint.HasMouseOver ? 1f : 0.85f;\r\n\t\t\tvar iconRect = new Rect( rect.Left + 4, rect.Top, 20, rect.Height );\r\n\t\t\tPaint.SetPen( tint.WithAlphaMultiplied( hoverOpacity ) );\r\n\t\t\tPaint.DrawIcon( iconRect, _icon, 16, TextFlag.Center );\r\n\r\n\t\t\tvar textRect = rect;\r\n\t\t\ttextRect.Left = iconRect.Right + 2;\r\n\t\t\ttextRect.Right -= 4;\r\n\t\t\tPaint.SetPen( Theme.Text.WithAlphaMultiplied( hoverOpacity ) );\r\n\t\t\tPaint.SetDefaultFont();\r\n\t\t\tPaint.DrawText( textRect, _type.ToString(), TextFlag.LeftCenter );\r\n\t\t}\r\n\r\n\t\tprotected override void OnMouseClick( MouseEvent e )\r\n\t\t{\r\n\t\t\tbase.OnMouseClick( e );\r\n\t\t\tif ( e.LeftMouseButton )\r\n\t\t\t\t_owner.NotifyTypeClicked( _type );\r\n\t\t}\r\n\r\n\t\tprotected override void OnDragStart()\r\n\t\t{\r\n\t\t\tbase.OnDragStart();\r\n\r\n\t\t\tvar drag = new Drag( this );\r\n\t\t\tdrag.Data.Object = _type;\r\n\t\t\tdrag.Data.Text = $\"palette:{_type}\";\r\n\t\t\tdrag.Execute();\r\n\r\n\t\t\tLog.Info( $\"{LogPrefix} PaletteTypeButton.OnDragStart: {_type}\" );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate sealed class PaletteTemplateButton : Widget\r\n\t{\r\n\t\tprivate readonly PalettePanel _owner;\r\n\t\tprivate readonly PaletteTemplate _template;\r\n\r\n\t\tpublic PaletteTemplateButton( Widget parent, PalettePanel owner, PaletteTemplate template ) : base( parent )\r\n\t\t{\r\n\t\t\t_owner = owner;\r\n\t\t\t_template = template;\r\n\r\n\t\t\tToolTip = template.Name;\r\n\t\t\tCursor = CursorShape.Finger;\r\n\t\t\tMouseTracking = true;\r\n\t\t\tIsDraggable = true;\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tvar rect = LocalRect.Shrink( 1 );\r\n\t\t\tPaint.Antialiasing = true;\r\n\t\t\tPaint.TextAntialiasing = true;\r\n\r\n\t\t\tvar tint = ControlPresentation.TemplateTint;\r\n\t\t\tvar fillAlpha = Paint.HasMouseOver ? 0.18f : 0.08f;\r\n\t\t\tvar borderAlpha = Paint.HasMouseOver ? 0.55f : 0.25f;\r\n\t\t\tPaint.SetBrush( tint.WithAlpha( fillAlpha ) );\r\n\t\t\tPaint.SetPen( tint.WithAlpha( borderAlpha ) );\r\n\t\t\tPaint.DrawRect( rect, 3 );\r\n\r\n\t\t\tvar hoverOpacity = Paint.HasMouseOver ? 1f : 0.85f;\r\n\t\t\tvar iconRect = new Rect( rect.Left + 4, rect.Top, 20, rect.Height );\r\n\t\t\tvar icon = string.IsNullOrEmpty( _template.IconName ) ? \"bookmark\" : _template.IconName;\r\n\t\t\tPaint.SetPen( tint.WithAlphaMultiplied( hoverOpacity ) );\r\n\t\t\tPaint.DrawIcon( iconRect, icon, 16, TextFlag.Center );\r\n\r\n\t\t\tvar textRect = rect;\r\n\t\t\ttextRect.Left = iconRect.Right + 2;\r\n\t\t\ttextRect.Right -= 4;\r\n\t\t\tPaint.SetPen( Theme.Text.WithAlphaMultiplied( hoverOpacity ) );\r\n\t\t\tPaint.SetDefaultFont();\r\n\t\t\tPaint.DrawText( textRect, _template.Name, TextFlag.LeftCenter );\r\n\t\t}\r\n\r\n\t\tprotected override void OnMouseClick( MouseEvent e )\r\n\t\t{\r\n\t\t\tbase.OnMouseClick( e );\r\n\t\t\tif ( e.LeftMouseButton )\r\n\t\t\t\t_owner.NotifyTemplateClicked( _template );\r\n\t\t}\r\n\r\n\t\tprotected override void OnContextMenu( ContextMenuEvent e )\r\n\t\t{\r\n\t\t\tbase.OnContextMenu( e );\r\n\t\t\tvar menu = new Menu( this );\r\n\t\t\tmenu.AddOption( \"Delete\u2026\", \"delete\", () => _owner.RequestTemplateDelete( _template ) );\r\n\t\t\tmenu.OpenAtCursor();\r\n\t\t\te.Accepted = true;\r\n\t\t}\r\n\r\n\t\tprotected override void OnDragStart()\r\n\t\t{\r\n\t\t\tbase.OnDragStart();\r\n\t\t\tvar drag = new Drag( this );\r\n\t\t\tdrag.Data.Object = _template;\r\n\t\t\tdrag.Data.Text = $\"template:{_template.Name}\";\r\n\t\t\tdrag.Execute();\r\n\t\t\tLog.Info( $\"{LogPrefix} PaletteTemplateButton.OnDragStart: \\\"{_template.Name}\\\"\" );\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/CSharp/CSharpOp.cs",
            "FileName": "CSharpOp.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Projection.CSharp;\r\n\r\npublic abstract record CSharpOp;\r\n\r\n// File-level scaffold\r\npublic sealed record HeaderBanner( string ClassName, string Namespace ) : CSharpOp;\r\npublic sealed record UsingDirective( string Namespace ) : CSharpOp;\r\npublic sealed record NamespaceOpen( string Namespace ) : CSharpOp;\r\npublic sealed record ClassOpen( string ClassName, string BaseClass ) : CSharpOp;\r\npublic sealed record ClassClose() : CSharpOp;\r\n\r\npublic sealed record FieldDecl(\r\n    string Visibility, string Type, string Name, string InitialExpr,\r\n    bool IsParameter, bool IsProperty = false ) : CSharpOp;\r\n\r\npublic sealed record MethodOpen(\r\n    string Visibility, bool IsOverride, bool IsAsync,\r\n    string ReturnType, string Name, string ParameterList ) : CSharpOp;\r\n\r\npublic sealed record MethodClose() : CSharpOp;\r\n\r\n// Body-level\r\npublic sealed record Statement( string Code ) : CSharpOp;          // single `;`-terminated line\r\npublic sealed record BlockOpen( string Header ) : CSharpOp;        // e.g. `if ( <cond> )` \u2014 applier writes \"<header> {\\n\" and indents\r\npublic sealed record BlockClose() : CSharpOp;\r\npublic sealed record BlankLine() : CSharpOp;\r\npublic sealed record Comment( string Text ) : CSharpOp;            // `// <text>`\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/CSharp/CSharpProjector.cs",
            "FileName": "CSharpProjector.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System.Collections.Generic;\r\nusing Grains.RazorDesigner.Projection.CSharp.Projectors;\r\n\r\nnamespace Grains.RazorDesigner.Projection.CSharp;\r\n\r\npublic static class CSharpProjector\r\n{\r\n    public static CSharpResult Project(\r\n        IReadOnlyWiring wiring,\r\n        bool documentHasAnyBindings )\r\n    {\r\n        if ( wiring.Symbols.Count == 0 && !documentHasAnyBindings )\r\n            return new CSharpResult( System.Array.Empty<CSharpOp>(), null );\r\n\r\n        var ctx = new CSharpProjectorContext( wiring );\r\n        var ops = new List<CSharpOp>( 64 );\r\n\r\n        ops.Add( new HeaderBanner( wiring.ClassName, wiring.Namespace ) );\r\n        ops.Add( new UsingDirective( \"Sandbox\" ) );\r\n        ops.Add( new UsingDirective( \"Sandbox.UI\" ) );\r\n        foreach ( var u in wiring.Usings )\r\n            ops.Add( new UsingDirective( u ) );\r\n        ops.Add( new NamespaceOpen( wiring.Namespace ) );\r\n        ops.Add( new ClassOpen( wiring.ClassName, wiring.BaseClass ) );\r\n\r\n        // Step 3: body. SymbolProjector handles grouping + sorting + per-kind dispatch.\r\n        SymbolProjector.EmitAll( wiring, ops, ctx );\r\n\r\n        ops.Add( new ClassClose() );\r\n\r\n        var source = CSharpApplier.Apply( ops ).Replace( \"\\r\\n\", \"\\n\" );\r\n\r\n        return new CSharpResult( ops, source );\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/CSharp/Projectors/ParameterSymbolProjector.cs",
            "FileName": "ParameterSymbolProjector.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System.Collections.Generic;\r\nusing Grains.RazorDesigner.Wiring;\r\n\r\nnamespace Grains.RazorDesigner.Projection.CSharp.Projectors;\r\n\r\npublic static class ParameterSymbolProjector\r\n{\r\n    public static void Emit( ParameterSymbol s, List<CSharpOp> ops, CSharpProjectorContext ctx )\r\n    {\r\n        var initial = s.Initial is null ? \"default\" : ExpressionEmitter.Emit( s.Initial, ctx );\r\n        ops.Add( new FieldDecl(\r\n            Visibility: \"public\", Type: s.Type, Name: s.Name,\r\n            InitialExpr: initial, IsParameter: true ) );\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/Escape.cs",
            "FileName": "Escape.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Projection;\r\n\r\npublic static class Escape\r\n{\r\n    public static string Html( string s )\r\n    {\r\n        if ( string.IsNullOrEmpty( s ) ) return \"\";\r\n        return s\r\n            .Replace( \"&\", \"&amp;\" )\r\n            .Replace( \"<\", \"&lt;\" )\r\n            .Replace( \">\", \"&gt;\" )\r\n            .Replace( \"\\\"\", \"&quot;\" );\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/IReadOnlyNode.cs",
            "FileName": "IReadOnlyNode.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox; // Color\r\n\r\nnamespace Grains.RazorDesigner.Projection;\r\n\r\npublic interface IReadOnlyStateRule\r\n{\r\n    Document.PseudoKind State { get; }\r\n    Document.NthChildMode NthChildMode { get; }\r\n    int NthChildArg { get; }\r\n    IAppearance Delta { get; }\r\n\r\n    public static int CompareCanonical( IReadOnlyStateRule a, IReadOnlyStateRule b )\r\n    {\r\n        int c = ((int)a.State).CompareTo( (int)b.State );\r\n        if ( c != 0 ) return c;\r\n        c = ((int)a.NthChildMode).CompareTo( (int)b.NthChildMode );\r\n        if ( c != 0 ) return c;\r\n        return a.NthChildArg.CompareTo( b.NthChildArg );\r\n    }\r\n}\r\n\r\npublic interface IReadOnlyNode\r\n{\r\n    Guid Id { get; }\r\n    string Kind { get; }         // == ControlType.ToString()\r\n    string ClassName { get; }\r\n    IAppearance Appearance { get; }\r\n    IPayload Payload { get; }\r\n    IReadOnlyList<IReadOnlyNode> Children { get; }                              // non-slot children\r\n    IReadOnlyDictionary<string, IReadOnlyList<IReadOnlyNode>> Slots { get; }    // slot-name -> slot children (only SplitContainer populates)\r\n    IReadOnlyList<IReadOnlyStateRule> StateRules { get; }                       // per-state style deltas; canonical order not guaranteed here (the Applier sorts)\r\n}\r\n\r\npublic interface IAppearance\r\n{\r\n    // Layout\r\n    Document.Length Width { get; }\r\n    Document.Length Height { get; }\r\n\r\n    // Flex container\r\n    Document.FlexDirection Direction { get; }\r\n    Document.JustifyContent Justify { get; }\r\n    Document.AlignItems Align { get; }\r\n    float Gap { get; }\r\n    Document.Edges Padding { get; }\r\n    Document.FlexWrap Wrap { get; }\r\n\r\n    // Positioning (grd-7t2z)\r\n    Document.PositionKind Position { get; }\r\n    Document.Length Top { get; }\r\n    Document.Length Left { get; }\r\n    Document.Length Right { get; }\r\n    Document.Length Bottom { get; }\r\n\r\n    // Flex self\r\n    float FlexGrow { get; }\r\n    float FlexShrink { get; }\r\n    Document.Length FlexBasis { get; }\r\n    Document.AlignSelfKind AlignSelf { get; }\r\n\r\n    // Typography + OverrideTypography\r\n    bool OverrideTypography { get; }\r\n    string FontFamily { get; }\r\n    Document.Length FontSize { get; }\r\n    int FontWeight { get; }\r\n    Color Color { get; }\r\n    Document.TextAlignment TextAlign { get; }\r\n    bool FontStyleItalic { get; }\r\n    Document.TextTransformKind TextTransform { get; }\r\n    Document.Length LetterSpacing { get; }\r\n    Document.Length LineHeight { get; }\r\n\r\n    // Background + OverrideBackground\r\n    bool OverrideBackground { get; }\r\n    Color BackgroundColor { get; }\r\n    string BackgroundImage { get; }\r\n    string BackgroundSize { get; }\r\n    string BackgroundPosition { get; }\r\n    string BackgroundRepeat { get; }\r\n\r\n    // Border + OverrideBorder\r\n    bool OverrideBorder { get; }\r\n    Document.Length BorderRadius { get; }\r\n    Color BorderColor { get; }\r\n    Document.Length BorderWidth { get; }\r\n\r\n    // Effects + OverrideEffects\r\n    bool OverrideEffects { get; }\r\n    Document.Length BoxShadowX { get; }\r\n    Document.Length BoxShadowY { get; }\r\n    Document.Length BoxShadowBlur { get; }\r\n    Color BoxShadowColor { get; }\r\n    bool BoxShadowInset { get; }\r\n    float Opacity { get; }\r\n\r\n    // Constraints + OverrideConstraints\r\n    bool OverrideConstraints { get; }\r\n    Document.Edges Margin { get; }\r\n    Document.Length MinWidth { get; }\r\n    Document.Length MaxWidth { get; }\r\n    Document.Length MinHeight { get; }\r\n    Document.Length MaxHeight { get; }\r\n\r\n    // Interaction + OverrideInteraction\r\n    bool OverrideInteraction { get; }\r\n    Document.CursorKind Cursor { get; }\r\n    Document.OverflowKind Overflow { get; }\r\n    int ZIndex { get; }\r\n    bool PointerEvents { get; }\r\n}\r\n\r\npublic interface IPayload\r\n{\r\n    string Content { get; }       // Label/Button text; Checkbox label\r\n    string Placeholder { get; }   // TextEntry\r\n    string Source { get; }        // Image src\r\n    string IconName { get; }      // IconPanel glyph\r\n    Document.Length CheckboxSize { get; }  // Checkbox box size\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/Projectors/ButtonProjector.cs",
            "FileName": "ButtonProjector.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System.Collections.Generic;\r\nusing Grains.RazorDesigner.Projection.Appearance;\r\nusing Grains.RazorDesigner.Projection.Razor;\r\n\r\nnamespace Grains.RazorDesigner.Projection.Projectors;\r\n\r\n[Projector( \"Button\" )]\r\npublic sealed class ButtonProjector : IControlProjector\r\n{\r\n    public string Kind => \"Button\";\r\n\r\n    public ProjectionResult Project( IReadOnlyNode node, IAppearance a, IPayload p, ProjectionContext ctx )\r\n    {\r\n        var scss = AppearanceScss.Emit(\r\n            a,\r\n            isRoot:       node.ClassName == Document.DesignerDocument.RootClassName,\r\n            isContainer:  false,\r\n            childCount:   0,\r\n            isLabel:      false,\r\n            isCheckbox:   false,\r\n            checkboxSize: default );\r\n\r\n        var nodeId = node.Id.ToString();\r\n        var ops = new PanelOp[]\r\n        {\r\n            new SetAttribute( \"data-grd-node-id\", nodeId ),\r\n            new SetInnerText( p.Content ?? \"\" ),\r\n        };\r\n\r\n        var razorAttrs = new[] { RazorEmit.Attr( \"data-grd-node-id\", nodeId ) };\r\n\r\n        return new ProjectionResult(\r\n            PanelOps:        ops,\r\n            ScssLines:       scss,\r\n            RazorAttributes: razorAttrs,\r\n            RazorInnerText:  Escape.Html( p.Content ?? \"\" ) );\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/Projectors/FieldProjector.cs",
            "FileName": "FieldProjector.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System.Collections.Generic;\r\nusing Grains.RazorDesigner.Projection.Appearance;\r\nusing Grains.RazorDesigner.Projection.Razor;\r\n\r\nnamespace Grains.RazorDesigner.Projection.Projectors;\r\n\r\n[Projector( \"Field\" )]\r\npublic sealed class FieldProjector : IControlProjector\r\n{\r\n    public string Kind => \"Field\";\r\n\r\n    public ProjectionResult Project( IReadOnlyNode node, IAppearance a, IPayload p, ProjectionContext ctx )\r\n    {\r\n        var scss = AppearanceScss.Emit(\r\n            a,\r\n            isRoot:       node.ClassName == Document.DesignerDocument.RootClassName,\r\n            isContainer:  true,\r\n            childCount:   node.Children.Count,\r\n            isLabel:      false,\r\n            isCheckbox:   false,\r\n            checkboxSize: default );\r\n\r\n        var nodeId = node.Id.ToString();\r\n        var ops = new PanelOp[]\r\n        {\r\n            new SetAttribute( \"data-grd-node-id\", nodeId ),\r\n        };\r\n\r\n        var razorAttrs = new[] { RazorEmit.Attr( \"data-grd-node-id\", nodeId ) };\r\n\r\n        return new ProjectionResult(\r\n            PanelOps:        ops,\r\n            ScssLines:       scss,\r\n            RazorAttributes: razorAttrs,\r\n            RazorInnerText:  null );\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/Razor/RazorEmit.cs",
            "FileName": "RazorEmit.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Projection.Razor;\r\n\r\npublic static class RazorEmit\r\n{\r\n    public static string Attr( string name, string value ) => $\"{name}=\\\"{Escape.Html( value )}\\\"\";\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Projection/Tests/PanelOpExhaustivenessTest.cs",
            "FileName": "PanelOpExhaustivenessTest.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System;\r\n\r\nnamespace Grains.RazorDesigner.Projection.Tests;\r\n\r\npublic static class PanelOpExhaustivenessTest\r\n{\r\n    public static (bool pass, string message) Run()\r\n    {\r\n        var ops = new PanelOp[]\r\n        {\r\n            new SetClass( \"\" ),\r\n            new SetStyle( \"\", \"\" ),\r\n            new SetAttribute( \"\", \"\" ),\r\n            new SetInnerText( \"\" ),\r\n        };\r\n        try\r\n        {\r\n            foreach ( var op in ops )\r\n                Applier.ApplyOpToScratch( op );\r\n            return (true, $\"PanelOpExhaustivenessTest: {ops.Length} variants OK\");\r\n        }\r\n        catch ( Exception e )\r\n        {\r\n            return (false, $\"PanelOpExhaustivenessTest FAILED: {e.GetType().Name}: {e.Message}\");\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Serialization/IR/IRWriter.cs",
            "FileName": "IRWriter.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Security.Cryptography;\r\nusing System.Text;\r\nusing System.Text.Json;\r\nusing Grains.RazorDesigner.Document;\r\n\r\nnamespace Grains.RazorDesigner.Serialization.IR;\r\n\r\npublic static class IRWriter\r\n{\r\n\tprivate const string LogPrefix = \"[Grains.RazorDesigner]\";\r\n\r\n\t// Empty collections reused for nodes that have no slots/children/metadata.\r\n\tprivate static readonly IReadOnlyDictionary<string, object> _emptyMetadata = new Dictionary<string, object>();\r\n\tprivate static readonly IReadOnlyList<IRNodeEnvelope>      _emptyChildren = System.Array.Empty<IRNodeEnvelope>();\r\n\tprivate static readonly IReadOnlyDictionary<string, IRNodeEnvelope> _emptySlots = new Dictionary<string, IRNodeEnvelope>();\r\n\r\n\tpublic static string WriteDocument( DesignerDocument doc )\r\n\t{\r\n\t\tif ( doc is null )\r\n\t\t\tthrow new ArgumentNullException( nameof( doc ) );\r\n\r\n\t\tLog.Info( $\"{LogPrefix} IRWriter.WriteDocument: serialising document (root children: {doc.RootRecord.Children.Count})\" );\r\n\r\n\t\tvar envelope = new IRDocumentEnvelope\r\n\t\t{\r\n\t\t\tRoot   = ToNode( doc.RootRecord ),\r\n\t\t\tWiring = doc.Wiring ?? Grains.RazorDesigner.Wiring.WiringEnvelope.Empty,\r\n\t\t};\r\n\r\n\t\tvar json = JsonSerializer.Serialize( envelope, DesignerIRJson.Options );\r\n\r\n\t\t// Normalise CRLF \u2192 LF (canonical form; .gitattributes also pins LF as a backstop).\r\n\t\tif ( json.Contains( '\\r' ) )\r\n\t\t\tjson = json.Replace( \"\\r\\n\", \"\\n\" ).Replace( \"\\r\", \"\\n\" );\r\n\r\n\t\tLog.Info( $\"{LogPrefix} IRWriter.WriteDocument: OK ({json.Length} chars)\" );\r\n\t\treturn json;\r\n\t}\r\n\r\n\tpublic static string CanonicalHash( string json )\r\n\t{\r\n\t\tif ( json is null )\r\n\t\t\tthrow new ArgumentNullException( nameof( json ) );\r\n\r\n\t\tvar bytes = Encoding.UTF8.GetBytes( json );\r\n\t\tvar hash  = SHA256.HashData( bytes );\r\n\t\treturn Convert.ToHexString( hash ).ToLowerInvariant();\r\n\t}\r\n\r\n\t// Recursively converts a ControlRecord to its IRNodeEnvelope representation.\r\n\tprivate static IRNodeEnvelope ToNode( ControlRecord r )\r\n\t{\r\n\t\tDictionary<string, IRNodeEnvelope> slotDict  = null;\r\n\t\tList<IRNodeEnvelope>               childList = null;\r\n\r\n\t\tforeach ( var child in r.Children )\r\n\t\t{\r\n\t\t\tif ( child.IsSlot )\r\n\t\t\t{\r\n\t\t\t\tslotDict ??= new Dictionary<string, IRNodeEnvelope>();\r\n\t\t\t\tslotDict[child.SlotName] = ToNode( child );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tchildList ??= new List<IRNodeEnvelope>();\r\n\t\t\t\tchildList.Add( ToNode( child ) );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn new IRNodeEnvelope\r\n\t\t{\r\n\t\t\tId         = r.Id,\r\n\t\t\tKind       = r.Type,\r\n\t\t\tClassName  = r.ClassName,\r\n\t\t\tAppearance = r.Appearance,\r\n\t\t\tPayload    = r.Payload,\r\n\t\t\tSlots      = slotDict  is not null\r\n\t\t\t\t? (IReadOnlyDictionary<string, IRNodeEnvelope>)slotDict\r\n\t\t\t\t: _emptySlots,\r\n\t\t\tChildren   = childList is not null\r\n\t\t\t\t? (IReadOnlyList<IRNodeEnvelope>)childList\r\n\t\t\t\t: _emptyChildren,\r\n\t\t\tStates = r.StateRules.Count == 0\r\n\t\t\t\t? null\r\n\t\t\t\t: r.StateRules\r\n\t\t\t\t\t.OrderBy( rule => rule, Comparer<StateRule>.Create( StateRule.CompareCanonical ) )\r\n\t\t\t\t\t.Select( rule => new IRStateEnvelope\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tState       = rule.State,\r\n\t\t\t\t\t\tNthChildMode = rule.NthChildMode,\r\n\t\t\t\t\t\tNthChildArg  = rule.NthChildArg,\r\n\t\t\t\t\t\tDelta        = rule.Delta,\r\n\t\t\t\t\t} )\r\n\t\t\t\t\t.ToList(),\r\n\t\t\tBindings   = r.Bindings.Count == 0\r\n\t\t\t\t? System.Array.Empty<Grains.RazorDesigner.Wiring.Binding>()\r\n\t\t\t\t: r.Bindings.ToArray(),\r\n\r\n\t\t\t\tCustomStyles = r.CustomStyles.Count == 0 \r\n\t\t\t\t? null \r\n\t\t\t\t: new Dictionary<string, string>( r.CustomStyles ),\r\n\t\t};\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Wiring/Actions/Action.cs",
            "FileName": "Action.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace Grains.RazorDesigner.Wiring;\r\n\r\n[JsonPolymorphic( TypeDiscriminatorPropertyName = \"$type\" )]\r\n[JsonDerivedType( typeof( SetAction ),              \"Set\" )]\r\n[JsonDerivedType( typeof( CallAction ),             \"Call\" )]\r\n[JsonDerivedType( typeof( IfAction ),               \"If\" )]\r\n[JsonDerivedType( typeof( StateHasChangedAction ),  \"StateHasChanged\" )]\r\n[JsonDerivedType( typeof( LogAction ),              \"Log\" )]\r\n[JsonDerivedType( typeof( ReturnAction ),           \"Return\" )]\r\n[JsonDerivedType( typeof( InlineAction ),           \"Inline\" )]\r\npublic abstract record Action\r\n{\r\n    public Guid Id { get; init; } = Guid.NewGuid();\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Wiring/Actions/InlineAction.cs",
            "FileName": "InlineAction.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Wiring;\r\n\r\npublic sealed record InlineAction : Action\r\n{\r\n    public string Code { get; init; } = \"\";\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Wiring/Actions/SetAction.cs",
            "FileName": "SetAction.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Wiring;\r\n\r\n// `Target = Value;` \u2014 assignment to a Symbol field.\r\npublic sealed record SetAction : Action\r\n{\r\n    public TargetRef Target { get; init; }\r\n    public Expression Value { get; init; }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Wiring/Bindings/EventBinding.cs",
            "FileName": "EventBinding.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using System.Collections.Generic;\r\n\r\nnamespace Grains.RazorDesigner.Wiring;\r\n\r\npublic sealed record EventBinding : Binding\r\n{\r\n    public string Event { get; init; } = \"\";\r\n    public IReadOnlyList<Action> Body { get; init; } = System.Array.Empty<Action>();\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Wiring/Bindings/VisibleBinding.cs",
            "FileName": "VisibleBinding.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Wiring;\r\n\r\npublic sealed record VisibleBinding : Binding\r\n{\r\n    public Expression Condition { get; init; }\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Wiring/Symbols/SymbolVisibility.cs",
            "FileName": "SymbolVisibility.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "namespace Grains.RazorDesigner.Wiring;\r\n\r\npublic enum SymbolVisibility\r\n{\r\n    Private,\r\n    Public,\r\n    Internal,\r\n    Protected,\r\n}\r\n"
        },
        {
            "Ident": "sklmr.razordesigner",
            "Path": "Editor/Canvas/DesignerCanvas.cs",
            "FileName": "DesignerCanvas.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 306734,
            "Code": "using Editor;\r\nusing Grains.RazorDesigner.Document;\r\nusing Sandbox;\r\n\r\nnamespace Grains.RazorDesigner.Canvas;\r\n\r\n// Same shape as ShaderGraph PreviewPanel: override PreFrame to advance the scene.\r\npublic class DesignerCanvas : SceneRenderingWidget\r\n{\r\n\tprivate const string LogPrefix = \"[Grains.RazorDesigner]\";\r\n\r\n\tprivate readonly DesignerScene _designerScene;\r\n\r\n\tpublic DesignerCanvas( Widget parent ) : base( parent )\r\n\t{\r\n\t\tLog.Info( $\"{LogPrefix} DesignerCanvas ctor\" );\r\n\r\n\t\t_designerScene = new DesignerScene();\r\n\r\n\t\tScene = _designerScene.Scene;\r\n\t\tCamera = _designerScene.Camera;\r\n\r\n\t\tHorizontalSizeMode = SizeMode.Default | SizeMode.Expand;\r\n\t\tVerticalSizeMode = SizeMode.Default | SizeMode.Expand;\r\n\r\n\t\t// Without AcceptDrops, OnDragHover/OnDragDrop are never invoked.\r\n\t\tAcceptDrops = true;\r\n\r\n\t\tMouseTracking = true;\r\n\t}\r\n\r\n\tpublic DesignerScene DesignerScene => _designerScene;\r\n\r\n\t// Assigned by DesignerWindow after construction (mirrors how _viewportFrame.Canvas is wired).\r\n\tpublic OverlayController Overlay { get; set; }\r\n\r\n\tpublic CanvasViewportFrame ViewportFrame { get; set; }\r\n\r\n\tpublic event System.Action<Vector2, bool, Sandbox.KeyboardModifiers> CanvasClicked;\r\n\tpublic event System.Action<Vector2, Sandbox.KeyboardModifiers> CanvasMoved;\r\n\tpublic event System.Action<Vector2> CanvasReleased;\r\n\r\n\tpublic event System.Action<Vector2> CanvasPanDragged; // delta, screen px\r\n\r\n\t// Fires when the cursor leaves the canvas widget. Used to clear hover-pick state.\r\n\tpublic event System.Action CanvasHoverEnded;\r\n\r\n\tpublic event System.Action<ControlType, Vector2> RecordDropped;\r\n\t// Mirrors RecordDropped but for saved palette templates.\r\n\tpublic event System.Action<Grains.RazorDesigner.Templates.PaletteTemplate, Vector2> TemplateDropped;\r\n\r\n\tprivate bool _middlePanning;\r\n\tprivate Vector2 _lastPanScreen;\r\n\r\n\tprivate const bool ProbeFrameCost = false;\r\n\tprivate const int ProbeWindow = 120;\r\n\tprivate readonly System.Diagnostics.Stopwatch _probeSw = new();\r\n\tprivate double _probeTickMs;\r\n\tprivate double _probeUpdateMs;\r\n\tprivate int _probeFrames;\r\n\r\n\tpublic override void OnDragHover( DragEvent ev )\r\n\t{\r\n\t\tbase.OnDragHover( ev );\r\n\t\tif ( ev.Data.Object is ControlType || ev.Data.Object is Grains.RazorDesigner.Templates.PaletteTemplate )\r\n\t\t{\r\n\t\t\tev.Action = DropAction.Copy;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic override void OnDragDrop( DragEvent ev )\r\n\t{\r\n\t\tbase.OnDragDrop( ev );\r\n\t\tif ( ev.Data.Object is ControlType type )\r\n\t\t{\r\n\t\t\tLog.Info( $\"{LogPrefix} DesignerCanvas drop: {type} at widget ({ev.LocalPosition.x:F0}, {ev.LocalPosition.y:F0})\" );\r\n\t\t\tRecordDropped?.Invoke( type, ev.LocalPosition );\r\n\t\t}\r\n\t\telse if ( ev.Data.Object is Grains.RazorDesigner.Templates.PaletteTemplate template )\r\n\t\t{\r\n\t\t\tLog.Info( $\"{LogPrefix} DesignerCanvas drop: template \\\"{template.Name}\\\" at widget ({ev.LocalPosition.x:F0}, {ev.LocalPosition.y:F0})\" );\r\n\t\t\tTemplateDropped?.Invoke( template, ev.LocalPosition );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tif ( e.MiddleMouseButton )\r\n\t\t{\r\n\t\t\t// Swallow entirely \u2014 don't let base (SceneRenderingWidget) see the middle drag.\r\n\t\t\t_middlePanning = true;\r\n\t\t\t_lastPanScreen = e.ScreenPosition;\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tbase.OnMousePress( e );\r\n\t\tif ( e.LeftMouseButton || e.RightMouseButton )\r\n\t\t{\r\n\t\t\tvar pos = e.LocalPosition;\r\n\t\t\tLog.Info( $\"{LogPrefix} DesignerCanvas click at widget ({pos.x:F0}, {pos.y:F0}) right={e.RightMouseButton}\" );\r\n\t\t\tCanvasClicked?.Invoke( pos, e.RightMouseButton, e.KeyboardModifiers );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tif ( _middlePanning )\r\n\t\t{\r\n\t\t\tvar s = e.ScreenPosition;\r\n\t\t\tCanvasPanDragged?.Invoke( s - _lastPanScreen );\r\n\t\t\t_lastPanScreen = s;\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tbase.OnMouseMove( e );\r\n\t\tCanvasMoved?.Invoke( e.LocalPosition, e.KeyboardModifiers );\r\n\t}\r\n\r\n\tprotected override void OnMouseReleased( MouseEvent e )\r\n\t{\r\n\t\tif ( _middlePanning )\r\n\t\t{\r\n\t\t\t_middlePanning = false;\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tbase.OnMouseReleased( e );\r\n\t\tCanvasReleased?.Invoke( e.LocalPosition );\r\n\t}\r\n\r\n\tprotected override void OnMouseLeave()\r\n\t{\r\n\t\tbase.OnMouseLeave();\r\n\t\tCanvasHoverEnded?.Invoke();\r\n\t}\r\n\r\n\tprotected override void PreFrame()\r\n\t{\r\n\t\tbase.PreFrame();\r\n\r\n\t\tif ( !_designerScene.Scene.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tif ( ProbeFrameCost ) _probeSw.Restart();\r\n\r\n\t\tusing ( _designerScene.Scene.Push() )\r\n\t\t{\r\n\t\t\t// EditorTick (not GameTick): preview scene, no SceneNetworkUpdate / fixed physics.\r\n\t\t\t_designerScene.Scene.EditorTick( RealTime.Now, RealTime.Delta );\r\n\t\t}\r\n\r\n\t\tif ( ProbeFrameCost ) { _probeTickMs += _probeSw.Elapsed.TotalMilliseconds; _probeSw.Restart(); }\r\n\r\n\t\t// Layout in framebuffer space (Size * DpiScale); DesignerScene sets Scale = DpiScale so authoring stays in logical px.\r\n\t\t_designerScene.Update( Size.x, Size.y, DpiScale );\r\n\r\n\t\tOverlay?.Tick( _designerScene, DpiScale );\r\n\r\n\t\tif ( ProbeFrameCost )\r\n\t\t{\r\n\t\t\t_probeUpdateMs += _probeSw.Elapsed.TotalMilliseconds;\r\n\t\t\tif ( ++_probeFrames >= ProbeWindow )\r\n\t\t\t{\r\n\t\t\t\tLog.Info( $\"{LogPrefix} probe: EditorTick avg {_probeTickMs / _probeFrames:F3}ms | Update avg {_probeUpdateMs / _probeFrames:F3}ms (over {_probeFrames} frames, canvas {Size.x:F0}x{Size.y:F0})\" );\r\n\t\t\t\t_probeTickMs = 0;\r\n\t\t\t\t_probeUpdateMs = 0;\r\n\t\t\t\t_probeFrames = 0;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnKeyPress( KeyEvent e )\r\n\t{\r\n\t\tbase.OnKeyPress( e );\r\n\r\n\t\tif ( ViewportFrame is not { CanPanZoom: true } frame ) return;\r\n\r\n\t\tvar mods  = e.KeyboardModifiers;\r\n\t\tvar ctrl  = (mods & Sandbox.KeyboardModifiers.Ctrl)  != 0;\r\n\t\tvar shift = (mods & Sandbox.KeyboardModifiers.Shift) != 0;\r\n\t\tvar alt   = (mods & Sandbox.KeyboardModifiers.Alt)   != 0;\r\n\r\n\t\tif ( !ctrl || alt ) return; // every shortcut is Ctrl-based; Alt clears it.\r\n\r\n\t\tswitch ( e.Key )\r\n\t\t{\r\n\t\t\tcase KeyCode.Num0:\r\n\t\t\t\tif ( shift )\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Info( $\"{LogPrefix} OnKeyPress Ctrl+Shift+0 \u2192 ZoomToSelection\" );\r\n\t\t\t\t\tZoomToSelectionViaFrame( frame );\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.Info( $\"{LogPrefix} OnKeyPress Ctrl+0 \u2192 ApplyFit\" );\r\n\t\t\t\t\tframe.ApplyFit();\r\n\t\t\t\t}\r\n\t\t\t\te.Accepted = true;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase KeyCode.Num1:\r\n\t\t\t\tif ( shift ) return;\r\n\t\t\t\tLog.Info( $\"{LogPrefix} OnKeyPress Ctrl+1 \u2192 ResetZoomOnly\" );\r\n\t\t\t\tframe.ResetZoomOnly();\r\n\t\t\t\te.Accepted = true;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase KeyCode.Equal:\r\n\t\t\t\tif ( shift ) return;\r\n\t\t\t\tframe.ZoomBy( 1.25f );\r\n\t\t\t\te.Accepted = true;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase KeyCode.Minus:\r\n\t\t\t\tif ( shift ) return;\r\n\t\t\t\tframe.ZoomBy( 1f / 1.25f );\r\n\t\t\t\te.Accepted = true;\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void ZoomToSelectionViaFrame( CanvasViewportFrame frame )\r\n\t{\r\n\t\tvar record = frame.Selection?.Selected;\r\n\t\tvar live = record?.LivePanel;\r\n\t\tif ( live is null || !live.IsValid )\r\n\t\t{\r\n\t\t\tLog.Info( $\"{LogPrefix} Ctrl+Shift+0: no valid selection\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tframe.ApplyZoomToRect( live.Box.Rect, /* maxZoom */ 4f, /* padding */ 0.10f );\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\tLog.Info( $\"{LogPrefix} DesignerCanvas.OnDestroyed\" );\r\n\t\tbase.OnDestroyed();\r\n\t\t_designerScene.Dispose();\r\n\t}\r\n}\r\n"
        }
    ]
}