Controller overlay showing which player is bound to which controller

This commit is contained in:
Barış Hamil 2025-06-20 17:44:51 +03:00
parent 5d136980a3
commit 75d6cc4946
3 changed files with 243 additions and 0 deletions

View file

@ -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;
@ -1686,11 +1687,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;

View file

@ -0,0 +1,50 @@
<window:StyleableAppWindow
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="300"
x:Class="Ryujinx.Ava.UI.Windows.ControllerOverlayWindow"
Title="Controller Overlay"
Focusable="False"
Topmost="True"
WindowStartupLocation="CenterScreen"
Width="400"
Height="250"
SizeToContent="Height">
<Border Background="#E0000000"
CornerRadius="12"
Padding="24"
BorderBrush="#40FFFFFF"
BorderThickness="1">
<StackPanel Spacing="16">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="8">
<TextBlock Text="🎮"
FontSize="18"
VerticalAlignment="Center"/>
<TextBlock Text="Controller Bindings"
FontSize="18"
FontWeight="Bold"
Foreground="White"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Name="PlayerBindings" Spacing="10">
<!-- Player bindings will be added programmatically -->
</StackPanel>
<TextBlock Text="This overlay will disappear in a few seconds"
FontSize="11"
Foreground="#AAAAAA"
HorizontalAlignment="Center"
Margin="0,8,0,0"
FontStyle="Italic"/>
</StackPanel>
</Border>
</window:StyleableAppWindow>

View file

@ -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<InputConfig> inputConfigs)
{
// Clear existing bindings
PlayerBindings.Children.Clear();
// Group controllers by player index
var playerBindings = new Dictionary<PlayerIndex, List<InputConfig>>();
foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= PlayerIndex.Player4))
{
if (!playerBindings.ContainsKey(config.PlayerIndex))
{
playerBindings[config.PlayerIndex] = new List<InputConfig>();
}
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;
}
}
}