From c3af1dbf1a82ce593c587d7ed4f9e5f8eea4b7ef Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 2 Mar 2025 20:43:31 -0600 Subject: [PATCH] Stick Visualizer (#579) ![](https://i.imgur.com/iSaXRMr.png) --------- Co-authored-by: MutantAura --- .../UI/Applet/ProfileSelectorDialog.axaml | 8 +- .../UI/Controls/ApplicationGridView.axaml | 11 +- .../UI/Controls/ApplicationListView.axaml | 5 +- .../UI/Controls/UpdateWaitWindow.axaml | 10 +- .../UI/Models/Input/StickVisualizer.cs | 260 ++++++++++++++++++ .../Input/ControllerInputViewModel.cs | 37 ++- .../UI/ViewModels/Input/InputViewModel.cs | 23 +- .../Input/KeyboardInputViewModel.cs | 27 +- .../UI/Views/Input/ControllerInputView.axaml | 145 ++++++---- src/Ryujinx/UI/Views/Input/InputView.axaml | 42 +-- .../UI/Views/Input/KeyboardInputView.axaml | 104 +++++-- .../UI/Views/Input/MotionInputView.axaml | 22 +- .../UI/Views/Input/RumbleInputView.axaml | 6 +- .../UI/Views/Settings/SettingsInputView.axaml | 7 +- .../UI/Views/Settings/SettingsUIView.axaml | 14 +- .../UI/Views/User/UserEditorView.axaml | 10 +- .../User/UserFirmwareAvatarSelectorView.axaml | 8 +- .../User/UserProfileImageSelectorView.axaml | 7 +- .../UI/Views/User/UserRecovererView.axaml | 12 +- .../UI/Views/User/UserSaveManagerView.axaml | 25 +- .../UI/Views/User/UserSelectorView.axaml | 6 +- .../Windows/GameSpecificSettingsWindow.axaml | 16 +- src/Ryujinx/UI/Windows/ModManagerWindow.axaml | 6 +- src/Ryujinx/UI/Windows/SettingsWindow.axaml | 2 +- src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml | 44 +-- 25 files changed, 551 insertions(+), 306 deletions(-) create mode 100644 src/Ryujinx/UI/Models/Input/StickVisualizer.cs 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,*">