From 8765dc9901d46f6e4eb9cb5eedbc87f6ff3eee10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Hamil?= Date: Fri, 20 Jun 2025 17:44:51 +0300 Subject: [PATCH] Controller overlay showing which player is bound to which controller --- .../UI/ViewModels/MainWindowViewModel.cs | 27 +++ .../UI/Windows/ControllerOverlayWindow.axaml | 50 ++++++ .../Windows/ControllerOverlayWindow.axaml.cs | 166 ++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml create mode 100644 src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 4ca21e788..83dcc7882 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; using Ryujinx.Common.UI; @@ -1690,11 +1691,37 @@ namespace Ryujinx.Ava.UI.ViewModels SetMainContent(RendererHostControl); RendererHostControl.Focus(); + + // Show controller overlay + ShowControllerOverlay(); }); public static void UpdateGameMetadata(string titleId, TimeSpan playTime) => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); + private void ShowControllerOverlay() + { + try + { + var inputConfigs = ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig + ? ConfigurationState.InstanceExtra.Hid.InputConfig.Value + : ConfigurationState.Instance.Hid.InputConfig.Value; + + // Only show overlay if there are actual controller configurations for players 1-4 + if (inputConfigs?.Any(c => c.PlayerIndex <= PlayerIndex.Player4) == true) + { + var overlay = new Windows.ControllerOverlayWindow(Window); + overlay.ShowControllerBindings(inputConfigs); + overlay.Show(); + } + } + catch (Exception ex) + { + // Log the error but don't let it crash the game launch + Logger.Error?.Print(LogClass.UI, $"Failed to show controller overlay: {ex.Message}"); + } + } + public void RefreshFirmwareStatus() { SystemVersion version = null; diff --git a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml new file mode 100644 index 000000000..d5c205667 --- /dev/null +++ b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs new file mode 100644 index 000000000..e70b8239e --- /dev/null +++ b/src/Ryujinx/UI/Windows/ControllerOverlayWindow.axaml.cs @@ -0,0 +1,166 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using Ryujinx.Common.Configuration.Hid; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class ControllerOverlayWindow : StyleableWindow + { + private const int AutoHideDelayMs = 4000; // 4 seconds + + public ControllerOverlayWindow() + { + InitializeComponent(); + + TransparencyLevelHint = [WindowTransparencyLevel.Transparent]; + SystemDecorations = SystemDecorations.None; + ExtendClientAreaTitleBarHeightHint = 0; + Background = Brushes.Transparent; + CanResize = false; + ShowInTaskbar = false; + } + + public ControllerOverlayWindow(Window owner) : this() + { + if (owner != null) + { + // Position the overlay in the top-right corner of the owner window + WindowStartupLocation = WindowStartupLocation.Manual; + + // Set position after the window is loaded + Loaded += (s, e) => + { + if (owner.WindowState != WindowState.Minimized) + { + Position = new Avalonia.PixelPoint( + (int)(owner.Position.X + owner.Width - Width - 20), + (int)(owner.Position.Y + 50) + ); + } + }; + } + } + + public void ShowControllerBindings(List inputConfigs) + { + // Clear existing bindings + PlayerBindings.Children.Clear(); + + // Group controllers by player index + var playerBindings = new Dictionary>(); + + foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= PlayerIndex.Player4)) + { + if (!playerBindings.ContainsKey(config.PlayerIndex)) + { + playerBindings[config.PlayerIndex] = new List(); + } + playerBindings[config.PlayerIndex].Add(config); + } + + // Add player bindings to UI + for (int i = 0; i < 4; i++) + { + var playerIndex = (PlayerIndex)i; + var playerPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 12 }; + + // Player number with colored background + var playerNumberBorder = new Border + { + Background = GetPlayerColor(i), + CornerRadius = new Avalonia.CornerRadius(12), + Padding = new Avalonia.Thickness(8, 4), + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + + var playerLabel = new TextBlock + { + Text = $"P{i + 1}", + FontWeight = FontWeight.Bold, + Foreground = Brushes.White, + FontSize = 12, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center + }; + playerNumberBorder.Child = playerLabel; + playerPanel.Children.Add(playerNumberBorder); + + if (playerBindings.ContainsKey(playerIndex)) + { + var controllers = playerBindings[playerIndex]; + var controllerNames = controllers.Select(c => GetControllerDisplayName(c)).ToList(); + + var controllerText = new TextBlock + { + Text = string.Join(", ", controllerNames), + Foreground = Brushes.LightGreen, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + FontWeight = FontWeight.SemiBold + }; + playerPanel.Children.Add(controllerText); + } + else + { + var noControllerText = new TextBlock + { + Text = "No controller assigned", + Foreground = Brushes.Gray, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + FontStyle = FontStyle.Italic + }; + playerPanel.Children.Add(noControllerText); + } + + PlayerBindings.Children.Add(playerPanel); + } + + // Auto-hide after delay + _ = Task.Run(async () => + { + await Task.Delay(AutoHideDelayMs); + await Dispatcher.UIThread.InvokeAsync(() => + { + Close(); + }); + }); + } + + private static IBrush GetPlayerColor(int playerIndex) + { + return playerIndex switch + { + 0 => new SolidColorBrush(Color.FromRgb(255, 92, 92)), // Red for Player 1 + 1 => new SolidColorBrush(Color.FromRgb(54, 162, 235)), // Blue for Player 2 + 2 => new SolidColorBrush(Color.FromRgb(255, 206, 84)), // Yellow for Player 3 + 3 => new SolidColorBrush(Color.FromRgb(75, 192, 192)), // Green for Player 4 + _ => new SolidColorBrush(Color.FromRgb(128, 128, 128)) // Gray fallback + }; + } + + private static string GetControllerDisplayName(InputConfig config) + { + if (string.IsNullOrEmpty(config.Name)) + { + return config.Backend switch + { + InputBackendType.WindowKeyboard => "Keyboard", + InputBackendType.GamepadSDL2 => "Controller", + _ => "Unknown" + }; + } + + // Truncate long controller names + string name = config.Name; + if (name.Length > 25) + { + name = name.Substring(0, 22) + "..."; + } + + return name; + } + } +}