diff --git a/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
index d929cc501..20d466031 100644
--- a/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
+++ b/src/Ryujinx/UI/Applet/ProfileSelectorDialog.axaml
@@ -17,12 +17,8 @@
-
-
-
-
-
-
+
+
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
+ VerticalAlignment="Stretch" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
_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 2b644cffa..96da58b5d 100644
--- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
@@ -1,5 +1,9 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Input;
+using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input;
using Ryujinx.Common.Utilities;
@@ -10,8 +14,30 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
public partial class ControllerInputViewModel : BaseModel
{
- [ObservableProperty] private GamepadInputConfig _config;
+ private GamepadInputConfig _config;
+ public GamepadInputConfig Config
+ {
+ get => _config;
+ set
+ {
+ _config = value;
+ OnPropertyChanged();
+ }
+ }
+
+ private StickVisualizer _visualizer;
+ public StickVisualizer Visualizer
+ {
+ get => _visualizer;
+ set
+ {
+ _visualizer = value;
+
+ OnPropertyChanged();
+ }
+ }
+
private bool _isLeft;
public bool IsLeft
{
@@ -37,14 +63,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
public bool HasSides => IsLeft ^ IsRight;
-
+
[ObservableProperty] private SvgImage _image;
-
+
public InputViewModel ParentModel { get; }
-
- public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
+
+ public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
{
ParentModel = model;
+ Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged();
config.PropertyChanged += (_, args) =>
diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
index 5b7bcfd32..b324d39e8 100644
--- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private int _controller;
private string _controllerImage;
private int _device;
- [ObservableProperty] private object _configViewModel;
+ private object _configViewModel;
[ObservableProperty] private string _profileName;
private bool _isLoaded;
@@ -74,6 +74,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
}
}
+ public StickVisualizer VisualStick { get; private set; }
public ObservableCollection PlayerIndexes { get; set; }
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
@@ -94,6 +95,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsModified { get; set; }
public event Action NotifyChangesEvent;
+ public object ConfigViewModel
+ {
+ get => _configViewModel;
+ set
+ {
+ _configViewModel = value;
+
+ VisualStick.UpdateConfig(value);
+
+ OnPropertyChanged();
+ }
+ }
+
public PlayerIndex PlayerIdChoose
{
get => _playerIdChoose;
@@ -269,6 +283,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Devices = [];
ProfilesList = [];
DeviceList = [];
+ VisualStick = new StickVisualizer(this);
ControllerImage = ProControllerResource;
@@ -289,12 +304,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);
}
}
@@ -893,6 +908,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 5ff9bb578..bab8db7ce 100644
--- a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs
@@ -6,7 +6,29 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
public partial class KeyboardInputViewModel : BaseModel
{
- [ObservableProperty] private KeyboardInputConfig _config;
+ private KeyboardInputConfig _config;
+ public KeyboardInputConfig Config
+ {
+ get => _config;
+ set
+ {
+ _config = value;
+
+ OnPropertyChanged();
+ }
+ }
+
+ private StickVisualizer _visualizer;
+ public StickVisualizer Visualizer
+ {
+ get => _visualizer;
+ set
+ {
+ _visualizer = value;
+
+ OnPropertyChanged();
+ }
+ }
private bool _isLeft;
public bool IsLeft
@@ -38,9 +60,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 49c2cfd4c..555ded9fc 100644
--- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml
+++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml
@@ -34,12 +34,7 @@
-
-
-
-
-
+ MinHeight="450" ColumnDefinitions="Auto,*,Auto">
-
-
-
-
-
-
-
-
+ HorizontalAlignment="Stretch" ColumnDefinitions="*,*" RowDefinitions="*,*">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -345,8 +414,8 @@
Minimum="0"
Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" />
+ Width="25"
+ Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
@@ -438,11 +507,7 @@
CornerRadius="5"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch">
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+ HorizontalAlignment="Stretch" ColumnDefinitions="*,*" RowDefinitions="*,*">
-
-
-
-
-
-
+
-
-
-
-
+ VerticalAlignment="Center" ColumnDefinitions="Auto,*">
-
-
-
-
-
-
-
+ VerticalAlignment="Center" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
-
-
-
-
-
-
+
-
-
-
-
-
+ HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
-
-
-
-
+ VerticalAlignment="Center" ColumnDefinitions="Auto,*">
-
-
-
-
-
+ MinHeight="450" ColumnDefinitions="Auto,*,Auto">
-
-
-
-
-
-
-
-
+ HorizontalAlignment="Stretch" ColumnDefinitions="*,*" RowDefinitions="*,*">
-
+ MinHeight="90">
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+ HorizontalAlignment="Stretch" ColumnDefinitions="*,*" RowDefinitions="*,*">
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
+
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
index 2a46dcf49..7dd5211a7 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
@@ -177,12 +177,7 @@
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+ VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
-
-
-
-
-
+ VerticalAlignment="Center" RowDefinitions="Auto,70,Auto">
-
-
-
-
+ VerticalAlignment="Stretch" RowDefinitions="*,Auto">
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
+ HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*">
-
-
-
-
+ Margin="10,0, 0, 0" ColumnDefinitions="Auto,*">
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"
+ Margin="0,0,0,10"
+ TextAlignment="Center" Grid.Row="0" />
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
@@ -30,12 +23,7 @@
Margin="0 0 10 10"
IsVisible="{Binding !Processing}"
Grid.Row="1">
-
-
-
-
-
-
+
@@ -145,11 +133,7 @@
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+