UI/Home/MarqueeText.cs
using Sandbox.UI;

namespace sGBA;

public sealed class MarqueeText : Panel
{
	private static readonly Color DefaultTextColor = new( 0.12f, 0.46f, 0.68f, 1f );
	private const float ExtraTextWidth = 8f;
	private string _lastTitle;
	private float _startedAt;
	private float _lastScale;
	private float _lastClipWidth;
	private float _textWidth;
	private Label _primary;
	private Label _duplicate;

	[Parameter]
	public string Title { get; set; }

	[Parameter]
	public float FontSize { get; set; } = 30f;

	[Parameter]
	public float Speed { get; set; } = 92f;

	[Parameter]
	public float Gap { get; set; } = 88f;

	[Parameter]
	public float StartHold { get; set; } = 0.65f;

	[Parameter]
	public string FontName { get; set; } = "Poppins";

	[Parameter]
	public int FontWeight { get; set; } = 400;

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

		UpdateLabels();
	}

	private void UpdateLabels()
	{
		if ( string.IsNullOrWhiteSpace( Title ) )
		{
			SetLabelVisible( false );
			return;
		}

		EnsureLabels();

		float scale = MathF.Max( 0.001f, ScaleToScreen );
		float width = Box.Rect.Width / scale;
		float height = Box.Rect.Height / scale;
		if ( width <= 0f || height <= 0f )
		{
			SetLabelVisible( false );
			return;
		}

		if ( _lastTitle != Title || MathF.Abs( _lastScale - scale ) > 0.001f || MathF.Abs( _lastClipWidth - width ) > 0.001f )
		{
			_lastTitle = Title;
			_lastScale = scale;
			_lastClipWidth = width;
			_startedAt = RealTime.Now;
			_textWidth = MeasureTitleWidth( scale );
			_primary.Text = Title;
			_duplicate.Text = Title;
			_primary.Style.Width = _textWidth;
			_duplicate.Style.Width = _textWidth;
		}
		float textWidth = MathF.Max( 1f, _textWidth );
		_primary.Style.Width = textWidth;
		_duplicate.Style.Width = textWidth;
		_primary.Style.Top = 0f;
		_duplicate.Style.Top = 0f;
		_primary.Style.Height = height;
		_duplicate.Style.Height = height;

		float spacing = MathF.Max( Gap, width * 0.35f );
		float span = textWidth + spacing;
		float scrollSpeed = MathF.Max( 1f, Speed );
		float scrollDuration = span / scrollSpeed;
		float elapsed = PositiveModulo( RealTime.Now - _startedAt, StartHold + scrollDuration );
		float offset = elapsed <= StartHold ? 0f : (elapsed - StartHold) * scrollSpeed;

		_primary.Style.Left = -offset;
		_duplicate.Style.Left = span - offset;
		SetLabelVisible( true );
	}

	private void EnsureLabels()
	{
		if ( _primary != null && _duplicate != null )
			return;

		_primary = AddChild<Label>();
		_duplicate = AddChild<Label>();
		ConfigureLabel( _primary );
		ConfigureLabel( _duplicate );
	}

	private void ConfigureLabel( Label label )
	{
		label.Selectable = false;
		label.Style.Position = PositionMode.Absolute;
		label.Style.FontFamily = FontName;
		label.Style.FontSize = FontSize;
		label.Style.FontWeight = FontWeight;
		label.Style.LineHeight = 38f;
		label.Style.FontColor = DefaultTextColor;
		label.Style.AlignItems = Align.Center;
		label.Style.JustifyContent = Justify.Center;
		label.Style.WhiteSpace = WhiteSpace.NoWrap;
		label.Style.TextAlign = TextAlign.Left;
	}

	private void SetLabelVisible( bool visible )
	{
		if ( _primary != null )
			_primary.Style.Display = visible ? DisplayMode.Flex : DisplayMode.None;

		if ( _duplicate != null )
			_duplicate.Style.Display = visible ? DisplayMode.Flex : DisplayMode.None;
	}

	private float MeasureTitleWidth( float scale )
	{
		var scope = new TextRendering.Scope( Title, DefaultTextColor, FontSize * scale, FontName, FontWeight );
		return MathF.Max( 1f, scope.Measure().x / scale + ExtraTextWidth );
	}

	private static float PositiveModulo( float value, float length )
	{
		if ( length <= 0f )
			return 0f;

		value %= length;
		return value < 0f ? value + length : value;
	}

	protected override int BuildHash()
	{
		return HashCode.Combine( Title, FontSize, Speed, Gap, StartHold, FontName, FontWeight );
	}
}