mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-06-27 15:36:23 +02:00
Merge branch 'input-cycle' into 'master'
Input cycling and overlay system See merge request [ryubing/ryujinx!72](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/72)
This commit is contained in:
commit
0e9acdfed0
35 changed files with 2186 additions and 151 deletions
|
@ -24372,6 +24372,156 @@
|
|||
"zh_TW": "只在按下時"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabHotkeysCycleInputDevicePlayerX",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Cycle Input Device {0}:",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ControllerOverlayTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Controller Bindings",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ControllerOverlayNoController",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "No controller assigned",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ControllerOverlayKeyboard",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Keyboard",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ControllerOverlayController",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Controller",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ControllerOverlayUnknown",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Unknown",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListLastUpdated",
|
||||
"Translations": {
|
||||
|
@ -25021,6 +25171,131 @@
|
|||
"zh_CN": "动态 Rich Presence",
|
||||
"zh_TW": "動態 Rich Presence"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabUIControllerOverlay",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Controller Overlay",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabUIControllerOverlayGameStartDuration",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show on game start (seconds):",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabUIControllerOverlayGameStartDurationTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Duration to show controller overlay when game starts (0 = disabled)",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabUIControllerOverlayInputCycleDuration",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Show on input cycle (seconds):",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SettingsTabUIControllerOverlayInputCycleDurationTooltip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Duration to show controller overlay when cycling inputs (0 = disabled)",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides default input configurations for keyboard and controller devices
|
||||
/// </summary>
|
||||
public static class DefaultInputConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a default keyboard input configuration
|
||||
/// </summary>
|
||||
/// <param name="id">Device ID</param>
|
||||
/// <param name="name">Device name</param>
|
||||
/// <param name="playerIndex">Player index</param>
|
||||
/// <param name="controllerType">Controller type (defaults to ProController)</param>
|
||||
/// <returns>Default keyboard input configuration</returns>
|
||||
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfig(string id, string name, PlayerIndex playerIndex, ControllerType controllerType = ControllerType.ProController)
|
||||
{
|
||||
return new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = id,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
PlayerIndex = playerIndex,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default controller input configuration
|
||||
/// </summary>
|
||||
/// <param name="id">Device ID</param>
|
||||
/// <param name="name">Device name</param>
|
||||
/// <param name="playerIndex">Player index</param>
|
||||
/// <param name="isNintendoStyle">Whether to use Nintendo-style button mapping</param>
|
||||
/// <returns>Default controller input configuration</returns>
|
||||
public static StandardControllerInputConfig CreateDefaultControllerConfig(string id, string name, PlayerIndex playerIndex, bool isNintendoStyle = false)
|
||||
{
|
||||
// Split the ID for controller configs
|
||||
string cleanId = id.Split(" ")[0];
|
||||
|
||||
return new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL2,
|
||||
Id = cleanId,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
PlayerIndex = playerIndex,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
EnableMotion = true,
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
GyroDeadzone = 1,
|
||||
Sensitivity = 100,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
EnableRumble = false,
|
||||
WeakRumble = 1f,
|
||||
StrongRumble = 1f,
|
||||
},
|
||||
Led = new LedConfigController
|
||||
{
|
||||
EnableLed = false,
|
||||
TurnOffLed = false,
|
||||
UseRainbow = false,
|
||||
LedColor = 0xFFFFFFFF,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the short name of a gamepad by removing SDL prefix and truncating if too long
|
||||
/// </summary>
|
||||
/// <param name="name">Full gamepad name</param>
|
||||
/// <param name="maxLength">Maximum length before truncation (default: 50)</param>
|
||||
/// <returns>Short gamepad name</returns>
|
||||
public static string GetShortGamepadName(string name, int maxLength = 50)
|
||||
{
|
||||
const string SdlGamepadNamePrefix = "SDL2 Gamepad ";
|
||||
const string Ellipsis = "...";
|
||||
|
||||
// First remove SDL prefix if present
|
||||
string shortName = name;
|
||||
if (name.StartsWith(SdlGamepadNamePrefix))
|
||||
{
|
||||
shortName = name[SdlGamepadNamePrefix.Length..];
|
||||
}
|
||||
|
||||
// Then truncate if too long
|
||||
if (shortName.Length > maxLength)
|
||||
{
|
||||
return $"{shortName.AsSpan(0, maxLength - Ellipsis.Length)}{Ellipsis}";
|
||||
}
|
||||
|
||||
return shortName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a controller uses Nintendo-style button mapping
|
||||
/// </summary>
|
||||
/// <param name="name">Controller name</param>
|
||||
/// <returns>True if Nintendo-style mapping should be used</returns>
|
||||
public static bool IsNintendoStyleController(string name)
|
||||
{
|
||||
return name.Contains("Nintendo");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,5 +15,14 @@ namespace Ryujinx.Common.Configuration.Hid
|
|||
public Key CustomVSyncIntervalDecrement { get; set; }
|
||||
public Key TurboMode { get; set; }
|
||||
public bool TurboModeWhileHeld { get; set; }
|
||||
public Key CycleInputDevicePlayer1 { get; set; }
|
||||
public Key CycleInputDevicePlayer2 { get; set; }
|
||||
public Key CycleInputDevicePlayer3 { get; set; }
|
||||
public Key CycleInputDevicePlayer4 { get; set; }
|
||||
public Key CycleInputDevicePlayer5 { get; set; }
|
||||
public Key CycleInputDevicePlayer6 { get; set; }
|
||||
public Key CycleInputDevicePlayer7 { get; set; }
|
||||
public Key CycleInputDevicePlayer8 { get; set; }
|
||||
public Key CycleInputDeviceHandheld { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,5 +41,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||
public void SetScalingFilterLevel(float level) { }
|
||||
|
||||
public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying implementation window for direct access
|
||||
/// </summary>
|
||||
public IWindow BaseWindow => _impl.Window;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||
/// GPU synchronization manager.
|
||||
/// </summary>
|
||||
public SynchronizationManager Synchronization { get; }
|
||||
public IOverlayManager OverlayManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Presentation window.
|
||||
|
@ -121,14 +122,18 @@ namespace Ryujinx.Graphics.Gpu
|
|||
/// Creates a new instance of the GPU emulation context.
|
||||
/// </summary>
|
||||
/// <param name="renderer">Host renderer</param>
|
||||
public GpuContext(IRenderer renderer, DirtyHacks hacks)
|
||||
/// <param name="hacks">Enabled dirty hacks</param>
|
||||
/// <param name="overlayManager">Overlay manager for rendering overlays</param>
|
||||
public GpuContext(IRenderer renderer, DirtyHacks hacks, IOverlayManager overlayManager)
|
||||
{
|
||||
Renderer = renderer;
|
||||
|
||||
GPFifo = new GPFifoDevice(this);
|
||||
|
||||
Synchronization = new SynchronizationManager();
|
||||
|
||||
|
||||
OverlayManager = overlayManager;
|
||||
|
||||
Window = new Window(this);
|
||||
|
||||
HostInitalized = new ManualResetEvent(false);
|
||||
|
@ -462,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu
|
|||
RunDeferredActions();
|
||||
|
||||
Renderer.Dispose();
|
||||
|
||||
OverlayManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
54
src/Ryujinx.Graphics.Gpu/IOverlay.cs
Normal file
54
src/Ryujinx.Graphics.Gpu/IOverlay.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for overlay functionality
|
||||
/// </summary>
|
||||
public interface IOverlay : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the overlay
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the overlay is visible
|
||||
/// </summary>
|
||||
bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Opacity of the overlay (0.0 to 1.0)
|
||||
/// </summary>
|
||||
float Opacity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// X position of the overlay
|
||||
/// </summary>
|
||||
float X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Y position of the overlay
|
||||
/// </summary>
|
||||
float Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Z-index for overlay ordering
|
||||
/// </summary>
|
||||
int ZIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay (for animations)
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time elapsed since last update</param>
|
||||
/// <param name="screenSize">Current screen size</param>
|
||||
void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render this overlay
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to</param>
|
||||
void Render(SKCanvas canvas);
|
||||
}
|
||||
}
|
30
src/Ryujinx.Graphics.Gpu/IOverlayManager.cs
Normal file
30
src/Ryujinx.Graphics.Gpu/IOverlayManager.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for overlay management functionality
|
||||
/// </summary>
|
||||
public interface IOverlayManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an overlay to the manager
|
||||
/// </summary>
|
||||
/// <param name="overlay">The overlay to add</param>
|
||||
void AddOverlay(IOverlay overlay);
|
||||
|
||||
/// <summary>
|
||||
/// Update all overlays (for animations)
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time elapsed since last update</param>
|
||||
/// <param name="screenSize">Current screen size</param>
|
||||
void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render all visible overlays
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to render to</param>
|
||||
void Render(SKCanvas canvas);
|
||||
}
|
||||
}
|
|
@ -14,4 +14,8 @@
|
|||
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
using Ryujinx.Graphics.Gpu.Memory;
|
||||
using Ryujinx.Graphics.Texture;
|
||||
using Ryujinx.Memory.Range;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Graphics.Gpu
|
||||
|
@ -15,6 +19,7 @@ namespace Ryujinx.Graphics.Gpu
|
|||
public class Window
|
||||
{
|
||||
private readonly GpuContext _context;
|
||||
private DateTime? _lastUpdateTime = null;
|
||||
|
||||
/// <summary>
|
||||
/// Texture presented on the window.
|
||||
|
@ -207,6 +212,9 @@ namespace Ryujinx.Graphics.Gpu
|
|||
|
||||
texture.SynchronizeMemory();
|
||||
|
||||
// Add overlays by modifying texture data directly
|
||||
AddOverlaysToTexture(texture, pt.Crop);
|
||||
|
||||
ImageCrop crop = new(
|
||||
(int)(pt.Crop.Left * texture.ScaleFactor),
|
||||
(int)MathF.Ceiling(pt.Crop.Right * texture.ScaleFactor),
|
||||
|
@ -244,6 +252,99 @@ namespace Ryujinx.Graphics.Gpu
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add overlay to the overlay manager
|
||||
/// </summary>
|
||||
public void AddOverlay(IOverlay overlay)
|
||||
{
|
||||
_context.OverlayManager.AddOverlay(overlay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add overlays to the texture using SkiaSharp
|
||||
/// </summary>
|
||||
/// <param name="texture">The texture to modify</param>
|
||||
/// <param name="crop">The crop information containing flip flags</param>
|
||||
private void AddOverlaysToTexture(Image.Texture texture, ImageCrop crop)
|
||||
{
|
||||
try
|
||||
{
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
if (_lastUpdateTime != null)
|
||||
{
|
||||
// Calculate delta time for lifespan updates
|
||||
float deltaTime = (float)(currentTime - _lastUpdateTime.Value).TotalSeconds;
|
||||
_context.OverlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height));
|
||||
}
|
||||
|
||||
// Update overlay animations
|
||||
_lastUpdateTime = currentTime;
|
||||
|
||||
// Get texture data from host texture
|
||||
using var pinnedData = texture.HostTexture.GetData();
|
||||
var data = pinnedData.Get().ToArray();
|
||||
if (data == null || data.Length == 0)
|
||||
return;
|
||||
|
||||
int width = texture.Info.Width;
|
||||
int height = texture.Info.Height;
|
||||
int bytesPerPixel = texture.Info.FormatInfo.BytesPerPixel;
|
||||
|
||||
// Determine the SKColorType based on bytes per pixel
|
||||
SKColorType colorType = bytesPerPixel switch
|
||||
{
|
||||
4 => SKColorType.Rgba8888,
|
||||
3 => SKColorType.Rgb888x,
|
||||
2 => SKColorType.Rgb565,
|
||||
_ => SKColorType.Rgba8888
|
||||
};
|
||||
|
||||
// Create SKBitmap from texture data
|
||||
var imageInfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
|
||||
using var bitmap = new SKBitmap(imageInfo);
|
||||
|
||||
// Copy texture data to bitmap
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
bitmap.SetPixels((IntPtr)dataPtr);
|
||||
}
|
||||
}
|
||||
|
||||
// Create canvas for drawing overlays
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
|
||||
// Flip Y-axis if the game/texture requires it
|
||||
// Some games have textures that are already flipped, while others need flipping
|
||||
if (crop.FlipY)
|
||||
{
|
||||
canvas.Scale(1, -1);
|
||||
canvas.Translate(0, -height);
|
||||
}
|
||||
|
||||
// Render all overlays
|
||||
_context.OverlayManager.Render(canvas);
|
||||
|
||||
// Copy modified bitmap data back to texture data array
|
||||
var pixels = bitmap.Bytes;
|
||||
if (pixels.Length <= data.Length)
|
||||
{
|
||||
Array.Copy(pixels, data, pixels.Length);
|
||||
}
|
||||
|
||||
// Upload modified data back to texture
|
||||
var memoryOwner = MemoryOwner<byte>.Rent(data.Length);
|
||||
data.CopyTo(memoryOwner.Span);
|
||||
texture.HostTexture.SetData(memoryOwner); // SetData will dispose the MemoryOwner
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail if overlay rendering doesn't work
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Overlay rendering failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that a frame on the queue is ready to be acquired.
|
||||
/// </summary>
|
||||
|
|
|
@ -3,6 +3,7 @@ using Ryujinx.Audio.Integration;
|
|||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
|
@ -65,6 +66,12 @@ namespace Ryujinx.HLE
|
|||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal IHostUIHandler HostUIHandler { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The overlay manager to use for all overlay operations.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal IOverlayManager OverlayManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Control the memory configuration used by the emulation context.
|
||||
/// </summary>
|
||||
|
@ -262,7 +269,8 @@ namespace Ryujinx.HLE
|
|||
UserChannelPersistence userChannelPersistence,
|
||||
IRenderer gpuRenderer,
|
||||
IHardwareDeviceDriver audioDeviceDriver,
|
||||
IHostUIHandler hostUIHandler
|
||||
IHostUIHandler hostUIHandler,
|
||||
IOverlayManager overlayManager
|
||||
)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
|
@ -273,6 +281,7 @@ namespace Ryujinx.HLE
|
|||
GpuRenderer = gpuRenderer;
|
||||
AudioDeviceDriver = audioDeviceDriver;
|
||||
HostUIHandler = hostUIHandler;
|
||||
OverlayManager = overlayManager;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace Ryujinx.HLE
|
|||
DirtyHacks = new DirtyHacks(Configuration.Hacks);
|
||||
AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver);
|
||||
Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags);
|
||||
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks);
|
||||
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks, Configuration.OverlayManager);
|
||||
System = new HOS.Horizon(this);
|
||||
Statistics = new PerformanceStatistics(this);
|
||||
Hid = new Hid(this, System.HidStorage);
|
||||
|
|
|
@ -15,5 +15,14 @@ namespace Ryujinx.Ava.Common
|
|||
CustomVSyncIntervalIncrement,
|
||||
CustomVSyncIntervalDecrement,
|
||||
TurboMode,
|
||||
CycleInputDevicePlayer1,
|
||||
CycleInputDevicePlayer2,
|
||||
CycleInputDevicePlayer3,
|
||||
CycleInputDevicePlayer4,
|
||||
CycleInputDevicePlayer5,
|
||||
CycleInputDevicePlayer6,
|
||||
CycleInputDevicePlayer7,
|
||||
CycleInputDevicePlayer8,
|
||||
CycleInputDeviceHandheld,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions;
|
||||
using Projektanker.Icons.Avalonia;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
|
@ -18,11 +19,19 @@ namespace Ryujinx.Ava.Common.Markup
|
|||
|
||||
internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension<string>
|
||||
{
|
||||
public IValueConverter Converter { get; set; }
|
||||
|
||||
public override string Name => "Translation";
|
||||
protected override string Value => LocaleManager.Instance[key];
|
||||
|
||||
protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension)
|
||||
=> bindingExtension.Source = LocaleManager.Instance;
|
||||
{
|
||||
bindingExtension.Source = LocaleManager.Instance;
|
||||
if (Converter != null)
|
||||
{
|
||||
bindingExtension.Converter = Converter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class WindowTitleExtension(LocaleKeys key, bool includeVersion) : BasicMarkupExtension<string>
|
||||
|
|
|
@ -17,6 +17,7 @@ using Ryujinx.Graphics.OpenGL;
|
|||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.UI.Overlay;
|
||||
using Silk.NET.Vulkan;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
@ -348,7 +349,8 @@ namespace Ryujinx.Headless
|
|||
_userChannelPersistence,
|
||||
renderer.TryMakeThreaded(options.BackendThreading),
|
||||
new SDL2HardwareDeviceDriver(),
|
||||
window
|
||||
window,
|
||||
new OverlayManager()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ using Ryujinx.Common.Utilities;
|
|||
using Ryujinx.Graphics.GAL;
|
||||
using Ryujinx.Graphics.GAL.Multithreading;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
using Ryujinx.UI.Overlay;
|
||||
using Ryujinx.Graphics.OpenGL;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
|
@ -40,6 +41,11 @@ using Ryujinx.HLE.HOS;
|
|||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System.Linq;
|
||||
using SkiaSharp;
|
||||
using SPB.Graphics.Vulkan;
|
||||
using System;
|
||||
|
@ -124,6 +130,7 @@ namespace Ryujinx.Ava.Systems
|
|||
private readonly bool _isFirmwareTitle;
|
||||
|
||||
private readonly Lock _lockObject = new();
|
||||
private ControllerOverlay _controllerOverlay;
|
||||
|
||||
public event EventHandler AppExit;
|
||||
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
|
||||
|
@ -924,9 +931,13 @@ namespace Ryujinx.Ava.Systems
|
|||
_userChannelPersistence,
|
||||
renderer.TryMakeThreaded(ConfigurationState.Instance.Graphics.BackendThreading),
|
||||
InitializeAudio(),
|
||||
_viewModel.UiHandler
|
||||
_viewModel.UiHandler,
|
||||
new OverlayManager()
|
||||
)
|
||||
);
|
||||
|
||||
_controllerOverlay = new ControllerOverlay();
|
||||
Device.Gpu.Window.AddOverlay(_controllerOverlay);
|
||||
}
|
||||
|
||||
private static IHardwareDeviceDriver InitializeAudio()
|
||||
|
@ -1333,6 +1344,33 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
_viewModel.Volume = Device.GetVolume();
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer1:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player1);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer2:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player2);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer3:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player3);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer4:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player4);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer5:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player5);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer6:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player6);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer7:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player7);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDevicePlayer8:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player8);
|
||||
break;
|
||||
case KeyboardHotkeyState.CycleInputDeviceHandheld:
|
||||
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Handheld);
|
||||
break;
|
||||
case KeyboardHotkeyState.None:
|
||||
(_keyboardInterface as AvaloniaKeyboard).Clear();
|
||||
break;
|
||||
|
@ -1366,6 +1404,118 @@ namespace Ryujinx.Ava.Systems
|
|||
return true;
|
||||
}
|
||||
|
||||
private void CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex playerIndex)
|
||||
{
|
||||
// Get current input configuration
|
||||
List<InputConfig> currentConfig = new(ConfigurationState.Instance.Hid.InputConfig.Value);
|
||||
|
||||
// Find current configuration for this player
|
||||
InputConfig playerConfig = currentConfig.FirstOrDefault(x => x.PlayerIndex == (PlayerIndex)playerIndex);
|
||||
|
||||
// Get available devices from InputManager
|
||||
List<(DeviceType Type, string Id, string Name)> availableDevices = [];
|
||||
|
||||
// Add disabled option
|
||||
availableDevices.Add((DeviceType.None, "disabled", "Disabled"));
|
||||
|
||||
// Add keyboard devices
|
||||
foreach (string id in _inputManager.KeyboardDriver.GamepadsIds)
|
||||
{
|
||||
using var gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
|
||||
if (gamepad != null)
|
||||
{
|
||||
availableDevices.Add((DeviceType.Keyboard, id, gamepad.Name));
|
||||
}
|
||||
}
|
||||
|
||||
// Add controller devices
|
||||
int controllerNumber = 0;
|
||||
foreach (string id in _inputManager.GamepadDriver.GamepadsIds)
|
||||
{
|
||||
using var gamepad = _inputManager.GamepadDriver.GetGamepad(id);
|
||||
if (gamepad != null)
|
||||
{
|
||||
string name = $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber++})";
|
||||
availableDevices.Add((DeviceType.Controller, id, name));
|
||||
}
|
||||
}
|
||||
|
||||
// Find current device index
|
||||
int currentIndex = 0;
|
||||
if (playerConfig != null)
|
||||
{
|
||||
DeviceType currentType = DeviceType.None;
|
||||
if (playerConfig is StandardKeyboardInputConfig)
|
||||
currentType = DeviceType.Keyboard;
|
||||
else if (playerConfig is StandardControllerInputConfig)
|
||||
currentType = DeviceType.Controller;
|
||||
|
||||
currentIndex = availableDevices.FindIndex(x => x.Type == currentType && x.Id == playerConfig.Id);
|
||||
if (currentIndex == -1) currentIndex = 0;
|
||||
}
|
||||
|
||||
// Cycle to next device
|
||||
int nextIndex = (currentIndex + 1) % availableDevices.Count;
|
||||
var nextDevice = availableDevices[nextIndex];
|
||||
|
||||
// Remove existing configuration for this player
|
||||
currentConfig.RemoveAll(x => x.PlayerIndex == (PlayerIndex)playerIndex);
|
||||
|
||||
// Add new configuration if not disabled
|
||||
if (nextDevice.Type != DeviceType.None)
|
||||
{
|
||||
InputConfig newConfig = CreateDefaultInputConfig(nextDevice, (PlayerIndex)playerIndex);
|
||||
if (newConfig != null)
|
||||
{
|
||||
currentConfig.Add(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the new configuration
|
||||
ConfigurationState.Instance.Hid.InputConfig.Value = currentConfig;
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
|
||||
// Reload the input system
|
||||
NpadManager.ReloadConfiguration(currentConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
||||
|
||||
// Show controller overlay
|
||||
ShowControllerOverlay(currentConfig, ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value);
|
||||
}
|
||||
|
||||
private InputConfig CreateDefaultInputConfig((DeviceType Type, string Id, string Name) device, PlayerIndex playerIndex)
|
||||
{
|
||||
if (device.Type == DeviceType.Keyboard)
|
||||
{
|
||||
return DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(device.Id, device.Name, playerIndex);
|
||||
}
|
||||
else if (device.Type == DeviceType.Controller)
|
||||
{
|
||||
bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(device.Name);
|
||||
return DefaultInputConfigurationProvider.CreateDefaultControllerConfig(device.Id, device.Name, playerIndex, isNintendoStyle);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void ShowControllerOverlay(List<InputConfig> inputConfigs, int duration)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_controllerOverlay != null)
|
||||
{
|
||||
_controllerOverlay.ShowControllerBindings(inputConfigs, duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - ControllerOverlay is null");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private KeyboardHotkeyState GetHotkeyState()
|
||||
{
|
||||
KeyboardHotkeyState state = KeyboardHotkeyState.None;
|
||||
|
@ -1418,6 +1568,42 @@ namespace Ryujinx.Ava.Systems
|
|||
{
|
||||
state = KeyboardHotkeyState.TurboMode;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer1))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer1;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer2))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer2;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer3))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer3;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer4))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer4;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer5))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer5;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer6))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer6;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer7))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer7;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer8))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDevicePlayer8;
|
||||
}
|
||||
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDeviceHandheld))
|
||||
{
|
||||
state = KeyboardHotkeyState.CycleInputDeviceHandheld;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 70;
|
||||
public const int CurrentVersion = 72;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
|
@ -217,6 +217,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
/// </summary>
|
||||
public HideCursorMode HideCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when game starts (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public int ControllerOverlayGameStartDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when input is cycled (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public int ControllerOverlayInputCycleDuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Vertical Sync
|
||||
/// </summary>
|
||||
|
|
|
@ -53,6 +53,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
ShowOldUI.Value = shouldLoadFromFile ? cff.ShowTitleBar : ShowOldUI.Value; // Get from global config only
|
||||
EnableHardwareAcceleration.Value = shouldLoadFromFile ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only
|
||||
HideCursor.Value = cff.HideCursor;
|
||||
ControllerOverlayGameStartDuration.Value = cff.ControllerOverlayGameStartDuration;
|
||||
ControllerOverlayInputCycleDuration.Value = cff.ControllerOverlayInputCycleDuration;
|
||||
|
||||
Logger.EnableFileLog.Value = cff.EnableFileLog;
|
||||
Logger.EnableDebug.Value = cff.LoggingEnableDebug;
|
||||
|
@ -479,7 +481,40 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
};
|
||||
}
|
||||
),
|
||||
(69, static cff => cff.SkipUserProfiles = false)
|
||||
(69, static cff => cff.SkipUserProfiles = false),
|
||||
(71, static cff =>
|
||||
{
|
||||
cff.ControllerOverlayGameStartDuration = 3;
|
||||
cff.ControllerOverlayInputCycleDuration = 2;
|
||||
}),
|
||||
(72, static cff =>
|
||||
{
|
||||
cff.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
|
||||
Screenshot = cff.Hotkeys.Screenshot,
|
||||
ShowUI = cff.Hotkeys.ShowUI,
|
||||
Pause = cff.Hotkeys.Pause,
|
||||
ToggleMute = cff.Hotkeys.ToggleMute,
|
||||
ResScaleUp = cff.Hotkeys.ResScaleUp,
|
||||
ResScaleDown = cff.Hotkeys.ResScaleDown,
|
||||
VolumeUp = cff.Hotkeys.VolumeUp,
|
||||
VolumeDown = cff.Hotkeys.VolumeDown,
|
||||
CustomVSyncIntervalIncrement = cff.Hotkeys.CustomVSyncIntervalIncrement,
|
||||
CustomVSyncIntervalDecrement = cff.Hotkeys.CustomVSyncIntervalDecrement,
|
||||
TurboMode = cff.Hotkeys.TurboMode,
|
||||
TurboModeWhileHeld = cff.Hotkeys.TurboModeWhileHeld,
|
||||
CycleInputDevicePlayer1 = Key.Unbound,
|
||||
CycleInputDevicePlayer2 = Key.Unbound,
|
||||
CycleInputDevicePlayer3 = Key.Unbound,
|
||||
CycleInputDevicePlayer4 = Key.Unbound,
|
||||
CycleInputDevicePlayer5 = Key.Unbound,
|
||||
CycleInputDevicePlayer6 = Key.Unbound,
|
||||
CycleInputDevicePlayer7 = Key.Unbound,
|
||||
CycleInputDevicePlayer8 = Key.Unbound,
|
||||
CycleInputDeviceHandheld = Key.Unbound
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -846,6 +846,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
/// </summary>
|
||||
public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when game starts (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ControllerOverlayGameStartDuration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration to show controller overlay when input is cycled (seconds, 0 = disabled)
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ControllerOverlayInputCycleDuration { get; private set; }
|
||||
|
||||
private ConfigurationState()
|
||||
{
|
||||
UI = new UISection();
|
||||
|
@ -863,6 +873,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
RememberWindowState = new ReactiveObject<bool>();
|
||||
ShowOldUI = new ReactiveObject<bool>();
|
||||
EnableHardwareAcceleration = new ReactiveObject<bool>();
|
||||
ControllerOverlayGameStartDuration = new ReactiveObject<int>();
|
||||
ControllerOverlayInputCycleDuration = new ReactiveObject<int>();
|
||||
}
|
||||
|
||||
public HleConfiguration CreateHleConfiguration() =>
|
||||
|
|
|
@ -65,6 +65,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
ShowTitleBar = ShowOldUI,
|
||||
EnableHardwareAcceleration = EnableHardwareAcceleration,
|
||||
HideCursor = HideCursor,
|
||||
ControllerOverlayGameStartDuration = ControllerOverlayGameStartDuration,
|
||||
ControllerOverlayInputCycleDuration = ControllerOverlayInputCycleDuration,
|
||||
VSyncMode = Graphics.VSyncMode,
|
||||
EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval,
|
||||
CustomVSyncInterval = Graphics.CustomVSyncInterval,
|
||||
|
@ -190,6 +192,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
ShowOldUI.Value = !OperatingSystem.IsWindows();
|
||||
EnableHardwareAcceleration.Value = true;
|
||||
HideCursor.Value = HideCursorMode.OnIdle;
|
||||
ControllerOverlayGameStartDuration.Value = 3;
|
||||
ControllerOverlayInputCycleDuration.Value = 2;
|
||||
Graphics.VSyncMode.Value = VSyncMode.Switch;
|
||||
Graphics.CustomVSyncInterval.Value = 120;
|
||||
Graphics.EnableCustomVSyncInterval.Value = false;
|
||||
|
@ -269,7 +273,16 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||
CustomVSyncIntervalIncrement = Key.Unbound,
|
||||
CustomVSyncIntervalDecrement = Key.Unbound,
|
||||
TurboMode = Key.Unbound,
|
||||
TurboModeWhileHeld = false
|
||||
TurboModeWhileHeld = false,
|
||||
CycleInputDevicePlayer1 = Key.Unbound,
|
||||
CycleInputDevicePlayer2 = Key.Unbound,
|
||||
CycleInputDevicePlayer3 = Key.Unbound,
|
||||
CycleInputDevicePlayer4 = Key.Unbound,
|
||||
CycleInputDevicePlayer5 = Key.Unbound,
|
||||
CycleInputDevicePlayer6 = Key.Unbound,
|
||||
CycleInputDevicePlayer7 = Key.Unbound,
|
||||
CycleInputDevicePlayer8 = Key.Unbound,
|
||||
CycleInputDeviceHandheld = Key.Unbound
|
||||
};
|
||||
Hid.RainbowSpeed.Value = 1f;
|
||||
Hid.InputConfig.Value =
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class PlayerHotkeyLabelConverter : IValueConverter
|
||||
{
|
||||
public static readonly PlayerHotkeyLabelConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string playerName && !string.IsNullOrEmpty(playerName))
|
||||
{
|
||||
string baseText = LocaleManager.Instance[LocaleKeys.SettingsTabHotkeysCycleInputDevicePlayerX];
|
||||
return string.Format(baseText, playerName);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,24 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||
|
||||
[ObservableProperty] private bool _turboModeWhileHeld;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer1;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer2;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer3;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer4;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer5;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer6;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer7;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDevicePlayer8;
|
||||
|
||||
[ObservableProperty] private Key _cycleInputDeviceHandheld;
|
||||
|
||||
public HotkeyConfig(KeyboardHotkeys config)
|
||||
{
|
||||
if (config == null)
|
||||
|
@ -50,6 +68,15 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
|
||||
TurboMode = config.TurboMode;
|
||||
TurboModeWhileHeld = config.TurboModeWhileHeld;
|
||||
CycleInputDevicePlayer1 = config.CycleInputDevicePlayer1;
|
||||
CycleInputDevicePlayer2 = config.CycleInputDevicePlayer2;
|
||||
CycleInputDevicePlayer3 = config.CycleInputDevicePlayer3;
|
||||
CycleInputDevicePlayer4 = config.CycleInputDevicePlayer4;
|
||||
CycleInputDevicePlayer5 = config.CycleInputDevicePlayer5;
|
||||
CycleInputDevicePlayer6 = config.CycleInputDevicePlayer6;
|
||||
CycleInputDevicePlayer7 = config.CycleInputDevicePlayer7;
|
||||
CycleInputDevicePlayer8 = config.CycleInputDevicePlayer8;
|
||||
CycleInputDeviceHandheld = config.CycleInputDeviceHandheld;
|
||||
}
|
||||
|
||||
public KeyboardHotkeys GetConfig() =>
|
||||
|
@ -67,7 +94,16 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
|
||||
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
|
||||
TurboMode = TurboMode,
|
||||
TurboModeWhileHeld = TurboModeWhileHeld
|
||||
TurboModeWhileHeld = TurboModeWhileHeld,
|
||||
CycleInputDevicePlayer1 = CycleInputDevicePlayer1,
|
||||
CycleInputDevicePlayer2 = CycleInputDevicePlayer2,
|
||||
CycleInputDevicePlayer3 = CycleInputDevicePlayer3,
|
||||
CycleInputDevicePlayer4 = CycleInputDevicePlayer4,
|
||||
CycleInputDevicePlayer5 = CycleInputDevicePlayer5,
|
||||
CycleInputDevicePlayer6 = CycleInputDevicePlayer6,
|
||||
CycleInputDevicePlayer7 = CycleInputDevicePlayer7,
|
||||
CycleInputDevicePlayer8 = CycleInputDevicePlayer8,
|
||||
CycleInputDeviceHandheld = CycleInputDeviceHandheld
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
292
src/Ryujinx/UI/Overlay/ControllerOverlay.cs
Normal file
292
src/Ryujinx/UI/Overlay/ControllerOverlay.cs
Normal file
|
@ -0,0 +1,292 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OriginalInputConfig = Ryujinx.Common.Configuration.Hid.InputConfig;
|
||||
using OriginalPlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex;
|
||||
using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendType;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller overlay that shows controller bindings matching the original AXAML design
|
||||
/// </summary>
|
||||
public class ControllerOverlay : Overlay
|
||||
{
|
||||
private const float OverlayWidth = 400;
|
||||
private const float Padding = 24;
|
||||
private const float PlayerSpacing = 12;
|
||||
private const float PlayerRowHeight = 32;
|
||||
|
||||
private const float TitleTextSize = 25;
|
||||
private const float PlayerTextSize = 22;
|
||||
|
||||
private float _lifespan = 0f;
|
||||
|
||||
public ControllerOverlay() : base("ControllerOverlay")
|
||||
{
|
||||
CreateBaseElements();
|
||||
}
|
||||
|
||||
private void CreateBaseElements()
|
||||
{
|
||||
// Main background container
|
||||
var background = new RectangleElement(0, 0, OverlayWidth, 200, // Dynamic height will be set later
|
||||
new SKColor(0, 0, 0, 224)) // #E0000000
|
||||
{
|
||||
Name = "Background",
|
||||
CornerRadius = 12,
|
||||
BorderColor = new SKColor(255, 255, 255, 64), // #40FFFFFF
|
||||
BorderWidth = 1
|
||||
};
|
||||
AddElement(background);
|
||||
|
||||
// Title text (will be updated with localized text)
|
||||
var titleText = new TextElement(Padding + 30, Padding, LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle], TitleTextSize, SKColors.White)
|
||||
{
|
||||
Name = "TitleText",
|
||||
FontStyle = SKFontStyle.Bold
|
||||
};
|
||||
AddElement(titleText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show controller bindings with localized strings
|
||||
/// </summary>
|
||||
public void ShowControllerBindings(List<OriginalInputConfig> inputConfigs, int durationSeconds)
|
||||
{
|
||||
// Update title text
|
||||
var titleElement = FindElement<TextElement>("TitleText");
|
||||
if (titleElement != null)
|
||||
{
|
||||
titleElement.Text = LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle];
|
||||
}
|
||||
|
||||
// Reset lifespan and opacity
|
||||
_lifespan = durationSeconds;
|
||||
|
||||
// Clear existing player bindings
|
||||
ClearPlayerBindings();
|
||||
|
||||
// Group controllers by player index (support all players + handheld)
|
||||
var playerBindings = new Dictionary<OriginalPlayerIndex, List<OriginalInputConfig>>();
|
||||
|
||||
foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Handheld))
|
||||
{
|
||||
if (!playerBindings.ContainsKey(config.PlayerIndex))
|
||||
{
|
||||
playerBindings[config.PlayerIndex] = new List<OriginalInputConfig>();
|
||||
}
|
||||
playerBindings[config.PlayerIndex].Add(config);
|
||||
}
|
||||
|
||||
float currentY = Padding + 40; // After title
|
||||
|
||||
// Add player bindings to UI (support 8 players + handheld)
|
||||
var playerIndices = new[]
|
||||
{
|
||||
OriginalPlayerIndex.Player1, OriginalPlayerIndex.Player2, OriginalPlayerIndex.Player3, OriginalPlayerIndex.Player4,
|
||||
OriginalPlayerIndex.Player5, OriginalPlayerIndex.Player6, OriginalPlayerIndex.Player7, OriginalPlayerIndex.Player8,
|
||||
OriginalPlayerIndex.Handheld
|
||||
};
|
||||
|
||||
for (int i = 0; i < playerIndices.Length; i++)
|
||||
{
|
||||
var playerIndex = playerIndices[i];
|
||||
float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing));
|
||||
|
||||
// Player number with colored background (circular badge)
|
||||
var playerColor = GetPlayerColor(i);
|
||||
var playerBadge = new RectangleElement(Padding, rowY, 24, 20, playerColor)
|
||||
{
|
||||
Name = $"PlayerBadge_{i}",
|
||||
CornerRadius = 12
|
||||
};
|
||||
AddElement(playerBadge);
|
||||
|
||||
// Player number text
|
||||
string playerLabel = playerIndex == OriginalPlayerIndex.Handheld ? "H" : $"P{(int)playerIndex + 1}";
|
||||
var playerLabelElement = new TextElement(Padding + 12, rowY + 2, playerLabel, PlayerTextSize, SKColors.White)
|
||||
{
|
||||
Name = $"PlayerLabel_{i}",
|
||||
FontStyle = SKFontStyle.Bold,
|
||||
TextAlign = SKTextAlign.Center
|
||||
};
|
||||
AddElement(playerLabelElement);
|
||||
|
||||
// Controller info
|
||||
if (playerBindings.ContainsKey(playerIndex))
|
||||
{
|
||||
var controllers = playerBindings[playerIndex];
|
||||
var controllerNames = GetUniqueControllerDisplayNames(controllers);
|
||||
|
||||
var controllerTextElement = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen
|
||||
{
|
||||
Name = $"ControllerText_{i}",
|
||||
FontStyle = SKFontStyle.Bold
|
||||
};
|
||||
AddElement(controllerTextElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
var noControllerTextElement = new TextElement(Padding + 56, rowY + 2, LocaleManager.Instance[LocaleKeys.ControllerOverlayNoController], PlayerTextSize, new SKColor(128, 128, 128)) // Gray
|
||||
{
|
||||
Name = $"NoControllerText_{i}",
|
||||
FontStyle = SKFontStyle.Italic
|
||||
};
|
||||
AddElement(noControllerTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total height and update background
|
||||
float totalHeight = Padding + 40 + (playerIndices.Length * (PlayerRowHeight + PlayerSpacing)) + Padding + 20;
|
||||
var background = FindElement<RectangleElement>("Background");
|
||||
if (background != null)
|
||||
{
|
||||
background.Height = totalHeight;
|
||||
}
|
||||
|
||||
// Show the overlay (position will be set by Window class with actual dimensions)
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
private static SKColor GetPlayerColor(int playerIndex)
|
||||
{
|
||||
return playerIndex switch
|
||||
{
|
||||
0 => new SKColor(255, 92, 92), // Red for Player 1
|
||||
1 => new SKColor(54, 162, 235), // Blue for Player 2
|
||||
2 => new SKColor(255, 206, 84), // Yellow for Player 3
|
||||
3 => new SKColor(75, 192, 192), // Green for Player 4
|
||||
4 => new SKColor(153, 102, 255), // Purple for Player 5
|
||||
5 => new SKColor(255, 159, 64), // Orange for Player 6
|
||||
6 => new SKColor(199, 199, 199), // Light Gray for Player 7
|
||||
7 => new SKColor(83, 102, 255), // Indigo for Player 8
|
||||
8 => new SKColor(255, 99, 132), // Pink for Handheld
|
||||
_ => new SKColor(128, 128, 128) // Gray fallback
|
||||
};
|
||||
}
|
||||
|
||||
private List<string> GetUniqueControllerDisplayNames(List<OriginalInputConfig> controllers)
|
||||
{
|
||||
var nameGroups = new Dictionary<string, List<int>>();
|
||||
var displayNames = new List<string>();
|
||||
|
||||
// First pass: get base names and group them
|
||||
for (int i = 0; i < controllers.Count; i++)
|
||||
{
|
||||
string baseName = GetControllerDisplayName(controllers[i]);
|
||||
|
||||
if (!nameGroups.ContainsKey(baseName))
|
||||
{
|
||||
nameGroups[baseName] = new List<int>();
|
||||
}
|
||||
nameGroups[baseName].Add(i);
|
||||
displayNames.Add(baseName);
|
||||
}
|
||||
|
||||
// Second pass: add numbering for duplicates
|
||||
foreach (var group in nameGroups.Where(g => g.Value.Count > 1))
|
||||
{
|
||||
for (int i = 0; i < group.Value.Count; i++)
|
||||
{
|
||||
int index = group.Value[i];
|
||||
displayNames[index] = $"{group.Key} #{i + 1}";
|
||||
}
|
||||
}
|
||||
|
||||
return displayNames;
|
||||
}
|
||||
|
||||
private string GetControllerDisplayName(OriginalInputConfig config)
|
||||
{
|
||||
if (string.IsNullOrEmpty(config.Name))
|
||||
{
|
||||
return config.Backend switch
|
||||
{
|
||||
OriginalInputBackendType.WindowKeyboard => LocaleManager.Instance[LocaleKeys.ControllerOverlayKeyboard],
|
||||
OriginalInputBackendType.GamepadSDL2 => LocaleManager.Instance[LocaleKeys.ControllerOverlayController],
|
||||
_ => LocaleManager.Instance[LocaleKeys.ControllerOverlayUnknown]
|
||||
};
|
||||
}
|
||||
|
||||
// Truncate long controller names from the middle
|
||||
string name = config.Name;
|
||||
if (name.Length > 25)
|
||||
{
|
||||
int keepLength = 22; // Total characters to keep (excluding "...")
|
||||
int leftLength = (keepLength + 1) / 2; // Round up for left side
|
||||
int rightLength = keepLength / 2; // Round down for right side
|
||||
|
||||
name = name.Substring(0, leftLength) + "..." + name.Substring(name.Length - rightLength);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all player bindings
|
||||
/// </summary>
|
||||
private void ClearPlayerBindings()
|
||||
{
|
||||
var elementsToRemove = new List<OverlayElement>();
|
||||
|
||||
foreach (var element in GetElements())
|
||||
{
|
||||
if (element.Name.StartsWith("PlayerBadge_") ||
|
||||
element.Name.StartsWith("PlayerLabel_") ||
|
||||
element.Name.StartsWith("ControllerText_") ||
|
||||
element.Name.StartsWith("NoControllerText_"))
|
||||
{
|
||||
elementsToRemove.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var element in elementsToRemove)
|
||||
{
|
||||
RemoveElement(element);
|
||||
element.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay
|
||||
/// </summary>
|
||||
public override void Update(float deltaTime, SKSize screenSize)
|
||||
{
|
||||
_lifespan -= deltaTime;
|
||||
|
||||
if (_lifespan <= 0)
|
||||
{
|
||||
IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lifespan <= 0.5f)
|
||||
{
|
||||
// Fade out during the last 0.5 seconds
|
||||
Opacity = _lifespan / 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Opacity = 1;
|
||||
}
|
||||
|
||||
// Update position if screen size is provided
|
||||
if (screenSize.Width > 0 && screenSize.Height > 0)
|
||||
{
|
||||
SetPositionToTopRight(screenSize.Width, screenSize.Height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position overlay to top-right matching original AXAML positioning
|
||||
/// </summary>
|
||||
public void SetPositionToTopRight(float screenWidth, float screenHeight)
|
||||
{
|
||||
X = screenWidth - OverlayWidth - 20; // 20px margin from right
|
||||
Y = 50; // 50px margin from top
|
||||
}
|
||||
}
|
||||
}
|
146
src/Ryujinx/UI/Overlay/ImageElement.cs
Normal file
146
src/Ryujinx/UI/Overlay/ImageElement.cs
Normal file
|
@ -0,0 +1,146 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Image overlay element
|
||||
/// </summary>
|
||||
public class ImageElement : OverlayElement
|
||||
{
|
||||
private SKBitmap _bitmap;
|
||||
private byte[] _imageData;
|
||||
private string _imagePath;
|
||||
|
||||
public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium;
|
||||
public bool MaintainAspectRatio { get; set; } = true;
|
||||
|
||||
public ImageElement()
|
||||
{
|
||||
}
|
||||
|
||||
public ImageElement(float x, float y, float width, float height, byte[] imageData)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SetImageData(imageData);
|
||||
}
|
||||
|
||||
public ImageElement(float x, float y, float width, float height, string imagePath)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
SetImagePath(imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from byte array
|
||||
/// </summary>
|
||||
public void SetImageData(byte[] imageData)
|
||||
{
|
||||
_imageData = imageData;
|
||||
_imagePath = null;
|
||||
LoadBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from file path
|
||||
/// </summary>
|
||||
public void SetImagePath(string imagePath)
|
||||
{
|
||||
_imagePath = imagePath;
|
||||
_imageData = null;
|
||||
LoadBitmap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set image from existing SKBitmap
|
||||
/// </summary>
|
||||
public void SetBitmap(SKBitmap bitmap)
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = bitmap;
|
||||
_imageData = null;
|
||||
_imagePath = null;
|
||||
}
|
||||
|
||||
private void LoadBitmap()
|
||||
{
|
||||
try
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = null;
|
||||
|
||||
if (_imageData != null)
|
||||
{
|
||||
_bitmap = SKBitmap.Decode(_imageData);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_imagePath))
|
||||
{
|
||||
_bitmap = SKBitmap.Decode(_imagePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Gpu, $"Failed to load image: {ex.Message}");
|
||||
_bitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || _bitmap == null || Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
FilterQuality = FilterQuality,
|
||||
Color = SKColors.White.WithAlpha((byte)(255 * effectiveOpacity))
|
||||
};
|
||||
|
||||
var sourceRect = new SKRect(0, 0, _bitmap.Width, _bitmap.Height);
|
||||
var destRect = new SKRect(X, Y, X + Width, Y + Height);
|
||||
|
||||
if (MaintainAspectRatio)
|
||||
{
|
||||
// Calculate aspect ratio preserving destination rectangle
|
||||
float sourceAspect = (float)_bitmap.Width / _bitmap.Height;
|
||||
float destAspect = Width / Height;
|
||||
|
||||
if (sourceAspect > destAspect)
|
||||
{
|
||||
// Source is wider, fit to width
|
||||
float newHeight = Width / sourceAspect;
|
||||
float yOffset = (Height - newHeight) / 2;
|
||||
destRect = new SKRect(X, Y + yOffset, X + Width, Y + yOffset + newHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Source is taller, fit to height
|
||||
float newWidth = Height * sourceAspect;
|
||||
float xOffset = (Width - newWidth) / 2;
|
||||
destRect = new SKRect(X + xOffset, Y, X + xOffset + newWidth, Y + Height);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_bitmap?.Dispose();
|
||||
_bitmap = null;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
116
src/Ryujinx/UI/Overlay/Overlay.cs
Normal file
116
src/Ryujinx/UI/Overlay/Overlay.cs
Normal file
|
@ -0,0 +1,116 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Base overlay class containing multiple elements
|
||||
/// </summary>
|
||||
public abstract class Overlay : IOverlay
|
||||
{
|
||||
private readonly List<OverlayElement> _elements = new();
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public int ZIndex { get; set; } = 0;
|
||||
|
||||
public Overlay()
|
||||
{
|
||||
}
|
||||
|
||||
public Overlay(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an element to this overlay
|
||||
/// </summary>
|
||||
public void AddElement(OverlayElement element)
|
||||
{
|
||||
_elements.Add(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an element from this overlay
|
||||
/// </summary>
|
||||
public void RemoveElement(OverlayElement element)
|
||||
{
|
||||
_elements.Remove(element);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all elements
|
||||
/// </summary>
|
||||
public IReadOnlyList<OverlayElement> GetElements()
|
||||
{
|
||||
return _elements.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find element by name
|
||||
/// </summary>
|
||||
public T FindElement<T>(string name) where T : OverlayElement
|
||||
{
|
||||
return _elements.OfType<T>().FirstOrDefault(e => e.Name == name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all elements
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
element.Dispose();
|
||||
}
|
||||
_elements.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update overlay
|
||||
/// </summary>
|
||||
public abstract void Update(float deltaTime, SKSize screenSize = default);
|
||||
|
||||
/// <summary>
|
||||
/// Render this overlay
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
if (!IsVisible || Opacity <= 0.0f)
|
||||
return;
|
||||
|
||||
// Save canvas state
|
||||
canvas.Save();
|
||||
|
||||
// Apply overlay position offset
|
||||
if (X != 0 || Y != 0)
|
||||
{
|
||||
canvas.Translate(X, Y);
|
||||
}
|
||||
|
||||
// Render all elements
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
if (element.IsVisible)
|
||||
{
|
||||
element.Render(canvas, Opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore canvas state
|
||||
canvas.Restore();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
66
src/Ryujinx/UI/Overlay/OverlayElement.cs
Normal file
66
src/Ryujinx/UI/Overlay/OverlayElement.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all overlay elements
|
||||
/// </summary>
|
||||
public abstract class OverlayElement : IDisposable
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Width { get; set; }
|
||||
public float Height { get; set; }
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Render this element to the canvas
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw on</param>
|
||||
/// <param name="globalOpacity">Global opacity multiplier</param>
|
||||
public abstract void Render(SKCanvas canvas, float globalOpacity = 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a point is within this element's bounds
|
||||
/// </summary>
|
||||
public virtual bool Contains(float x, float y)
|
||||
{
|
||||
return x >= X && x <= X + Width && y >= Y && y <= Y + Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bounds of this element
|
||||
/// </summary>
|
||||
public SKRect GetBounds()
|
||||
{
|
||||
return new SKRect(X, Y, X + Width, Y + Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply opacity to a color
|
||||
/// </summary>
|
||||
protected SKColor ApplyOpacity(SKColor color, float opacity)
|
||||
{
|
||||
return color.WithAlpha((byte)(color.Alpha * opacity));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of resources
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
161
src/Ryujinx/UI/Overlay/OverlayManager.cs
Normal file
161
src/Ryujinx/UI/Overlay/OverlayManager.cs
Normal file
|
@ -0,0 +1,161 @@
|
|||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ryujinx.Graphics.Gpu.Image;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages multiple overlays and handles rendering
|
||||
/// </summary>
|
||||
public class OverlayManager : IOverlayManager
|
||||
{
|
||||
private readonly List<IOverlay> _overlays = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Add an overlay to the manager
|
||||
/// </summary>
|
||||
public void AddOverlay(IOverlay overlay)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overlays.Add(overlay);
|
||||
SortOverlays();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an overlay from the manager
|
||||
/// </summary>
|
||||
public void RemoveOverlay(Overlay overlay)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_overlays.Remove(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove overlay by name
|
||||
/// </summary>
|
||||
public void RemoveOverlay(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var overlay = _overlays.FirstOrDefault(o => o.Name == name);
|
||||
if (overlay != null)
|
||||
{
|
||||
_overlays.Remove(overlay);
|
||||
overlay.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find overlay by name
|
||||
/// </summary>
|
||||
public IOverlay FindOverlay(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overlays.FirstOrDefault(o => o.Name == name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all overlays
|
||||
/// </summary>
|
||||
public IReadOnlyList<IOverlay> GetOverlays()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _overlays.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all overlays
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays)
|
||||
{
|
||||
overlay.Dispose();
|
||||
}
|
||||
_overlays.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all overlays (for animations)
|
||||
/// </summary>
|
||||
public void Update(float deltaTime, SKSize screenSize = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays.Where(o => o.IsVisible))
|
||||
{
|
||||
overlay.Update(deltaTime, screenSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render all visible overlays
|
||||
/// </summary>
|
||||
public void Render(SKCanvas canvas)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var overlay in _overlays.Where(o => o.IsVisible && o.Opacity > 0.0f))
|
||||
{
|
||||
overlay.Render(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort overlays by Z-index
|
||||
/// </summary>
|
||||
private void SortOverlays()
|
||||
{
|
||||
_overlays.Sort((a, b) => a.ZIndex.CompareTo(b.ZIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show overlay
|
||||
/// </summary>
|
||||
public void ShowOverlay(string name)
|
||||
{
|
||||
var overlay = FindOverlay(name);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.IsVisible = true;
|
||||
overlay.Opacity = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide overlay
|
||||
/// </summary>
|
||||
public void HideOverlay(string name)
|
||||
{
|
||||
var overlay = FindOverlay(name);
|
||||
if (overlay != null)
|
||||
{
|
||||
overlay.IsVisible = false;
|
||||
overlay.Opacity = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
}
|
78
src/Ryujinx/UI/Overlay/RectangleElement.cs
Normal file
78
src/Ryujinx/UI/Overlay/RectangleElement.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
using SkiaSharp;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Rectangle overlay element
|
||||
/// </summary>
|
||||
public class RectangleElement : OverlayElement
|
||||
{
|
||||
public SKColor BackgroundColor { get; set; } = SKColors.Transparent;
|
||||
public SKColor BorderColor { get; set; } = SKColors.Transparent;
|
||||
public float BorderWidth { get; set; } = 0;
|
||||
public float CornerRadius { get; set; } = 0;
|
||||
|
||||
public RectangleElement()
|
||||
{
|
||||
}
|
||||
|
||||
public RectangleElement(float x, float y, float width, float height, SKColor backgroundColor)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
BackgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
var bounds = new SKRect(X, Y, X + Width, Y + Height);
|
||||
|
||||
// Draw background
|
||||
if (BackgroundColor.Alpha > 0)
|
||||
{
|
||||
using var backgroundPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(BackgroundColor, effectiveOpacity),
|
||||
Style = SKPaintStyle.Fill,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, backgroundPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.DrawRect(bounds, backgroundPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw border
|
||||
if (BorderWidth > 0 && BorderColor.Alpha > 0)
|
||||
{
|
||||
using var borderPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(BorderColor, effectiveOpacity),
|
||||
Style = SKPaintStyle.Stroke,
|
||||
StrokeWidth = BorderWidth,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, borderPaint);
|
||||
}
|
||||
else
|
||||
{
|
||||
canvas.DrawRect(bounds, borderPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
src/Ryujinx/UI/Overlay/TextElement.cs
Normal file
121
src/Ryujinx/UI/Overlay/TextElement.cs
Normal file
|
@ -0,0 +1,121 @@
|
|||
using SkiaSharp;
|
||||
|
||||
namespace Ryujinx.UI.Overlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Text overlay element
|
||||
/// </summary>
|
||||
public class TextElement : OverlayElement
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public SKColor TextColor { get; set; } = SKColors.White;
|
||||
public float FontSize { get; set; } = 16;
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
public SKFontStyle FontStyle { get; set; } = SKFontStyle.Normal;
|
||||
public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left;
|
||||
public bool IsAntialias { get; set; } = true;
|
||||
|
||||
// Shadow properties
|
||||
public bool HasShadow { get; set; } = false;
|
||||
public SKColor ShadowColor { get; set; } = SKColors.Black;
|
||||
public float ShadowOffsetX { get; set; } = 1;
|
||||
public float ShadowOffsetY { get; set; } = 1;
|
||||
public float ShadowBlur { get; set; } = 0;
|
||||
|
||||
public TextElement()
|
||||
{
|
||||
}
|
||||
|
||||
public TextElement(float x, float y, string text, float fontSize = 16, SKColor? color = null)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Text = text;
|
||||
FontSize = fontSize;
|
||||
TextColor = color ?? SKColors.White;
|
||||
|
||||
// Auto-calculate width and height based on text
|
||||
UpdateDimensions();
|
||||
}
|
||||
|
||||
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
|
||||
{
|
||||
if (!IsVisible || string.IsNullOrEmpty(Text))
|
||||
return;
|
||||
|
||||
float effectiveOpacity = Opacity * globalOpacity;
|
||||
|
||||
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(TextColor, effectiveOpacity),
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface,
|
||||
TextAlign = TextAlign,
|
||||
IsAntialias = IsAntialias
|
||||
};
|
||||
|
||||
float textX = X;
|
||||
float textY = Y + FontSize; // Baseline adjustment
|
||||
|
||||
// Adjust X position based on alignment
|
||||
if (TextAlign == SKTextAlign.Center)
|
||||
{
|
||||
textX += Width / 2;
|
||||
}
|
||||
else if (TextAlign == SKTextAlign.Right)
|
||||
{
|
||||
textX += Width;
|
||||
}
|
||||
|
||||
// Draw shadow if enabled
|
||||
if (HasShadow)
|
||||
{
|
||||
using var shadowPaint = new SKPaint
|
||||
{
|
||||
Color = ApplyOpacity(ShadowColor, effectiveOpacity),
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface,
|
||||
TextAlign = TextAlign,
|
||||
IsAntialias = IsAntialias
|
||||
};
|
||||
|
||||
if (ShadowBlur > 0)
|
||||
{
|
||||
shadowPaint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlur);
|
||||
}
|
||||
|
||||
canvas.DrawText(Text, textX + ShadowOffsetX, textY + ShadowOffsetY, shadowPaint);
|
||||
}
|
||||
|
||||
// Draw main text
|
||||
canvas.DrawText(Text, textX, textY, paint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update width and height based on current text and font settings
|
||||
/// </summary>
|
||||
public void UpdateDimensions()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text))
|
||||
{
|
||||
Width = 0;
|
||||
Height = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
TextSize = FontSize,
|
||||
Typeface = typeface
|
||||
};
|
||||
|
||||
var bounds = new SKRect();
|
||||
paint.MeasureText(Text, ref bounds);
|
||||
|
||||
Width = bounds.Width;
|
||||
Height = FontSize; // Use font size as height for consistency
|
||||
}
|
||||
}
|
||||
}
|
|
@ -526,18 +526,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||
}
|
||||
}
|
||||
|
||||
private static string GetShortGamepadName(string str)
|
||||
{
|
||||
const string Ellipsis = "...";
|
||||
const int MaxSize = 50;
|
||||
|
||||
if (str.Length > MaxSize)
|
||||
{
|
||||
return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string GetShortGamepadId(string str)
|
||||
{
|
||||
|
@ -551,7 +540,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||
{
|
||||
string GetGamepadName(IGamepad gamepad, int controllerNumber)
|
||||
{
|
||||
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
||||
return $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
||||
}
|
||||
|
||||
string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber)
|
||||
|
@ -579,7 +568,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||
|
||||
if (gamepad != null)
|
||||
{
|
||||
Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
|
||||
Devices.Add((DeviceType.Keyboard, id, $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -652,138 +641,21 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||
InputConfig config;
|
||||
if (activeDevice.Type == DeviceType.Keyboard)
|
||||
{
|
||||
string id = activeDevice.Id;
|
||||
string name = activeDevice.Name;
|
||||
|
||||
config = new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = id,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick =
|
||||
new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
};
|
||||
config = DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(activeDevice.Id, activeDevice.Name, _playerId);
|
||||
}
|
||||
else if (activeDevice.Type == DeviceType.Controller)
|
||||
{
|
||||
bool isNintendoStyle = Devices.ToList().FirstOrDefault(x => x.Id == activeDevice.Id).Name.Contains("Nintendo");
|
||||
|
||||
string id = activeDevice.Id.Split(" ")[0];
|
||||
string name = activeDevice.Name;
|
||||
|
||||
config = new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL2,
|
||||
Id = id,
|
||||
Name = name,
|
||||
ControllerType = ControllerType.ProController,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
},
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
EnableMotion = true,
|
||||
Sensitivity = 100,
|
||||
GyroDeadzone = 1,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
EnableRumble = false,
|
||||
},
|
||||
};
|
||||
bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(activeDevice.Name);
|
||||
config = DefaultInputConfigurationProvider.CreateDefaultControllerConfig(activeDevice.Id, activeDevice.Name, _playerId, isNintendoStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new InputConfig();
|
||||
config = new InputConfig
|
||||
{
|
||||
PlayerIndex = _playerId
|
||||
};
|
||||
}
|
||||
|
||||
config.PlayerIndex = _playerId;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -1599,9 +1600,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
// Code where conditions will be executed after loading user configuration
|
||||
if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != backendThreadingInit)
|
||||
{
|
||||
Rebooter.RebootAppWithGame(application.Path,
|
||||
Rebooter.RebootAppWithGame(application.Path,
|
||||
[
|
||||
"--bt",
|
||||
"--bt",
|
||||
ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()
|
||||
]);
|
||||
|
||||
|
@ -1690,11 +1691,34 @@ 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;
|
||||
|
||||
// Always show overlay - if no configs, it will show test data
|
||||
int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value;
|
||||
// Show overlay through the GPU context window directly
|
||||
AppHost.ShowControllerOverlay(inputConfigs, duration);
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -146,6 +146,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
public bool EnableMouse { get; set; }
|
||||
public bool DisableInputWhenOutOfFocus { get; set; }
|
||||
public int FocusLostActionType { get; set; }
|
||||
public int ControllerOverlayGameStartDuration { get; set; }
|
||||
public int ControllerOverlayInputCycleDuration { get; set; }
|
||||
|
||||
public bool UseGlobalInputConfig
|
||||
{
|
||||
|
@ -587,6 +589,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
HideCursor = (int)config.HideCursor.Value;
|
||||
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
|
||||
FocusLostActionType = (int)config.FocusLostActionType.Value;
|
||||
ControllerOverlayGameStartDuration = config.ControllerOverlayGameStartDuration.Value;
|
||||
ControllerOverlayInputCycleDuration = config.ControllerOverlayInputCycleDuration.Value;
|
||||
|
||||
GameDirectories.Clear();
|
||||
GameDirectories.AddRange(config.UI.GameDirs.Value);
|
||||
|
@ -698,6 +702,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
config.HideCursor.Value = (HideCursorMode)HideCursor;
|
||||
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
|
||||
config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
|
||||
config.ControllerOverlayGameStartDuration.Value = ControllerOverlayGameStartDuration;
|
||||
config.ControllerOverlayInputCycleDuration.Value = ControllerOverlayInputCycleDuration;
|
||||
config.UI.GameDirs.Value = [.. GameDirectories];
|
||||
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
|
||||
|
||||
|
|
|
@ -120,6 +120,60 @@
|
|||
<TextBlock Text="{ext:Locale SettingsTabHotkeysOnlyWhilePressed}" Margin="10,0" />
|
||||
<CheckBox IsChecked="{Binding KeyboardHotkey.TurboModeWhileHeld}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer1, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer1">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer1, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer2, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer2">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer2, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer3, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer3">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer3, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer4, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer4">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer4, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer5, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer5">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer5, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer6, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer6">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer6, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer7, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer7">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer7, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsPlayer8, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDevicePlayer8">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer8, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{ext:Locale ControllerSettingsHandheld, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
|
||||
<ToggleButton Name="CycleInputDeviceHandheld">
|
||||
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDeviceHandheld, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
|
|
|
@ -82,7 +82,16 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
|||
{ "VolumeDown", () => viewModel.KeyboardHotkey.VolumeDown = Key.Unbound },
|
||||
{ "CustomVSyncIntervalIncrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = Key.Unbound },
|
||||
{ "CustomVSyncIntervalDecrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = Key.Unbound },
|
||||
{ "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound }
|
||||
{ "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer1", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer1 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer2", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer2 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer3", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer3 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer4", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer4 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer5", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer5 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer6", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer6 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer7", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer7 = Key.Unbound },
|
||||
{ "CycleInputDevicePlayer8", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer8 = Key.Unbound },
|
||||
{ "CycleInputDeviceHandheld", () => viewModel.KeyboardHotkey.CycleInputDeviceHandheld = Key.Unbound }
|
||||
};
|
||||
|
||||
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
|
||||
|
@ -162,6 +171,33 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
|||
case "TurboMode":
|
||||
ViewModel.KeyboardHotkey.TurboMode = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer1":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer1 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer2":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer2 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer3":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer3 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer4":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer4 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer5":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer5 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer6":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer6 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer7":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer7 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDevicePlayer8":
|
||||
ViewModel.KeyboardHotkey.CycleInputDevicePlayer8 = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
case "CycleInputDeviceHandheld":
|
||||
ViewModel.KeyboardHotkey.CycleInputDeviceHandheld = buttonValue.AsHidType<Key>();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -157,6 +157,32 @@
|
|||
</ComboBox>
|
||||
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
|
||||
</StackPanel>
|
||||
<Separator Height="1" Margin="0,15,0,15" />
|
||||
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabUIControllerOverlay}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabUIControllerOverlayGameStartDuration}"
|
||||
Width="200" />
|
||||
<NumericUpDown Value="{Binding ControllerOverlayGameStartDuration}"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
Increment="1"
|
||||
FormatString="0"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayGameStartDurationTooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{ext:Locale SettingsTabUIControllerOverlayInputCycleDuration}"
|
||||
Width="200" />
|
||||
<NumericUpDown Value="{Binding ControllerOverlayInputCycleDuration}"
|
||||
Minimum="0"
|
||||
Maximum="30"
|
||||
Increment="1"
|
||||
FormatString="0"
|
||||
ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayInputCycleDurationTooltip}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1"
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main"
|
||||
xmlns:viewsMisc="clr-namespace:Ryujinx.Ava.UI.Views.Misc"
|
||||
xmlns:overlayControls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
Cursor="{Binding Cursor}"
|
||||
Title="{Binding Title}"
|
||||
WindowState="{Binding WindowState}"
|
||||
|
@ -178,5 +179,7 @@
|
|||
Name="StatusBarView"
|
||||
Grid.Row="2" />
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
</window:StyleableAppWindow>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue