Consolidate most logic into StickVisualizer.

This commit is contained in:
MutantAura 2024-06-03 23:10:04 +01:00
parent de08974f82
commit 40625f395a
6 changed files with 233 additions and 196 deletions

View file

@ -1,10 +1,13 @@
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Input;
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Models.Input namespace Ryujinx.Ava.UI.Models.Input
{ {
public class StickVisualizer : BaseModel public class StickVisualizer : BaseModel, IDisposable
{ {
public const int DrawStickPollRate = 50; // Milliseconds per poll. public const int DrawStickPollRate = 50; // Milliseconds per poll.
public const int DrawStickCircumference = 5; public const int DrawStickCircumference = 5;
@ -14,12 +17,26 @@ namespace Ryujinx.Ava.UI.Models.Input
public const float DrawStickCanvasCenter = (DrawStickCanvasSize - DrawStickCircumference) / 2; public const float DrawStickCanvasCenter = (DrawStickCanvasSize - DrawStickCircumference) / 2;
public const float MaxVectorLength = DrawStickCanvasSize / 2; public const float MaxVectorLength = DrawStickCanvasSize / 2;
public CancellationTokenSource PollTokenSource = new(); public CancellationTokenSource PollTokenSource;
public CancellationToken PollToken; public CancellationToken PollToken;
private static float _vectorLength; private static float _vectorLength;
private static float _vectorMultiplier; private static float _vectorMultiplier;
private bool disposedValue;
private DeviceType _type;
public DeviceType Type
{
get => _type;
set
{
_type = value;
OnPropertyChanged();
}
}
private GamepadInputConfig _gamepadConfig; private GamepadInputConfig _gamepadConfig;
public GamepadInputConfig GamepadConfig public GamepadInputConfig GamepadConfig
{ {
@ -86,22 +103,119 @@ namespace Ryujinx.Ava.UI.Models.Input
public float? UiDeadzoneLeft => _gamepadConfig?.DeadzoneLeft * DrawStickCanvasSize - DrawStickCircumference; public float? UiDeadzoneLeft => _gamepadConfig?.DeadzoneLeft * DrawStickCanvasSize - DrawStickCircumference;
public float? UiDeadzoneRight => _gamepadConfig?.DeadzoneRight * 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) public void UpdateConfig(object config)
{ {
if (config is GamepadInputConfig padConfig) if (config is ControllerInputViewModel padConfig)
{ {
GamepadConfig = padConfig; GamepadConfig = padConfig.Config;
Type = DeviceType.Controller;
return; return;
} }
else if (config is KeyboardInputConfig keyConfig) else if (config is KeyboardInputViewModel keyConfig)
{ {
KeyboardConfig = keyConfig; KeyboardConfig = keyConfig.Config;
Type = DeviceType.Keyboard;
return; return;
} }
throw new ArgumentException($"Invalid configuration: {config}"); 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) public static (float, float) ClampVector((float, float) vect)
@ -119,5 +233,28 @@ namespace Ryujinx.Ava.UI.Models.Input
return vect; 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);
}
} }
} }

View file

@ -1,29 +1,11 @@
using Avalonia.Svg.Skia; using Avalonia.Svg.Skia;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input; using Ryujinx.Ava.UI.Views.Input;
using Ryujinx.Input;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class ControllerInputViewModel : BaseModel public class ControllerInputViewModel : BaseModel
{ {
private IGamepad _selectedGamepad;
private StickVisualizer _stickVisualizer;
public StickVisualizer StickVisualizer
{
get => _stickVisualizer;
set
{
_stickVisualizer = value;
OnPropertyChanged();
}
}
private GamepadInputConfig _config; private GamepadInputConfig _config;
public GamepadInputConfig Config public GamepadInputConfig Config
{ {
@ -31,7 +13,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
set set
{ {
_config = value; _config = value;
StickVisualizer.UpdateConfig(Config);
OnPropertyChanged();
}
}
private StickVisualizer _visualizer;
public StickVisualizer Visualizer
{
get => _visualizer;
set
{
_visualizer = value;
OnPropertyChanged(); OnPropertyChanged();
} }
@ -76,17 +69,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public readonly InputViewModel ParentModel; public readonly InputViewModel ParentModel;
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config) public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
{ {
ParentModel = model; ParentModel = model;
Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged; model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged(); OnParentModelChanged();
_stickVisualizer = new();
Config = config; Config = config;
StickVisualizer.PollToken = StickVisualizer.PollTokenSource.Token;
Task.Run(() => PollSticks(StickVisualizer.PollToken));
} }
public async void ShowMotionConfig() public async void ShowMotionConfig()
@ -99,24 +88,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
await RumbleInputView.Show(this); await RumbleInputView.Show(this);
} }
private async Task PollSticks(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
_selectedGamepad = ParentModel.SelectedGamepad;
if (_selectedGamepad != null && _selectedGamepad is not AvaloniaKeyboard)
{
StickVisualizer.UiStickLeft = _selectedGamepad.GetStick(StickInputId.Left);
StickVisualizer.UiStickRight = _selectedGamepad.GetStick(StickInputId.Right);
}
await Task.Delay(StickVisualizer.DrawStickPollRate, token);
}
StickVisualizer.PollTokenSource.Dispose();
}
public void OnParentModelChanged() public void OnParentModelChanged()
{ {
IsLeft = ParentModel.IsLeft; IsLeft = ParentModel.IsLeft;

View file

@ -56,6 +56,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public IGamepadDriver AvaloniaKeyboardDriver { get; } public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepad SelectedGamepad { get; private set; } public IGamepad SelectedGamepad { get; private set; }
public StickVisualizer VisualStick { get; private set; }
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; } public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { 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; _configViewModel = value;
VisualStick.UpdateConfig(value);
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@ -261,6 +264,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>(); Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>();
ProfilesList = new AvaloniaList<string>(); ProfilesList = new AvaloniaList<string>();
DeviceList = new AvaloniaList<string>(); DeviceList = new AvaloniaList<string>();
VisualStick = new StickVisualizer(this);
ControllerImage = ProControllerResource; ControllerImage = ProControllerResource;
@ -281,12 +285,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Config is StandardKeyboardInputConfig keyboardInputConfig) 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) if (Config is StandardControllerInputConfig controllerInputConfig)
{ {
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig)); ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
} }
} }
@ -879,10 +883,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
(ConfigViewModel as ControllerInputViewModel)?.StickVisualizer.PollTokenSource.Cancel();
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates(); _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
VisualStick.Dispose();
SelectedGamepad?.Dispose(); SelectedGamepad?.Dispose();
AvaloniaKeyboardDriver.Dispose(); AvaloniaKeyboardDriver.Dispose();

View file

@ -1,27 +1,10 @@
using Avalonia.Svg.Skia; using Avalonia.Svg.Skia;
using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Input;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class KeyboardInputViewModel : BaseModel public class KeyboardInputViewModel : BaseModel
{ {
private (float, float) _leftBuffer = (0, 0);
private (float, float) _rightBuffer = (0, 0);
private StickVisualizer _stickVisualizer;
public StickVisualizer StickVisualizer
{
get => _stickVisualizer;
set
{
_stickVisualizer = value;
OnPropertyChanged();
}
}
private KeyboardInputConfig _config; private KeyboardInputConfig _config;
public KeyboardInputConfig Config public KeyboardInputConfig Config
{ {
@ -29,7 +12,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
set set
{ {
_config = value; _config = value;
StickVisualizer.UpdateConfig(_config);
OnPropertyChanged();
}
}
private StickVisualizer _visualizer;
public StickVisualizer Visualizer
{
get => _visualizer;
set
{
_visualizer = value;
OnPropertyChanged(); OnPropertyChanged();
} }
@ -74,70 +68,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public readonly InputViewModel ParentModel; public readonly InputViewModel ParentModel;
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config, StickVisualizer visualizer)
{ {
ParentModel = model; ParentModel = model;
Visualizer = visualizer;
model.NotifyChangesEvent += OnParentModelChanged; model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged(); OnParentModelChanged();
_stickVisualizer = new();
Config = config; Config = config;
StickVisualizer.PollToken = StickVisualizer.PollTokenSource.Token;
Task.Run(() => PollKeyboard(StickVisualizer.PollToken));
}
private async Task PollKeyboard(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
if (ParentModel.IsKeyboard)
{
IKeyboard keyboard = (IKeyboard)ParentModel.AvaloniaKeyboardDriver.GetGamepad("0");
var snap = keyboard.GetKeyboardStateSnapshot();
if (snap.IsPressed((Key)Config.LeftStickRight))
{
_leftBuffer.Item1 += 1;
}
if (snap.IsPressed((Key)Config.LeftStickLeft))
{
_leftBuffer.Item1 -= 1;
}
if (snap.IsPressed((Key)Config.LeftStickUp))
{
_leftBuffer.Item2 += 1;
}
if (snap.IsPressed((Key)Config.LeftStickDown))
{
_leftBuffer.Item2 -= 1;
}
if (snap.IsPressed((Key)Config.RightStickRight))
{
_rightBuffer.Item1 += 1;
}
if (snap.IsPressed((Key)Config.RightStickLeft))
{
_rightBuffer.Item1 -= 1;
}
if (snap.IsPressed((Key)Config.RightStickUp))
{
_rightBuffer.Item2 += 1;
}
if (snap.IsPressed((Key)Config.RightStickDown))
{
_rightBuffer.Item2 -= 1;
}
StickVisualizer.UiStickLeft = _leftBuffer;
StickVisualizer.UiStickRight = _rightBuffer;
}
await Task.Delay(StickVisualizer.DrawStickPollRate, token);
}
StickVisualizer.PollTokenSource.Dispose();
} }
public void OnParentModelChanged() public void OnParentModelChanged()

View file

@ -333,72 +333,72 @@
BorderBrush="{DynamicResource ThemeControlBorderColor}" BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="5" CornerRadius="5"
Height="{Binding StickVisualizer.UiStickBorderSize}" Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding StickVisualizer.UiStickBorderSize}" Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsLeft}"> IsVisible="{Binding IsLeft}">
<Canvas <Canvas
Background="{DynamicResource ThemeBackgroundColor}" Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}"> Width="{Binding Visualizer.UiCanvasSize}">
<Grid <Grid
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}"> Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse <Ellipse
HorizontalAlignment="Center" HorizontalAlignment="Center"
Stroke="Black" Stroke="Black"
StrokeThickness="1" StrokeThickness="1"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding StickVisualizer.UiCanvasSize}"/> Height="{Binding Visualizer.UiCanvasSize}"/>
<Ellipse <Ellipse
HorizontalAlignment="Center" HorizontalAlignment="Center"
Fill="Gray" Fill="Gray"
Opacity="100" Opacity="100"
Height="{Binding StickVisualizer.UiDeadzoneLeft}" Height="{Binding Visualizer.UiDeadzoneLeft}"
Width="{Binding StickVisualizer.UiDeadzoneLeft}"/> Width="{Binding Visualizer.UiDeadzoneLeft}"/>
</Grid> </Grid>
<Ellipse <Ellipse
Fill="Red" Fill="Red"
Width="{Binding StickVisualizer.UiStickCircumference}" Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding StickVisualizer.UiStickCircumference}" Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding StickVisualizer.UiStickLeftY}" Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
Canvas.Left="{Binding StickVisualizer.UiStickLeftX}" /> Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
</Canvas> </Canvas>
</Border> </Border>
<Border <Border
BorderBrush="{DynamicResource ThemeControlBorderColor}" BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="5" CornerRadius="5"
Height="{Binding StickVisualizer.UiStickBorderSize}" Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding StickVisualizer.UiStickBorderSize}" Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsRight}"> IsVisible="{Binding IsRight}">
<Canvas <Canvas
Background="{DynamicResource ThemeBackgroundColor}" Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}"> Width="{Binding Visualizer.UiCanvasSize}">
<Grid <Grid
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}"> Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse <Ellipse
HorizontalAlignment="Center" HorizontalAlignment="Center"
Stroke="Black" Stroke="Black"
StrokeThickness="1" StrokeThickness="1"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding StickVisualizer.UiCanvasSize}"/> Height="{Binding Visualizer.UiCanvasSize}"/>
<Ellipse <Ellipse
HorizontalAlignment="Center" HorizontalAlignment="Center"
Fill="Gray" Fill="Gray"
Opacity="100" Opacity="100"
Height="{Binding StickVisualizer.UiDeadzoneRight}" Height="{Binding Visualizer.UiDeadzoneRight}"
Width="{Binding StickVisualizer.UiDeadzoneRight}"/> Width="{Binding Visualizer.UiDeadzoneRight}"/>
</Grid> </Grid>
<Ellipse <Ellipse
Fill="Red" Fill="Red"
Width="{Binding StickVisualizer.UiStickCircumference}" Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding StickVisualizer.UiStickCircumference}" Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding StickVisualizer.UiStickRightY}" Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
Canvas.Left="{Binding StickVisualizer.UiStickRightX}" /> Canvas.Left="{Binding Visualizer.UiStickRightX}" />
</Canvas> </Canvas>
</Border> </Border>
</StackPanel> </StackPanel>

View file

@ -327,72 +327,60 @@
BorderBrush="{DynamicResource ThemeControlBorderColor}" BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="5" CornerRadius="5"
Height="{Binding StickVisualizer.UiStickBorderSize}" Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding StickVisualizer.UiStickBorderSize}" Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsLeft}"> IsVisible="{Binding IsLeft}">
<Canvas <Canvas
Background="{DynamicResource ThemeBackgroundColor}" Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}"> Width="{Binding Visualizer.UiCanvasSize}">
<Grid <Grid
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}"> Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse <Ellipse
HorizontalAlignment="Center" HorizontalAlignment="Center"
Stroke="Black" Stroke="Black"
StrokeThickness="1" StrokeThickness="1"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding StickVisualizer.UiCanvasSize}"/> Height="{Binding Visualizer.UiCanvasSize}"/>
<Ellipse
HorizontalAlignment="Center"
Fill="Gray"
Opacity="100"
Height="{Binding StickVisualizer.UiDeadzoneLeft}"
Width="{Binding StickVisualizer.UiDeadzoneLeft}"/>
</Grid> </Grid>
<Ellipse <Ellipse
Fill="Red" Fill="Red"
Width="{Binding StickVisualizer.UiStickCircumference}" Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding StickVisualizer.UiStickCircumference}" Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding StickVisualizer.UiStickLeftY}" Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
Canvas.Left="{Binding StickVisualizer.UiStickLeftX}" /> Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
</Canvas> </Canvas>
</Border> </Border>
<Border <Border
BorderBrush="{DynamicResource ThemeControlBorderColor}" BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1" BorderThickness="1"
CornerRadius="5" CornerRadius="5"
Height="{Binding StickVisualizer.UiStickBorderSize}" Height="{Binding Visualizer.UiStickBorderSize}"
Width="{Binding StickVisualizer.UiStickBorderSize}" Width="{Binding Visualizer.UiStickBorderSize}"
IsVisible="{Binding IsRight}"> IsVisible="{Binding IsRight}">
<Canvas <Canvas
Background="{DynamicResource ThemeBackgroundColor}" Background="{DynamicResource ThemeBackgroundColor}"
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}"> Width="{Binding Visualizer.UiCanvasSize}">
<Grid <Grid
Height="{Binding StickVisualizer.UiCanvasSize}" Height="{Binding Visualizer.UiCanvasSize}"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Background="{DynamicResource ThemeBackgroundColor}"> Background="{DynamicResource ThemeBackgroundColor}">
<Ellipse <Ellipse
HorizontalAlignment="Center" HorizontalAlignment="Center"
Stroke="Black" Stroke="Black"
StrokeThickness="1" StrokeThickness="1"
Width="{Binding StickVisualizer.UiCanvasSize}" Width="{Binding Visualizer.UiCanvasSize}"
Height="{Binding StickVisualizer.UiCanvasSize}"/> Height="{Binding Visualizer.UiCanvasSize}"/>
<Ellipse
HorizontalAlignment="Center"
Fill="Gray"
Opacity="100"
Height="{Binding StickVisualizer.UiDeadzoneRight}"
Width="{Binding StickVisualizer.UiDeadzoneRight}"/>
</Grid> </Grid>
<Ellipse <Ellipse
Fill="Red" Fill="Red"
Width="{Binding StickVisualizer.UiStickCircumference}" Width="{Binding Visualizer.UiStickCircumference}"
Height="{Binding StickVisualizer.UiStickCircumference}" Height="{Binding Visualizer.UiStickCircumference}"
Canvas.Bottom="{Binding StickVisualizer.UiStickRightY}" Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
Canvas.Left="{Binding StickVisualizer.UiStickRightX}" /> Canvas.Left="{Binding Visualizer.UiStickRightX}" />
</Canvas> </Canvas>
</Border> </Border>
</StackPanel> </StackPanel>