Document model
The shape of a .sui document — what gets serialized, how the tree is stored, and how elements are identified.
Table of contents
- The root type
- Identity
- Tree representation
- Element data blocks
- Serialization format
- Schema versioning
- Validation
- See also
The root type
public sealed class SuiDocument
{
public int SchemaVersion { get; set; } // 1 currently
public string DocumentId { get; set; } // e.g. "sui_my_hud_a3b2c1d4"
public string Name { get; set; } // matches the .sui filename
public SuiCanvasSettings Canvas { get; set; }
public SuiDocumentSettings Settings { get; set; }
public List<SuiElement> Elements { get; set; }
public SuiOutputSettings Output { get; set; }
public SuiGeneratedFileManifest Manifest { get; set; }
// Reserved for V1.5+
public List<SuiEventBinding> Events { get; set; }
public List<SuiAnimationData> Animations { get; set; }
public List<SuiPropertyBinding> Bindings { get; set; }
}
Defined in Code/Runtime/SuiDocument.cs.
Identity
Three identifiers worth distinguishing:
| Field | Lifetime | Used for |
|---|---|---|
Document.DocumentId | Stable for life | File ownership (manifest), preventing collisions when class is renamed |
Document.Name | Equal to .sui filename | Display only |
Document.Output.ClassName | User-editable | Name of generated C# class (MyHud) and file basename |
DocumentId is generated once via SuiDocument.NewDocumentId(nameHint) and never changes. Renaming the file does NOT rotate it — the manifest stays tied to the original ID so old generated files are correctly tracked.
Per-element IDs:
"el_a3f9b21c" // 8-char hex from a Guid
Generated by SuiDocument.NewElementId(). Stable across renames and across compiles — the SCSS generator uses them to emit unique sui-<id> classes (see Canvas renderer).
The root element always has Id = "root" — a special-cased value.
Tree representation
The document is a flat list of SuiElement with parent pointers, not a nested object graph. Each element has:
ParentId— null only for the rootChildren— ordered list of child IDs (denormalized for fast iteration)
public sealed class SuiElement
{
public string Id { get; set; }
public string Name { get; set; }
public SuiElementType Type { get; set; }
public string ParentId { get; set; }
public List<string> Children { get; set; }
public SuiElementFlags Flags { get; set; }
public SuiLayoutData Layout { get; set; }
public SuiStyleData Style { get; set; }
public SuiElementProps Props { get; set; }
public string Notes { get; set; }
public string TooltipText { get; set; }
public bool IsVisible { get; set; }
public string ClassOverride { get; set; }
public string StyleRef { get; set; }
}
Parent.Children and Child.ParentId are kept in sync by SuiDocumentValidator. When commands mutate the tree they update both; the validator runs after load to repair any drift from hand-edited JSON.
Why flat?
System.Text.Jsonround-trips it natively. Deeply nested polymorphic graphs require custom converters and break on V2 schema additions.- Lookup by ID is
O(n)linear scan, which is fine — documents rarely exceed 100 elements. - Reordering children is a single list operation on the parent’s
Childrenarray.
Lookup helpers
doc.GetRoot() // first element with ParentId == null
doc.GetElement( "el_x" ) // linear scan by ID
For multiple lookups in a row, build a Dictionary<string, SuiElement> once.
Element data blocks
Every element carries 4 nested data blocks:
Flags — designer-only
public sealed class SuiElementFlags
{
public bool IsVariable; // V1.5 — expose as [Property] in generated C#
public bool Locked; // can't move/resize in canvas
public bool HiddenInDesigner; // hidden in canvas (still in doc + generated)
}
Not emitted to runtime SCSS. Used by the canvas widget for interaction policy.
Layout — position + sizing
public sealed class SuiLayoutData
{
public SuiLayoutMode Mode; // Absolute | Flex
// Absolute mode
public float X, Y, Width, Height;
public float? MinWidth, MinHeight, MaxWidth, MaxHeight;
public SuiAnchor Anchor;
public float PivotX, PivotY;
public int ZIndex;
// Flex mode
public SuiFlexDirection FlexDirection;
public SuiJustifyContent JustifyContent;
public SuiAlignItems AlignItems;
public SuiFlexWrap FlexWrap;
public float Gap;
// Shared
public SuiSpacing Margin;
public SuiSpacing Padding;
}
Both Absolute and Flex fields are serialized regardless of Mode — toggling modes preserves the user’s values. The generator and canvas only read the relevant subset.
Stretch anchors interpret X/Y/Width/Height as margins (left/top/right/bottom insets), not as a content box. See anchors and pivot.
Style — visual style
public sealed class SuiStyleData
{
public string ClassName;
public List<string> CustomClasses;
public string BackgroundColor;
public string BorderColor;
public float BorderWidth;
public float BorderRadius;
public float Opacity;
public SuiVisibility Visibility;
public SuiPointerEvents PointerEvents;
public SuiOverflow Overflow;
}
Null/default = “don’t emit this rule”. The generator uses default values as the “skip” sentinel.
Props — type-specific properties
A flat bag of every type-specific field SUI knows about:
public sealed class SuiElementProps
{
// Text fields (used when Type == Text or Button)
public string Text;
public float FontSize;
public SuiFontWeight FontWeight;
public string Color;
public SuiTextAlign TextAlign;
public SuiTextSizeMode TextSizeMode;
// ...
// Image fields (used when Type == Image)
public string ImagePath;
public string Tint;
public SuiImageFitMode FitMode;
// ...
// Grid fields
public int Columns, Rows;
public float CellWidth, CellHeight, GridGap;
public SuiGridGenerationStrategy GridStrategy;
// Button
public string ButtonText;
// ProgressBar
public float ProgressMin, ProgressMax, ProgressPreviewValue;
public string ProgressFillColor;
public SuiProgressDirection ProgressDirection;
// InventorySlot / ItemIcon
public int SlotIndex;
public string PreviewIconPath;
public int PreviewCount;
}
Why a flat bag and not a polymorphic hierarchy? Because Sandbox’s GameResource JSON serializer round-trips this cleanly without polymorphic type discriminators. The generator picks the subset relevant to the element’s Type.
Serialization format
Documents are stored as JSON via System.Text.Json. The on-disk shape mirrors the C# types one-to-one with default JsonSerializerOptions (camelCase NOT applied — fields are PascalCase to match C#).
A minimal .sui file looks like:
{
"SchemaVersion": 1,
"DocumentId": "sui_my_hud_a3b2c1d4",
"Name": "my_hud",
"Canvas": {
"BaseWidth": 1920,
"BaseHeight": 1080
},
"Output": {
"ClassName": "MyHud",
"Namespace": "Game.UI"
},
"Elements": [
{
"Id": "root",
"Name": "Root",
"Type": "Canvas",
"ParentId": null,
"Children": ["el_health"],
"Layout": { "Mode": "Absolute", "Width": 1920, "Height": 1080 }
},
{
"Id": "el_health",
"Name": "HealthBar",
"Type": "ProgressBar",
"ParentId": "root",
"Children": [],
"Layout": { "Mode": "Absolute", "X": 40, "Y": 40, "Width": 200, "Height": 18 },
"Style": { "BackgroundColor": "#22222288" },
"Props": {
"ProgressMin": 0, "ProgressMax": 100, "ProgressPreviewValue": 75,
"ProgressFillColor": "#ef4444"
}
}
]
}
See SUI JSON schema reference for the full schema.
Schema versioning
public static class SuiSchemaVersion
{
public const int Current = 1;
public const int MinimumSupported = 1;
public const string DesignerVersion = "0.1.0";
}
When the schema changes, SuiDocumentMigration is responsible for upgrading old documents on load. Currently only V1 exists — no migrations yet.
Validation
SuiDocumentValidator runs after every load and before every compile. It:
- Repairs parent/child link drift (
Parent.Children⇄Child.ParentId). - Sanitizes class names (
MyHudnotMy-Hud!) and identifier slugs (forDocumentId). - Clamps
Opacityto[0, 1]. - Validates
ClassNameuniqueness within the document (warns, doesn’t block).
Validation produces a SuiCompileResult with Info / Warning / Error rows surfaced in the Compile Results panel.