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 d291f09a0..6c683a50a 100644 --- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Views.Input; @@ -10,8 +11,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 +60,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 = config; @@ -59,7 +83,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { await RumbleInputView.Show(this); } - + public RelayCommand LedDisabledChanged => Commands.Create(() => { if (!Config.EnableLedChanging) return; diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index cd0488f5f..188ce436b 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -48,7 +48,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; @@ -67,6 +67,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; } @@ -87,6 +88,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; @@ -262,6 +276,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>(); ProfilesList = new AvaloniaList(); DeviceList = new AvaloniaList(); + VisualStick = new StickVisualizer(this); ControllerImage = ProControllerResource; @@ -282,12 +297,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); } } @@ -886,6 +901,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 1662f4a3d..7bb71b960 100644 --- a/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/ControllerInputView.axaml @@ -319,17 +319,103 @@ HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -348,8 +434,8 @@ Minimum="0" Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" /> + Width="25" + Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" /> - - + MinHeight="90"> + + + + + + + + + + + + + + + + + + +