diff --git a/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs index a66eed9..ee7e54e 100644 --- a/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView.Core/Android/AndroidNativeWebViewDialog.cs @@ -21,7 +21,7 @@ internal class AndroidNativeWebViewDialog(Action AndroidWebViewDialogActivity.WebViewRegistry.Get(_webViewId); - public Color DefaultBackground { get; set; } + public Color DefaultBackground { get; set; } = Colors.White; public string? Title { get; set; } public bool CanUserResize { get => false; set { } } diff --git a/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs index 73ec2a2..e66be92 100644 --- a/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView.Core/Browser/BrowserWindowNativeWebViewDialog.cs @@ -21,7 +21,7 @@ internal class BrowserWindowNativeWebViewDialog(Action _adapter; diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs index fade712..04bd8a1 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs @@ -142,7 +142,7 @@ internal static partial IntPtr webkit_website_data_manager_new( public static extern nint webkit_web_view_run_javascript_finish(IntPtr webView, IntPtr result, GError** error); [DllImport(LibWebKit)] - public static extern void webkit_web_view_set_background_color(IntPtr webView, GdkRGBA color); + public static extern void webkit_web_view_set_background_color(IntPtr webView, GdkRGBA* color); [DllImport(LibWebKit)] public static extern void webkit_javascript_result_unref(IntPtr jsResult); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs index 0a138e7..7d778cd 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkNativeWebViewDialog.cs @@ -25,7 +25,33 @@ internal sealed class GtkNativeWebViewDialog : INativeWebViewDialog, IGtkWebView private bool _isShown; private sealed class DialogGtkWebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs args) - : GtkWebViewAdapter(args); + : GtkWebViewAdapter(args) + { + public override Color DefaultBackground + { + set + { + var rgba = new GdkRGBA + { + red = value.R / 255.0, + green = value.G / 255.0, + blue = value.B / 255.0, + alpha = value.A / 255.0 + }; + RunOnGlibThreadAsync(() => + { + if (WebViewHandle == IntPtr.Zero) + return; + + unsafe + { + var color = rgba; + webkit_web_view_set_background_color(WebViewHandle, &color); + } + }); + } + } + } private GtkNativeWebViewDialog(GtkWebViewEnvironmentRequestedEventArgs args) { diff --git a/src/Avalonia.Controls.WebView.Core/Headless/HeadlessWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Headless/HeadlessWebViewAdapter.cs index 763cb2a..a3f7694 100644 --- a/src/Avalonia.Controls.WebView.Core/Headless/HeadlessWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Headless/HeadlessWebViewAdapter.cs @@ -66,11 +66,14 @@ public Uri Source public Color DefaultBackground { - set - { - } + set => LastDefaultBackground = value; } + /// + /// Last value assigned to , useful with unit testing. + /// + internal Color? LastDefaultBackground { get; private set; } + public string? UserAgent { get => null; set { } } public void SizeChanged(PixelSize containerSize) diff --git a/src/Avalonia.Controls.WebView/NativeWebDialog.cs b/src/Avalonia.Controls.WebView/NativeWebDialog.cs index f89fcb8..8cd6ed7 100644 --- a/src/Avalonia.Controls.WebView/NativeWebDialog.cs +++ b/src/Avalonia.Controls.WebView/NativeWebDialog.cs @@ -7,6 +7,8 @@ using AvPlatform = Avalonia.Platform; using Core = Avalonia.Controls; using IPlatformHandle = Avalonia.Platform.IPlatformHandle; +using Color = Avalonia.Media.Color; +using Colors = Avalonia.Media.Colors; #if WPF using AvaloniaUI.Xpf.WpfAbstractions; using Window = System.Windows.Window; @@ -38,6 +40,7 @@ public class NativeWebDialog : Core.IWebView, Core.IWebViewHolder, IDisposable private bool? _initialCanUserResize; private PixelSize? _initialSize; private PixelPoint? _initialPosition; + private Color? _initialDefaultBackground; private bool _disposed; private bool _dialogInitialized; @@ -276,6 +279,24 @@ public bool CanUserResize } } + /// + /// Gets or sets the background color of the dialog and of the webview hosted inside of it. + /// If null, the owner background is used, falling back to white. + /// + public Color? DefaultBackground + { + get => _initialDefaultBackground; + set + { + _initialDefaultBackground = value; + if (value is { } background + && TryGetImpl() is { } impl) + { + impl.DefaultBackground = background; + } + } + } + /// public event EventHandler? Closing; /// @@ -295,6 +316,12 @@ public async void Show(TopLevel owner) { var impl = await GetOrInitialize(); + // Not stored in _initialDefaultBackground, so the owner is still resolved again on the next Show call. + if (_initialDefaultBackground is null) + { + impl.DefaultBackground = GetOwnerBackground(owner); + } + #if WPF var avTopLevel = XpfWpfAbstraction.GetAvaloniaTopLevelForWindow(owner); #elif AVALONIA @@ -317,6 +344,18 @@ public async void Show(TopLevel owner) } } +#if WPF + private static Color GetOwnerBackground(Window owner) => + owner.Background is System.Windows.Media.SolidColorBrush solid ? + new Color(solid.Color.A, solid.Color.R, solid.Color.G, solid.Color.B) : + Colors.White; +#elif AVALONIA + private static Color GetOwnerBackground(TopLevel owner) => + owner.Background is Media.ISolidColorBrush solid ? + solid.Color : + Colors.White; +#endif + private async Task GetOrInitialize() { if (TryGetImpl() is { } impl) @@ -439,6 +478,8 @@ private async Task Initialize() dialogImpl.Move(position.X, position.Y); if (_initialSize is { } size) dialogImpl.Resize(size.Width, size.Height); + if (_initialDefaultBackground is { } background) + dialogImpl.DefaultBackground = background; _implTcs.SetResult(dialogImpl); diff --git a/src/Avalonia.Controls.WebView/WebAuthenticationBroker.cs b/src/Avalonia.Controls.WebView/WebAuthenticationBroker.cs index a861497..d1da46c 100644 --- a/src/Avalonia.Controls.WebView/WebAuthenticationBroker.cs +++ b/src/Avalonia.Controls.WebView/WebAuthenticationBroker.cs @@ -146,7 +146,7 @@ private static NativeWebDialog DefaultFactory() { var dialog = new NativeWebDialog(); dialog.Title = "Authentication"; - dialog.CanUserResize = false; + dialog.CanUserResize = true; dialog.Resize(600, 700); return dialog; } diff --git a/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs b/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs index ca919ac..d6c44ab 100644 --- a/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs +++ b/src/Avalonia.Controls.WebView/WindowNativeWebViewDialog.cs @@ -56,7 +56,10 @@ async void CompleteAdapter() _controlHostImpl.AdapterCreated += (_, adapter) => { - adapter.DefaultBackground = _initialDefaultBackground ?? Colors.Transparent; + // Opaque by default, so web pages without their own background stay readable with a dark theme. + var background = _initialDefaultBackground ?? Colors.White; + adapter.DefaultBackground = background; + ApplyWindowBackground(background); AdapterCreated?.Invoke(this, new Core.WebViewAdapterEventArgs(adapter)); }; _controlHostImpl.AdapterDestroyed += (_, adapter) => AdapterDestroyed?.Invoke(this, new Core.WebViewAdapterEventArgs(adapter)); @@ -77,17 +80,26 @@ public Color DefaultBackground { set { + _initialDefaultBackground = value; if (_controlHostImpl?.TryGetAdapter() is { } adapter) { adapter.DefaultBackground = value; } - else - { - _initialDefaultBackground = value; - } + ApplyWindowBackground(value); } } + // The webview may be composited into the window (or transparent on macOS), + // so the window itself has to use the same color. + private void ApplyWindowBackground(Color color) + { +#if WPF + Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B)); +#elif AVALONIA + Background = new Media.SolidColorBrush(color); +#endif + } + public void Dispose() => Close(); event EventHandler? Core.INativeWebViewDialog.Closing diff --git a/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs b/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs index 8c0c368..cf0f062 100644 --- a/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs +++ b/tests/Avalonia.Controls.WebView.Tests/NativeWebDialogTests.cs @@ -1,4 +1,6 @@ -using Avalonia.Headless.XUnit; +using Avalonia.Controls.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Media; using System; using System.Threading.Tasks; using Xunit; @@ -56,6 +58,72 @@ public void Should_Set_And_Reflect_Position_And_Size() Assert.Equal(60, underlying.Position.Y); } + [AvaloniaFact] + public async Task Should_Use_White_Background_Without_Owner() + { + var dialog = new NativeWebDialog(); + dialog.Show(); + + await WaitForAdapterCreation(dialog); + + Assert.Null(dialog.DefaultBackground); + Assert.Equal(Colors.White, GetAdapterBackground(dialog)); + Assert.Equal(Colors.White, GetWindowBackground(dialog)); + } + + [AvaloniaFact] + public async Task Should_Use_Owner_Background_When_Not_Set() + { + var owner = new Window { Background = new SolidColorBrush(Colors.Blue) }; + owner.Show(); + + var dialog = new NativeWebDialog(); + dialog.Show(owner); + + await WaitForAdapterCreation(dialog); + + Assert.Null(dialog.DefaultBackground); + Assert.Equal(Colors.Blue, GetAdapterBackground(dialog)); + Assert.Equal(Colors.Blue, GetWindowBackground(dialog)); + } + + [AvaloniaFact] + public async Task Should_Set_DefaultBackground_Before_Show() + { + var owner = new Window { Background = new SolidColorBrush(Colors.Blue) }; + owner.Show(); + + var dialog = new NativeWebDialog(); + dialog.DefaultBackground = Colors.Red; + dialog.Show(owner); + + await WaitForAdapterCreation(dialog); + + // Explicit value wins over the owner background. + Assert.Equal(Colors.Red, dialog.DefaultBackground); + Assert.Equal(Colors.Red, GetAdapterBackground(dialog)); + Assert.Equal(Colors.Red, GetWindowBackground(dialog)); + } + + [AvaloniaFact] + public async Task Should_Set_DefaultBackground_After_Show() + { + var dialog = new NativeWebDialog(); + dialog.Show(); + + await WaitForAdapterCreation(dialog); + + dialog.DefaultBackground = Colors.Red; + Assert.Equal(Colors.Red, GetAdapterBackground(dialog)); + Assert.Equal(Colors.Red, GetWindowBackground(dialog)); + } + + private static Color? GetAdapterBackground(NativeWebDialog dialog) => + ((HeadlessWebViewAdapter)dialog.TryGetWebViewPlatformHandle()!).LastDefaultBackground; + + private static Color? GetWindowBackground(NativeWebDialog dialog) => + (dialog.TryGetWindow()!.Background as ISolidColorBrush)?.Color; + [AvaloniaFact] public void Should_Expose_Platform_Handle() {