aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/UI/Helpers
diff options
context:
space:
mode:
authorIsaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>2022-12-29 14:24:05 +0000
committerGitHub <noreply@github.com>2022-12-29 15:24:05 +0100
commit76671d63d4f3ea18f8ad99e9ce9f0b2ec9a2599d (patch)
tree05013214e4696a9254369d0706173f58877f6a83 /Ryujinx.Ava/UI/Helpers
parent3d1a0bf3749afa14da5b5ba1e0666fdb78c99beb (diff)
Ava GUI: Restructure `Ryujinx.Ava` (#4165)
* Restructure `Ryujinx.Ava` * Stylistic consistency * Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Fix redundancies * Remove redunancies * Add back elses Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/UI/Helpers')
-rw-r--r--Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs16
-rw-r--r--Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs16
-rw-r--r--Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs16
-rw-r--r--Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs35
-rw-r--r--Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs118
-rw-r--r--Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs400
-rw-r--r--Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs233
-rw-r--r--Ryujinx.Ava/UI/Helpers/Glyph.cs9
-rw-r--r--Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs49
-rw-r--r--Ryujinx.Ava/UI/Helpers/HotKeyControl.cs52
-rw-r--r--Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs25
-rw-r--r--Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs46
-rw-r--r--Ryujinx.Ava/UI/Helpers/MiniCommand.cs71
-rw-r--r--Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs40
-rw-r--r--Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs82
-rw-r--r--Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs20
-rw-r--r--Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs47
-rw-r--r--Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs91
-rw-r--r--Ryujinx.Ava/UI/Helpers/UserResult.cs12
-rw-r--r--Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs52
-rw-r--r--Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs107
21 files changed, 1537 insertions, 0 deletions
diff --git a/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs b/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs
new file mode 100644
index 00000000..ebf5c16e
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs
@@ -0,0 +1,16 @@
+using Avalonia.Interactivity;
+using Ryujinx.Ui.App.Common;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class ApplicationOpenedEventArgs : RoutedEventArgs
+ {
+ public ApplicationData Application { get; }
+
+ public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent)
+ {
+ Application = application;
+ RoutedEvent = routedEvent;
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs b/Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs
new file mode 100644
index 00000000..6b696ba7
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs
@@ -0,0 +1,16 @@
+using SPB.Graphics;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ [SupportedOSPlatform("linux")]
+ internal class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext
+ {
+ public AvaloniaGlxContext(IntPtr handle)
+ : base(FramebufferFormat.Default, 0, 0, 0, false, null)
+ {
+ ContextHandle = handle;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs b/Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs
new file mode 100644
index 00000000..b63a973a
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs
@@ -0,0 +1,16 @@
+using SPB.Graphics;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ [SupportedOSPlatform("windows")]
+ internal class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext
+ {
+ public AvaloniaWglContext(IntPtr handle)
+ : base(FramebufferFormat.Default, 0, 0, 0, false, null)
+ {
+ ContextHandle = handle;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs b/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs
new file mode 100644
index 00000000..3fd368f8
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs
@@ -0,0 +1,35 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using System;
+using System.Globalization;
+using System.IO;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal class BitmapArrayValueConverter : IValueConverter
+ {
+ public static BitmapArrayValueConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ if (value is byte[] buffer && targetType == typeof(IImage))
+ {
+ MemoryStream mem = new(buffer);
+ return new Bitmap(mem);
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs
new file mode 100644
index 00000000..6730b571
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs
@@ -0,0 +1,118 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.LogicalTree;
+using Avalonia.Threading;
+using Ryujinx.Input;
+using Ryujinx.Input.Assigner;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal class ButtonKeyAssigner
+ {
+ internal class ButtonAssignedEventArgs : EventArgs
+ {
+ public ToggleButton Button { get; }
+ public bool IsAssigned { get; }
+
+ public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned)
+ {
+ Button = button;
+ IsAssigned = isAssigned;
+ }
+ }
+
+ public ToggleButton ToggledButton { get; set; }
+
+ private bool _isWaitingForInput;
+ private bool _shouldUnbind;
+ public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
+
+ public ButtonKeyAssigner(ToggleButton toggleButton)
+ {
+ ToggledButton = toggleButton;
+ }
+
+ public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ ToggledButton.IsChecked = true;
+ });
+
+ if (_isWaitingForInput)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ Cancel();
+ });
+
+ return;
+ }
+
+ _isWaitingForInput = true;
+
+ assigner.Initialize();
+
+ await Task.Run(async () =>
+ {
+ while (true)
+ {
+ if (!_isWaitingForInput)
+ {
+ return;
+ }
+
+ await Task.Delay(10);
+
+ assigner.ReadInput();
+
+ if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape)))
+ {
+ break;
+ }
+ }
+ });
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ string pressedButton = assigner.GetPressedButton();
+
+ if (_shouldUnbind)
+ {
+ SetButtonText(ToggledButton, "Unbound");
+ }
+ else if (pressedButton != "")
+ {
+ SetButtonText(ToggledButton, pressedButton);
+ }
+
+ _shouldUnbind = false;
+ _isWaitingForInput = false;
+
+ ToggledButton.IsChecked = false;
+
+ ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null));
+
+ static void SetButtonText(ToggleButton button, string text)
+ {
+ ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock);
+
+ if (textBlock != null && textBlock is TextBlock block)
+ {
+ block.Text = text;
+ }
+ }
+ });
+ }
+
+ public void Cancel(bool shouldUnbind = false)
+ {
+ _isWaitingForInput = false;
+ ToggledButton.IsChecked = false;
+ _shouldUnbind = shouldUnbind;
+ }
+ }
+}
diff --git a/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs b/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
new file mode 100644
index 00000000..cf30d99b
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
@@ -0,0 +1,400 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Media;
+using Avalonia.Threading;
+using FluentAvalonia.Core;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Controls;
+using Ryujinx.Ava.UI.Windows;
+using Ryujinx.Common.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public static class ContentDialogHelper
+ {
+ private static bool _isChoiceDialogOpen;
+
+ private async static Task<UserResult> ShowContentDialog(
+ string title,
+ string primaryText,
+ string secondaryText,
+ string primaryButton,
+ string secondaryButton,
+ string closeButton,
+ int iconSymbol,
+ UserResult primaryButtonResult = UserResult.Ok,
+ ManualResetEvent deferResetEvent = null,
+ Func<Window, Task> doWhileDeferred = null,
+ TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
+ {
+ UserResult result = UserResult.None;
+
+ bool useOverlay = false;
+ Window mainWindow = null;
+
+ if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
+ {
+ foreach (var item in al.Windows)
+ {
+ if (item.IsActive && item is MainWindow window && window.ViewModel.IsGameRunning)
+ {
+ mainWindow = window;
+ useOverlay = true;
+ break;
+ }
+ }
+ }
+
+ ContentDialog contentDialog = null;
+ ContentDialogOverlayWindow overlay = null;
+
+ if (useOverlay)
+ {
+ overlay = new ContentDialogOverlayWindow()
+ {
+ Height = mainWindow.Bounds.Height,
+ Width = mainWindow.Bounds.Width,
+ Position = mainWindow.PointToScreen(new Point())
+ };
+
+ mainWindow.PositionChanged += OverlayOnPositionChanged;
+
+ void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
+ {
+ overlay.Position = mainWindow.PointToScreen(new Point());
+ }
+
+ contentDialog = overlay.ContentDialog;
+
+ bool opened = false;
+
+ overlay.Opened += OverlayOnActivated;
+
+ async void OverlayOnActivated(object sender, EventArgs e)
+ {
+ if (opened)
+ {
+ return;
+ }
+
+ opened = true;
+
+ overlay.Position = mainWindow.PointToScreen(new Point());
+
+ await ShowDialog();
+ }
+
+ await overlay.ShowDialog(mainWindow);
+ }
+ else
+ {
+ contentDialog = new ContentDialog();
+
+ await ShowDialog();
+ }
+
+ async Task ShowDialog()
+ {
+ contentDialog.Title = title;
+ contentDialog.PrimaryButtonText = primaryButton;
+ contentDialog.SecondaryButtonText = secondaryButton;
+ contentDialog.CloseButtonText = closeButton;
+ contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
+
+ contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
+ {
+ result = primaryButtonResult;
+ });
+ contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
+ {
+ result = UserResult.No;
+ contentDialog.PrimaryButtonClick -= deferCloseAction;
+ });
+ contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
+ {
+ result = UserResult.Cancel;
+ contentDialog.PrimaryButtonClick -= deferCloseAction;
+ });
+
+ if (deferResetEvent != null)
+ {
+ contentDialog.PrimaryButtonClick += deferCloseAction;
+ }
+
+ if (useOverlay)
+ {
+ await contentDialog.ShowAsync(overlay, ContentDialogPlacement.Popup);
+
+ overlay!.Close();
+ }
+ else
+ {
+ await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
+ }
+ }
+
+ if (useOverlay)
+ {
+ overlay.Content = null;
+ overlay.Close();
+ }
+
+ return result;
+ }
+
+ public async static Task<UserResult> ShowDeferredContentDialog(
+ StyleableWindow window,
+ string title,
+ string primaryText,
+ string secondaryText,
+ string primaryButton,
+ string secondaryButton,
+ string closeButton,
+ int iconSymbol,
+ ManualResetEvent deferResetEvent,
+ Func<Window, Task> doWhileDeferred = null)
+ {
+ bool startedDeferring = false;
+ UserResult result = UserResult.None;
+
+ return await ShowContentDialog(
+ title,
+ primaryText,
+ secondaryText,
+ primaryButton,
+ secondaryButton,
+ closeButton,
+ iconSymbol,
+ primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok,
+ deferResetEvent,
+ doWhileDeferred,
+ DeferClose);
+
+ async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ if (startedDeferring)
+ {
+ return;
+ }
+
+ sender.PrimaryButtonClick -= DeferClose;
+
+ startedDeferring = true;
+
+ var deferral = args.GetDeferral();
+
+ result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
+
+ sender.PrimaryButtonClick -= DeferClose;
+
+#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+ Task.Run(() =>
+ {
+ deferResetEvent.WaitOne();
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ deferral.Complete();
+ });
+ });
+#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+
+ if (doWhileDeferred != null)
+ {
+ await doWhileDeferred(window);
+
+ deferResetEvent.Set();
+ }
+ }
+ }
+
+ private static Grid CreateDialogTextContent(string primaryText, string secondaryText, int symbol)
+ {
+ Grid content = new Grid();
+ content.RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() };
+ content.ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() };
+
+ content.MinHeight = 80;
+
+ SymbolIcon icon = new SymbolIcon { Symbol = (Symbol)symbol, Margin = new Thickness(10) };
+ icon.FontSize = 40;
+ icon.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
+ Grid.SetColumn(icon, 0);
+ Grid.SetRowSpan(icon, 2);
+ Grid.SetRow(icon, 0);
+
+ TextBlock primaryLabel = new TextBlock()
+ {
+ Text = primaryText,
+ Margin = new Thickness(5),
+ TextWrapping = TextWrapping.Wrap,
+ MaxWidth = 450
+ };
+ TextBlock secondaryLabel = new TextBlock()
+ {
+ Text = secondaryText,
+ Margin = new Thickness(5),
+ TextWrapping = TextWrapping.Wrap,
+ MaxWidth = 450
+ };
+
+ Grid.SetColumn(primaryLabel, 1);
+ Grid.SetColumn(secondaryLabel, 1);
+ Grid.SetRow(primaryLabel, 0);
+ Grid.SetRow(secondaryLabel, 1);
+
+ content.Children.Add(icon);
+ content.Children.Add(primaryLabel);
+ content.Children.Add(secondaryLabel);
+
+ return content;
+ }
+
+ public static async Task<UserResult> CreateInfoDialog(
+ string primary,
+ string secondaryText,
+ string acceptButton,
+ string closeButton,
+ string title)
+ {
+ return await ShowContentDialog(
+ title,
+ primary,
+ secondaryText,
+ acceptButton,
+ "",
+ closeButton,
+ (int)Symbol.Important);
+ }
+
+ internal static async Task<UserResult> CreateConfirmationDialog(
+ string primaryText,
+ string secondaryText,
+ string acceptButtonText,
+ string cancelButtonText,
+ string title,
+ UserResult primaryButtonResult = UserResult.Yes)
+ {
+ return await ShowContentDialog(
+ string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance["DialogConfirmationTitle"] : title,
+ primaryText,
+ secondaryText,
+ acceptButtonText,
+ "",
+ cancelButtonText,
+ (int)Symbol.Help,
+ primaryButtonResult);
+ }
+
+ internal static UpdateWaitWindow CreateWaitingDialog(string mainText, string secondaryText)
+ {
+ return new(mainText, secondaryText);
+ }
+
+ internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText)
+ {
+ await ShowContentDialog(
+ LocaleManager.Instance["DialogUpdaterTitle"],
+ primary,
+ secondaryText,
+ "",
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Important);
+ }
+
+ internal static async Task CreateWarningDialog(string primary, string secondaryText)
+ {
+ await ShowContentDialog(
+ LocaleManager.Instance["DialogWarningTitle"],
+ primary,
+ secondaryText,
+ "",
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Important);
+ }
+
+ internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "")
+ {
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ await ShowContentDialog(
+ LocaleManager.Instance["DialogErrorTitle"],
+ LocaleManager.Instance["DialogErrorMessage"],
+ errorMessage,
+ secondaryErrorMessage,
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Dismiss);
+ }
+
+ internal static async Task<bool> CreateChoiceDialog(string title, string primary, string secondaryText)
+ {
+ if (_isChoiceDialogOpen)
+ {
+ return false;
+ }
+
+ _isChoiceDialogOpen = true;
+
+ UserResult response =
+ await ShowContentDialog(
+ title,
+ primary,
+ secondaryText,
+ LocaleManager.Instance["InputDialogYes"],
+ "",
+ LocaleManager.Instance["InputDialogNo"],
+ (int)Symbol.Help,
+ UserResult.Yes);
+
+ _isChoiceDialogOpen = false;
+
+ return response == UserResult.Yes;
+ }
+
+ internal static async Task<bool> CreateExitDialog()
+ {
+ return await CreateChoiceDialog(
+ LocaleManager.Instance["DialogExitTitle"],
+ LocaleManager.Instance["DialogExitMessage"],
+ LocaleManager.Instance["DialogExitSubMessage"]);
+ }
+
+ internal static async Task<bool> CreateStopEmulationDialog()
+ {
+ return await CreateChoiceDialog(
+ LocaleManager.Instance["DialogStopEmulationTitle"],
+ LocaleManager.Instance["DialogStopEmulationMessage"],
+ LocaleManager.Instance["DialogExitSubMessage"]);
+ }
+
+ internal static async Task<string> CreateInputDialog(
+ string title,
+ string mainText,
+ string subText,
+ uint maxLength = int.MaxValue,
+ string input = "")
+ {
+ var result = await InputDialog.ShowInputDialog(
+ title,
+ mainText,
+ input,
+ subText,
+ maxLength);
+
+ if (result.Result == UserResult.Ok)
+ {
+ return result.Input;
+ }
+
+ return string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
new file mode 100644
index 00000000..bdeceaea
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
@@ -0,0 +1,233 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Platform;
+using Ryujinx.Ava.UI.Helper;
+using SPB.Graphics;
+using SPB.Platform;
+using SPB.Platform.GLX;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Threading.Tasks;
+using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class EmbeddedWindow : NativeControlHost
+ {
+ private WindowProc _wndProcDelegate;
+ private string _className;
+
+ protected GLXWindow X11Window { get; set; }
+ protected IntPtr WindowHandle { get; set; }
+ protected IntPtr X11Display { get; set; }
+ protected IntPtr NsView { get; set; }
+ protected IntPtr MetalLayer { get; set; }
+
+ private UpdateBoundsCallbackDelegate _updateBoundsCallback;
+
+ public event EventHandler<IntPtr> WindowCreated;
+ public event EventHandler<Size> SizeChanged;
+
+ protected virtual void OnWindowDestroyed() { }
+ protected virtual void OnWindowDestroying()
+ {
+ WindowHandle = IntPtr.Zero;
+ X11Display = IntPtr.Zero;
+ }
+
+ public EmbeddedWindow()
+ {
+ var stateObserverable = this.GetObservable(BoundsProperty);
+
+ stateObserverable.Subscribe(StateChanged);
+
+ this.Initialized += NativeEmbeddedWindow_Initialized;
+ }
+
+ public virtual void OnWindowCreated() { }
+
+ private void NativeEmbeddedWindow_Initialized(object sender, EventArgs e)
+ {
+ OnWindowCreated();
+
+ Task.Run(() =>
+ {
+ WindowCreated?.Invoke(this, WindowHandle);
+ });
+ }
+
+ private void StateChanged(Rect rect)
+ {
+ SizeChanged?.Invoke(this, rect.Size);
+ _updateBoundsCallback?.Invoke(rect);
+ }
+
+ protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
+ {
+ if (OperatingSystem.IsLinux())
+ {
+ return CreateLinux(parent);
+ }
+ else if (OperatingSystem.IsWindows())
+ {
+ return CreateWin32(parent);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ return CreateMacOs(parent);
+ }
+
+ return base.CreateNativeControlCore(parent);
+ }
+
+ protected override void DestroyNativeControlCore(IPlatformHandle control)
+ {
+ OnWindowDestroying();
+
+ if (OperatingSystem.IsLinux())
+ {
+ DestroyLinux();
+ }
+ else if (OperatingSystem.IsWindows())
+ {
+ DestroyWin32(control);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ DestroyMacOS();
+ }
+ else
+ {
+ base.DestroyNativeControlCore(control);
+ }
+
+ OnWindowDestroyed();
+ }
+
+ [SupportedOSPlatform("linux")]
+ protected virtual IPlatformHandle CreateLinux(IPlatformHandle parent)
+ {
+ X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
+ WindowHandle = X11Window.WindowHandle.RawHandle;
+ X11Display = X11Window.DisplayHandle.RawHandle;
+
+ return new PlatformHandle(WindowHandle, "X11");
+ }
+
+ [SupportedOSPlatform("windows")]
+ IPlatformHandle CreateWin32(IPlatformHandle parent)
+ {
+ _className = "NativeWindow-" + Guid.NewGuid();
+ _wndProcDelegate = WndProc;
+ var wndClassEx = new WNDCLASSEX
+ {
+ cbSize = Marshal.SizeOf<WNDCLASSEX>(),
+ hInstance = GetModuleHandle(null),
+ lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
+ style = ClassStyles.CS_OWNDC,
+ lpszClassName = Marshal.StringToHGlobalUni(_className),
+ hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
+ };
+
+ var atom = RegisterClassEx(ref wndClassEx);
+
+ var handle = CreateWindowEx(
+ 0,
+ _className,
+ "NativeWindow",
+ WindowStyles.WS_CHILD,
+ 0,
+ 0,
+ 640,
+ 480,
+ parent.Handle,
+ IntPtr.Zero,
+ IntPtr.Zero,
+ IntPtr.Zero);
+
+ WindowHandle = handle;
+
+ Marshal.FreeHGlobal(wndClassEx.lpszClassName);
+
+ return new PlatformHandle(WindowHandle, "HWND");
+ }
+
+ [SupportedOSPlatform("windows")]
+ IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
+ {
+ var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF);
+ var root = VisualRoot as Window;
+ bool isLeft = false;
+ switch (msg)
+ {
+ case WindowsMessages.LBUTTONDOWN:
+ case WindowsMessages.RBUTTONDOWN:
+ isLeft = msg == WindowsMessages.LBUTTONDOWN;
+ this.RaiseEvent(new PointerPressedEventArgs(
+ this,
+ new Pointer(0, PointerType.Mouse, true),
+ root,
+ this.TranslatePoint(point, root).Value,
+ (ulong)Environment.TickCount64,
+ new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed),
+ KeyModifiers.None));
+ break;
+ case WindowsMessages.LBUTTONUP:
+ case WindowsMessages.RBUTTONUP:
+ isLeft = msg == WindowsMessages.LBUTTONUP;
+ this.RaiseEvent(new PointerReleasedEventArgs(
+ this,
+ new Pointer(0, PointerType.Mouse, true),
+ root,
+ this.TranslatePoint(point, root).Value,
+ (ulong)Environment.TickCount64,
+ new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased),
+ KeyModifiers.None,
+ isLeft ? MouseButton.Left : MouseButton.Right));
+ break;
+ case WindowsMessages.MOUSEMOVE:
+ this.RaiseEvent(new PointerEventArgs(
+ PointerMovedEvent,
+ this,
+ new Pointer(0, PointerType.Mouse, true),
+ root,
+ this.TranslatePoint(point, root).Value,
+ (ulong)Environment.TickCount64,
+ new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
+ KeyModifiers.None));
+ break;
+ }
+ return DefWindowProc(hWnd, msg, wParam, lParam);
+ }
+
+ [SupportedOSPlatform("macos")]
+ IPlatformHandle CreateMacOs(IPlatformHandle parent)
+ {
+ MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback);
+
+ NsView = nsView;
+
+ return new PlatformHandle(nsView, "NSView");
+ }
+
+ void DestroyLinux()
+ {
+ X11Window?.Dispose();
+ }
+
+ [SupportedOSPlatform("windows")]
+ void DestroyWin32(IPlatformHandle handle)
+ {
+ DestroyWindow(handle.Handle);
+ UnregisterClass(_className, GetModuleHandle(null));
+ }
+
+ [SupportedOSPlatform("macos")]
+ void DestroyMacOS()
+ {
+ MetalHelper.DestroyMetalLayer(NsView, MetalLayer);
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/Glyph.cs b/Ryujinx.Ava/UI/Helpers/Glyph.cs
new file mode 100644
index 00000000..4aae854f
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/Glyph.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public enum Glyph
+ {
+ List,
+ Grid,
+ Chip
+ }
+}
diff --git a/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs b/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs
new file mode 100644
index 00000000..3d6c9c01
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs
@@ -0,0 +1,49 @@
+using Avalonia.Data;
+using Avalonia.Markup.Xaml;
+using FluentAvalonia.UI.Controls;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class GlyphValueConverter : MarkupExtension
+ {
+ private string _key;
+
+ private static Dictionary<Glyph, string> _glyphs = new Dictionary<Glyph, string>
+ {
+ { Glyph.List, char.ConvertFromUtf32((int)Symbol.List).ToString() },
+ { Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll).ToString() },
+ { Glyph.Chip, char.ConvertFromUtf32(59748).ToString() }
+ };
+
+ public GlyphValueConverter(string key)
+ {
+ _key = key;
+ }
+
+ public string this[string key]
+ {
+ get
+ {
+ if (_glyphs.TryGetValue(Enum.Parse<Glyph>(key), out var val))
+ {
+ return val;
+ }
+
+ return string.Empty;
+ }
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension binding = new($"[{_key}]")
+ {
+ Mode = BindingMode.OneWay,
+ Source = this
+ };
+
+ return binding.ProvideValue(serviceProvider);
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs b/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs
new file mode 100644
index 00000000..f1fad157
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs
@@ -0,0 +1,52 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using System;
+using System.Windows.Input;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class HotKeyControl : ContentControl, ICommandSource
+ {
+ public static readonly StyledProperty<object> CommandParameterProperty =
+ AvaloniaProperty.Register<HotKeyControl, object>(nameof(CommandParameter));
+
+ public static readonly DirectProperty<HotKeyControl, ICommand> CommandProperty =
+ AvaloniaProperty.RegisterDirect<HotKeyControl, ICommand>(nameof(Command),
+ control => control.Command, (control, command) => control.Command = command, enableDataValidation: true);
+
+ public static readonly StyledProperty<KeyGesture> HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner<Button>();
+
+ private ICommand _command;
+ private bool _commandCanExecute;
+
+ public ICommand Command
+ {
+ get { return _command; }
+ set { SetAndRaise(CommandProperty, ref _command, value); }
+ }
+
+ public KeyGesture HotKey
+ {
+ get { return GetValue(HotKeyProperty); }
+ set { SetValue(HotKeyProperty, value); }
+ }
+
+ public object CommandParameter
+ {
+ get { return GetValue(CommandParameterProperty); }
+ set { SetValue(CommandParameterProperty, value); }
+ }
+
+ public void CanExecuteChanged(object sender, EventArgs e)
+ {
+ var canExecute = Command == null || Command.CanExecute(CommandParameter);
+
+ if (canExecute != _commandCanExecute)
+ {
+ _commandCanExecute = canExecute;
+ UpdateIsEffectivelyEnabled();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs b/Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs
new file mode 100644
index 00000000..e69774c3
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs
@@ -0,0 +1,25 @@
+using Avalonia.OpenGL;
+using SPB.Graphics.OpenGL;
+using System;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal static class IGlContextExtension
+ {
+ public static OpenGLContextBase AsOpenGLContextBase(this IGlContext context)
+ {
+ var handle = (IntPtr)context.GetType().GetProperty("Handle").GetValue(context);
+
+ if (OperatingSystem.IsWindows())
+ {
+ return new AvaloniaWglContext(handle);
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ return new AvaloniaGlxContext(handle);
+ }
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
new file mode 100644
index 00000000..8d5c2815
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
@@ -0,0 +1,46 @@
+using Avalonia.Data.Converters;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using System;
+using System.Globalization;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal class KeyValueConverter : IValueConverter
+ {
+ public static KeyValueConverter Instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+
+ return value.ToString();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ object key = null;
+
+ if (value != null)
+ {
+ if (targetType == typeof(Key))
+ {
+ key = Enum.Parse<Key>(value.ToString());
+ }
+ else if (targetType == typeof(GamepadInputId))
+ {
+ key = Enum.Parse<GamepadInputId>(value.ToString());
+ }
+ else if (targetType == typeof(StickInputId))
+ {
+ key = Enum.Parse<StickInputId>(value.ToString());
+ }
+ }
+
+ return key;
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/MiniCommand.cs b/Ryujinx.Ava/UI/Helpers/MiniCommand.cs
new file mode 100644
index 00000000..305182c9
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/MiniCommand.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public sealed class MiniCommand<T> : MiniCommand, ICommand
+ {
+ private readonly Action<T> _callback;
+ private bool _busy;
+ private Func<T, Task> _asyncCallback;
+
+ public MiniCommand(Action<T> callback)
+ {
+ _callback = callback;
+ }
+
+ public MiniCommand(Func<T, Task> callback)
+ {
+ _asyncCallback = callback;
+ }
+
+ private bool Busy
+ {
+ get => _busy;
+ set
+ {
+ _busy = value;
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public override event EventHandler CanExecuteChanged;
+ public override bool CanExecute(object parameter) => !_busy;
+
+ public override async void Execute(object parameter)
+ {
+ if (Busy)
+ {
+ return;
+ }
+ try
+ {
+ Busy = true;
+ if (_callback != null)
+ {
+ _callback((T)parameter);
+ }
+ else
+ {
+ await _asyncCallback((T)parameter);
+ }
+ }
+ finally
+ {
+ Busy = false;
+ }
+ }
+ }
+
+ public abstract class MiniCommand : ICommand
+ {
+ public static MiniCommand Create(Action callback) => new MiniCommand<object>(_ => callback());
+ public static MiniCommand Create<TArg>(Action<TArg> callback) => new MiniCommand<TArg>(callback);
+ public static MiniCommand CreateFromTask(Func<Task> callback) => new MiniCommand<object>(_ => callback());
+
+ public abstract bool CanExecute(object parameter);
+ public abstract void Execute(object parameter);
+ public abstract event EventHandler CanExecuteChanged;
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs b/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs
new file mode 100644
index 00000000..785e785c
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs
@@ -0,0 +1,40 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class OffscreenTextBox : TextBox
+ {
+ public RoutedEvent<KeyEventArgs> GetKeyDownRoutedEvent()
+ {
+ return KeyDownEvent;
+ }
+
+ public RoutedEvent<KeyEventArgs> GetKeyUpRoutedEvent()
+ {
+ return KeyUpEvent;
+ }
+
+ public void SendKeyDownEvent(KeyEventArgs keyEvent)
+ {
+ OnKeyDown(keyEvent);
+ }
+
+ public void SendKeyUpEvent(KeyEventArgs keyEvent)
+ {
+ OnKeyUp(keyEvent);
+ }
+
+ public void SendText(string text)
+ {
+ OnTextInput(new TextInputEventArgs()
+ {
+ Text = text,
+ Device = KeyboardDevice.Instance,
+ Source = this,
+ RoutedEvent = TextInputEvent
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs
new file mode 100644
index 00000000..db77f66b
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs
@@ -0,0 +1,82 @@
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common.Configuration;
+using SPB.Graphics;
+using SPB.Graphics.OpenGL;
+using SPB.Platform;
+using SPB.Platform.WGL;
+using SPB.Windowing;
+using System;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class OpenGLEmbeddedWindow : EmbeddedWindow
+ {
+ private readonly int _major;
+ private readonly int _minor;
+ private readonly GraphicsDebugLevel _graphicsDebugLevel;
+ private SwappableNativeWindowBase _window;
+ public OpenGLContextBase Context { get; set; }
+
+ public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
+ {
+ _major = major;
+ _minor = minor;
+ _graphicsDebugLevel = graphicsDebugLevel;
+ }
+
+ protected override void OnWindowDestroying()
+ {
+ Context.Dispose();
+ base.OnWindowDestroying();
+ }
+
+ public override void OnWindowCreated()
+ {
+ base.OnWindowCreated();
+
+ if (OperatingSystem.IsWindows())
+ {
+ _window = new WGLWindow(new NativeHandle(WindowHandle));
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ _window = X11Window;
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ var flags = OpenGLContextFlags.Compat;
+ if (_graphicsDebugLevel != GraphicsDebugLevel.None)
+ {
+ flags |= OpenGLContextFlags.Debug;
+ }
+
+ Context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, _major, _minor, flags);
+
+ Context.Initialize(_window);
+ Context.MakeCurrent(_window);
+
+ var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress);
+
+ GL.LoadBindings(bindingsContext);
+ Context.MakeCurrent(null);
+ }
+
+ public void MakeCurrent()
+ {
+ Context?.MakeCurrent(_window);
+ }
+
+ public void MakeCurrent(NativeWindowBase window)
+ {
+ Context?.MakeCurrent(window);
+ }
+
+ public void SwapBuffers()
+ {
+ _window.SwapBuffers();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs b/Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs
new file mode 100644
index 00000000..efb703ba
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs
@@ -0,0 +1,20 @@
+using OpenTK;
+using System;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal class OpenToolkitBindingsContext : IBindingsContext
+ {
+ private readonly Func<string, IntPtr> _getProcAddress;
+
+ public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress)
+ {
+ _getProcAddress = getProcAddress;
+ }
+
+ public IntPtr GetProcAddress(string procName)
+ {
+ return _getProcAddress(procName);
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs b/Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs
new file mode 100644
index 00000000..21f206c8
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs
@@ -0,0 +1,47 @@
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Graphics.OpenGL;
+using SPB.Graphics;
+using SPB.Graphics.OpenGL;
+using SPB.Platform;
+using SPB.Windowing;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ class SPBOpenGLContext : IOpenGLContext
+ {
+ private OpenGLContextBase _context;
+ private NativeWindowBase _window;
+
+ private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
+ {
+ _context = context;
+ _window = window;
+ }
+
+ public void Dispose()
+ {
+ _context.Dispose();
+ _window.Dispose();
+ }
+
+ public void MakeCurrent()
+ {
+ _context.MakeCurrent(_window);
+ }
+
+ public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
+ {
+ OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
+ NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
+
+ context.Initialize(window);
+ context.MakeCurrent(window);
+
+ GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress));
+
+ context.MakeCurrent(null);
+
+ return new SPBOpenGLContext(context, window);
+ }
+ }
+}
diff --git a/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs b/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs
new file mode 100644
index 00000000..ab8d6edc
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs
@@ -0,0 +1,91 @@
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.UI.Windows;
+using Ryujinx.Ui.Common;
+using Ryujinx.Ui.Common.Helper;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal class UserErrorDialog
+ {
+ private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide";
+
+ private static string GetErrorCode(UserError error)
+ {
+ return $"RYU-{(uint)error:X4}";
+ }
+
+ private static string GetErrorTitle(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeys"],
+ UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmware"],
+ UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailed"],
+ UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFound"],
+ UserError.Unknown => LocaleManager.Instance["UserErrorUnknown"],
+ _ => LocaleManager.Instance["UserErrorUndefined"]
+ };
+ }
+
+ private static string GetErrorDescription(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeysDescription"],
+ UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmwareDescription"],
+ UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailedDescription"],
+ UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFoundDescription"],
+ UserError.Unknown => LocaleManager.Instance["UserErrorUnknownDescription"],
+ _ => LocaleManager.Instance["UserErrorUndefinedDescription"]
+ };
+ }
+
+ private static bool IsCoveredBySetupGuide(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys or
+ UserError.NoFirmware or
+ UserError.FirmwareParsingFailed => true,
+ _ => false
+ };
+ }
+
+ private static string GetSetupGuideUrl(UserError error)
+ {
+ if (!IsCoveredBySetupGuide(error))
+ {
+ return null;
+ }
+
+ return error switch
+ {
+ UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys",
+ UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware",
+ _ => SetupGuideUrl
+ };
+ }
+
+ public static async Task ShowUserErrorDialog(UserError error, StyleableWindow owner)
+ {
+ string errorCode = GetErrorCode(error);
+
+ bool isInSetupGuide = IsCoveredBySetupGuide(error);
+
+ string setupButtonLabel = isInSetupGuide ? LocaleManager.Instance["OpenSetupGuideMessage"] : "";
+
+ var result = await ContentDialogHelper.CreateInfoDialog(
+ string.Format(LocaleManager.Instance["DialogUserErrorDialogMessage"], errorCode, GetErrorTitle(error)),
+ GetErrorDescription(error) + (isInSetupGuide
+ ? LocaleManager.Instance["DialogUserErrorDialogInfoMessage"]
+ : ""), setupButtonLabel, LocaleManager.Instance["InputDialogOk"],
+ string.Format(LocaleManager.Instance["DialogUserErrorDialogTitle"], errorCode));
+
+ if (result == UserResult.Ok)
+ {
+ OpenHelper.OpenUrl(GetSetupGuideUrl(error));
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/UserResult.cs b/Ryujinx.Ava/UI/Helpers/UserResult.cs
new file mode 100644
index 00000000..57802804
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/UserResult.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public enum UserResult
+ {
+ Ok,
+ Yes,
+ No,
+ Abort,
+ Cancel,
+ None,
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs
new file mode 100644
index 00000000..6581610b
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs
@@ -0,0 +1,52 @@
+using Avalonia.Platform;
+using Silk.NET.Vulkan;
+using SPB.Graphics.Vulkan;
+using SPB.Platform.GLX;
+using SPB.Platform.Metal;
+using SPB.Platform.Win32;
+using SPB.Platform.X11;
+using SPB.Windowing;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ public class VulkanEmbeddedWindow : EmbeddedWindow
+ {
+ private NativeWindowBase _window;
+
+ [SupportedOSPlatform("linux")]
+ protected override IPlatformHandle CreateLinux(IPlatformHandle parent)
+ {
+ X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(parent.Handle));
+ WindowHandle = X11Window.WindowHandle.RawHandle;
+ X11Display = X11Window.DisplayHandle.RawHandle;
+
+ X11Window.Hide();
+
+ return new PlatformHandle(WindowHandle, "X11");
+ }
+
+ public SurfaceKHR CreateSurface(Instance instance)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _window = new SimpleWin32Window(new NativeHandle(WindowHandle));
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ _window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ _window = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, _window));
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
new file mode 100644
index 00000000..1e6e3c3b
--- /dev/null
+++ b/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ [SupportedOSPlatform("windows")]
+ internal partial class Win32NativeInterop
+ {
+ [Flags]
+ public enum ClassStyles : uint
+ {
+ CS_CLASSDC = 0x40,
+ CS_OWNDC = 0x20,
+ }
+
+ [Flags]
+ public enum WindowStyles : uint
+ {
+ WS_CHILD = 0x40000000
+ }
+
+ public enum Cursors : uint
+ {
+ IDC_ARROW = 32512
+ }
+
+ public enum WindowsMessages : uint
+ {
+ MOUSEMOVE = 0x0200,
+ LBUTTONDOWN = 0x0201,
+ LBUTTONUP = 0x0202,
+ LBUTTONDBLCLK = 0x0203,
+ RBUTTONDOWN = 0x0204,
+ RBUTTONUP = 0x0205,
+ RBUTTONDBLCLK = 0x0206,
+ MBUTTONDOWN = 0x0207,
+ MBUTTONUP = 0x0208,
+ MBUTTONDBLCLK = 0x0209,
+ MOUSEWHEEL = 0x020A,
+ XBUTTONDOWN = 0x020B,
+ XBUTTONUP = 0x020C,
+ XBUTTONDBLCLK = 0x020D,
+ MOUSEHWHEEL = 0x020E,
+ MOUSELAST = 0x020E
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ internal delegate IntPtr WindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct WNDCLASSEX
+ {
+ public int cbSize;
+ public ClassStyles style;
+ public IntPtr lpfnWndProc; // not WndProc
+ public int cbClsExtra;
+ public int cbWndExtra;
+ public IntPtr hInstance;
+ public IntPtr hIcon;
+ public IntPtr hCursor;
+ public IntPtr hbrBackground;
+ public IntPtr lpszMenuName;
+ public IntPtr lpszClassName;
+ public IntPtr hIconSm;
+
+ public WNDCLASSEX()
+ {
+ cbSize = Marshal.SizeOf<WNDCLASSEX>();
+ }
+ }
+
+ [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
+ public static partial ushort RegisterClassEx(ref WNDCLASSEX param);
+
+ [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "UnregisterClassW")]
+ public static partial short UnregisterClass([MarshalAs(UnmanagedType.LPWStr)] string lpClassName, IntPtr instance);
+
+ [LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")]
+ public static partial IntPtr DefWindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam);
+
+ [LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleA")]
+ public static partial IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPStr)] string lpModuleName);
+
+ [LibraryImport("user32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool DestroyWindow(IntPtr hwnd);
+
+ [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "LoadCursorA")]
+ public static partial IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName);
+
+ [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "CreateWindowExW")]
+ public static partial IntPtr CreateWindowEx(
+ uint dwExStyle,
+ [MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
+ [MarshalAs(UnmanagedType.LPWStr)] string lpWindowName,
+ WindowStyles dwStyle,
+ int x,
+ int y,
+ int nWidth,
+ int nHeight,
+ IntPtr hWndParent,
+ IntPtr hMenu,
+ IntPtr hInstance,
+ IntPtr lpParam);
+ }
+}