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); + } +}