Demos/ComposableHud/HotbarView.cs
using Goo;
using Goo.Animation;
using Sandbox;
using Sandbox.UI;

namespace Sandbox.ComposableHud;

// Inventory hotbar: N slots, each an item icon + stack count, with a selected slot (number keys 1-8) that pops via its own per-slot spring. Every slot's select-pop is a plain array entry on this single view and Build() is one loop, the same one-view-owns-N-arrays idiom as the squad bars and the radial. No per-slot fiber, so root-only-state never bites.
sealed class HotbarView
{
    const int   N             = 4;
    const float SlotSize      = 60f;
    const float SlotGap       = 8f;
    const float IconSize      = 38f;
    const float PopFreq       = 12f;
    const float PopDamping    = 0.5f;
    const float PopScale      = 0.16f;
    const float SelectedScale = 1.08f;

    static readonly Color  SlotBg     = new Color( 0f, 0f, 0f ).WithAlpha( 0.45f );
    static readonly Color  SelectedBg = new Color( 0.20f, 0.45f, 0.75f ).WithAlpha( 0.65f );
    const           string IconTint   = "#f2f2fa";                    // SvgPanel.Color is a CSS string
    static readonly Color  BadgeColor = new( 0.92f, 0.92f, 0.70f );

    readonly record struct Item( string Icon, int Count );
    static readonly Item[] Items =
    {
        new( "SVGs/mining.svg",      3 ),
        new( "SVGs/chop.svg",        1 ),
        new( "SVGs/pole.svg",        12 ),
        new( "SVGs/turd.svg",        99 ),
    };

    int _selected;
    readonly SpringFloat[] _pop  = new SpringFloat[N];
    readonly bool[]        _prev = new bool[N];   // number-key edge detect
    bool _dirty = true;
    void Invalidate() => _dirty = true;

    public HotbarView() => Reset();

    public void Reset()
    {
        _selected = 0;
        for ( int i = 0; i < N; i++ )
        {
            _pop[i]  = new SpringFloat( 0f, PopFreq, PopDamping );
            _prev[i] = false;
        }
        _pop[_selected].Velocity = 1f;
        Invalidate();
    }

    public bool Tick( Scene? scene, float dt )
    {
        for ( int i = 0; i < N; i++ )
        {
            bool down = Sandbox.Input.Keyboard.Down( (i + 1).ToString() );
            if ( down && !_prev[i] ) Select( i );
            _prev[i] = down;
            _pop[i].Update( dt );
        }
        if ( NeedsRebuild() ) Invalidate();
        bool d = _dirty; _dirty = false; return d;
    }

    void Select( int i )
    {
        _selected = i;
        _pop[i].Velocity = 1f;   // kick the selected slot's pop
        Invalidate();
    }

    bool NeedsRebuild()
    {
        for ( int i = 0; i < N; i++ )
            if ( !_pop[i].IsSettled ) return true;
        return false;
    }

    public Container Build()
    {
        var bar = new Container { Key = "hotbar", FlexDirection = FlexDirection.Row, Gap = SlotGap };
        for ( int i = 0; i < N; i++ )
            bar.Children.Add( Slot( i ) );
        return bar;
    }

    Container Slot( int i )
    {
        bool  selected = i == _selected;
        float pop      = (selected ? SelectedScale : 1f) + _pop[i].Current * PopScale;

        var slot = new Container
        {
            Key             = $"slot-{i}",
            Position        = PositionMode.Relative,
            Width           = SlotSize,
            Height          = SlotSize,
            BackgroundColor = selected ? SelectedBg : SlotBg,
            BorderRadius    = 6f,
            JustifyContent  = Justify.Center,
            AlignItems      = Align.Center,
            Transform       = Goo.PanelTransform.Scale( pop ),
            PointerEvents   = PointerEvents.None,
        };

        slot.Children.Add( new Goo.SvgPanel
        {
            Key    = "icon",
            Path   = Items[i].Icon,
            Color  = IconTint,
            Width  = IconSize,
            Height = IconSize,
        } );

        // Stack-count badge, bottom-right.
        slot.Children.Add( new Container
        {
            Key       = "badge",
            Position  = PositionMode.Absolute,
            Top       = SlotSize - 18f,
            Left      = SlotSize - 26f,
            FontColor = BadgeColor,
            FontSize  = 13f,
            Children  = { new Text( $"x{Items[i].Count}" ) { Key = "n" } },
        } );

        return slot;
    }
}