diff --git a/src/Avalonia.Controls.WebView.Core/Platform/LinuxWpeWebViewEnvironmentRequestedEventArgs.cs b/src/Avalonia.Controls.WebView.Core/Platform/LinuxWpeWebViewEnvironmentRequestedEventArgs.cs
index 75ae5aa..4eda1f0 100644
--- a/src/Avalonia.Controls.WebView.Core/Platform/LinuxWpeWebViewEnvironmentRequestedEventArgs.cs
+++ b/src/Avalonia.Controls.WebView.Core/Platform/LinuxWpeWebViewEnvironmentRequestedEventArgs.cs
@@ -1,3 +1,4 @@
+using System;
using Avalonia.Controls;
// ReSharper disable once CheckNamespace
@@ -35,5 +36,6 @@ internal LinuxWpeWebViewEnvironmentRequestedEventArgs(DeferralManager deferralMa
/// Gets or sets a value indicating whether to prefer WebKitGTK instead of WPE WebKit.
/// When set to true, the GTK-based WebView adapter will be used even if WPE is available.
///
+ [Obsolete("Please use NativeWebView.AdapterPreference or NativeWebDialog.AdapterPreference instead.")]
public bool PreferWebKitGtkInstead { get; set; }
}
diff --git a/src/Avalonia.Controls.WebView.Core/Platform/WindowsWebView2EnvironmentRequestedEventArgs.cs b/src/Avalonia.Controls.WebView.Core/Platform/WindowsWebView2EnvironmentRequestedEventArgs.cs
index 2d11ae9..133484b 100644
--- a/src/Avalonia.Controls.WebView.Core/Platform/WindowsWebView2EnvironmentRequestedEventArgs.cs
+++ b/src/Avalonia.Controls.WebView.Core/Platform/WindowsWebView2EnvironmentRequestedEventArgs.cs
@@ -11,11 +11,6 @@ internal WindowsWebView2EnvironmentRequestedEventArgs(DeferralManager deferralMa
{
}
- ///
- /// Gets or sets a value indicating whether to prefer WebView1 instead of WebView2.
- ///
- internal bool PreferWebView1Instead { get; set; }
-
///
/// Gets or sets a value indicating whether to enable offscreen composition mode.
/// WebView will render into an offscreen buffer before presenting to the screen.
diff --git a/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs
index d9f755f..de2925d 100644
--- a/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs
+++ b/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Platform;
@@ -6,6 +7,11 @@ namespace Avalonia.Controls;
internal static class WebViewAdapter
{
+ internal static readonly IReadOnlyList WindowsDefaultOrder =
+ [WebViewAdapterType.WebView2, WebViewAdapterType.WebView1];
+ internal static readonly IReadOnlyList LinuxDefaultOrder =
+ [WebViewAdapterType.WpeWebKit, WebViewAdapterType.WebKitGtk];
+
public static bool UseHeadless { get; set; }
public abstract record AdapterFactory(WebViewAdapterInfo Info);
@@ -19,7 +25,9 @@ public record NativeHostAdapterFactory(NativeWebViewAdapterBuilder InvokeAsync,
public record CompositorHostAdapterFactory(OffscreenWebViewAdapterBuilder InvokeAsync, WebViewAdapterInfo Info) : AdapterFactory(Info);
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
- public static async Task CreateFactory(Action environmentRequested)
+ public static async Task CreateFactory(
+ Action environmentRequested,
+ IReadOnlyList? adapterPreference = null)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var deferralManager = new DeferralManager();
@@ -83,74 +91,91 @@ public record CompositorHostAdapterFactory(OffscreenWebViewAdapterBuilder Invoke
}, Macios.MaciosWebViewAdapter.GetWkWebViewInfo());
}
+ var hasExplicitPref = adapterPreference?.Count > 0;
+
if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))
{
+ var adapterTypes = GetAdapterTypes(adapterPreference,
+ WindowsDefaultOrder);
+
+ foreach (var type in adapterTypes)
{
- var args = new WindowsWebView2EnvironmentRequestedEventArgs(deferralManager);
- environmentRequested(args);
- await deferralManager.WaitForDeferralsAsync();
- if (!args.PreferWebView1Instead
- && Win.WebView2.CoreWebView2Environment.TryFindWebView2Runtime(args.BrowserExecutableFolder) !=
- IntPtr.Zero)
+ if (type == WebViewAdapterType.WebView2)
{
+ var args = new WindowsWebView2EnvironmentRequestedEventArgs(deferralManager);
+ environmentRequested(args);
+ await deferralManager.WaitForDeferralsAsync();
+
if (args.ExperimentalOffscreen && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763))
{
- var info = Win.WebView2.WebView2BaseAdapter.GetWebView2Info(args.BrowserExecutableFolder, WebViewEmbeddingScenario.OffscreenRenderer);
+ var info = Win.WebView2.WebView2BaseAdapter.GetWebView2Info(args.BrowserExecutableFolder,
+ WebViewEmbeddingScenario.OffscreenRenderer);
var builder = await Win.WebView2.WebView2CompAdapter.CreateBuilder(args);
return new CompositorHostAdapterFactory(builder, info);
}
- else
- {
- var info = Win.WebView2.WebView2BaseAdapter.GetWebView2Info(args.BrowserExecutableFolder);
- var builder = await Win.WebView2.WebView2HwndAdapter.CreateBuilder(args);
- return new NativeHostAdapterFactory(builder, info);
- }
+
+ var nativeInfo = Win.WebView2.WebView2BaseAdapter.GetWebView2Info(args.BrowserExecutableFolder);
+ var nativeBuilder = await Win.WebView2.WebView2HwndAdapter.CreateBuilder(args);
+ return new NativeHostAdapterFactory(nativeBuilder, nativeInfo);
}
- }
- {
- var args = new WindowsWebView1EnvironmentRequestedEventArgs(deferralManager);
- environmentRequested(args);
- await deferralManager.WaitForDeferralsAsync();
- if (Win.WebView1.WebView1Process.GetOrCreateProcess(args) is { } process)
+
+ if (type == WebViewAdapterType.WebView1)
{
- var builder = await Win.WebView1.WebView1Adapter.CreateBuilder(process);
- return new NativeHostAdapterFactory(
- builder,
- Win.WebView1.WebView1Adapter.GetWebView1Info());
+ var args = new WindowsWebView1EnvironmentRequestedEventArgs(deferralManager);
+ environmentRequested(args);
+ await deferralManager.WaitForDeferralsAsync();
+ if (Win.WebView1.WebView1Process.GetOrCreateProcess(args) is { } process)
+ {
+ var builder = await Win.WebView1.WebView1Adapter.CreateBuilder(process);
+ return new NativeHostAdapterFactory(
+ builder,
+ Win.WebView1.WebView1Adapter.GetWebView1Info());
+ }
}
}
}
if (OperatingSystem.IsLinux())
{
- if (Linux.WpeWebViewAdapter.IsAvailable())
- {
- var args = new LinuxWpeWebViewEnvironmentRequestedEventArgs(deferralManager);
- environmentRequested(args);
- await deferralManager.WaitForDeferralsAsync();
-
- if (!args.PreferWebKitGtkInstead)
- {
- var builder = await Linux.WpeWebViewAdapter.CreateBuilder(args);
- return new CompositorHostAdapterFactory(
- builder,
- Linux.WpeWebViewAdapter.GetWpeInfo());
- }
- }
+ var adapterTypes = GetAdapterTypes(adapterPreference,
+ LinuxDefaultOrder);
+ foreach (var type in adapterTypes)
{
- var args = new GtkWebViewEnvironmentRequestedEventArgs(deferralManager);
- environmentRequested(args);
- await deferralManager.WaitForDeferralsAsync();
- if (args.ExperimentalOffscreen)
+ if (type == WebViewAdapterType.WpeWebKit && Linux.WpeWebViewAdapter.IsAvailable())
{
- var builder = await Gtk.GtkOffscreenAvaloniaWebViewAdapter.CreateBuilder(args);
- return new CompositorHostAdapterFactory(builder, Gtk.GtkWebViewAdapter.GetWebKitGtkInfo(WebViewEmbeddingScenario.OffscreenRenderer));
+ var args = new LinuxWpeWebViewEnvironmentRequestedEventArgs(deferralManager);
+ environmentRequested(args);
+ await deferralManager.WaitForDeferralsAsync();
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ if (hasExplicitPref || !args.PreferWebKitGtkInstead)
+#pragma warning restore CS0618 // Type or member is obsolete
+ {
+ var builder = await Linux.WpeWebViewAdapter.CreateBuilder(args);
+ return new CompositorHostAdapterFactory(
+ builder,
+ Linux.WpeWebViewAdapter.GetWpeInfo());
+ }
}
- else
+
+ if (type == WebViewAdapterType.WebKitGtk)
{
- var builder = await Gtk.GtkX11WebViewAdapter.CreateBuilder(args);
- return new NativeHostAdapterFactory(builder, Gtk.GtkWebViewAdapter.GetWebKitGtkInfo());
+ if (!Gtk.GtkWebViewAdapter.GetWebKitGtkInfo().IsInstalled)
+ continue;
+
+ var args = new GtkWebViewEnvironmentRequestedEventArgs(deferralManager);
+ environmentRequested(args);
+ await deferralManager.WaitForDeferralsAsync();
+ if (args.ExperimentalOffscreen)
+ {
+ var builder = await Gtk.GtkOffscreenAvaloniaWebViewAdapter.CreateBuilder(args);
+ return new CompositorHostAdapterFactory(builder,
+ Gtk.GtkWebViewAdapter.GetWebKitGtkInfo(WebViewEmbeddingScenario.OffscreenRenderer));
+ }
+
+ var nativeBuilder = await Gtk.GtkX11WebViewAdapter.CreateBuilder(args);
+ return new NativeHostAdapterFactory(nativeBuilder, Gtk.GtkWebViewAdapter.GetWebKitGtkInfo());
}
}
}
@@ -158,4 +183,22 @@ public record CompositorHostAdapterFactory(OffscreenWebViewAdapterBuilder Invoke
return null;
}
+
+ internal static IReadOnlyList GetAdapterTypes(
+ IReadOnlyList? preferredOrder,
+ IReadOnlyList defaultOrder)
+ {
+ if (preferredOrder is null || preferredOrder.Count == 0)
+ return defaultOrder;
+
+ var supported = new HashSet(defaultOrder);
+ var candidates = new List(preferredOrder.Count);
+ foreach (var candidate in preferredOrder)
+ {
+ if (supported.Remove(candidate))
+ candidates.Add(candidate);
+ }
+
+ return candidates;
+ }
}
diff --git a/src/Avalonia.Controls.WebView/NativeWebDialog.cs b/src/Avalonia.Controls.WebView/NativeWebDialog.cs
index 9f04ff6..0f2cb9c 100644
--- a/src/Avalonia.Controls.WebView/NativeWebDialog.cs
+++ b/src/Avalonia.Controls.WebView/NativeWebDialog.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Linq;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Controls.Gtk;
@@ -442,6 +444,16 @@ public bool Move(int x, int y)
public event EventHandler? AdapterDestroyed;
public event EventHandler? EnvironmentRequested;
+ ///
+ /// Gets or sets the exhaustive order in which compatible WebView adapters are attempted.
+ /// A null or empty list uses the platform default order.
+ ///
+ ///
+ /// Set this property before the dialog is initialized. Adapter preference is currently supported
+ /// for WebView1 and WebView2 on Windows, and WPE WebKit and WebKitGTK on Linux.
+ ///
+ public IReadOnlyList? AdapterPreference { get; set; }
+
///
public Core.NativeWebViewCommandManager? TryGetCommandManager() => TryGetAdapter() switch
{
@@ -493,6 +505,7 @@ public Task PrintToPdfStreamAsync(AvPlatform.WebViewPrintSettings printS
private async Task Initialize()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
+ var adapterPreference = AdapterPreference?.ToArray();
#if ANDROID
var dialogImpl = new Android.AndroidNativeWebViewDialog(args => EnvironmentRequested?.Invoke(this, args));
#elif BROWSER
@@ -502,12 +515,51 @@ private async Task Initialize()
// Special case for GTK, as we want to use GTK window instead of Avalonia window there.
if (OperatingSystem.IsLinux() && !Core.WebViewAdapter.UseHeadless)
{
- dialogImpl = await GtkNativeWebViewDialog.CreateAsync(args => EnvironmentRequested?.Invoke(this, args));
+ if (adapterPreference is not { Length: > 0 })
+ {
+ dialogImpl = await GtkNativeWebViewDialog.CreateAsync(
+ args => EnvironmentRequested?.Invoke(this, args));
+ }
+ else
+ {
+ Core.INativeWebViewDialog? preferredDialog = null;
+ var adapterTypes = Core.WebViewAdapter.GetAdapterTypes(
+ adapterPreference,
+ Core.WebViewAdapter.LinuxDefaultOrder);
+ foreach (var type in adapterTypes)
+ {
+ if (type == AvPlatform.WebViewAdapterType.WpeWebKit)
+ {
+ var factory = await Core.WebViewAdapter.CreateFactory(
+ args => EnvironmentRequested?.Invoke(this, args),
+ [type]);
+ if (factory is not null)
+ {
+ preferredDialog = new WindowNativeWebViewDialog(Task.FromResult(factory));
+ break;
+ }
+ }
+ else if (type == AvPlatform.WebViewAdapterType.WebKitGtk)
+ {
+ if (!Core.Gtk.GtkWebViewAdapter.GetWebKitGtkInfo().IsInstalled)
+ continue;
+
+ preferredDialog = await GtkNativeWebViewDialog.CreateAsync(
+ args => EnvironmentRequested?.Invoke(this, args));
+ break;
+ }
+ }
+
+ dialogImpl = preferredDialog ??
+ new WindowNativeWebViewDialog(Task.FromResult(null));
+ }
}
else
{
// Don't await factoryTask here, we want to get Window accessible as early as possible
- var factoryTask = Core.WebViewAdapter.CreateFactory(args => EnvironmentRequested?.Invoke(this, args));
+ var factoryTask = Core.WebViewAdapter.CreateFactory(
+ args => EnvironmentRequested?.Invoke(this, args),
+ adapterPreference);
dialogImpl = new WindowNativeWebViewDialog(factoryTask);
}
#endif
diff --git a/src/Avalonia.Controls.WebView/NativeWebView.cs b/src/Avalonia.Controls.WebView/NativeWebView.cs
index 1ad5eca..c71a3e5 100644
--- a/src/Avalonia.Controls.WebView/NativeWebView.cs
+++ b/src/Avalonia.Controls.WebView/NativeWebView.cs
@@ -1,5 +1,6 @@
#if AVALONIA || WPF
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -133,6 +134,16 @@ public NativeWebView()
///
public event EventHandler? EnvironmentRequested;
+ ///
+ /// Gets or sets the exhaustive order in which compatible WebView adapters are attempted.
+ /// A null or empty list uses the platform default order.
+ ///
+ ///
+ /// Set this property before the control is attached. Adapter preference is currently supported
+ /// for WebView1 and WebView2 on Windows, and WPE WebKit and WebKitGTK on Linux.
+ ///
+ public IReadOnlyList? AdapterPreference { get; set; }
+
///
public event EventHandler? NavigationCompleted
{
@@ -451,7 +462,10 @@ private async void OnAttached()
if (_controlHostImplTcs.Task.IsCompleted)
return;
- var adapterFactory = await Core.WebViewAdapter.CreateFactory(args => EnvironmentRequested?.Invoke(this, args));
+ var adapterPreference = AdapterPreference?.ToArray();
+ var adapterFactory = await Core.WebViewAdapter.CreateFactory(
+ args => EnvironmentRequested?.Invoke(this, args),
+ adapterPreference);
INativeWebViewControlImpl controlHostImpl = adapterFactory switch
{
#if !WPF
diff --git a/tests/Avalonia.Controls.WebView.Tests/WebViewAdapterSelectionTests.cs b/tests/Avalonia.Controls.WebView.Tests/WebViewAdapterSelectionTests.cs
new file mode 100644
index 0000000..aa3bb0b
--- /dev/null
+++ b/tests/Avalonia.Controls.WebView.Tests/WebViewAdapterSelectionTests.cs
@@ -0,0 +1,56 @@
+using Avalonia.Platform;
+using Xunit;
+
+namespace Avalonia.Controls.WebView.Tests;
+
+public class WebViewAdapterSelectionTests
+{
+ [Fact]
+ public void Should_Use_Default_Order_For_Null_Or_Empty_Preference()
+ {
+ WebViewAdapterType[] defaultOrder =
+ [
+ WebViewAdapterType.WebView2,
+ WebViewAdapterType.WebView1
+ ];
+
+ Assert.Equal(defaultOrder, WebViewAdapter.GetAdapterTypes(null, defaultOrder));
+ Assert.Equal(defaultOrder, WebViewAdapter.GetAdapterTypes([], defaultOrder));
+ }
+
+ [Fact]
+ public void Should_Use_Explicit_Exhaustive_Order()
+ {
+ WebViewAdapterType[] preference =
+ [
+ WebViewAdapterType.WebKitGtk,
+ WebViewAdapterType.WebView1,
+ WebViewAdapterType.WebView2,
+ WebViewAdapterType.WebView1
+ ];
+
+ WebViewAdapterType[] defaultOrder =
+ [
+ WebViewAdapterType.WebView2,
+ WebViewAdapterType.WebView1
+ ];
+
+ var types = WebViewAdapter.GetAdapterTypes(
+ preference,
+ defaultOrder);
+
+ Assert.Equal(
+ [WebViewAdapterType.WebView1, WebViewAdapterType.WebView2],
+ types);
+ }
+
+ [Fact]
+ public void Should_Not_Append_Omitted_Or_Unsupported_Adapters()
+ {
+ var types = WebViewAdapter.GetAdapterTypes(
+ [WebViewAdapterType.WpeWebKit],
+ [WebViewAdapterType.WebView2, WebViewAdapterType.WebView1]);
+
+ Assert.Empty(types);
+ }
+}