From 05f1136b60217fc47dae60d90b7b3e3dbfa14668 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 25 Jun 2026 16:29:42 +0100 Subject: [PATCH 1/3] webview: add ordered adapter preferences Applications may depend on capabilities that are only available from a particular WebView implementation, but backend-specific switches do not scale as more adapters and hosting modes are added. Expose a common ordered preference list on NativeWebView and NativeWebDialog. A non-empty list is exhaustive, allowing callers to express fallback order or require a single adapter, while an empty list preserves the existing platform defaults. Apply selection to WebView1 and WebView2 on Windows and to WPE WebKit and WebKitGTK on Linux. This also lets authentication callers select an adapter through the existing NativeWebDialogFactory without adding WebView-specific options to WebAuthenticatorOptions. Signed-off-by: Matthew John Cheetham --- .../WebViewAdapter.cs | 141 ++++++++++++------ .../NativeWebDialog.cs | 56 ++++++- .../NativeWebView.cs | 16 +- .../WebViewAdapterSelectionTests.cs | 56 +++++++ 4 files changed, 219 insertions(+), 50 deletions(-) create mode 100644 tests/Avalonia.Controls.WebView.Tests/WebViewAdapterSelectionTests.cs diff --git a/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs index d9f755f..784c716 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,95 @@ 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 ((!hasExplicitPref && args.PreferWebView1Instead) + || Win.WebView2.CoreWebView2Environment.TryFindWebView2Runtime(args.BrowserExecutableFolder) == + IntPtr.Zero) + { + continue; + } + 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(); + + if (hasExplicitPref || !args.PreferWebKitGtkInstead) + { + 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 +187,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); + } +} From 9b52c9ca3ad328c58755b449dffad8f8fa13b1aa Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 16 Sep 2026 12:19:15 +0100 Subject: [PATCH 2/3] webview: make PreferWebKitGtkInstead obsolete Now that we have a general way to specify webview adapter preferences, we no longer need the specific WPE WebView 'PreferWebKitGtkInstead' flag on the environment requested event args. Mark this property as Obsolete with a hint to callers to use the new API 'AdapterPreference' on either the NativeWebView or NativeWebDialog. Signed-off-by: Matthew John Cheetham --- .../Platform/LinuxWpeWebViewEnvironmentRequestedEventArgs.cs | 2 ++ src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs | 2 ++ 2 files changed, 4 insertions(+) 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/WebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs index 784c716..5c0458f 100644 --- a/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs @@ -154,7 +154,9 @@ public record CompositorHostAdapterFactory(OffscreenWebViewAdapterBuilder Invoke 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( From 3a3e5c1ecb5128541f987b77e2caa1f7fd0402f5 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 16 Sep 2026 12:21:17 +0100 Subject: [PATCH 3/3] webview: remove obsolete internal PreferWebView1Instead flag Remove the internal-only API to prefer using the WebView1 adapter for Windows (over WebView2) in favour of the generic 'AdapterPreference' ordered preference list on either NativeWebView or NativeWebDialog. Signed-off-by: Matthew John Cheetham --- .../WindowsWebView2EnvironmentRequestedEventArgs.cs | 5 ----- src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs | 6 ------ 2 files changed, 11 deletions(-) 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 5c0458f..de2925d 100644 --- a/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs @@ -105,12 +105,6 @@ public record CompositorHostAdapterFactory(OffscreenWebViewAdapterBuilder Invoke var args = new WindowsWebView2EnvironmentRequestedEventArgs(deferralManager); environmentRequested(args); await deferralManager.WaitForDeferralsAsync(); - if ((!hasExplicitPref && args.PreferWebView1Instead) - || Win.WebView2.CoreWebView2Environment.TryFindWebView2Runtime(args.BrowserExecutableFolder) == - IntPtr.Zero) - { - continue; - } if (args.ExperimentalOffscreen && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763)) {