aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/Applet
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/Applet
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/Applet')
-rw-r--r--Ryujinx.Ava/Ui/Applet/AvaHostUiHandler.cs149
-rw-r--r--Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs162
-rw-r--r--Ryujinx.Ava/Ui/Applet/AvaloniaHostUiTheme.cs43
-rw-r--r--Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml31
-rw-r--r--Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml.cs89
-rw-r--r--Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml32
-rw-r--r--Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml.cs152
7 files changed, 658 insertions, 0 deletions
diff --git a/Ryujinx.Ava/Ui/Applet/AvaHostUiHandler.cs b/Ryujinx.Ava/Ui/Applet/AvaHostUiHandler.cs
new file mode 100644
index 00000000..f573e167
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/AvaHostUiHandler.cs
@@ -0,0 +1,149 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.HLE;
+using Ryujinx.HLE.HOS.Applets;
+using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
+using Ryujinx.HLE.Ui;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.Applet
+{
+ internal class AvaHostUiHandler : IHostUiHandler
+ {
+ private readonly MainWindow _parent;
+
+ public IHostUiTheme HostUiTheme { get; }
+
+ public AvaHostUiHandler(MainWindow parent)
+ {
+ _parent = parent;
+
+ HostUiTheme = new AvaloniaHostUiTheme(parent);
+ }
+
+ public bool DisplayMessageDialog(ControllerAppletUiArgs args)
+ {
+ string playerCount = args.PlayerCountMin == args.PlayerCountMax
+ ? args.PlayerCountMin.ToString()
+ : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
+
+ string key = args.PlayerCountMin == args.PlayerCountMax ? "DialogControllerAppletMessage" : "DialogControllerAppletMessagePlayerRange";
+
+ string message = string.Format(LocaleManager.Instance[key],
+ playerCount,
+ args.SupportedStyles,
+ string.Join(", ", args.SupportedPlayers),
+ args.IsDocked ? LocaleManager.Instance["DialogControllerAppletDockModeSet"] : "");
+
+ return DisplayMessageDialog(LocaleManager.Instance["DialogControllerAppletTitle"], message);
+ }
+
+ public bool DisplayMessageDialog(string title, string message)
+ {
+ // TODO : Show controller applet. Needs settings window to be implemented.
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ ContentDialogHelper.ShowNotAvailableMessage(_parent);
+ });
+
+ return true;
+ }
+
+ public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
+ {
+ ManualResetEvent dialogCloseEvent = new(false);
+
+ bool okPressed = false;
+ bool error = false;
+ string inputText = args.InitialText ?? "";
+
+ Dispatcher.UIThread.Post(async () =>
+ {
+ try
+ {
+ var response = await SwkbdAppletDialog.ShowInputDialog(_parent, LocaleManager.Instance["SoftwareKeyboard"], args);
+
+ if (response.Result == UserResult.Ok)
+ {
+ inputText = response.Input;
+ okPressed = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ error = true;
+ ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogSoftwareKeyboardErrorExceptionMessage"], ex));
+ }
+ finally
+ {
+ dialogCloseEvent.Set();
+ }
+ });
+
+ dialogCloseEvent.WaitOne();
+
+ userText = error ? null : inputText;
+
+ return error || okPressed;
+ }
+
+ public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
+ {
+ device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
+ if (_parent.AppHost != null)
+ {
+ _parent.AppHost.Stop();
+ }
+ }
+
+ public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
+ {
+ ManualResetEvent dialogCloseEvent = new(false);
+
+ bool showDetails = false;
+
+ Dispatcher.UIThread.Post(async () =>
+ {
+ try
+ {
+ ErrorAppletWindow msgDialog = new(_parent, buttons, message)
+ {
+ Title = title,
+ WindowStartupLocation = WindowStartupLocation.CenterScreen,
+ Width = 400
+ };
+
+ object response = await msgDialog.Run();
+
+ if (response != null && buttons.Length > 1 && (int)response != buttons.Length - 1)
+ {
+ showDetails = true;
+ }
+
+ dialogCloseEvent.Set();
+
+ msgDialog.Close();
+ }
+ catch (Exception ex)
+ {
+ dialogCloseEvent.Set();
+ ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogErrorAppletErrorExceptionMessage"], ex));
+ }
+ });
+
+ dialogCloseEvent.WaitOne();
+
+ return showDetails;
+ }
+
+ public IDynamicTextInputHandler CreateDynamicTextInputHandler()
+ {
+ return new AvaloniaDynamicTextInputHandler(_parent);
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs b/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs
new file mode 100644
index 00000000..714b6360
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs
@@ -0,0 +1,162 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using OpenTK.Windowing.Common;
+using Ryujinx.Ava.Input;
+using Ryujinx.Ava.Ui.Controls;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.HLE.Ui;
+using System;
+using System.Threading;
+
+namespace Ryujinx.Ava.Ui.Applet
+{
+ class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
+ {
+ private MainWindow _parent;
+ private OffscreenTextBox _hiddenTextBox;
+ private bool _canProcessInput;
+ private IDisposable _textChangedSubscription;
+ private IDisposable _selectionStartChangedSubscription;
+ private IDisposable _selectionEndtextChangedSubscription;
+
+ public AvaloniaDynamicTextInputHandler(MainWindow parent)
+ {
+ _parent = parent;
+
+ (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
+ (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
+ (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput += AvaloniaDynamicTextInputHandler_TextInput;
+
+ _hiddenTextBox = _parent.HiddenTextBox;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _textChangedSubscription = _hiddenTextBox.GetObservable(TextBox.TextProperty).Subscribe(TextChanged);
+ _selectionStartChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionStartProperty).Subscribe(SelectionChanged);
+ _selectionEndtextChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionEndProperty).Subscribe(SelectionChanged);
+ });
+ }
+
+ private void TextChanged(string text)
+ {
+ TextChangedEvent?.Invoke(text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true);
+ }
+
+ private void SelectionChanged(int selection)
+ {
+ if (_hiddenTextBox.SelectionEnd < _hiddenTextBox.SelectionStart)
+ {
+ _hiddenTextBox.SelectionStart = _hiddenTextBox.SelectionEnd;
+ }
+
+ TextChangedEvent?.Invoke(_hiddenTextBox.Text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true);
+ }
+
+ private void AvaloniaDynamicTextInputHandler_TextInput(object sender, TextInputEventArgs e)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (_canProcessInput)
+ {
+ _hiddenTextBox.SendText(e.AsString);
+ }
+ });
+ }
+
+ private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, Avalonia.Input.KeyEventArgs e)
+ {
+ var key = (Key)AvaloniaMappingHelper.ToInputKey(e.Key);
+
+ if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
+ {
+ return;
+ }
+
+ e.RoutedEvent = _hiddenTextBox.GetKeyUpRoutedEvent();
+
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (_canProcessInput)
+ {
+ _hiddenTextBox.SendKeyUpEvent(e);
+ }
+ });
+ }
+
+ private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, Avalonia.Input.KeyEventArgs e)
+ {
+ var key = (Key)AvaloniaMappingHelper.ToInputKey(e.Key);
+
+ if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
+ {
+ return;
+ }
+
+ e.RoutedEvent = _hiddenTextBox.GetKeyUpRoutedEvent();
+
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (_canProcessInput)
+ {
+ _hiddenTextBox.SendKeyDownEvent(e);
+ }
+ });
+ }
+
+ public bool TextProcessingEnabled
+ {
+ get
+ {
+ return Volatile.Read(ref _canProcessInput);
+ }
+ set
+ {
+ Volatile.Write(ref _canProcessInput, value);
+ }
+ }
+
+ public event DynamicTextChangedHandler TextChangedEvent;
+ public event KeyPressedHandler KeyPressedEvent;
+ public event KeyReleasedHandler KeyReleasedEvent;
+
+ public void Dispose()
+ {
+ (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
+ (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
+ (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
+
+ _textChangedSubscription?.Dispose();
+ _selectionStartChangedSubscription?.Dispose();
+ _selectionEndtextChangedSubscription?.Dispose();
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _hiddenTextBox.Clear();
+ _parent.GlRenderer.Focus();
+
+ _parent = null;
+ });
+ }
+
+ public void SetText(string text, int cursorBegin)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ _hiddenTextBox.Text = text;
+ _hiddenTextBox.CaretIndex = cursorBegin;
+ });
+ }
+
+ public void SetText(string text, int cursorBegin, int cursorEnd)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ _hiddenTextBox.Text = text;
+ _hiddenTextBox.SelectionStart = cursorBegin;
+ _hiddenTextBox.SelectionEnd = cursorEnd;
+ });
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Applet/AvaloniaHostUiTheme.cs b/Ryujinx.Ava/Ui/Applet/AvaloniaHostUiTheme.cs
new file mode 100644
index 00000000..8034b506
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/AvaloniaHostUiTheme.cs
@@ -0,0 +1,43 @@
+using Avalonia.Media;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.HLE.Ui;
+using System;
+
+namespace Ryujinx.Ava.Ui.Applet
+{
+ class AvaloniaHostUiTheme : IHostUiTheme
+ {
+ public AvaloniaHostUiTheme(MainWindow parent)
+ {
+ FontFamily = OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000, 0) ? "Segoe UI Variable" : parent.FontFamily.Name;
+ DefaultBackgroundColor = BrushToThemeColor(parent.Background);
+ DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
+ DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
+ SelectionBackgroundColor = BrushToThemeColor(parent.SearchBox.SelectionBrush);
+ SelectionForegroundColor = BrushToThemeColor(parent.SearchBox.SelectionForegroundBrush);
+ }
+
+ public string FontFamily { get; }
+
+ public ThemeColor DefaultBackgroundColor { get; }
+ public ThemeColor DefaultForegroundColor { get; }
+ public ThemeColor DefaultBorderColor { get; }
+ public ThemeColor SelectionBackgroundColor { get; }
+ public ThemeColor SelectionForegroundColor { get; }
+
+ private ThemeColor BrushToThemeColor(IBrush brush)
+ {
+ if (brush is SolidColorBrush solidColor)
+ {
+ return new ThemeColor((float)solidColor.Color.A / 255,
+ (float)solidColor.Color.R / 255,
+ (float)solidColor.Color.G / 255,
+ (float)solidColor.Color.B / 255);
+ }
+ else
+ {
+ return new ThemeColor();
+ }
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml b/Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml
new file mode 100644
index 00000000..6d365abf
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml
@@ -0,0 +1,31 @@
+<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.Applet.ErrorAppletWindow"
+ xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
+ CanResize="False"
+ SizeToContent="Height"
+ Width="450"
+ Height="340"
+ Title="{locale:Locale ErrorWindowTitle}">
+ <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="*" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Image Grid.Row="1" Grid.RowSpan="2" Margin="5, 10, 20 , 10" Grid.Column="0"
+ Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" Height="80" MinWidth="50" />
+ <TextBlock Grid.Row="1" Margin="10" Grid.Column="1" VerticalAlignment="Stretch" TextWrapping="Wrap"
+ Text="{Binding Message}" />
+ <StackPanel Name="ButtonStack" Margin="10" Spacing="10" Grid.Row="2" Grid.Column="1"
+ HorizontalAlignment="Right" Orientation="Horizontal" />
+ </Grid>
+</Window> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml.cs b/Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml.cs
new file mode 100644
index 00000000..d4ec45a8
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/ErrorAppletWindow.axaml.cs
@@ -0,0 +1,89 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Windows;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.Applet
+{
+ public class ErrorAppletWindow : StyleableWindow
+ {
+ private readonly Window _owner;
+ private object _buttonResponse;
+
+ public ErrorAppletWindow(Window owner, string[] buttons, string message)
+ {
+ _owner = owner;
+ Message = message;
+ DataContext = this;
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ int responseId = 0;
+
+ if (buttons != null)
+ {
+ foreach (string buttonText in buttons)
+ {
+ AddButton(buttonText, responseId);
+ responseId++;
+ }
+ }
+ else
+ {
+ AddButton(LocaleManager.Instance["InputDialogOk"], 0);
+ }
+ }
+
+ public ErrorAppletWindow()
+ {
+ DataContext = this;
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ public string Message { get; set; }
+
+ public StackPanel ButtonStack { get; set; }
+
+ private void AddButton(string label, object tag)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ Button button = new() { Content = label, Tag = tag };
+
+ button.Click += Button_Click;
+ ButtonStack.Children.Add(button);
+ });
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button)
+ {
+ _buttonResponse = button.Tag;
+ }
+
+ Close();
+ }
+
+ public async Task<object> Run()
+ {
+ await ShowDialog(_owner);
+
+ return _buttonResponse;
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ ButtonStack = this.FindControl<StackPanel>("ButtonStack");
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml b/Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml
new file mode 100644
index 00000000..b027a230
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml
@@ -0,0 +1,32 @@
+<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"
+ xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
+ mc:Ignorable="d"
+ x:Class="Ryujinx.Ava.Ui.Controls.SwkbdAppletDialog"
+ Width="400">
+ <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ <RowDefinition Height="Auto" />
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto" />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Image Grid.Row="1" VerticalAlignment="Center" Grid.RowSpan="5" Margin="5, 10, 20 , 10"
+ Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" Height="80"
+ MinWidth="50" />
+ <TextBlock Grid.Row="1" Margin="5" Grid.Column="1" Text="{Binding MainText}" TextWrapping="Wrap" />
+ <TextBlock Grid.Row="2" Margin="5" Grid.Column="1" Text="{Binding SecondaryText}" TextWrapping="Wrap" />
+ <TextBox Name="Input" KeyUp="Message_KeyUp" UseFloatingWatermark="True" TextInput="Message_TextInput"
+ Text="{Binding Message}" Grid.Row="2"
+ Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" TextWrapping="Wrap" />
+ <TextBlock Name="Error" Margin="5" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Stretch"
+ TextWrapping="Wrap" />
+ </Grid>
+</UserControl> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml.cs b/Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml.cs
new file mode 100644
index 00000000..e81f75bd
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Applet/SwkbdAppletDialog.axaml.cs
@@ -0,0 +1,152 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using FluentAvalonia.Core;
+using FluentAvalonia.UI.Controls;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ava.Ui.Windows;
+using Ryujinx.HLE.HOS.Applets;
+using System;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ava.Ui.Controls
+{
+ public class SwkbdAppletDialog : UserControl
+ {
+ private Predicate<int> _checkLength;
+ private int _inputMax;
+ private int _inputMin;
+ private string _placeholder;
+
+ private ContentDialog _host;
+
+ public SwkbdAppletDialog(string mainText, string secondaryText, string placeholder)
+ {
+ MainText = mainText;
+ SecondaryText = secondaryText;
+ DataContext = this;
+ _placeholder = placeholder;
+ InitializeComponent();
+
+ SetInputLengthValidation(0, int.MaxValue); // Disable by default.
+ }
+
+ public SwkbdAppletDialog()
+ {
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ public string Message { get; set; } = "";
+ public string MainText { get; set; } = "";
+ public string SecondaryText { get; set; } = "";
+
+ public TextBlock Error { get; private set; }
+ public TextBox Input { get; set; }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ Error = this.FindControl<TextBlock>("Error");
+ Input = this.FindControl<TextBox>("Input");
+
+ Input.Watermark = _placeholder;
+
+ Input.AddHandler(TextInputEvent, Message_TextInput, RoutingStrategies.Tunnel, true);
+ }
+
+ public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, SoftwareKeyboardUiArgs args)
+ {
+ ContentDialog contentDialog = window.ContentDialog;
+
+ UserResult result = UserResult.Cancel;
+
+ SwkbdAppletDialog content = new SwkbdAppletDialog(args.HeaderText, args.SubtitleText, args.GuideText)
+ {
+ Message = args.InitialText ?? ""
+ };
+
+ string input = string.Empty;
+
+ content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
+
+ if (contentDialog != null)
+ {
+ content._host = contentDialog;
+ contentDialog.Title = title;
+ contentDialog.PrimaryButtonText = args.SubmitText;
+ contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
+ contentDialog.SecondaryButtonText = "";
+ contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
+ contentDialog.Content = content;
+ TypedEventHandler<ContentDialog, ContentDialogClosedEventArgs> handler = (sender, eventArgs) =>
+ {
+ if (eventArgs.Result == ContentDialogResult.Primary)
+ {
+ result = UserResult.Ok;
+ input = content.Input.Text;
+ }
+ };
+ contentDialog.Closed += handler;
+ await contentDialog.ShowAsync();
+ contentDialog.Closed -= handler;
+ }
+
+ return (result, input);
+ }
+
+ public void SetInputLengthValidation(int min, int max)
+ {
+ _inputMin = Math.Min(min, max);
+ _inputMax = Math.Max(min, max);
+
+ Error.IsVisible = false;
+ Error.FontStyle = FontStyle.Italic;
+
+ if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable.
+ {
+ Error.IsVisible = false;
+
+ _checkLength = length => true;
+ }
+ else if (_inputMin > 0 && _inputMax == int.MaxValue)
+ {
+ Error.IsVisible = true;
+ Error.Text = string.Format(LocaleManager.Instance["SwkbdMinCharacters"], _inputMin);
+
+ _checkLength = length => _inputMin <= length;
+ }
+ else
+ {
+ Error.IsVisible = true;
+ Error.Text = string.Format(LocaleManager.Instance["SwkbdMinRangeCharacters"], _inputMin, _inputMax);
+
+ _checkLength = length => _inputMin <= length && length <= _inputMax;
+ }
+
+ Message_TextInput(this, new TextInputEventArgs());
+ }
+
+ private void Message_TextInput(object sender, TextInputEventArgs e)
+ {
+ if (_host != null)
+ {
+ _host.IsPrimaryButtonEnabled = _checkLength(Message.Length);
+ }
+ }
+
+ private void Message_KeyUp(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter && _host.IsPrimaryButtonEnabled)
+ {
+ _host.Hide(ContentDialogResult.Primary);
+ }
+ else
+ {
+ _host.IsPrimaryButtonEnabled = _checkLength(Message.Length);
+ }
+ }
+ }
+} \ No newline at end of file