Runtime/SuiDocument.cs
using System;
using System.Collections.Generic;
namespace SboxUiDesigner.Runtime;
/// <summary>
/// Root .sui document. Source of truth for a single UI document — the visual
/// designer reads/writes this; the generator emits Razor/SCSS from it.
///
/// The full document is persisted via <see cref="SboxUiDesigner.Runtime.SuiAsset"/>
/// (a GameResource — see Code/Runtime/SuiAsset.cs).
/// </summary>
public sealed class SuiDocument
{
// ---------- Identity ----------
/// <summary>Schema version for migration. <see cref="SuiSchemaVersion.Current"/> at save time.</summary>
public int SchemaVersion { get; set; } = SuiSchemaVersion.Current;
/// <summary>Stable unique id for file ownership and bindings. Never changes on rename.</summary>
public string DocumentId { get; set; }
/// <summary>User-facing document name (matches the .sui asset filename without extension).</summary>
public string Name { get; set; }
public string CreatedWith { get; set; } = SuiSchemaVersion.CreatedWithTag;
public string DesignerVersion { get; set; } = SuiSchemaVersion.DesignerVersion;
// ---------- Settings & canvas ----------
public SuiCanvasSettings Canvas { get; set; } = new();
public SuiDocumentSettings Settings { get; set; } = new();
// ---------- Tree ----------
public List<SuiElement> Elements { get; set; } = new();
// ---------- Reserved (V1.5+) ----------
public List<SuiEventBinding> Events { get; set; } = new();
public List<SuiAnimationData> Animations { get; set; } = new();
/// <summary>Property bindings (target.Property → Source.Path) displayed in the Bindings tab.</summary>
public List<SuiPropertyBinding> Bindings { get; set; } = new();
// ---------- Output & manifest ----------
public SuiOutputSettings Output { get; set; } = new();
public SuiGeneratedFileManifest Manifest { get; set; } = new();
// ---------- Lookup helpers ----------
/// <summary>Returns the root element (parentId == null) or null if document is empty/invalid.</summary>
public SuiElement GetRoot()
{
foreach ( var el in Elements )
{
if ( string.IsNullOrEmpty( el.ParentId ) )
return el;
}
return null;
}
public SuiElement GetElement( string id )
{
if ( string.IsNullOrEmpty( id ) ) return null;
foreach ( var el in Elements )
{
if ( el.Id == id ) return el;
}
return null;
}
/// <summary>
/// Generate a stable id like "el_a3f9b21c" derived from a guid.
/// IDs are short, readable, and cheap to compare.
/// </summary>
public static string NewElementId()
{
var g = Guid.NewGuid();
return "el_" + g.ToString( "N" ).Substring( 0, 8 );
}
/// <summary>
/// Generate a stable document id like "sui_inventoryui_8f31_a3b2c1d4" — slug-prefixed
/// so manifest/log output is human-scannable, suffix from a fresh guid for uniqueness.
/// </summary>
public static string NewDocumentId( string nameHint )
{
var slug = SuiDocumentValidator.SanitizeIdentifierSlug( nameHint );
var g = Guid.NewGuid().ToString( "N" ).Substring( 0, 8 );
return string.IsNullOrEmpty( slug )
? "sui_" + g
: $"sui_{slug}_{g}";
}
/// <summary>
/// Build a fresh document with a single Root canvas element. Used by the
/// "New" asset flow so users always see a non-empty document on first open.
/// </summary>
public static SuiDocument CreateDefault( string documentName )
{
var doc = new SuiDocument
{
SchemaVersion = SuiSchemaVersion.Current,
DocumentId = NewDocumentId( documentName ),
Name = documentName,
CreatedWith = SuiSchemaVersion.CreatedWithTag,
DesignerVersion = SuiSchemaVersion.DesignerVersion,
};
var root = new SuiElement
{
Id = "root",
Name = "Root",
Type = SuiElementType.Canvas,
ParentId = null,
};
root.Style.ClassName = "root";
root.Layout.Mode = SuiLayoutMode.Absolute;
root.Layout.Width = doc.Canvas.BaseWidth;
root.Layout.Height = doc.Canvas.BaseHeight;
doc.Elements.Add( root );
doc.Output.ClassName = SuiDocumentValidator.SanitizeClassName( documentName );
return doc;
}
public SuiDocument Clone()
{
var clone = new SuiDocument
{
SchemaVersion = SchemaVersion,
DocumentId = DocumentId,
Name = Name,
CreatedWith = CreatedWith,
DesignerVersion = DesignerVersion,
Canvas = Canvas?.Clone() ?? new(),
Settings = Settings?.Clone() ?? new(),
Output = Output?.Clone() ?? new(),
Manifest = Manifest?.Clone() ?? new(),
};
foreach ( var el in Elements ) clone.Elements.Add( el.Clone() );
foreach ( var ev in Events ) clone.Events.Add( ev.Clone() );
foreach ( var an in Animations ) clone.Animations.Add( an.Clone() );
return clone;
}
}