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

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 root
  • Children — 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.Json round-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 Children array.

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.ChildrenChild.ParentId).
  • Sanitizes class names (MyHud not My-Hud!) and identifier slugs (for DocumentId).
  • Clamps Opacity to [0, 1].
  • Validates ClassName uniqueness within the document (warns, doesn’t block).

Validation produces a SuiCompileResult with Info / Warning / Error rows surfaced in the Compile Results panel.

See also


SUI Designer · MIT license · Built for the s&box ecosystem.

This site uses Just the Docs, a documentation theme for Jekyll.