DiamondPanel.Buttons.cs
using System;
using Sandbox;

namespace Diamonds;

/// <summary>Button hover/click sounds and the title menu's keyboard/controller selection.</summary>
public sealed partial class DiamondPanel
{
	public enum UiButton { None, TitlePlay, TitleLeaderboard, TitleInstructions, TitleQuit, Instructions, Options, Leave, Restart, Close, Tab1, Tab2, Tab3, DisableFluid, LeaderboardMode }

	const string HoverSound = "sfx/hover.sound", ClickSound = "sfx/click.sound";
	const int TitleButtonCount = 4;
	// Hotload leaves new fields at their defaults, so PLAY (0) must be the default selection.
	int titleSelection;
	UiButton hoveredButton;
	Vector2 lastHoverMouse;

	/// <summary>The highlighted title button, 0 (PLAY) to 3 (QUIT).</summary>
	public int TitleSelection => titleSelection;

	void PlayUiSound( string path )
	{
		if ( IsMuted ) return;
		var sound = Sound.Play( path );
		if ( sound is null ) return;
		sound.SpacialBlend = 0;
		sound.Volume *= DiamondSettings.EffectsGain;
	}

	void PlayClickSound()
	{
		PlayUiSound( ClickSound );
		DiamondHaptics.MenuConfirm();
	}

	/// <summary>
	/// Plays the hover sound when the mouse moves onto a button, and lets a moving mouse select title buttons.
	/// A button appearing under a still mouse is neither announced nor selected, so keyboard/controller selection stays put.
	/// </summary>
	public void UpdateButtonHover()
	{
		var mouse = MousePosition;
		var hovered = HoveredButton();
		bool moved = mouse != lastHoverMouse;
		if ( moved && hovered != hoveredButton && hovered != UiButton.None ) PlayUiSound( HoverSound );
		if ( moved && TitleScreen && hovered is >= UiButton.TitlePlay and <= UiButton.TitleQuit ) titleSelection = hovered - UiButton.TitlePlay;
		hoveredButton = hovered;
		lastHoverMouse = mouse;
	}

	/// <summary>Moves the title selection up/down (clamped, never wrapping onto QUIT), playing the hover sound, and confirms it.</summary>
	public void UpdateTitleMenu( bool up, bool down, bool confirm )
	{
		if ( !TitleScreen || ModalOpen || TransitionPending ) return;
		int previous = titleSelection;
		if ( up ) titleSelection = Math.Max( 0, titleSelection - 1 );
		if ( down ) titleSelection = Math.Min( TitleButtonCount - 1, titleSelection + 1 );
		// Stepping sounds like a hover; pressing into either end doesn't.
		if ( titleSelection != previous )
		{
			PlayUiSound( HoverSound );
			DiamondHaptics.MenuStep();
		}
		if ( confirm ) ActivateTitleButton( titleSelection );
	}

	void ActivateTitleButton( int index )
	{
		titleSelection = Math.Clamp( index, 0, TitleButtonCount - 1 );
		PlayClickSound();
		switch ( titleSelection )
		{
			case 0: BeginTransition( toGame: true ); break;
			case 1: OpenLeaderboard(); break;
			case 2: OpenTutorial(); break;
			default: QuitRequested?.Invoke(); break;
		}
	}

	/// <summary>The clickable button under the mouse, mirroring each click handler's hit test.</summary>
	UiButton HoveredButton()
	{
		if ( TransitionPending ) return UiButton.None;
		var size = Box.Rect.Size;
		var mouse = MousePosition;
		if ( LeaderboardOpen )
		{
			var frame = LeaderboardFrame( size );
			if ( TutorialClose( frame ).IsInside( mouse ) ) return UiButton.Close;
			return LeaderboardOtherTitle.IsInside( LeaderboardLocal( mouse, frame ) ) ? UiButton.LeaderboardMode : UiButton.None;
		}
		if ( OptionsOpen )
		{
			var frame = OptionsFrame( size );
			if ( TutorialClose( frame, OptionsWidth ).IsInside( mouse ) ) return UiButton.Close;
			return DraggingSlider == OptionsSlider.None && DisableFluidRow.IsInside( OptionsLocal( mouse, frame ) ) ? UiButton.DisableFluid : UiButton.None;
		}
		if ( TutorialOpen )
		{
			var frame = TutorialFrame( size );
			if ( TutorialClose( frame ).IsInside( mouse ) ) return UiButton.Close;
			float modalScale = TutorialScale( frame );
			if ( modalScale <= 0 ) return UiButton.None;
			var local = (mouse - TutorialOrigin( frame )) / modalScale;
			for ( int i = 0; i < 3; i++ )
				if ( TutorialTab( i ).IsInside( local ) ) return UiButton.Tab1 + i;
			return UiButton.None;
		}
		float scale = MathF.Min( size.x / CanvasWidth, size.y / CanvasHeight );
		if ( scale <= 0 ) return UiButton.None;
		var point = (mouse - (size - new Vector2( CanvasWidth, CanvasHeight ) * scale) * 0.5f) / scale;
		if ( TitleScreen )
		{
			point -= TitleShakeOffset;
			for ( int i = 0; i < TitleButtonCount; i++ )
				if ( TitleMenuButton( i ).IsInside( point ) ) return UiButton.TitlePlay + i;
			return UiButton.None;
		}
		if ( Board is { GameOver: true } && RestartOverlayFade < 1 && GameOverRestartButton.IsInside( point ) ) return UiButton.Restart;
		if ( TutorialButton( size ).IsInside( point ) ) return UiButton.Instructions;
		if ( OptionsButton( size ).IsInside( point ) ) return UiButton.Options;
		if ( LeaveButton( size ).IsInside( point ) ) return UiButton.Leave;
		return UiButton.None;
	}
}