aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/Controls
diff options
context:
space:
mode:
authorEmmanuel Hansen <emmausssss@gmail.com>2022-05-15 11:30:15 +0000
committerGitHub <noreply@github.com>2022-05-15 13:30:15 +0200
commitdeb99d2cae3e80bdf70cb52c6c160094dc7c9292 (patch)
treee60f44d1b4bd45bbf36fcfa750fb99787febfdbe /Ryujinx.Ava/Ui/Controls
parent9ba73ffbe5f78c0403cf102b95768f388da05122 (diff)
Avalonia UI - Part 1 (#3270)
* avalonia part 1 * remove vulkan ui backend * move ui common files to ui common project * get name for oading screen from device * rebase. * review 1 * review 1.1 * review * cleanup * addressed review * use cancellation token * review * review * rebased * cancel library loading when closing window * remove star image, use fonticon instead * delete render control frame buffer when game ends. change position of fav star * addressed @Thog review * ensure the right ui is downloaded in updates * fix crash when showing not supported dialog during controller request * add prefix to artifact names * Auto-format Avalonia project * Fix input * Fix build, simplify app disposal * remove nv stutter thread * addressed review * add missing change * maintain window size if new size is zero length * add game, handheld, docked to local * reverse scale main window * Update de_DE.json * Update de_DE.json * Update de_DE.json * Update italian json * Update it_IT.json * let render timer poll with no wait * remove unused code * more unused code * enabled tiered compilation and trimming * check if window event is not closed before signaling * fix atmospher case * locale fix * locale fix * remove explicit tiered compilation declarations * Remove ) it_IT.json * Remove ) de_DE.json * Update it_IT.json * Update pt_BR locale with latest strings * Remove ')' * add more strings to locale * update locale * remove extra slash * remove extra slash * set firmware version to 0 if key's not found * fix * revert timer changes * lock on object instead * Update it_IT.json * remove unused method * add load screen text to locale * drop swap event * Update de_DE.json * Update de_DE.json * do null check when stopping emulator * Update de_DE.json * Create tr_TR.json * Add tr_TR * Add tr_TR + Turkish * Update it_IT.json * Update Ryujinx.Ava/Input/AvaloniaMappingHelper.cs Co-authored-by: Ac_K <Acoustik666@gmail.com> * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * Apply suggestions from code review Co-authored-by: Ac_K <Acoustik666@gmail.com> * addressed review * Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs Co-authored-by: gdkchan <gab.dark.100@gmail.com> * use avalonia's inbuilt renderer on linux * removed whitespace * workaround for queue render crash with vsync off * drop custom backend * format files * fix not closing issue * remove warnings * rebase * update avalonia library * Reposition the Text and Button on About Page * Assign build version * Remove appveyor text Co-authored-by: gdk <gab.dark.100@gmail.com> Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com> Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com> Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com> Co-authored-by: Ac_K <Acoustik666@gmail.com> Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/Ui/Controls')
-rw-r--r--Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs16
-rw-r--r--Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs16
-rw-r--r--Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs16
-rw-r--r--Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs35
-rw-r--r--Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs357
-rw-r--r--Ryujinx.Ava/Ui/Controls/GameGridView.axaml188
-rw-r--r--Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs82
-rw-r--r--Ryujinx.Ava/Ui/Controls/GameListView.axaml188
-rw-r--r--Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs82
-rw-r--r--Ryujinx.Ava/Ui/Controls/Glyph.cs9
-rw-r--r--Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs49
-rw-r--r--Ryujinx.Ava/Ui/Controls/HotKeyControl.cs52
-rw-r--r--Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs25
-rw-r--r--Ryujinx.Ava/Ui/Controls/InputDialog.axaml18
-rw-r--r--Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs67
-rw-r--r--Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs46
-rw-r--r--Ryujinx.Ava/Ui/Controls/MiniCommand.cs71
-rw-r--r--Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs40
-rw-r--r--Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs20
-rw-r--r--Ryujinx.Ava/Ui/Controls/RenderTimer.cs100
-rw-r--r--Ryujinx.Ava/Ui/Controls/RendererControl.cs273
-rw-r--r--Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs47
-rw-r--r--Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml28
-rw-r--r--Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs35
-rw-r--r--Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs91
-rw-r--r--Ryujinx.Ava/Ui/Controls/UserResult.cs12
26 files changed, 1963 insertions, 0 deletions
diff --git a/Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs b/Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs
new file mode 100644
index 00000000..9909bd6a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs
@@ -0,0 +1,16 @@
+using Avalonia.Interactivity;
+using Ryujinx.Ui.App.Common;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ 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/Controls/AvaloniaGlxContext.cs b/Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs
new file mode 100644
index 00000000..05f28cb1
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs
@@ -0,0 +1,16 @@
+using SPB.Graphics;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ [SupportedOSPlatform("linux")]
+ public 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/Controls/AvaloniaWglContext.cs b/Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs
new file mode 100644
index 00000000..d7781fac
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs
@@ -0,0 +1,16 @@
+using SPB.Graphics;
+using System;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ [SupportedOSPlatform("windows")]
+ public 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/Controls/BitmapArrayValueConverter.cs b/Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs
new file mode 100644
index 00000000..7222d67b
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/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.Controls
+{
+ public 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/Controls/ContentDialogHelper.cs b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
new file mode 100644
index 00000000..2794a815
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs
@@ -0,0 +1,357 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.Common.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public static class ContentDialogHelper
+ {
+ private static bool _isChoiceDialogOpen;
+
+ private async static Task<UserResult> ShowContentDialog(
+ StyleableWindow window,
+ string title,
+ string primaryText,
+ string secondaryText,
+ string primaryButton,
+ string secondaryButton,
+ string closeButton,
+ int iconSymbol,
+ UserResult primaryButtonResult = UserResult.Ok)
+ {
+ UserResult result = UserResult.None;
+
+ ContentDialog contentDialog = window.ContentDialog;
+
+ await ShowDialog();
+
+ async Task ShowDialog()
+ {
+ if (contentDialog != null)
+ {
+ 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.CloseButtonCommand = MiniCommand.Create(() =>
+ {
+ result = UserResult.Cancel;
+ });
+
+ await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
+ };
+ }
+
+ 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;
+
+ ContentDialog contentDialog = window.ContentDialog;
+
+ Window overlay = window;
+
+ if (contentDialog != null)
+ {
+ contentDialog.PrimaryButtonClick += DeferClose;
+ contentDialog.Title = title;
+ contentDialog.PrimaryButtonText = primaryButton;
+ contentDialog.SecondaryButtonText = secondaryButton;
+ contentDialog.CloseButtonText = closeButton;
+ contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
+
+ contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
+ {
+ result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
+ });
+ contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
+ {
+ result = UserResult.No;
+ });
+ contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
+ {
+ result = UserResult.Cancel;
+ });
+ await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
+ };
+
+ return result;
+
+ async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ if (startedDeferring)
+ {
+ return;
+ }
+
+ startedDeferring = true;
+
+ var deferral = args.GetDeferral();
+
+ result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
+
+ contentDialog.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(overlay);
+
+ 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 Avalonia.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 Avalonia.Thickness(5),
+ TextWrapping = Avalonia.Media.TextWrapping.Wrap,
+ MaxWidth = 450
+ };
+ TextBlock secondaryLabel = new TextBlock()
+ {
+ Text = secondaryText,
+ Margin = new Avalonia.Thickness(5),
+ TextWrapping = Avalonia.Media.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(
+ StyleableWindow window,
+ string primary,
+ string secondaryText,
+ string acceptButton,
+ string closeButton,
+ string title)
+ {
+ return await ShowContentDialog(
+ window,
+ title,
+ primary,
+ secondaryText,
+ acceptButton,
+ "",
+ closeButton,
+ (int)Symbol.Important);
+ }
+
+ internal static async Task<UserResult> CreateConfirmationDialog(
+ StyleableWindow window,
+ string primaryText,
+ string secondaryText,
+ string acceptButtonText,
+ string cancelButtonText,
+ string title,
+ UserResult primaryButtonResult = UserResult.Yes)
+ {
+ return await ShowContentDialog(
+ window,
+ 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 void CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText)
+ {
+ await ShowContentDialog(
+ window,
+ LocaleManager.Instance["DialogUpdaterTitle"],
+ primary,
+ secondaryText,
+ "",
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Important);
+ }
+
+ internal static async void ShowNotAvailableMessage(StyleableWindow window)
+ {
+ // Temporary placeholder for features to be added
+ await ShowContentDialog(
+ window,
+ "Feature Not Available",
+ "The selected feature is not available in this version.",
+ "",
+ "",
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Important);
+ }
+
+ internal static async void CreateWarningDialog(StyleableWindow window, string primary, string secondaryText)
+ {
+ await ShowContentDialog(
+ window,
+ LocaleManager.Instance["DialogWarningTitle"],
+ primary,
+ secondaryText,
+ "",
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Important);
+ }
+
+ internal static async void CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "")
+ {
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ await ShowContentDialog(
+ owner,
+ LocaleManager.Instance["DialogErrorTitle"],
+ LocaleManager.Instance["DialogErrorMessage"],
+ errorMessage,
+ secondaryErrorMessage,
+ "",
+ LocaleManager.Instance["InputDialogOk"],
+ (int)Symbol.Dismiss);
+ }
+
+ internal static async Task<bool> CreateChoiceDialog(StyleableWindow window, string title, string primary, string secondaryText)
+ {
+ if (_isChoiceDialogOpen)
+ {
+ return false;
+ }
+
+ _isChoiceDialogOpen = true;
+
+ UserResult response =
+ await ShowContentDialog(
+ window,
+ 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(StyleableWindow owner)
+ {
+ return await CreateChoiceDialog(
+ owner,
+ LocaleManager.Instance["DialogExitTitle"],
+ LocaleManager.Instance["DialogExitMessage"],
+ LocaleManager.Instance["DialogExitSubMessage"]);
+ }
+
+ internal static async Task<bool> CreateStopEmulationDialog(StyleableWindow owner)
+ {
+ return await CreateChoiceDialog(
+ owner,
+ LocaleManager.Instance["DialogStopEmulationTitle"],
+ LocaleManager.Instance["DialogStopEmulationMessage"],
+ LocaleManager.Instance["DialogExitSubMessage"]);
+ }
+
+ internal static async Task<string> CreateInputDialog(
+ string title,
+ string mainText,
+ string subText,
+ StyleableWindow owner,
+ uint maxLength = int.MaxValue,
+ string input = "")
+ {
+ var result = await InputDialog.ShowInputDialog(
+ owner,
+ 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/Controls/GameGridView.axaml b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml
new file mode 100644
index 00000000..13b75f11
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml
@@ -0,0 +1,188 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="Ryujinx.Ava.Ui.Controls.GameGridView">
+ <UserControl.Resources>
+ <controls:BitmapArrayValueConverter x:Key="ByteImage" />
+ <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
+ <MenuItem
+ Command="{Binding ToggleFavorite}"
+ Header="{locale:Locale GameListContextMenuToggleFavorite}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenUserSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDeviceSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenBcatSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenTitleUpdateManager}"
+ Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDlcManager}"
+ Header="{locale:Locale GameListContextMenuManageDlc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
+ <MenuItem
+ Command="{Binding OpenCheatManager}"
+ Header="{locale:Locale GameListContextMenuManageCheat}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
+ <MenuItem
+ Command="{Binding OpenModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenSdModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
+ <Separator />
+ <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
+ <MenuItem
+ Command="{Binding PurgePtcCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
+ <MenuItem
+ Command="{Binding PurgeShaderCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
+ <MenuItem
+ Command="{Binding OpenPtcDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenShaderCacheDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
+ </MenuItem>
+ <MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
+ <MenuItem
+ Command="{Binding ExtractExeFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractRomFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractLogo}"
+ Header="{locale:Locale GameListContextMenuExtractDataLogo}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
+ </MenuItem>
+ </MenuFlyout>
+ </UserControl.Resources>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*" />
+ </Grid.RowDefinitions>
+ <ListBox Grid.Row="0"
+ Padding="8"
+ HorizontalAlignment="Stretch"
+ DoubleTapped="GameList_DoubleTapped"
+ SelectionChanged="GameList_SelectionChanged"
+ ContextFlyout="{StaticResource GameContextMenu}"
+ VerticalAlignment="Stretch"
+ Items="{Binding AppsObservableList}">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <flex:FlexPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" JustifyContent="Center"
+ AlignContent="FlexStart" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Padding" Value="0" />
+ <Setter Property="Margin" Value="5" />
+ <Setter Property="CornerRadius" Value="5" />
+ <Setter Property="Background" Value="{DynamicResource SystemAccentColorDark3}" />
+ <Style.Animations>
+ <Animation Duration="0:0:0.7">
+ <KeyFrame Cue="0%">
+ <Setter Property="MaxWidth" Value="0"/>
+ <Setter Property="Opacity" Value="0.0"/>
+ </KeyFrame>
+ <KeyFrame Cue="50%">
+ <Setter Property="MaxWidth" Value="1000"/>
+ <Setter Property="Opacity" Value="0.3"/>
+ </KeyFrame>
+ <KeyFrame Cue="100%">
+ <Setter Property="MaxWidth" Value="1000"/>
+ <Setter Property="Opacity" Value="1.0"/>
+ </KeyFrame>
+ </Animation>
+ </Style.Animations>
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Grid.Styles>
+ <Style Selector="ui|SymbolIcon.small.icon">
+ <Setter Property="FontSize" Value="15" />
+ </Style>
+ <Style Selector="ui|SymbolIcon.normal.icon">
+ <Setter Property="FontSize" Value="19" />
+ </Style>
+ <Style Selector="ui|SymbolIcon.large.icon">
+ <Setter Property="FontSize" Value="23" />
+ </Style>
+ <Style Selector="ui|SymbolIcon.huge.icon">
+ <Setter Property="FontSize" Value="26" />
+ </Style>
+ </Grid.Styles>
+ <Border
+ Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
+ Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
+ Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
+ Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
+ HorizontalAlignment="Stretch"
+ Padding="{Binding $parent[UserControl].DataContext.GridItemPadding}" CornerRadius="5"
+ VerticalAlignment="Stretch" Margin="0" ClipToBounds="True">
+ <Grid Margin="0">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Image HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="0" Grid.Row="0"
+ Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
+ <StackPanel IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}"
+ Height="50" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
+ Margin="5" Grid.Row="1">
+ <TextBlock Text="{Binding TitleName}" TextAlignment="Center" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ </StackPanel>
+ </Grid>
+ </Border>
+ <ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
+ Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
+ Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
+ Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
+ Foreground="Yellow" Symbol="StarFilled"
+ IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top"
+ HorizontalAlignment="Left" />
+ <ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
+ Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
+ Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
+ Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
+ Foreground="Black" Symbol="Star"
+ IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top"
+ HorizontalAlignment="Left" />
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs
new file mode 100644
index 00000000..4dfe4f0e
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs
@@ -0,0 +1,82 @@
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using LibHac.Common;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.Ui.App.Common;
+using System;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public partial class GameGridView : UserControl
+ {
+ private ApplicationData _selectedApplication;
+ public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
+ RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
+
+ public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
+ {
+ add { AddHandler(ApplicationOpenedEvent, value); }
+ remove { RemoveHandler(ApplicationOpenedEvent, value); }
+ }
+
+ public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
+ {
+ if (sender is ListBox listBox)
+ {
+ if (listBox.SelectedItem is ApplicationData selected)
+ {
+ RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
+ }
+ }
+ }
+
+ public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args)
+ {
+ if (sender is ListBox listBox)
+ {
+ var selected = listBox.SelectedItem as ApplicationData;
+
+ _selectedApplication = selected;
+ }
+ }
+
+ public ApplicationData SelectedApplication => _selectedApplication;
+
+ public GameGridView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
+ {
+ (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
+ }
+
+ private void MenuBase_OnMenuOpened(object sender, EventArgs e)
+ {
+ var selection = SelectedApplication;
+
+ if (selection != null)
+ {
+ if (sender is ContextMenu menu)
+ {
+ bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
+ bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
+ bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
+
+ ((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
+ ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
+ ((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
+ }
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Controls/GameListView.axaml b/Ryujinx.Ava/Ui/Controls/GameListView.axaml
new file mode 100644
index 00000000..7ab79c23
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/GameListView.axaml
@@ -0,0 +1,188 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
+ xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="Ryujinx.Ava.Ui.Controls.GameListView">
+ <UserControl.Resources>
+ <controls:BitmapArrayValueConverter x:Key="ByteImage" />
+ <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
+ <MenuItem
+ Command="{Binding ToggleFavorite}"
+ Header="{locale:Locale GameListContextMenuToggleFavorite}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenUserSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDeviceSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenBcatSaveDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
+ <Separator />
+ <MenuItem
+ Command="{Binding OpenTitleUpdateManager}"
+ Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
+ <MenuItem
+ Command="{Binding OpenDlcManager}"
+ Header="{locale:Locale GameListContextMenuManageDlc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
+ <MenuItem
+ Command="{Binding OpenCheatManager}"
+ Header="{locale:Locale GameListContextMenuManageCheat}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
+ <MenuItem
+ Command="{Binding OpenModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenSdModsDirectory}"
+ Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
+ <Separator />
+ <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
+ <MenuItem
+ Command="{Binding PurgePtcCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
+ <MenuItem
+ Command="{Binding PurgeShaderCache}"
+ Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
+ <MenuItem
+ Command="{Binding OpenPtcDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
+ <MenuItem
+ Command="{Binding OpenShaderCacheDirectory}"
+ Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
+ </MenuItem>
+ <MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
+ <MenuItem
+ Command="{Binding ExtractExeFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractRomFs}"
+ Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
+ <MenuItem
+ Command="{Binding ExtractLogo}"
+ Header="{locale:Locale GameListContextMenuExtractDataLogo}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
+ </MenuItem>
+ </MenuFlyout>
+ </UserControl.Resources>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="*" />
+ </Grid.RowDefinitions>
+ <ListBox Grid.Row="0"
+ Padding="8"
+ HorizontalAlignment="Stretch"
+ DoubleTapped="GameList_DoubleTapped"
+ SelectionChanged="GameList_SelectionChanged"
+ ContextFlyout="{StaticResource GameContextMenu}"
+ VerticalAlignment="Stretch"
+ Name="GameListBox"
+ Items="{Binding AppsObservableList}">
+ <ListBox.ItemsPanel>
+ <ItemsPanelTemplate>
+ <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical" Spacing="2" />
+ </ItemsPanelTemplate>
+ </ListBox.ItemsPanel>
+ <ListBox.Styles>
+ <Style Selector="ListBoxItem">
+ <Setter Property="Padding" Value="0" />
+ <Setter Property="Margin" Value="0" />
+ <Setter Property="CornerRadius" Value="5" />
+ <Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark3}" />
+ <Setter Property="BorderThickness" Value="2" />
+ <Style.Animations>
+ <Animation Duration="0:0:0.7">
+ <KeyFrame Cue="0%">
+ <Setter Property="MaxHeight" Value="0"/>
+ <Setter Property="Opacity" Value="0.0"/>
+ </KeyFrame>
+ <KeyFrame Cue="50%">
+ <Setter Property="MaxHeight" Value="1000"/>
+ <Setter Property="Opacity" Value="0.3"/>
+ </KeyFrame>
+ <KeyFrame Cue="100%">
+ <Setter Property="MaxHeight" Value="1000"/>
+ <Setter Property="Opacity" Value="1.0"/>
+ </KeyFrame>
+ </Animation>
+ </Style.Animations>
+ </Style>
+ </ListBox.Styles>
+ <ListBox.ItemTemplate>
+ <DataTemplate>
+ <Grid>
+ <Border HorizontalAlignment="Stretch"
+ Padding="10" CornerRadius="5"
+ VerticalAlignment="Stretch" Margin="0" ClipToBounds="True">
+ <Grid >
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="10"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <Image
+ Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
+ Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
+ Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
+ Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
+ Grid.RowSpan="3" Grid.Column="0" Margin="0"
+ Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
+ <StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Left"
+ Grid.Column="2">
+ <TextBlock Text="{Binding TitleName}" TextAlignment="Left" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ <TextBlock Text="{Binding Developer}" TextAlignment="Left" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ <TextBlock Text="{Binding Version}" TextAlignment="Left" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ </StackPanel>
+ <StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Right"
+ Grid.Column="3">
+ <TextBlock Text="{Binding TimePlayed}" TextAlignment="Right" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ <TextBlock Text="{Binding LastPlayed}" TextAlignment="Right" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ <TextBlock Text="{Binding FileSize}" TextAlignment="Right" TextWrapping="Wrap"
+ HorizontalAlignment="Stretch" />
+ </StackPanel>
+ <ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20"
+ Foreground="Yellow"
+ Symbol="StarFilled"
+ IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top"
+ HorizontalAlignment="Left" />
+ <ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20"
+ Foreground="Black"
+ Symbol="Star"
+ IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top"
+ HorizontalAlignment="Left" />
+ </Grid>
+ </Border>
+ </Grid>
+ </DataTemplate>
+ </ListBox.ItemTemplate>
+ </ListBox>
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs b/Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs
new file mode 100644
index 00000000..1cca3bea
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs
@@ -0,0 +1,82 @@
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using LibHac.Common;
+using Ryujinx.Ava.Ui.ViewModels;
+using Ryujinx.Ui.App.Common;
+using System;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public partial class GameListView : UserControl
+ {
+ private ApplicationData _selectedApplication;
+ public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
+ RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
+
+ public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
+ {
+ add { AddHandler(ApplicationOpenedEvent, value); }
+ remove { RemoveHandler(ApplicationOpenedEvent, value); }
+ }
+
+ public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
+ {
+ if (sender is ListBox listBox)
+ {
+ if (listBox.SelectedItem is ApplicationData selected)
+ {
+ RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
+ }
+ }
+ }
+
+ public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args)
+ {
+ if (sender is ListBox listBox)
+ {
+ var selected = listBox.SelectedItem as ApplicationData;
+
+ _selectedApplication = selected;
+ }
+ }
+
+ public ApplicationData SelectedApplication => _selectedApplication;
+
+ public GameListView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
+ {
+ (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
+ }
+
+ private void MenuBase_OnMenuOpened(object sender, EventArgs e)
+ {
+ var selection = SelectedApplication;
+
+ if (selection != null)
+ {
+ if (sender is ContextMenu menu)
+ {
+ bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
+ bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
+ bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
+
+ ((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
+ ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
+ ((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
+ }
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Controls/Glyph.cs b/Ryujinx.Ava/Ui/Controls/Glyph.cs
new file mode 100644
index 00000000..74a3c94a
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/Glyph.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public enum Glyph : int
+ {
+ List,
+ Grid,
+ Chip
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs b/Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs
new file mode 100644
index 00000000..63c6a17d
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/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.Controls
+{
+ 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/Controls/HotKeyControl.cs b/Ryujinx.Ava/Ui/Controls/HotKeyControl.cs
new file mode 100644
index 00000000..d3ab1e8f
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/HotKeyControl.cs
@@ -0,0 +1,52 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using System;
+using System.Windows.Input;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ 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/Controls/IGlContextExtension.cs b/Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs
new file mode 100644
index 00000000..4ca5bd59
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs
@@ -0,0 +1,25 @@
+using Avalonia.OpenGL;
+using SPB.Graphics.OpenGL;
+using System;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public 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/Controls/InputDialog.axaml b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml
new file mode 100644
index 00000000..6f320301
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml
@@ -0,0 +1,18 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ x:Class="Ryujinx.Ava.Ui.Controls.InputDialog">
+ <Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <TextBlock HorizontalAlignment="Center" Text="{Binding Message}" />
+ <TextBox MaxLength="{Binding MaxLength}" Grid.Row="1" Margin="10" Width="300" HorizontalAlignment="Center"
+ Text="{Binding Input, Mode=TwoWay}" />
+ <TextBlock Grid.Row="2" Margin="5, 5, 5, 10" HorizontalAlignment="Center" Text="{Binding SubMessage}" />
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs
new file mode 100644
index 00000000..b9bbb66d
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs
@@ -0,0 +1,67 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Models;
+using Ryujinx.Ava.Ui.Windows;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public class InputDialog : UserControl
+ {
+ public string Message { get; set; }
+ public string Input { get; set; }
+ public string SubMessage { get; set; }
+
+ public uint MaxLength { get; }
+
+ public InputDialog(string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue)
+ {
+ Message = message;
+ Input = input;
+ SubMessage = subMessage;
+ MaxLength = maxLength;
+
+ DataContext = this;
+
+ InitializeComponent();
+ }
+
+ public InputDialog()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue)
+ {
+ ContentDialog contentDialog = window.ContentDialog;
+
+ UserResult result = UserResult.Cancel;
+
+ InputDialog content = new InputDialog(message, input = "", subMessage = "", maxLength);
+
+ if (contentDialog != null)
+ {
+ contentDialog.Title = title;
+ contentDialog.PrimaryButtonText = LocaleManager.Instance["InputDialogOk"];
+ contentDialog.SecondaryButtonText = "";
+ contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
+ contentDialog.Content = content;
+ contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
+ {
+ result = UserResult.Ok;
+ input = content.Input;
+ });
+ await contentDialog.ShowAsync();
+ }
+
+ return (result, input);
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs b/Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs
new file mode 100644
index 00000000..8b3a30e6
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/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.Controls
+{
+ public 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/Controls/MiniCommand.cs b/Ryujinx.Ava/Ui/Controls/MiniCommand.cs
new file mode 100644
index 00000000..e14cfa6f
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/MiniCommand.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace Ryujinx.Ava.Ui.Models
+{
+ 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/Controls/OffscreenTextBox.cs b/Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs
new file mode 100644
index 00000000..ffe5bdde
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs
@@ -0,0 +1,40 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ 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/Controls/OpenToolkitBindingsContext.cs b/Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs
new file mode 100644
index 00000000..a1baba9e
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs
@@ -0,0 +1,20 @@
+using OpenTK;
+using System;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public 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/Controls/RenderTimer.cs b/Ryujinx.Ava/Ui/Controls/RenderTimer.cs
new file mode 100644
index 00000000..577115ea
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/RenderTimer.cs
@@ -0,0 +1,100 @@
+using Avalonia.Rendering;
+using System;
+using System.Threading;
+using System.Timers;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ internal class RenderTimer : IRenderTimer, IDisposable
+ {
+ public event Action<TimeSpan> Tick
+ {
+ add
+ {
+ _tick += value;
+
+ if (_subscriberCount++ == 0)
+ {
+ Start();
+ }
+ }
+
+ remove
+ {
+ if (--_subscriberCount == 0)
+ {
+ Stop();
+ }
+
+ _tick -= value;
+ }
+ }
+
+ private Thread _tickThread;
+ private readonly System.Timers.Timer _timer;
+
+ private Action<TimeSpan> _tick;
+ private int _subscriberCount;
+
+ private bool _isRunning;
+
+ private AutoResetEvent _resetEvent;
+
+ public RenderTimer()
+ {
+ _timer = new System.Timers.Timer(15);
+ _resetEvent = new AutoResetEvent(true);
+ _timer.Elapsed += Timer_Elapsed;
+ }
+
+ private void Timer_Elapsed(object sender, ElapsedEventArgs e)
+ {
+ TickNow();
+ }
+
+ public void Start()
+ {
+ _timer.Start();
+ if (_tickThread == null)
+ {
+ _tickThread = new Thread(RunTick);
+ _tickThread.Name = "RenderTimerTickThread";
+ _tickThread.IsBackground = true;
+ _isRunning = true;
+ _tickThread.Start();
+ }
+ }
+
+ public void RunTick()
+ {
+ while (_isRunning)
+ {
+ _resetEvent.WaitOne();
+ _tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
+ }
+ }
+
+ public void TickNow()
+ {
+ lock (_timer)
+ {
+ _resetEvent.Set();
+ }
+ }
+
+ public void Stop()
+ {
+ _timer.Stop();
+ }
+
+ public void Dispose()
+ {
+ _timer.Elapsed -= Timer_Elapsed;
+ _timer.Stop();
+ _isRunning = false;
+ _resetEvent.Set();
+ _tickThread.Join();
+ _resetEvent.Dispose();
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Controls/RendererControl.cs b/Ryujinx.Ava/Ui/Controls/RendererControl.cs
new file mode 100644
index 00000000..8321a04e
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/RendererControl.cs
@@ -0,0 +1,273 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Media;
+using Avalonia.OpenGL;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using Avalonia.Threading;
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common.Configuration;
+using SkiaSharp;
+using SPB.Graphics;
+using SPB.Graphics.OpenGL;
+using SPB.Platform;
+using SPB.Windowing;
+using System;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public class RendererControl : Control
+ {
+ private int _image;
+
+ static RendererControl()
+ {
+ AffectsRender<RendererControl>(ImageProperty);
+ }
+
+ public readonly static StyledProperty<int> ImageProperty =
+ AvaloniaProperty.Register<RendererControl, int>(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay);
+
+ protected int Image
+ {
+ get => _image;
+ set => SetAndRaise(ImageProperty, ref _image, value);
+ }
+
+ public event EventHandler<EventArgs> GlInitialized;
+ public event EventHandler<Size> SizeChanged;
+
+ protected Size RenderSize { get; private set; }
+ public bool IsStarted { get; private set; }
+
+ public int Major { get; }
+ public int Minor { get; }
+ public GraphicsDebugLevel DebugLevel { get; }
+ public OpenGLContextBase GameContext { get; set; }
+
+ public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>().PrimaryContext.AsOpenGLContextBase();
+
+ private SwappableNativeWindowBase _gameBackgroundWindow;
+
+ private bool _isInitialized;
+
+ private IntPtr _fence;
+
+ private GlDrawOperation _glDrawOperation;
+
+ public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
+ {
+ Major = major;
+ Minor = minor;
+ DebugLevel = graphicsDebugLevel;
+ IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty);
+
+ resizeObservable.Subscribe(Resized);
+
+ Focusable = true;
+ }
+
+ private void Resized(Rect rect)
+ {
+ SizeChanged?.Invoke(this, rect.Size);
+
+ RenderSize = rect.Size * Program.WindowScaleFactor;
+
+ _glDrawOperation?.Dispose();
+ _glDrawOperation = new GlDrawOperation(this);
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ if (!_isInitialized)
+ {
+ CreateWindow();
+
+ OnGlInitialized();
+ _isInitialized = true;
+ }
+
+ if (GameContext == null || !IsStarted || Image == 0)
+ {
+ return;
+ }
+
+ if (_glDrawOperation != null)
+ {
+ context.Custom(_glDrawOperation);
+ }
+
+ base.Render(context);
+ }
+
+ protected void OnGlInitialized()
+ {
+ GlInitialized?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void QueueRender()
+ {
+ Program.RenderTimer.TickNow();
+ }
+
+ internal void Present(object image)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ Image = (int)image;
+ }).Wait();
+
+ if (_fence != IntPtr.Zero)
+ {
+ GL.DeleteSync(_fence);
+ }
+
+ _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
+
+ QueueRender();
+
+ _gameBackgroundWindow.SwapBuffers();
+ }
+
+ internal void Start()
+ {
+ IsStarted = true;
+ QueueRender();
+ }
+
+ internal void Stop()
+ {
+ IsStarted = false;
+ }
+
+ public void DestroyBackgroundContext()
+ {
+ _image = 0;
+
+ if (_fence != IntPtr.Zero)
+ {
+ _glDrawOperation.Dispose();
+ GL.DeleteSync(_fence);
+ }
+
+ GlDrawOperation.DeleteFramebuffer();
+
+ GameContext?.Dispose();
+
+ _gameBackgroundWindow?.Dispose();
+ }
+
+ internal void MakeCurrent()
+ {
+ GameContext.MakeCurrent(_gameBackgroundWindow);
+ }
+
+ internal void MakeCurrent(SwappableNativeWindowBase window)
+ {
+ GameContext.MakeCurrent(window);
+ }
+
+ protected void CreateWindow()
+ {
+ var flags = OpenGLContextFlags.Compat;
+ if (DebugLevel != GraphicsDebugLevel.None)
+ {
+ flags |= OpenGLContextFlags.Debug;
+ }
+ _gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
+ _gameBackgroundWindow.Hide();
+
+ GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext);
+ GameContext.Initialize(_gameBackgroundWindow);
+ }
+
+ private class GlDrawOperation : ICustomDrawOperation
+ {
+ private static int _framebuffer;
+
+ public Rect Bounds { get; }
+
+ private readonly RendererControl _control;
+
+ public GlDrawOperation(RendererControl control)
+ {
+ _control = control;
+ Bounds = _control.Bounds;
+ }
+
+ public void Dispose() { }
+
+ public static void DeleteFramebuffer()
+ {
+ if (_framebuffer == 0)
+ {
+ GL.DeleteFramebuffer(_framebuffer);
+ }
+
+ _framebuffer = 0;
+ }
+
+ public bool Equals(ICustomDrawOperation other)
+ {
+ return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
+ }
+
+ public bool HitTest(Point p)
+ {
+ return Bounds.Contains(p);
+ }
+
+ private void CreateRenderTarget()
+ {
+ _framebuffer = GL.GenFramebuffer();
+ }
+
+ public void Render(IDrawingContextImpl context)
+ {
+ if (_control.Image == 0)
+ {
+ return;
+ }
+
+ if (_framebuffer == 0)
+ {
+ CreateRenderTarget();
+ }
+
+ int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding);
+
+ var image = _control.Image;
+ var fence = _control._fence;
+
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
+ GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0);
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer);
+
+ if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
+ {
+ return;
+ }
+
+ var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888);
+ var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat());
+
+ GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue);
+
+ using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo);
+ using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
+
+ if (surface == null)
+ {
+ return;
+ }
+
+ var rect = new Rect(new Point(), _control.RenderSize);
+
+ using var snapshot = surface.Snapshot();
+ skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs b/Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs
new file mode 100644
index 00000000..45c6187e
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/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.Controls
+{
+ 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/Controls/UpdateWaitWindow.axaml b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml
new file mode 100644
index 00000000..24aded24
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml
@@ -0,0 +1,28 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ mc:Ignorable="d"
+ x:Class="Ryujinx.Ava.Ui.Controls.UpdateWaitWindow"
+ WindowStartupLocation="CenterOwner"
+ SizeToContent="WidthAndHeight"
+ Title="Ryujinx - Waiting">
+ <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Image Grid.Row="1" Margin="5, 10, 20 , 10" Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common"
+ Height="70"
+ MinWidth="50" />
+ <StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Vertical">
+ <TextBlock Margin="5" Name="PrimaryText" />
+ <TextBlock VerticalAlignment="Center" Name="SecondaryText" Margin="5" />
+ </StackPanel>
+ </Grid>
+</Window> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs
new file mode 100644
index 00000000..e4108ba4
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs
@@ -0,0 +1,35 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Ui.Windows;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public class UpdateWaitWindow : StyleableWindow
+ {
+ public UpdateWaitWindow(string primaryText, string secondaryText) : this()
+ {
+ PrimaryText.Text = primaryText;
+ SecondaryText.Text = secondaryText;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ }
+
+ public UpdateWaitWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ public TextBlock PrimaryText { get; private set; }
+ public TextBlock SecondaryText { get; private set; }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ PrimaryText = this.FindControl<TextBlock>("PrimaryText");
+ SecondaryText = this.FindControl<TextBlock>("SecondaryText");
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs b/Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs
new file mode 100644
index 00000000..1f8f68e3
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/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.Controls
+{
+ 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(owner,
+ 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/Controls/UserResult.cs b/Ryujinx.Ava/Ui/Controls/UserResult.cs
new file mode 100644
index 00000000..6eb89a90
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Controls/UserResult.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public enum UserResult
+ {
+ Ok,
+ Yes,
+ No,
+ Abort,
+ Cancel,
+ None,
+ }
+} \ No newline at end of file