Code/InputGlyph.cs
using System;
using Sandbox.UI;

namespace Bimp;

/// <summary>
/// The button for an input action, drawn the way the player's device shows it: a key cap on keyboard, the pad's own
/// button (Xbox, PlayStation, Nintendo...) on a controller. It follows them live as they switch device. Sized to the
/// text around it (1.25em tall, as wide as the glyph).
/// </summary>
[StyleSheet.Inline( "bimp-inputglyph", Styles )]
public sealed class InputGlyph : Panel
{
	const string Styles = """
		.inputglyph { height: 1.25em; aspect-ratio: 1; flex-shrink: 0; background-size: contain; background-repeat: no-repeat; background-position: center; }
		""";

	/// <summary> The input action to show the button of, e.g. "use". </summary>
	public string Action { get; set; }

	/// <summary> An analog input to show instead of an action, by <see cref="InputAnalog"/> name, e.g. "LeftStickY". </summary>
	public string Analog { get; set; }

	/// <summary> A keyboard key to show instead of an action, e.g. "escape" (keyboard glyph whatever the device). </summary>
	public string Key { get; set; }

	/// <summary> Glyph resolution. Medium (128px) stays sharp on the big world screens. </summary>
	public InputGlyphSize Size { get; set; } = InputGlyphSize.Medium;

	Texture shown;

	public InputGlyph()
	{
		AddClass( "inputglyph" );
	}

	public override void Tick()
	{
		base.Tick();

		// cheap, and the device can change any frame (the engine's own advice for glyphs)
		var texture = Glyph();
		if ( texture == shown ) return;

		shown = texture;
		Style.SetBackgroundImage( texture );
		Style.AspectRatio = texture is { Height: > 0 } ? texture.Width / (float)texture.Height : 1;
	}

	Texture Glyph()
	{
		if ( !string.IsNullOrWhiteSpace( Key ) ) return Input.Keyboard.GetGlyph( Key, Size );
		if ( Enum.TryParse<InputAnalog>( Analog, true, out var analog ) ) return Input.GetGlyph( analog, Size );
		if ( !string.IsNullOrWhiteSpace( Action ) ) return Input.GetGlyph( Action, Size, false );
		return null;
	}
}