// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using SkiaSharp;
namespace Microsoft.Maui.Platform;
///
/// WebView implementation using WebKitGTK for Linux.
/// Renders web content in a native GTK window and composites to Skia.
///
public class SkiaWebView : SkiaView
{
#region Native Interop - GTK
private const string LibGtk4 = "libgtk-4.so.1";
private const string LibGtk3 = "libgtk-3.so.0";
private const string LibWebKit2Gtk4 = "libwebkitgtk-6.0.so.4";
private const string LibWebKit2Gtk3 = "libwebkit2gtk-4.1.so.0";
private const string LibGObject = "libgobject-2.0.so.0";
private const string LibGLib = "libglib-2.0.so.0";
private static bool _useGtk4;
private static bool _gtkInitialized;
private static string _webkitLib = LibWebKit2Gtk3;
// GTK functions
[DllImport(LibGtk4, EntryPoint = "gtk_init")]
private static extern void gtk4_init();
[DllImport(LibGtk3, EntryPoint = "gtk_init_check")]
private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv);
[DllImport(LibGtk4, EntryPoint = "gtk_window_new")]
private static extern IntPtr gtk4_window_new();
[DllImport(LibGtk3, EntryPoint = "gtk_window_new")]
private static extern IntPtr gtk3_window_new(int type);
[DllImport(LibGtk4, EntryPoint = "gtk_window_set_default_size")]
private static extern void gtk4_window_set_default_size(IntPtr window, int width, int height);
[DllImport(LibGtk3, EntryPoint = "gtk_window_set_default_size")]
private static extern void gtk3_window_set_default_size(IntPtr window, int width, int height);
[DllImport(LibGtk4, EntryPoint = "gtk_window_set_child")]
private static extern void gtk4_window_set_child(IntPtr window, IntPtr child);
[DllImport(LibGtk3, EntryPoint = "gtk_container_add")]
private static extern void gtk3_container_add(IntPtr container, IntPtr widget);
[DllImport(LibGtk4, EntryPoint = "gtk_widget_show")]
private static extern void gtk4_widget_show(IntPtr widget);
[DllImport(LibGtk3, EntryPoint = "gtk_widget_show_all")]
private static extern void gtk3_widget_show_all(IntPtr widget);
[DllImport(LibGtk4, EntryPoint = "gtk_widget_hide")]
private static extern void gtk4_widget_hide(IntPtr widget);
[DllImport(LibGtk3, EntryPoint = "gtk_widget_hide")]
private static extern void gtk3_widget_hide(IntPtr widget);
[DllImport(LibGtk4, EntryPoint = "gtk_widget_get_width")]
private static extern int gtk4_widget_get_width(IntPtr widget);
[DllImport(LibGtk4, EntryPoint = "gtk_widget_get_height")]
private static extern int gtk4_widget_get_height(IntPtr widget);
// GObject
[DllImport(LibGObject, EntryPoint = "g_object_unref")]
private static extern void g_object_unref(IntPtr obj);
[DllImport(LibGObject, EntryPoint = "g_signal_connect_data")]
private static extern ulong g_signal_connect_data(IntPtr instance,
[MarshalAs(UnmanagedType.LPStr)] string signal,
IntPtr handler, IntPtr data, IntPtr destroyData, int flags);
// GLib main loop (for event processing)
[DllImport(LibGLib, EntryPoint = "g_main_context_iteration")]
private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock);
#endregion
#region WebKit Functions
// We'll load these dynamically based on available version
private delegate IntPtr WebKitWebViewNewDelegate();
private delegate void WebKitWebViewLoadUriDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string uri);
private delegate void WebKitWebViewLoadHtmlDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string html, [MarshalAs(UnmanagedType.LPStr)] string? baseUri);
private delegate IntPtr WebKitWebViewGetUriDelegate(IntPtr webView);
private delegate IntPtr WebKitWebViewGetTitleDelegate(IntPtr webView);
private delegate void WebKitWebViewGoBackDelegate(IntPtr webView);
private delegate void WebKitWebViewGoForwardDelegate(IntPtr webView);
private delegate bool WebKitWebViewCanGoBackDelegate(IntPtr webView);
private delegate bool WebKitWebViewCanGoForwardDelegate(IntPtr webView);
private delegate void WebKitWebViewReloadDelegate(IntPtr webView);
private delegate void WebKitWebViewStopLoadingDelegate(IntPtr webView);
private delegate double WebKitWebViewGetEstimatedLoadProgressDelegate(IntPtr webView);
private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView);
private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled);
private static WebKitWebViewNewDelegate? _webkitWebViewNew;
private static WebKitWebViewLoadUriDelegate? _webkitLoadUri;
private static WebKitWebViewLoadHtmlDelegate? _webkitLoadHtml;
private static WebKitWebViewGetUriDelegate? _webkitGetUri;
private static WebKitWebViewGetTitleDelegate? _webkitGetTitle;
private static WebKitWebViewGoBackDelegate? _webkitGoBack;
private static WebKitWebViewGoForwardDelegate? _webkitGoForward;
private static WebKitWebViewCanGoBackDelegate? _webkitCanGoBack;
private static WebKitWebViewCanGoForwardDelegate? _webkitCanGoForward;
private static WebKitWebViewReloadDelegate? _webkitReload;
private static WebKitWebViewStopLoadingDelegate? _webkitStopLoading;
private static WebKitWebViewGetEstimatedLoadProgressDelegate? _webkitGetProgress;
private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings;
private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript;
[DllImport("libdl.so.2")]
private static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string? filename, int flags);
[DllImport("libdl.so.2")]
private static extern IntPtr dlsym(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string symbol);
[DllImport("libdl.so.2")]
private static extern IntPtr dlerror();
private const int RTLD_NOW = 2;
private const int RTLD_GLOBAL = 0x100;
private static IntPtr _webkitHandle;
#endregion
#region Fields
private IntPtr _gtkWindow;
private IntPtr _webView;
private string _source = "";
private string _html = "";
private bool _isInitialized;
private bool _javascriptEnabled = true;
private double _loadProgress;
#endregion
#region Properties
///
/// Gets or sets the URL to navigate to.
///
public string Source
{
get => _source;
set
{
if (_source != value)
{
_source = value;
if (_isInitialized && !string.IsNullOrEmpty(value))
{
LoadUrl(value);
}
Invalidate();
}
}
}
///
/// Gets or sets the HTML content to display.
///
public string Html
{
get => _html;
set
{
if (_html != value)
{
_html = value;
if (_isInitialized && !string.IsNullOrEmpty(value))
{
LoadHtml(value);
}
Invalidate();
}
}
}
///
/// Gets whether the WebView can navigate back.
///
public bool CanGoBack => _webView != IntPtr.Zero && _webkitCanGoBack?.Invoke(_webView) == true;
///
/// Gets whether the WebView can navigate forward.
///
public bool CanGoForward => _webView != IntPtr.Zero && _webkitCanGoForward?.Invoke(_webView) == true;
///
/// Gets the current URL.
///
public string? CurrentUrl
{
get
{
if (_webView == IntPtr.Zero || _webkitGetUri == null) return null;
var ptr = _webkitGetUri(_webView);
return ptr != IntPtr.Zero ? Marshal.PtrToStringAnsi(ptr) : null;
}
}
///
/// Gets the current page title.
///
public string? Title
{
get
{
if (_webView == IntPtr.Zero || _webkitGetTitle == null) return null;
var ptr = _webkitGetTitle(_webView);
return ptr != IntPtr.Zero ? Marshal.PtrToStringAnsi(ptr) : null;
}
}
///
/// Gets or sets whether JavaScript is enabled.
///
public bool JavaScriptEnabled
{
get => _javascriptEnabled;
set
{
_javascriptEnabled = value;
UpdateJavaScriptSetting();
}
}
///
/// Gets the load progress (0.0 to 1.0).
///
public double LoadProgress => _loadProgress;
///
/// Gets whether WebKit is available on this system.
///
public static bool IsSupported => InitializeWebKit();
#endregion
#region Events
public event EventHandler? Navigating;
public event EventHandler? Navigated;
public event EventHandler? TitleChanged;
public event EventHandler? LoadProgressChanged;
#endregion
#region Constructor
public SkiaWebView()
{
RequestedWidth = 400;
RequestedHeight = 300;
BackgroundColor = SKColors.White;
}
#endregion
#region Initialization
private static bool InitializeWebKit()
{
if (_webkitHandle != IntPtr.Zero) return true;
// Try WebKitGTK 6.0 (GTK4) first
_webkitHandle = dlopen(LibWebKit2Gtk4, RTLD_NOW | RTLD_GLOBAL);
if (_webkitHandle != IntPtr.Zero)
{
_useGtk4 = true;
_webkitLib = LibWebKit2Gtk4;
}
else
{
// Fall back to WebKitGTK 4.1 (GTK3)
_webkitHandle = dlopen(LibWebKit2Gtk3, RTLD_NOW | RTLD_GLOBAL);
if (_webkitHandle != IntPtr.Zero)
{
_useGtk4 = false;
_webkitLib = LibWebKit2Gtk3;
}
else
{
// Try older WebKitGTK 4.0
_webkitHandle = dlopen("libwebkit2gtk-4.0.so.37", RTLD_NOW | RTLD_GLOBAL);
if (_webkitHandle != IntPtr.Zero)
{
_useGtk4 = false;
_webkitLib = "libwebkit2gtk-4.0.so.37";
}
}
}
if (_webkitHandle == IntPtr.Zero)
{
Console.WriteLine("[WebView] WebKitGTK not found. Install with: sudo apt install libwebkit2gtk-4.1-0");
return false;
}
// Load function pointers
_webkitWebViewNew = LoadFunction("webkit_web_view_new");
_webkitLoadUri = LoadFunction("webkit_web_view_load_uri");
_webkitLoadHtml = LoadFunction("webkit_web_view_load_html");
_webkitGetUri = LoadFunction("webkit_web_view_get_uri");
_webkitGetTitle = LoadFunction("webkit_web_view_get_title");
_webkitGoBack = LoadFunction("webkit_web_view_go_back");
_webkitGoForward = LoadFunction("webkit_web_view_go_forward");
_webkitCanGoBack = LoadFunction("webkit_web_view_can_go_back");
_webkitCanGoForward = LoadFunction("webkit_web_view_can_go_forward");
_webkitReload = LoadFunction("webkit_web_view_reload");
_webkitStopLoading = LoadFunction("webkit_web_view_stop_loading");
_webkitGetProgress = LoadFunction("webkit_web_view_get_estimated_load_progress");
_webkitGetSettings = LoadFunction("webkit_web_view_get_settings");
_webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript");
Console.WriteLine($"[WebView] Using {_webkitLib}");
return _webkitWebViewNew != null;
}
private static T? LoadFunction(string name) where T : Delegate
{
var ptr = dlsym(_webkitHandle, name);
if (ptr == IntPtr.Zero) return null;
return Marshal.GetDelegateForFunctionPointer(ptr);
}
private void Initialize()
{
if (_isInitialized) return;
if (!InitializeWebKit()) return;
try
{
// Initialize GTK if needed
if (!_gtkInitialized)
{
if (_useGtk4)
{
gtk4_init();
}
else
{
int argc = 0;
IntPtr argv = IntPtr.Zero;
gtk3_init_check(ref argc, ref argv);
}
_gtkInitialized = true;
}
// Create WebKit view
_webView = _webkitWebViewNew!();
if (_webView == IntPtr.Zero)
{
Console.WriteLine("[WebView] Failed to create WebKit view");
return;
}
// Create GTK window to host the WebView
if (_useGtk4)
{
_gtkWindow = gtk4_window_new();
gtk4_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight);
gtk4_window_set_child(_gtkWindow, _webView);
}
else
{
_gtkWindow = gtk3_window_new(0); // GTK_WINDOW_TOPLEVEL
gtk3_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight);
gtk3_container_add(_gtkWindow, _webView);
}
UpdateJavaScriptSetting();
_isInitialized = true;
// Load initial content
if (!string.IsNullOrEmpty(_source))
{
LoadUrl(_source);
}
else if (!string.IsNullOrEmpty(_html))
{
LoadHtml(_html);
}
Console.WriteLine("[WebView] Initialized successfully");
}
catch (Exception ex)
{
Console.WriteLine($"[WebView] Initialization failed: {ex.Message}");
}
}
#endregion
#region Navigation
public void LoadUrl(string url)
{
if (!_isInitialized) Initialize();
if (_webView == IntPtr.Zero || _webkitLoadUri == null) return;
Navigating?.Invoke(this, new WebNavigatingEventArgs(url));
_webkitLoadUri(_webView, url);
}
public void LoadHtml(string html, string? baseUrl = null)
{
if (!_isInitialized) Initialize();
if (_webView == IntPtr.Zero || _webkitLoadHtml == null) return;
_webkitLoadHtml(_webView, html, baseUrl);
}
public void GoBack()
{
if (_webView != IntPtr.Zero && CanGoBack)
{
_webkitGoBack?.Invoke(_webView);
}
}
public void GoForward()
{
if (_webView != IntPtr.Zero && CanGoForward)
{
_webkitGoForward?.Invoke(_webView);
}
}
public void Reload()
{
if (_webView != IntPtr.Zero)
{
_webkitReload?.Invoke(_webView);
}
}
public void Stop()
{
if (_webView != IntPtr.Zero)
{
_webkitStopLoading?.Invoke(_webView);
}
}
private void UpdateJavaScriptSetting()
{
if (_webView == IntPtr.Zero || _webkitGetSettings == null || _webkitSetJavascript == null) return;
var settings = _webkitGetSettings(_webView);
if (settings != IntPtr.Zero)
{
_webkitSetJavascript(settings, _javascriptEnabled);
}
}
#endregion
#region Event Processing
///
/// Process pending GTK events. Call this from your main loop.
///
public void ProcessEvents()
{
if (!_isInitialized) return;
// Process GTK events
g_main_context_iteration(IntPtr.Zero, false);
// Update progress
if (_webView != IntPtr.Zero && _webkitGetProgress != null)
{
var progress = _webkitGetProgress(_webView);
if (Math.Abs(progress - _loadProgress) > 0.01)
{
_loadProgress = progress;
LoadProgressChanged?.Invoke(this, progress);
}
}
}
///
/// Show the native WebView window (for testing/debugging).
///
public void ShowNativeWindow()
{
if (!_isInitialized) Initialize();
if (_gtkWindow == IntPtr.Zero) return;
if (_useGtk4)
{
gtk4_widget_show(_gtkWindow);
}
else
{
gtk3_widget_show_all(_gtkWindow);
}
}
///
/// Hide the native WebView window.
///
public void HideNativeWindow()
{
if (_gtkWindow == IntPtr.Zero) return;
if (_useGtk4)
{
gtk4_widget_hide(_gtkWindow);
}
else
{
gtk3_widget_hide(_gtkWindow);
}
}
#endregion
#region Rendering
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{
base.OnDraw(canvas, bounds);
// Draw placeholder/loading state
using var bgPaint = new SKPaint { Color = BackgroundColor, Style = SKPaintStyle.Fill };
canvas.DrawRect(bounds, bgPaint);
// Draw border
using var borderPaint = new SKPaint
{
Color = new SKColor(200, 200, 200),
Style = SKPaintStyle.Stroke,
StrokeWidth = 1
};
canvas.DrawRect(bounds, borderPaint);
// Draw web icon and status
var centerX = bounds.MidX;
var centerY = bounds.MidY;
// Globe icon
using var iconPaint = new SKPaint
{
Color = new SKColor(100, 100, 100),
Style = SKPaintStyle.Stroke,
StrokeWidth = 2,
IsAntialias = true
};
canvas.DrawCircle(centerX, centerY - 20, 25, iconPaint);
canvas.DrawLine(centerX - 25, centerY - 20, centerX + 25, centerY - 20, iconPaint);
canvas.DrawArc(new SKRect(centerX - 15, centerY - 45, centerX + 15, centerY + 5), 0, 180, false, iconPaint);
// Status text
using var textPaint = new SKPaint
{
Color = new SKColor(80, 80, 80),
IsAntialias = true,
TextSize = 14
};
string statusText;
if (!IsSupported)
{
statusText = "WebKitGTK not installed";
}
else if (_isInitialized)
{
statusText = string.IsNullOrEmpty(_source) ? "No URL loaded" : $"Loading: {_source}";
if (_loadProgress > 0 && _loadProgress < 1)
{
statusText = $"Loading: {(int)(_loadProgress * 100)}%";
}
}
else
{
statusText = "WebView (click to open)";
}
var textWidth = textPaint.MeasureText(statusText);
canvas.DrawText(statusText, centerX - textWidth / 2, centerY + 30, textPaint);
// Draw install hint if not supported
if (!IsSupported)
{
using var hintPaint = new SKPaint
{
Color = new SKColor(120, 120, 120),
IsAntialias = true,
TextSize = 11
};
var hint = "Install: sudo apt install libwebkit2gtk-4.1-0";
var hintWidth = hintPaint.MeasureText(hint);
canvas.DrawText(hint, centerX - hintWidth / 2, centerY + 50, hintPaint);
}
// Progress bar
if (_loadProgress > 0 && _loadProgress < 1)
{
var progressRect = new SKRect(bounds.Left + 20, bounds.Bottom - 30, bounds.Right - 20, bounds.Bottom - 20);
using var progressBgPaint = new SKPaint { Color = new SKColor(230, 230, 230), Style = SKPaintStyle.Fill };
canvas.DrawRoundRect(new SKRoundRect(progressRect, 5), progressBgPaint);
var filledWidth = progressRect.Width * (float)_loadProgress;
var filledRect = new SKRect(progressRect.Left, progressRect.Top, progressRect.Left + filledWidth, progressRect.Bottom);
using var progressPaint = new SKPaint { Color = new SKColor(33, 150, 243), Style = SKPaintStyle.Fill };
canvas.DrawRoundRect(new SKRoundRect(filledRect, 5), progressPaint);
}
}
public override void OnPointerPressed(PointerEventArgs e)
{
base.OnPointerPressed(e);
if (!_isInitialized && IsSupported)
{
Initialize();
ShowNativeWindow();
}
else if (_isInitialized)
{
ShowNativeWindow();
}
}
#endregion
#region Cleanup
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_gtkWindow != IntPtr.Zero)
{
if (_useGtk4)
{
gtk4_widget_hide(_gtkWindow);
}
else
{
gtk3_widget_hide(_gtkWindow);
}
g_object_unref(_gtkWindow);
_gtkWindow = IntPtr.Zero;
}
_webView = IntPtr.Zero;
_isInitialized = false;
}
base.Dispose(disposing);
}
#endregion
}
#region Event Args
public class WebNavigatingEventArgs : EventArgs
{
public string Url { get; }
public bool Cancel { get; set; }
public WebNavigatingEventArgs(string url)
{
Url = url;
}
}
public class WebNavigatedEventArgs : EventArgs
{
public string Url { get; }
public bool Success { get; }
public string? Error { get; }
public WebNavigatedEventArgs(string url, bool success, string? error = null)
{
Url = url;
Success = success;
Error = error;
}
}
#endregion