Code/Demos/ComposableHud/ComposableHudUI.cs
using System.Collections.Generic;
using Goo;
using Sandbox;
using Sandbox.Compass;
using Sandbox.KeystrokeViz;
using Sandbox.RadialWheel;
using Sandbox.UI;

namespace Sandbox.ComposableHud;

// GooPanel HUD host built for composition. Owns a collection of HudElements; each frame it ticks every element, ORs their dirty flags, and rebuilds once.
// The three solo demos (compass, keystroke, radial) compose here with zero edits to their view classes via a single ViewElement delegate adapter.
// Lifecycle note: keystroke/radial need Reset() on enable, compass does not, so the host resets per-view. KeystrokeView must be placed at its own Anchor because it reads that value to pick its inner stacking direction (bd memory resolveanchor-positioning-container).
[Title( "Demo: Composable HUD" ), Category( "UI/Demo" ), Icon( "dashboard" )]
public sealed class ComposableHudUI : GooPanel<Container>
{
    CompassView?         _compass   = new();
    KeystrokeView?       _keystroke = new() { Anchor = Layout.Anchor.BottomLeft };
    RadialView?          _radial    = new();
    SquadHealthView?     _squad     = new();
    DamageIndicatorView? _damage    = new();
    HotbarView?          _hotbar    = new();
    PlayerHealthView?    _player    = new();
    List<HudElement>? _elements;

    // Lazy, null-healed element list. s&box builds hotload instances via uninitialized-object + field migration, so constructors and field initializers do NOT run on hotload and a newly added field migrates as null (engine-fact-component-no-ctor-on-hotload). Building lazily and healing null view fields first keeps the host robust across hotloads.
    List<HudElement> Elements()
    {
        _compass   ??= new();
        _keystroke ??= new() { Anchor = Layout.Anchor.BottomLeft };
        _radial    ??= new();
        _squad     ??= new();
        _damage    ??= new();
        _hotbar    ??= new();
        _player    ??= new();
        return _elements ??= new()
        {
            new ViewElement { Anchor = Layout.Anchor.TopCenter, Key = "compass",
                              OnTick = _compass.Tick,   OnBuild = _compass.Build },
            new ViewElement { Anchor = _keystroke.Anchor, Padding = _keystroke.AnchorMargin, Key = "keystrokes",
                              OnTick = _keystroke.Tick, OnBuild = _keystroke.Build },
            new ViewElement { Anchor = Layout.Anchor.Center, Key = "radial",
                              OnTick = _radial.Tick,    OnBuild = _radial.Build },
            new ViewElement { Anchor = Layout.Anchor.TopLeft, Padding = 16f, Key = "squad",
                              OnTick = _squad.Tick,     OnBuild = _squad.Build },
            new ViewElement { Anchor = Layout.Anchor.Center, Key = "dmg",
                              OnTick = _damage.Tick,    OnBuild = _damage.Build },
            new ViewElement { Anchor = Layout.Anchor.BottomCenter, Padding = 32f, Key = "hotbar",
                              OnTick = _hotbar.Tick,    OnBuild = _hotbar.Build },
            // Sits above the hotbar at bottom-center via larger bottom inset (uniform padding).
            new ViewElement { Anchor = Layout.Anchor.BottomCenter, Padding = 110f, Key = "player-hp",
                              OnTick = _player.Tick,    OnBuild = _player.Build },
        };
    }

    protected override void OnEnabled()
    {
        base.OnEnabled();
        Panel.Style.Width  = Length.Percent( 100 );
        Panel.Style.Height = Length.Percent( 100 );
        Elements();   // ensure the view fields exist before resetting them
        // Non-uniform lifecycle seam: only the stateful pieces reset on enable.
        _keystroke!.Reset();
        _radial!.Reset();
        _squad!.Reset();
        _damage!.Reset();
        _hotbar!.Reset();
        _player!.Reset();
    }

    protected override void OnUpdate()
    {
        bool dirty = false;
        foreach ( var e in Elements() )
            dirty |= e.Tick( Scene, Time.Delta );
        if ( dirty ) Rebuild();
        base.OnUpdate();
    }

    protected override Container Build()
    {
        var root = Hud.Overlay();
        foreach ( var e in Elements() )
            root.Children.Add( PlaceAt( e.Anchor, e.Build(), e.Padding, e.Key ) );
        return root;
    }

    // Inline anchored placement, verbatim HudLayout.Anchored. The positioning cell uses FlexDirection.Column so ResolveAnchor's Justify/Align resolve against the vertical main axis (bd memory resolveanchor-positioning-container); the engine-default Row would render top anchors center-left.
    static Container PlaceAt( Layout.Anchor anchor, Container content, float padding, string? key )
    {
        var r = Layout.ResolveAnchor( anchor );
        return new Container
        {
            Key            = key,
            Position       = PositionMode.Absolute,
            Top            = 0,
            Left           = 0,
            Width          = Length.Percent( 100 ),
            Height         = Length.Percent( 100 ),
            Padding        = padding,
            PointerEvents  = PointerEvents.None,
            FlexDirection  = FlexDirection.Column,
            JustifyContent = r.JustifyContent,
            AlignItems     = r.AlignItems,
            Children       = { content },
        };
    }
}