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;
}
}