// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Maui.Platform.Linux.Interop; using SkiaSharp; namespace Microsoft.Maui.Platform; /// /// Linux platform WebView using WebKitGTK. /// This is a native widget overlay that renders on top of the Skia surface. /// public class LinuxWebView : SkiaView { private IntPtr _webView; private IntPtr _gtkWindow; private bool _initialized; private bool _isVisible = true; private string? _currentUrl; private string? _userAgent; // Signal handler IDs for cleanup private ulong _loadChangedHandlerId; private ulong _decidePolicyHandlerId; private ulong _titleChangedHandlerId; // Keep delegates alive to prevent GC private WebKitGtk.LoadChangedCallback? _loadChangedCallback; private WebKitGtk.DecidePolicyCallback? _decidePolicyCallback; private WebKitGtk.NotifyCallback? _titleChangedCallback; /// /// Event raised when navigation starts. /// public event EventHandler? Navigating; /// /// Event raised when navigation completes. /// public event EventHandler? Navigated; /// /// Event raised when the page title changes. /// public event EventHandler? TitleChanged; /// /// Gets whether the WebView can navigate back. /// public bool CanGoBack => _webView != IntPtr.Zero && WebKitGtk.webkit_web_view_can_go_back(_webView); /// /// Gets whether the WebView can navigate forward. /// public bool CanGoForward => _webView != IntPtr.Zero && WebKitGtk.webkit_web_view_can_go_forward(_webView); /// /// Gets the current URL. /// public string? CurrentUrl { get { if (_webView == IntPtr.Zero) return _currentUrl; var uriPtr = WebKitGtk.webkit_web_view_get_uri(_webView); return WebKitGtk.PtrToStringUtf8(uriPtr) ?? _currentUrl; } } /// /// Gets or sets the user agent string. /// public string? UserAgent { get => _userAgent; set { _userAgent = value; if (_webView != IntPtr.Zero && value != null) { var settings = WebKitGtk.webkit_web_view_get_settings(_webView); WebKitGtk.webkit_settings_set_user_agent(settings, value); } } } public LinuxWebView() { // WebView will be initialized when first shown or when source is set } /// /// Initializes the WebKitGTK WebView. /// private void EnsureInitialized() { if (_initialized) return; try { // Initialize GTK if not already done int argc = 0; IntPtr argv = IntPtr.Zero; WebKitGtk.gtk_init_check(ref argc, ref argv); // Create a top-level window to host the WebView // GTK_WINDOW_TOPLEVEL = 0 _gtkWindow = WebKitGtk.gtk_window_new(0); if (_gtkWindow == IntPtr.Zero) { Console.WriteLine("[LinuxWebView] Failed to create GTK window"); return; } // Configure the window WebKitGtk.gtk_window_set_decorated(_gtkWindow, false); WebKitGtk.gtk_widget_set_can_focus(_gtkWindow, true); // Create the WebKit WebView _webView = WebKitGtk.webkit_web_view_new(); if (_webView == IntPtr.Zero) { Console.WriteLine("[LinuxWebView] Failed to create WebKit WebView"); WebKitGtk.gtk_widget_destroy(_gtkWindow); _gtkWindow = IntPtr.Zero; return; } // Configure settings var settings = WebKitGtk.webkit_web_view_get_settings(_webView); WebKitGtk.webkit_settings_set_enable_javascript(settings, true); WebKitGtk.webkit_settings_set_enable_webgl(settings, true); WebKitGtk.webkit_settings_set_enable_developer_extras(settings, true); WebKitGtk.webkit_settings_set_javascript_can_access_clipboard(settings, true); if (_userAgent != null) { WebKitGtk.webkit_settings_set_user_agent(settings, _userAgent); } // Connect signals ConnectSignals(); // Add WebView to window WebKitGtk.gtk_container_add(_gtkWindow, _webView); _initialized = true; Console.WriteLine("[LinuxWebView] WebKitGTK WebView initialized successfully"); } catch (Exception ex) { Console.WriteLine($"[LinuxWebView] Initialization failed: {ex.Message}"); Console.WriteLine($"[LinuxWebView] Make sure WebKitGTK is installed: sudo apt install libwebkit2gtk-4.1-0"); } } private void ConnectSignals() { // Keep callbacks alive _loadChangedCallback = OnLoadChanged; _decidePolicyCallback = OnDecidePolicy; _titleChangedCallback = OnTitleChanged; // Connect load-changed signal _loadChangedHandlerId = WebKitGtk.g_signal_connect_data( _webView, "load-changed", _loadChangedCallback, IntPtr.Zero, IntPtr.Zero, 0); // Connect decide-policy signal for navigation control _decidePolicyHandlerId = WebKitGtk.g_signal_connect_data( _webView, "decide-policy", _decidePolicyCallback, IntPtr.Zero, IntPtr.Zero, 0); // Connect notify::title for title changes _titleChangedHandlerId = WebKitGtk.g_signal_connect_data( _webView, "notify::title", _titleChangedCallback, IntPtr.Zero, IntPtr.Zero, 0); } private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData) { var url = CurrentUrl ?? ""; switch (loadEvent) { case WebKitGtk.WEBKIT_LOAD_STARTED: case WebKitGtk.WEBKIT_LOAD_REDIRECTED: Navigating?.Invoke(this, new WebViewNavigatingEventArgs(url)); break; case WebKitGtk.WEBKIT_LOAD_FINISHED: Navigated?.Invoke(this, new WebViewNavigatedEventArgs(url, true)); break; case WebKitGtk.WEBKIT_LOAD_COMMITTED: // Page content has started loading break; } } private bool OnDecidePolicy(IntPtr webView, IntPtr decision, int decisionType, IntPtr userData) { if (decisionType == WebKitGtk.WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { var action = WebKitGtk.webkit_navigation_action_get_request(decision); var uriPtr = WebKitGtk.webkit_uri_request_get_uri(action); var url = WebKitGtk.PtrToStringUtf8(uriPtr) ?? ""; var args = new WebViewNavigatingEventArgs(url); Navigating?.Invoke(this, args); if (args.Cancel) { WebKitGtk.webkit_policy_decision_ignore(decision); return true; } } WebKitGtk.webkit_policy_decision_use(decision); return true; } private void OnTitleChanged(IntPtr webView, IntPtr paramSpec, IntPtr userData) { var titlePtr = WebKitGtk.webkit_web_view_get_title(_webView); var title = WebKitGtk.PtrToStringUtf8(titlePtr); TitleChanged?.Invoke(this, title); } /// /// Navigates to the specified URL. /// public void LoadUrl(string url) { EnsureInitialized(); if (_webView == IntPtr.Zero) return; _currentUrl = url; WebKitGtk.webkit_web_view_load_uri(_webView, url); UpdateWindowPosition(); ShowWebView(); } /// /// Loads HTML content. /// public void LoadHtml(string html, string? baseUrl = null) { EnsureInitialized(); if (_webView == IntPtr.Zero) return; WebKitGtk.webkit_web_view_load_html(_webView, html, baseUrl); UpdateWindowPosition(); ShowWebView(); } /// /// Navigates back in history. /// public void GoBack() { if (_webView != IntPtr.Zero && CanGoBack) { WebKitGtk.webkit_web_view_go_back(_webView); } } /// /// Navigates forward in history. /// public void GoForward() { if (_webView != IntPtr.Zero && CanGoForward) { WebKitGtk.webkit_web_view_go_forward(_webView); } } /// /// Reloads the current page. /// public void Reload() { if (_webView != IntPtr.Zero) { WebKitGtk.webkit_web_view_reload(_webView); } } /// /// Stops loading the current page. /// public void Stop() { if (_webView != IntPtr.Zero) { WebKitGtk.webkit_web_view_stop_loading(_webView); } } /// /// Evaluates JavaScript and returns the result. /// public Task EvaluateJavaScriptAsync(string script) { var tcs = new TaskCompletionSource(); if (_webView == IntPtr.Zero) { tcs.SetResult(null); return tcs.Task; } // For now, use fire-and-forget JavaScript execution // Full async result handling requires GAsyncReadyCallback marshaling WebKitGtk.webkit_web_view_run_javascript(_webView, script, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); tcs.SetResult(null); // Return null for now, full implementation needs async callback return tcs.Task; } /// /// Evaluates JavaScript without waiting for result. /// public void Eval(string script) { if (_webView != IntPtr.Zero) { WebKitGtk.webkit_web_view_run_javascript(_webView, script, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); } } private void ShowWebView() { if (_gtkWindow != IntPtr.Zero && _isVisible) { WebKitGtk.gtk_widget_show_all(_gtkWindow); } } private void HideWebView() { if (_gtkWindow != IntPtr.Zero) { WebKitGtk.gtk_widget_hide(_gtkWindow); } } private void UpdateWindowPosition() { if (_gtkWindow == IntPtr.Zero) return; // Get the screen position of this view's bounds var bounds = Bounds; var screenX = (int)bounds.Left; var screenY = (int)bounds.Top; var width = (int)bounds.Width; var height = (int)bounds.Height; if (width > 0 && height > 0) { WebKitGtk.gtk_window_move(_gtkWindow, screenX, screenY); WebKitGtk.gtk_window_resize(_gtkWindow, width, height); } } protected override void OnBoundsChanged() { base.OnBoundsChanged(); UpdateWindowPosition(); } protected override void OnVisibilityChanged() { base.OnVisibilityChanged(); _isVisible = IsVisible; if (_isVisible) { ShowWebView(); } else { HideWebView(); } } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { // Draw a placeholder rectangle where the WebView will be overlaid using var paint = new SKPaint { Color = new SKColor(240, 240, 240), Style = SKPaintStyle.Fill }; canvas.DrawRect(bounds, paint); // Draw border using var borderPaint = new SKPaint { Color = new SKColor(200, 200, 200), Style = SKPaintStyle.Stroke, StrokeWidth = 1 }; canvas.DrawRect(bounds, borderPaint); // Draw "WebView" label if not yet initialized if (!_initialized) { using var textPaint = new SKPaint { Color = SKColors.Gray, TextSize = 14, IsAntialias = true }; var text = "WebView (WebKitGTK)"; var textBounds = new SKRect(); textPaint.MeasureText(text, ref textBounds); var x = bounds.MidX - textBounds.MidX; var y = bounds.MidY - textBounds.MidY; canvas.DrawText(text, x, y, textPaint); } // Process GTK events to keep WebView responsive WebKitGtk.ProcessGtkEvents(); } protected override void Dispose(bool disposing) { if (disposing) { // Disconnect signals if (_webView != IntPtr.Zero) { if (_loadChangedHandlerId != 0) WebKitGtk.g_signal_handler_disconnect(_webView, _loadChangedHandlerId); if (_decidePolicyHandlerId != 0) WebKitGtk.g_signal_handler_disconnect(_webView, _decidePolicyHandlerId); if (_titleChangedHandlerId != 0) WebKitGtk.g_signal_handler_disconnect(_webView, _titleChangedHandlerId); } // Destroy widgets if (_gtkWindow != IntPtr.Zero) { WebKitGtk.gtk_widget_destroy(_gtkWindow); _gtkWindow = IntPtr.Zero; _webView = IntPtr.Zero; // WebView is destroyed with window } _loadChangedCallback = null; _decidePolicyCallback = null; _titleChangedCallback = null; } base.Dispose(disposing); } } /// /// Event args for WebView navigation starting. /// public class WebViewNavigatingEventArgs : EventArgs { public string Url { get; } public bool Cancel { get; set; } public WebViewNavigatingEventArgs(string url) { Url = url; } } /// /// Event args for WebView navigation completed. /// public class WebViewNavigatedEventArgs : EventArgs { public string Url { get; } public bool Success { get; } public WebViewNavigatedEventArgs(string url, bool success) { Url = url; Success = success; } }