InteractionPromptHud.cs
using Sandbox;
using Sandbox.UI;
using Sandbox.UI.Construct;
using System.Linq;

// Draws "Hold E to ..." plus a fill bar for whichever IInteractable the
// player is currently standing in.
//
// Put this on the player prefab, not in a scene - then it follows the
// player into every map and picks up any interactable automatically,
// including ones added later. It creates its own ScreenPanel.
public sealed class InteractionPromptHud : PanelComponent
{
	Label _promptLabel;
	Panel _barFill;

	protected override void OnStart()
	{
		// Somebody else's copy of the player - their prompts are not our
		// business, and drawing them would put another player's UI on our
		// screen.
		if ( IsProxy )
		{
			Enabled = false;
			return;
		}

		Components.GetOrCreate<ScreenPanel>();

		Panel.StyleSheet.Load( "/InteractionPromptHud.cs.scss" );

		var wrapper = Panel.Add.Panel( "prompt" );
		_promptLabel = wrapper.Add.Label( "", "text" );

		var bar = wrapper.Add.Panel( "bar" );
		_barFill = bar.Add.Panel( "fill" );
	}

	protected override void OnUpdate()
	{
		var active = FindActiveInteractable();

		Panel.SetClass( "show", active is not null );

		if ( active is null )
			return;

		_promptLabel.Text = active.PromptText;

		// Width as a percentage so the bar fills left to right. Only shown
		// once the hold actually starts, so an idle prompt isn't sitting
		// there with a distracting empty bar under it.
		var fraction = active.HoldFraction.Clamp( 0f, 1f );
		_barFill.Style.Width = Length.Fraction( fraction );
		_barFill.SetClass( "active", fraction > 0f );
	}

	// Interfaces work with GetAllComponents, so this picks up anything
	// implementing IInteractable without needing a registry or any manual
	// wiring per interactable.
	//
	// Only one can realistically be in range at a time (their triggers
	// would have to overlap), so first match wins.
	IInteractable FindActiveInteractable()
	{
		return Scene.GetAllComponents<IInteractable>()
			.FirstOrDefault( i => i.PlayerInRange && !i.SuppressPrompt );
	}
}