diff --git a/Directory.Packages.props b/Directory.Packages.props
index fd61602a8..3fe05425d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,6 +8,7 @@
+
@@ -58,5 +59,7 @@
+
+
\ No newline at end of file
diff --git a/Ryujinx.sln b/Ryujinx.sln
index deddb97a0..07b996f4a 100644
--- a/Ryujinx.sln
+++ b/Ryujinx.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.1.32228.430
+# Visual Studio Version 18
+VisualStudioVersion = 18.2.11415.280 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}"
EndProject
@@ -21,7 +21,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.GAL", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.OpenGL", "src\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj", "{9558FB96-075D-4219-8FFF-401979DC0B69}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDoc", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDocApi", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Texture", "src\Ryujinx.Graphics.Texture\Ryujinx.Graphics.Texture.csproj", "{E1B1AD28-289D-47B7-A106-326972240207}"
EndProject
@@ -88,10 +88,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\canary.yml = .github\workflows\canary.yml
- Directory.Packages.props = Directory.Packages.props
Directory.Build.props = Directory.Build.props
- .github\workflows\release.yml = .github\workflows\release.yml
+ Directory.Packages.props = Directory.Packages.props
nuget.config = nuget.config
+ .github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
EndProject
Global
@@ -212,6 +212,18 @@ Global
{9558FB96-075D-4219-8FFF-401979DC0B69}.Release|x64.Build.0 = Release|Any CPU
{9558FB96-075D-4219-8FFF-401979DC0B69}.Release|x86.ActiveCfg = Release|Any CPU
{9558FB96-075D-4219-8FFF-401979DC0B69}.Release|x86.Build.0 = Release|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU
+ {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
{E1B1AD28-289D-47B7-A106-326972240207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1B1AD28-289D-47B7-A106-326972240207}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1B1AD28-289D-47B7-A106-326972240207}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -356,6 +368,30 @@ Global
{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}.Release|x64.Build.0 = Release|Any CPU
{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}.Release|x86.ActiveCfg = Release|Any CPU
{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}.Release|x86.Build.0 = Release|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x64.Build.0 = Debug|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x86.Build.0 = Debug|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x64.ActiveCfg = Release|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x64.Build.0 = Release|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x86.ActiveCfg = Release|Any CPU
+ {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x86.Build.0 = Release|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x64.Build.0 = Debug|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x86.Build.0 = Debug|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|Any CPU.Build.0 = Release|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.ActiveCfg = Release|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU
+ {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU
{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -392,6 +428,18 @@ Global
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|x64.Build.0 = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|x86.ActiveCfg = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|x86.Build.0 = Release|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x64.Build.0 = Debug|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x86.Build.0 = Debug|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x64.ActiveCfg = Release|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x64.Build.0 = Release|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x86.ActiveCfg = Release|Any CPU
+ {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x86.Build.0 = Release|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -535,44 +583,6 @@ Global
{F6F9826A-BC58-4D78-A700-F358A66B2B06}.Release|x64.Build.0 = Release|Any CPU
{F6F9826A-BC58-4D78-A700-F358A66B2B06}.Release|x86.ActiveCfg = Release|Any CPU
{F6F9826A-BC58-4D78-A700-F358A66B2B06}.Release|x86.Build.0 = Release|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x64.ActiveCfg = Debug|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x64.Build.0 = Debug|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x86.ActiveCfg = Debug|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Debug|x86.Build.0 = Debug|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|Any CPU.Build.0 = Release|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x64.ActiveCfg = Release|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x64.Build.0 = Release|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x86.ActiveCfg = Release|Any CPU
- {D728444C-3D1F-4A0E-B4C9-5C9375D47EA3}.Release|x86.Build.0 = Release|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x64.ActiveCfg = Debug|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x64.Build.0 = Debug|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x86.ActiveCfg = Debug|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Debug|x86.Build.0 = Debug|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|Any CPU.Build.0 = Release|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.ActiveCfg = Release|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU
- {988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU
- {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
- {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs
index ff17b4910..e682656a7 100644
--- a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs
+++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs
@@ -151,14 +151,11 @@ namespace Ryujinx.HLE.HOS.Applets.Browser
using CancellationTokenSource serverCts = new();
Task<(WebExitReason Reason, string LastUrl)> callbackTask = server.WaitForCallbackAsync(serverCts.Token);
- // Open the URL in the system browser and show a waiting dialog
+ // Show the web content in an embedded WebView and wait for callback
bool userWaited = true;
try
{
- // Open system browser
- OpenSystemBrowser(url);
-
- // Use the UI handler to show a blocking dialog
+ // Use the UI handler to display embedded web content
if (_system.Device.UIHandler != null)
{
using CancellationTokenSource uiCts = new();
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 8d03f81da..0ca8488dc 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Threading;
+using Avalonia.WebView.Desktop;
using DiscordRPC;
using Gommon;
using Projektanker.Icons.Avalonia;
@@ -45,6 +46,7 @@ namespace Ryujinx.Ava
private const uint MbIconwarning = 0x30;
+ [STAThread]
public static int Main(string[] args)
{
Version = ReleaseInformation.Version;
@@ -127,7 +129,8 @@ namespace Ryujinx.Ava
RenderingMode = UseHardwareAcceleration
? [Win32RenderingMode.AngleEgl, Win32RenderingMode.Software]
: [Win32RenderingMode.Software]
- });
+ })
+ .UseDesktopWebView();
private static bool ConsumeCommandLineArgument(ref string[] args, string targetArgument)
{
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index 5da152501..bb6003a53 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -51,6 +51,7 @@
+
@@ -74,6 +75,8 @@
+
+
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 90a7be4ad..1eb1ac794 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -1,3 +1,4 @@
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
@@ -9,6 +10,7 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
+using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
@@ -19,6 +21,7 @@ using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Applet
{
@@ -336,58 +339,30 @@ namespace Ryujinx.Ava.UI.Applet
public bool DisplayWebPage(string url, string title, CancellationToken cancellationToken)
{
ManualResetEvent dialogCloseEvent = new(false);
- bool userCancelled = false;
+ bool closedByCallback = false;
Dispatcher.UIThread.InvokeAsync(async () =>
{
try
{
- ContentDialog dialog = new()
+ if (OperatingSystem.IsLinux())
{
- Title = title,
- CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
- Content = new TextBlock
- {
- Text = "A web page has been opened in your system browser.\n\n" +
- "Please interact with it to continue.\n\n" +
- "This dialog will close automatically when the page sends a response.\n\n" +
- "Click Close to skip the web applet.",
- TextWrapping = Avalonia.Media.TextWrapping.Wrap,
- MaxWidth = 400,
- },
- };
-
- dialog.CloseButtonCommand = Commands.Create(() =>
+ // Linux: NativeWebView is not supported, use a fallback dialog
+ closedByCallback = await ShowFallbackDialog(title, cancellationToken);
+ }
+ else
{
- if (!cancellationToken.IsCancellationRequested)
- {
- userCancelled = true;
- }
- });
-
- // Register cancellation to close the dialog when the callback arrives
- using CancellationTokenRegistration registration = cancellationToken.Register(() =>
- {
- Dispatcher.UIThread.Post(() =>
- {
- try
- {
- dialog.Hide();
- }
- catch
- {
- // Dialog may already be closed
- }
- });
- });
-
- await ContentDialogHelper.ShowAsync(dialog);
+ // Windows / macOS: use embedded NativeWebView in an overlay window
+ closedByCallback = await ShowWebViewOverlay(url, title, cancellationToken);
+ }
}
catch (Exception ex)
{
- Ryujinx.Common.Logging.Logger.Warning?.Print(
- Ryujinx.Common.Logging.LogClass.ServiceAm,
- $"Error displaying web applet dialog: {ex.Message}");
+ Logger.Warning?.Print(LogClass.ServiceAm,
+ $"Error displaying embedded web applet: {ex.Message}");
+
+ // Fall back to a simple waiting dialog if WebView fails
+ closedByCallback = await ShowFallbackDialog(title, cancellationToken);
}
finally
{
@@ -397,6 +372,77 @@ namespace Ryujinx.Ava.UI.Applet
dialogCloseEvent.WaitOne();
+ return closedByCallback;
+ }
+
+ ///
+ /// Shows an embedded NativeWebView in a full-screen overlay window (Windows/macOS).
+ ///
+ private async Task ShowWebViewOverlay(string url, string title, CancellationToken cancellationToken)
+ {
+ MainWindow mainWindow = RyujinxApp.MainWindow;
+
+ WebAppletWindow webWindow = new()
+ {
+ Title = title,
+ Width = mainWindow.Bounds.Width,
+ Height = mainWindow.Bounds.Height,
+ Position = mainWindow.PointToScreen(new Point()),
+ };
+
+ webWindow.Navigate(url, cancellationToken);
+
+ await webWindow.ShowDialog(mainWindow);
+
+ return webWindow.ClosedByCallback;
+ }
+
+ ///
+ /// Fallback dialog shown when WebView is unavailable (Linux) or initialization fails.
+ ///
+ private async Task ShowFallbackDialog(string title, CancellationToken cancellationToken)
+ {
+ bool userCancelled = false;
+
+ ContentDialog dialog = new()
+ {
+ Title = title,
+ CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
+ Content = new TextBlock
+ {
+ Text = "The web applet is running.\n\n" +
+ "This dialog will close automatically when the applet completes.\n\n" +
+ "Click Close to skip.",
+ TextWrapping = Avalonia.Media.TextWrapping.Wrap,
+ MaxWidth = 400,
+ },
+ };
+
+ dialog.CloseButtonCommand = Commands.Create(() =>
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ userCancelled = true;
+ }
+ });
+
+ using CancellationTokenRegistration registration = cancellationToken.Register(() =>
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ try
+ {
+ dialog.Hide();
+ }
+ catch
+ {
+ // Dialog may already be closed
+ }
+ });
+ });
+
+ await ContentDialogHelper.ShowAsync(dialog);
+
return !userCancelled;
}
}
diff --git a/src/Ryujinx/UI/Applet/WebAppletWindow.axaml b/src/Ryujinx/UI/Applet/WebAppletWindow.axaml
new file mode 100644
index 000000000..7076d37f9
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/WebAppletWindow.axaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Applet/WebAppletWindow.axaml.cs b/src/Ryujinx/UI/Applet/WebAppletWindow.axaml.cs
new file mode 100644
index 000000000..7379c5151
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/WebAppletWindow.axaml.cs
@@ -0,0 +1,122 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Ryujinx.Common.Logging;
+using System;
+using System.Threading;
+
+namespace Ryujinx.Ava.UI.Applet
+{
+ public partial class WebAppletWindow : Window
+ {
+ private CancellationTokenRegistration _cancellationRegistration;
+ private string _pendingUrl;
+ private bool _navigated;
+
+ ///
+ /// True if the window was closed by the callback (not by user).
+ ///
+ public bool ClosedByCallback { get; private set; }
+
+ public WebAppletWindow()
+ {
+ InitializeComponent();
+
+ // Subscribe to WebView2Host events
+ WebViewHost.WebView2Ready += OnWebView2Ready;
+ WebViewHost.NavigationCompleted += OnNavigationCompleted;
+ }
+
+ ///
+ /// Registers the URL and cancellation token. Navigation happens once WebView2 is ready.
+ ///
+ public void Navigate(string url, CancellationToken cancellationToken)
+ {
+ Logger.Info?.Print(LogClass.ServiceAm, $"WebAppletWindow: URL queued: {url}");
+
+ _pendingUrl = url;
+
+ _cancellationRegistration = cancellationToken.Register(() =>
+ {
+ ClosedByCallback = true;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ try
+ {
+ Close();
+ }
+ catch
+ {
+ // Window may already be closed
+ }
+ });
+ });
+ }
+
+ protected override void OnOpened(EventArgs e)
+ {
+ base.OnOpened(e);
+ Logger.Info?.Print(LogClass.ServiceAm, "WebAppletWindow: Window opened");
+ }
+
+ private void OnWebView2Ready(bool success, string errorMessage)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (success)
+ {
+ Logger.Info?.Print(LogClass.ServiceAm, "WebAppletWindow: WebView2 ready, navigating...");
+
+ if (!_navigated && !string.IsNullOrEmpty(_pendingUrl))
+ {
+ _navigated = true;
+ WebViewHost.NavigateToUrl(_pendingUrl);
+ }
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.ServiceAm,
+ $"WebAppletWindow: WebView2 failed: {errorMessage}");
+
+ TextBlock loadingText = this.FindControl("LoadingText");
+ if (loadingText != null)
+ {
+ loadingText.Text = $"WebView2 initialization failed.\n{errorMessage}\n\nClick 'Skip' to continue.";
+ loadingText.TextAlignment = Avalonia.Media.TextAlignment.Center;
+ }
+ }
+ });
+ }
+
+ private void OnNavigationCompleted(bool success)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (success)
+ {
+ // Hide loading text once page loads
+ TextBlock loadingText = this.FindControl("LoadingText");
+ if (loadingText != null)
+ {
+ loadingText.IsVisible = false;
+ }
+ }
+ });
+ }
+
+ private void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ ClosedByCallback = false;
+ Close();
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ WebViewHost.WebView2Ready -= OnWebView2Ready;
+ WebViewHost.NavigationCompleted -= OnNavigationCompleted;
+ _cancellationRegistration.Dispose();
+ base.OnClosed(e);
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Applet/WebView2Host.cs b/src/Ryujinx/UI/Applet/WebView2Host.cs
new file mode 100644
index 000000000..3c9217780
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/WebView2Host.cs
@@ -0,0 +1,266 @@
+using Avalonia.Controls;
+using Avalonia.Platform;
+using Microsoft.Web.WebView2.Core;
+using Ryujinx.Common.Logging;
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Threading.Tasks;
+using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
+
+namespace Ryujinx.Ava.UI.Applet
+{
+ ///
+ /// An Avalonia NativeControlHost that embeds a WebView2 browser instance directly
+ /// using the CoreWebView2 COM API. Handles WM_SIZE to keep the WebView2 controller
+ /// properly sized when Avalonia resizes the host window.
+ ///
+ public class WebView2Host : NativeControlHost
+ {
+ private const uint WM_SIZE = 0x0005;
+
+ private nint _hwnd;
+ private CoreWebView2Controller _controller;
+ private CoreWebView2Environment _environment;
+ private string _pendingUrl;
+ private bool _isInitialized;
+ private WindowProc _wndProcDelegate;
+ private string _className;
+
+ /// Fires when the WebView2 engine is ready (or failed). Args: (success, errorMessage).
+ public event Action WebView2Ready;
+
+ /// Fires when navigation starts. Arg: URL.
+ public event Action NavigationStarted;
+
+ /// Fires when navigation completes. Arg: success.
+ public event Action NavigationCompleted;
+
+ /// True once the WebView2 engine is fully initialized.
+ public bool IsWebView2Ready => _isInitialized;
+
+ ///
+ /// Queues a URL for navigation. If WebView2 is already initialized, navigates immediately.
+ ///
+ public void NavigateToUrl(string url)
+ {
+ _pendingUrl = url;
+
+ if (_controller?.CoreWebView2 != null)
+ {
+ _controller.CoreWebView2.Navigate(url);
+ }
+ }
+
+ protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return CreateWin32(parent);
+ }
+
+ Logger.Warning?.Print(LogClass.ServiceAm, "WebView2Host: Only supported on Windows");
+ return base.CreateNativeControlCore(parent);
+ }
+
+ protected override void DestroyNativeControlCore(IPlatformHandle control)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ DestroyWin32();
+ }
+ else
+ {
+ base.DestroyNativeControlCore(control);
+ }
+ }
+
+ [SupportedOSPlatform("windows")]
+ private PlatformHandle CreateWin32(IPlatformHandle parent)
+ {
+ _className = "RyujinxWebView2-" + Guid.NewGuid();
+
+ // Window proc that handles WM_SIZE to keep WebView2 controller bounds in sync
+ _wndProcDelegate = (hWnd, msg, wParam, lParam) =>
+ {
+ if ((uint)msg == WM_SIZE && _controller != null)
+ {
+ int width = (int)(lParam & 0xFFFF);
+ int height = (int)((lParam >> 16) & 0xFFFF);
+
+ if (width > 0 && height > 0)
+ {
+ _controller.Bounds = new System.Drawing.Rectangle(0, 0, width, height);
+ }
+ }
+
+ return DefWindowProc(hWnd, msg, wParam, lParam);
+ };
+
+ WndClassEx wndClassEx = new()
+ {
+ cbSize = Marshal.SizeOf(),
+ hInstance = GetModuleHandle(null),
+ lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
+ style = ClassStyles.CsOwndc,
+ lpszClassName = Marshal.StringToHGlobalUni(_className),
+ hCursor = CreateArrowCursor(),
+ };
+
+ RegisterClassEx(ref wndClassEx);
+
+ _hwnd = CreateWindowEx(
+ 0, _className, "WebView2Host",
+ WindowStyles.WsChild,
+ 0, 0, 1, 1, // Initial size doesn't matter - NativeControlHost will resize via WM_SIZE
+ parent.Handle, nint.Zero, nint.Zero, nint.Zero);
+
+ Marshal.FreeHGlobal(wndClassEx.lpszClassName);
+
+ if (_hwnd == nint.Zero)
+ {
+ Logger.Error?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Failed to create child window. Error: {Marshal.GetLastWin32Error()}");
+ return new PlatformHandle(nint.Zero, "HWND");
+ }
+
+ Logger.Info?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Child window created (HWND=0x{_hwnd:X})");
+
+ // Start async WebView2 initialization
+ _ = InitializeWebView2Async();
+
+ return new PlatformHandle(_hwnd, "HWND");
+ }
+
+ [SupportedOSPlatform("windows")]
+ private async Task InitializeWebView2Async()
+ {
+ try
+ {
+ Logger.Info?.Print(LogClass.ServiceAm, "WebView2Host: Creating CoreWebView2Environment...");
+
+ string userDataFolder = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "Ryujinx", "WebView2");
+
+ _environment = await CoreWebView2Environment.CreateAsync(
+ browserExecutableFolder: null,
+ userDataFolder: userDataFolder);
+
+ Logger.Info?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Environment created (version={_environment.BrowserVersionString}). Creating controller...");
+
+ _controller = await _environment.CreateCoreWebView2ControllerAsync(_hwnd);
+
+ Logger.Info?.Print(LogClass.ServiceAm, "WebView2Host: Controller created. Configuring...");
+
+ // Query the actual HWND size (in physical pixels) and set controller bounds
+ if (GetClientRect(_hwnd, out RECT rect))
+ {
+ int width = rect.Right - rect.Left;
+ int height = rect.Bottom - rect.Top;
+ _controller.Bounds = new System.Drawing.Rectangle(0, 0,
+ Math.Max(1, width), Math.Max(1, height));
+
+ Logger.Info?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Initial bounds set to {width}x{height} (physical pixels)");
+ }
+
+ _controller.IsVisible = true;
+
+ // Configure settings
+ CoreWebView2Settings settings = _controller.CoreWebView2.Settings;
+ settings.AreDevToolsEnabled = false;
+ settings.AreDefaultContextMenusEnabled = false;
+ settings.IsStatusBarEnabled = false;
+ settings.IsZoomControlEnabled = false;
+
+ // Subscribe to events
+ _controller.CoreWebView2.NavigationStarting += (s, e) =>
+ {
+ Logger.Info?.Print(LogClass.ServiceAm, $"WebView2: Navigation starting: {e.Uri}");
+ NavigationStarted?.Invoke(e.Uri);
+ };
+
+ _controller.CoreWebView2.NavigationCompleted += (s, e) =>
+ {
+ Logger.Info?.Print(LogClass.ServiceAm,
+ $"WebView2: Navigation completed (Success={e.IsSuccess}, Status={e.WebErrorStatus})");
+ NavigationCompleted?.Invoke(e.IsSuccess);
+ };
+
+ _controller.CoreWebView2.NewWindowRequested += (s, e) =>
+ {
+ Logger.Info?.Print(LogClass.ServiceAm, $"WebView2: New window requested: {e.Uri}");
+ e.Handled = true;
+ _controller.CoreWebView2.Navigate(e.Uri);
+ };
+
+ _isInitialized = true;
+
+ Logger.Info?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Initialization complete! Browser version: {_environment.BrowserVersionString}");
+
+ // Navigate to pending URL
+ if (!string.IsNullOrEmpty(_pendingUrl))
+ {
+ Logger.Info?.Print(LogClass.ServiceAm, $"WebView2Host: Navigating to pending URL: {_pendingUrl}");
+ _controller.CoreWebView2.Navigate(_pendingUrl);
+ }
+
+ WebView2Ready?.Invoke(true, null);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Initialization FAILED: {ex}");
+ WebView2Ready?.Invoke(false, ex.Message);
+ }
+ }
+
+ [SupportedOSPlatform("windows")]
+ private void DestroyWin32()
+ {
+ try
+ {
+ _controller?.Close();
+ _controller = null;
+ }
+ catch (Exception ex)
+ {
+ Logger.Warning?.Print(LogClass.ServiceAm,
+ $"WebView2Host: Error closing controller: {ex.Message}");
+ }
+
+ if (_hwnd != nint.Zero)
+ {
+ DestroyWindow(_hwnd);
+ _hwnd = nint.Zero;
+ }
+
+ if (!string.IsNullOrEmpty(_className))
+ {
+ UnregisterClass(_className, GetModuleHandle(null));
+ }
+ }
+
+ #region Win32 Interop
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool GetClientRect(nint hWnd, out RECT lpRect);
+
+ #endregion
+ }
+}
diff --git a/src/Ryujinx/UI/RyujinxApp.axaml.cs b/src/Ryujinx/UI/RyujinxApp.axaml.cs
index c778f27fb..20d660efe 100644
--- a/src/Ryujinx/UI/RyujinxApp.axaml.cs
+++ b/src/Ryujinx/UI/RyujinxApp.axaml.cs
@@ -5,6 +5,7 @@ using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
+using AvaloniaWebView;
using FluentAvalonia.UI.Windowing;
using Gommon;
using Ryujinx.Ava.Common.Locale;
@@ -49,6 +50,14 @@ namespace Ryujinx.Ava
public static void SetTaskbarProgressValue(ulong current, ulong total) => MainWindow.PlatformFeatures.SetTaskBarProgressBarValue(current, total);
public static void SetTaskbarProgressValue(long current, long total) => SetTaskbarProgressValue(Convert.ToUInt64(current), Convert.ToUInt64(total));
+ public override void RegisterServices()
+ {
+ base.RegisterServices();
+
+ // Initialize the WebView builder so that embedded WebView controls work.
+ AvaloniaWebViewBuilder.Initialize(default);
+ }
+
public override void Initialize()
{
Name = FormatTitle();