Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Avalonia.Controls;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -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.
/// </summary>
[Obsolete("Please use NativeWebView.AdapterPreference or NativeWebDialog.AdapterPreference instead.")]
Comment thread
maxkatz6 marked this conversation as resolved.
public bool PreferWebKitGtkInstead { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ internal WindowsWebView2EnvironmentRequestedEventArgs(DeferralManager deferralMa
{
}

/// <summary>
/// Gets or sets a value indicating whether to prefer WebView1 instead of WebView2.
/// </summary>
internal bool PreferWebView1Instead { get; set; }
Comment thread
maxkatz6 marked this conversation as resolved.

/// <summary>
/// Gets or sets a value indicating whether to enable offscreen composition mode.
/// WebView will render into an offscreen buffer before presenting to the screen.
Expand Down
137 changes: 90 additions & 47 deletions src/Avalonia.Controls.WebView.Core/WebViewAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Platform;

namespace Avalonia.Controls;

internal static class WebViewAdapter
{
internal static readonly IReadOnlyList<WebViewAdapterType> WindowsDefaultOrder =
[WebViewAdapterType.WebView2, WebViewAdapterType.WebView1];
internal static readonly IReadOnlyList<WebViewAdapterType> LinuxDefaultOrder =
[WebViewAdapterType.WpeWebKit, WebViewAdapterType.WebKitGtk];

public static bool UseHeadless { get; set; }

public abstract record AdapterFactory(WebViewAdapterInfo Info);
Expand All @@ -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<AdapterFactory?> CreateFactory(Action<WebViewEnvironmentRequestedEventArgs> environmentRequested)
public static async Task<AdapterFactory?> CreateFactory(
Action<WebViewEnvironmentRequestedEventArgs> environmentRequested,
IReadOnlyList<WebViewAdapterType>? adapterPreference = null)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var deferralManager = new DeferralManager();
Expand Down Expand Up @@ -83,79 +91,114 @@ 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)
Comment thread
maxkatz6 marked this conversation as resolved.
#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());
}
}
}
#endif

return null;
}

internal static IReadOnlyList<WebViewAdapterType> GetAdapterTypes(
IReadOnlyList<WebViewAdapterType>? preferredOrder,
IReadOnlyList<WebViewAdapterType> defaultOrder)
{
if (preferredOrder is null || preferredOrder.Count == 0)
return defaultOrder;

var supported = new HashSet<WebViewAdapterType>(defaultOrder);
var candidates = new List<WebViewAdapterType>(preferredOrder.Count);
foreach (var candidate in preferredOrder)
{
if (supported.Remove(candidate))
candidates.Add(candidate);
}

return candidates;
}
}
56 changes: 54 additions & 2 deletions src/Avalonia.Controls.WebView/NativeWebDialog.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -442,6 +444,16 @@ public bool Move(int x, int y)
public event EventHandler<Core.WebViewAdapterEventArgs>? AdapterDestroyed;
public event EventHandler<Core.WebViewEnvironmentRequestedEventArgs>? EnvironmentRequested;

/// <summary>
/// Gets or sets the exhaustive order in which compatible WebView adapters are attempted.
/// A null or empty list uses the platform default order.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public IReadOnlyList<AvPlatform.WebViewAdapterType>? AdapterPreference { get; set; }

/// <inheritdoc/>
public Core.NativeWebViewCommandManager? TryGetCommandManager() => TryGetAdapter() switch
{
Expand Down Expand Up @@ -493,6 +505,7 @@ public Task<Stream> 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
Expand All @@ -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<Core.WebViewAdapter.AdapterFactory?>(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<Core.WebViewAdapter.AdapterFactory?>(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
Expand Down
16 changes: 15 additions & 1 deletion src/Avalonia.Controls.WebView/NativeWebView.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if AVALONIA || WPF
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -133,6 +134,16 @@ public NativeWebView()
/// <inheritdoc/>
public event EventHandler<Core.WebViewEnvironmentRequestedEventArgs>? EnvironmentRequested;

/// <summary>
/// Gets or sets the exhaustive order in which compatible WebView adapters are attempted.
/// A null or empty list uses the platform default order.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public IReadOnlyList<AvPlatform.WebViewAdapterType>? AdapterPreference { get; set; }

/// <inheritdoc/>
public event EventHandler<Core.WebViewNavigationCompletedEventArgs>? NavigationCompleted
{
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}