diff --git a/src/Ryujinx/Assets/Styles/Themes.xaml b/src/Ryujinx/Assets/Styles/Themes.xaml
index 3a0bd4217..3b6ae74e4 100644
--- a/src/Ryujinx/Assets/Styles/Themes.xaml
+++ b/src/Ryujinx/Assets/Styles/Themes.xaml
@@ -12,11 +12,13 @@
#C1C1C1
#b3ffffff
#80cccccc
+ #FF6347
#A0000000
#fffcd12a
#FF2EEAC9
#FFFF4554
#6483F5
+ #800080
#C1C1C1
#b3ffffff
#80cccccc
+ #FF6347
#A0000000
#fffcd12a
#13c3a4
#FFFF4554
#6483F5
+ #800080
#3D3D3D
#0FFFFFFF
#1EFFFFFF
+ #FF6347
#A0FFFFFF
#fffcd12a
#FF2EEAC9
#FFFF4554
#6483F5
+ #FFA500
diff --git a/src/Ryujinx/UI/Models/Input/StickVisualizer.cs b/src/Ryujinx/UI/Models/Input/StickVisualizer.cs
new file mode 100644
index 000000000..b7e9ec331
--- /dev/null
+++ b/src/Ryujinx/UI/Models/Input/StickVisualizer.cs
@@ -0,0 +1,260 @@
+using Ryujinx.Ava.UI.ViewModels;
+using Ryujinx.Ava.UI.ViewModels.Input;
+using Ryujinx.Input;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.UI.Models.Input
+{
+ public class StickVisualizer : BaseModel, IDisposable
+ {
+ public const int DrawStickPollRate = 50; // Milliseconds per poll.
+ public const int DrawStickCircumference = 5;
+ public const float DrawStickScaleFactor = DrawStickCanvasCenter;
+ public const int DrawStickCanvasSize = 100;
+ public const int DrawStickBorderSize = DrawStickCanvasSize + 5;
+ public const float DrawStickCanvasCenter = (DrawStickCanvasSize - DrawStickCircumference) / 2;
+ public const float MaxVectorLength = DrawStickCanvasSize / 2;
+
+ public CancellationTokenSource PollTokenSource;
+ public CancellationToken PollToken;
+
+ private static float _vectorLength;
+ private static float _vectorMultiplier;
+
+ private bool disposedValue;
+
+ private DeviceType _type;
+ public DeviceType Type
+ {
+ get => _type;
+ set
+ {
+ _type = value;
+
+ OnPropertyChanged();
+ }
+ }
+
+ private GamepadInputConfig _gamepadConfig;
+ public GamepadInputConfig GamepadConfig
+ {
+ get => _gamepadConfig;
+ set
+ {
+ _gamepadConfig = value;
+
+ OnPropertyChanged();
+ }
+ }
+
+ private KeyboardInputConfig _keyboardConfig;
+ public KeyboardInputConfig KeyboardConfig
+ {
+ get => _keyboardConfig;
+ set
+ {
+ _keyboardConfig = value;
+
+ OnPropertyChanged();
+ }
+ }
+
+ private (float, float) _uiStickLeft;
+ public (float, float) UiStickLeft
+ {
+ get => (_uiStickLeft.Item1 * DrawStickScaleFactor, _uiStickLeft.Item2 * DrawStickScaleFactor);
+ set
+ {
+ _uiStickLeft = value;
+
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(UiStickRightX));
+ OnPropertyChanged(nameof(UiStickRightY));
+ OnPropertyChanged(nameof(UiDeadzoneRight));
+ }
+ }
+
+ private (float, float) _uiStickRight;
+ public (float, float) UiStickRight
+ {
+ get => (_uiStickRight.Item1 * DrawStickScaleFactor, _uiStickRight.Item2 * DrawStickScaleFactor);
+ set
+ {
+ _uiStickRight = value;
+
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(UiStickLeftX));
+ OnPropertyChanged(nameof(UiStickLeftY));
+ OnPropertyChanged(nameof(UiDeadzoneLeft));
+ }
+ }
+
+ public float UiStickLeftX => ClampVector(UiStickLeft).Item1;
+ public float UiStickLeftY => ClampVector(UiStickLeft).Item2;
+ public float UiStickRightX => ClampVector(UiStickRight).Item1;
+ public float UiStickRightY => ClampVector(UiStickRight).Item2;
+
+ public int UiStickCircumference => DrawStickCircumference;
+ public int UiCanvasSize => DrawStickCanvasSize;
+ public int UiStickBorderSize => DrawStickBorderSize;
+
+ public float? UiDeadzoneLeft => _gamepadConfig?.DeadzoneLeft * DrawStickCanvasSize - DrawStickCircumference;
+ public float? UiDeadzoneRight => _gamepadConfig?.DeadzoneRight * DrawStickCanvasSize - DrawStickCircumference;
+
+ private InputViewModel Parent;
+
+ public StickVisualizer(InputViewModel parent)
+ {
+ Parent = parent;
+
+ PollTokenSource = new CancellationTokenSource();
+ PollToken = PollTokenSource.Token;
+
+ Task.Run(Initialize, PollToken);
+ }
+
+ public void UpdateConfig(object config)
+ {
+ if (config is ControllerInputViewModel padConfig)
+ {
+ GamepadConfig = padConfig.Config;
+ Type = DeviceType.Controller;
+
+ return;
+ }
+ else if (config is KeyboardInputViewModel keyConfig)
+ {
+ KeyboardConfig = keyConfig.Config;
+ Type = DeviceType.Keyboard;
+
+ return;
+ }
+
+ Type = DeviceType.None;
+ }
+
+ public async Task Initialize()
+ {
+ (float, float) leftBuffer;
+ (float, float) rightBuffer;
+
+ while (!PollToken.IsCancellationRequested)
+ {
+ leftBuffer = (0f, 0f);
+ rightBuffer = (0f, 0f);
+
+ switch (Type)
+ {
+ case DeviceType.Keyboard:
+ IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0");
+
+ if (keyboard != null)
+ {
+ KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
+
+ if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
+ {
+ leftBuffer.Item1 += 1;
+ }
+ if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
+ {
+ leftBuffer.Item1 -= 1;
+ }
+ if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
+ {
+ leftBuffer.Item2 += 1;
+ }
+ if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
+ {
+ leftBuffer.Item2 -= 1;
+ }
+
+ if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
+ {
+ rightBuffer.Item1 += 1;
+ }
+ if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
+ {
+ rightBuffer.Item1 -= 1;
+ }
+ if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
+ {
+ rightBuffer.Item2 += 1;
+ }
+ if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
+ {
+ rightBuffer.Item2 -= 1;
+ }
+
+ UiStickLeft = leftBuffer;
+ UiStickRight = rightBuffer;
+ }
+ break;
+
+ case DeviceType.Controller:
+ IGamepad controller = Parent.SelectedGamepad;
+
+ if (controller != null)
+ {
+ leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
+ rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
+ }
+ break;
+
+ case DeviceType.None:
+ break;
+ default:
+ throw new ArgumentException($"Unable to poll device type \"{Type}\"");
+ }
+
+ UiStickLeft = leftBuffer;
+ UiStickRight = rightBuffer;
+
+ await Task.Delay(DrawStickPollRate, PollToken);
+ }
+
+ PollTokenSource.Dispose();
+ }
+
+ public static (float, float) ClampVector((float, float) vect)
+ {
+ _vectorMultiplier = 1;
+ _vectorLength = MathF.Sqrt((vect.Item1 * vect.Item1) + (vect.Item2 * vect.Item2));
+
+ if (_vectorLength > MaxVectorLength)
+ {
+ _vectorMultiplier = MaxVectorLength / _vectorLength;
+ }
+
+ vect.Item1 = vect.Item1 * _vectorMultiplier + DrawStickCanvasCenter;
+ vect.Item2 = vect.Item2 * _vectorMultiplier + DrawStickCanvasCenter;
+
+ return vect;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ PollTokenSource.Cancel();
+ }
+
+ KeyboardConfig = null;
+ GamepadConfig = null;
+ Parent = null;
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
index 6ee79a371..c84590ea6 100644
--- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
@@ -13,10 +13,23 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
set
{
_config = value;
+
OnPropertyChanged();
}
}
+ private StickVisualizer _visualizer;
+ public StickVisualizer Visualizer
+ {
+ get => _visualizer;
+ set
+ {
+ _visualizer = value;
+
+ OnPropertyChanged();
+ }
+ }
+
private bool _isLeft;
public bool IsLeft
{
@@ -42,7 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
public bool HasSides => IsLeft ^ IsRight;
-
+
private SvgImage _image;
public SvgImage Image
{
@@ -55,10 +68,11 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
public readonly InputViewModel ParentModel;
-
- public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
+
+ public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
{
ParentModel = model;
+ Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged();
Config = config;
diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
index f16aad34a..1c230914a 100644
--- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
@@ -56,6 +56,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepad SelectedGamepad { get; private set; }
+ public StickVisualizer VisualStick { get; private set; }
public ObservableCollection PlayerIndexes { get; set; }
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
@@ -80,6 +81,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_configViewModel = value;
+ VisualStick.UpdateConfig(value);
+
OnPropertyChanged();
}
}
@@ -271,6 +274,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Devices = [];
ProfilesList = [];
DeviceList = [];
+ VisualStick = new StickVisualizer(this);
ControllerImage = ProControllerResource;
@@ -291,12 +295,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
{
- ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig));
+ ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
}
if (Config is StandardControllerInputConfig controllerInputConfig)
{
- ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig));
+ ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
}
}
@@ -901,6 +905,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
+ VisualStick.Dispose();
+
SelectedGamepad?.Dispose();
AvaloniaKeyboardDriver.Dispose();
diff --git a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
index 0b530eb09..9096cd845 100644
--- a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
@@ -12,6 +12,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
set
{
_config = value;
+
+ OnPropertyChanged();
+ }
+ }
+
+ private StickVisualizer _visualizer;
+ public StickVisualizer Visualizer
+ {
+ get => _visualizer;
+ set
+ {
+ _visualizer = value;
+
OnPropertyChanged();
}
}
@@ -55,9 +68,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public readonly InputViewModel ParentModel;
- public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config)
+ public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config, StickVisualizer visualizer)
{
ParentModel = model;
+ Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged();
Config = config;
diff --git a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml
index 58fe41b24..9c66fcefc 100644
--- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml
+++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml
@@ -308,17 +308,99 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -337,8 +419,8 @@
Minimum="0"
Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" />
+ Width="25"
+ Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml
index 8f274987e..23cc8f1b8 100644
--- a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml
+++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml
@@ -302,12 +302,79 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
-
+ MinHeight="90">
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/Ryujinx/UI/Views/User/UserEditorView.axaml b/src/Ryujinx/UI/Views/User/UserEditorView.axaml
index e6207a7a3..194d04bd8 100644
--- a/src/Ryujinx/UI/Views/User/UserEditorView.axaml
+++ b/src/Ryujinx/UI/Views/User/UserEditorView.axaml
@@ -114,4 +114,4 @@
Content="{locale:Locale Save}" />
-
\ No newline at end of file
+
diff --git a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
index 36bade62f..954c1a907 100644
--- a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
+++ b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml
@@ -105,4 +105,4 @@
Click="ChooseButton_OnClick" />
-
\ No newline at end of file
+
diff --git a/src/Ryujinx/UI/Views/User/UserRecovererView.axaml b/src/Ryujinx/UI/Views/User/UserRecovererView.axaml
index c4962f1b1..852decabc 100644
--- a/src/Ryujinx/UI/Views/User/UserRecovererView.axaml
+++ b/src/Ryujinx/UI/Views/User/UserRecovererView.axaml
@@ -39,11 +39,7 @@
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5">
-
-
-
-
-
+
-
\ No newline at end of file
+
diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml
index 3ffa52b47..86ac29289 100644
--- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml
+++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml
@@ -11,7 +11,7 @@
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
Width="1100"
- Height="918"
+ Height="927"
MinWidth="800"
MinHeight="480"
WindowStartupLocation="CenterOwner"