From 55d4a6eaad04f73d27cb9451d5160d639bd2194e Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Thu, 1 Jan 2026 14:27:33 -0500 Subject: [PATCH] Fix Views: SkiaEntry, SkiaEditor, SkiaShell, SkiaWebView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SkiaEntry.cs: TextProperty BindingMode.OneWay (was TwoWay) - SkiaEditor.cs: All BindingModes corrected (Text=OneWay, others=TwoWay) - SkiaShell.cs: Added FlyoutTextColor, ContentBackgroundColor properties, route registration system, query parameter support, OnScroll handler - SkiaWebView.cs: Full rewrite with X11 embedding, GTK window positioning, hardware acceleration settings, load-changed callbacks, position tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- MERGE_TRACKING.md | 8 +- Views/SkiaEditor.cs | 18 +- Views/SkiaEntry.cs | 2 +- Views/SkiaShell.cs | 257 +++++++++- Views/SkiaWebView.cs | 1150 ++++++++++++++++++++++++++++++++++-------- 5 files changed, 1194 insertions(+), 241 deletions(-) diff --git a/MERGE_TRACKING.md b/MERGE_TRACKING.md index 6323fdc..c3943b3 100644 --- a/MERGE_TRACKING.md +++ b/MERGE_TRACKING.md @@ -69,8 +69,8 @@ | SkiaContentPresenter.cs | [ ] | | | SkiaContextMenu.cs | [ ] | | | SkiaDatePicker.cs | [ ] | | -| SkiaEditor.cs | [ ] | | -| SkiaEntry.cs | [ ] | | +| SkiaEditor.cs | [x] | **FIXED 2026-01-01** - All BindingModes corrected (Text=OneWay, others=TwoWay) | +| SkiaEntry.cs | [x] | **FIXED 2026-01-01** - TextProperty BindingMode.OneWay, others TwoWay | | SkiaFlexLayout.cs | [ ] | | | SkiaFlyoutPage.cs | [ ] | | | SkiaGraphicsView.cs | [ ] | | @@ -89,7 +89,7 @@ | SkiaRefreshView.cs | [ ] | | | SkiaScrollView.cs | [ ] | | | SkiaSearchBar.cs | [ ] | | -| SkiaShell.cs | [ ] | Contains ShellSection, ShellContent | +| SkiaShell.cs | [x] | **FIXED 2026-01-01** - Added FlyoutTextColor, ContentBackgroundColor, route registration, query parameters, OnScroll | | SkiaSlider.cs | [x] | FIXED - Value=OneWay, rest TwoWay (agent had inverted all) | | SkiaStepper.cs | [ ] | | | SkiaSwipeView.cs | [ ] | | @@ -99,7 +99,7 @@ | SkiaTimePicker.cs | [ ] | | | SkiaView.cs | [x] | Made Arrange() virtual | | SkiaVisualStateManager.cs | [ ] | | -| SkiaWebView.cs | [ ] | Contains WebNavigatingEventArgs, WebNavigatedEventArgs (TO REMOVE - use MAUI's) | +| SkiaWebView.cs | [x] | **FIXED 2026-01-01** - Full X11 embedding, position tracking, hardware accel, load callbacks | --- diff --git a/Views/SkiaEditor.cs b/Views/SkiaEditor.cs index daa04c1..086c0a5 100644 --- a/Views/SkiaEditor.cs +++ b/Views/SkiaEditor.cs @@ -21,7 +21,7 @@ public class SkiaEditor : SkiaView typeof(string), typeof(SkiaEditor), "", - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).OnTextPropertyChanged((string)o, (string)n)); /// @@ -33,6 +33,7 @@ public class SkiaEditor : SkiaView typeof(string), typeof(SkiaEditor), "", + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -44,6 +45,7 @@ public class SkiaEditor : SkiaView typeof(SKColor), typeof(SkiaEditor), SKColors.Black, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -55,6 +57,7 @@ public class SkiaEditor : SkiaView typeof(SKColor), typeof(SkiaEditor), new SKColor(0x80, 0x80, 0x80), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -66,6 +69,7 @@ public class SkiaEditor : SkiaView typeof(SKColor), typeof(SkiaEditor), new SKColor(0xBD, 0xBD, 0xBD), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -77,6 +81,7 @@ public class SkiaEditor : SkiaView typeof(SKColor), typeof(SkiaEditor), new SKColor(0x21, 0x96, 0xF3, 0x60), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -88,6 +93,7 @@ public class SkiaEditor : SkiaView typeof(SKColor), typeof(SkiaEditor), new SKColor(0x21, 0x96, 0xF3), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -99,6 +105,7 @@ public class SkiaEditor : SkiaView typeof(string), typeof(SkiaEditor), "Sans", + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); /// @@ -110,6 +117,7 @@ public class SkiaEditor : SkiaView typeof(float), typeof(SkiaEditor), 14f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); /// @@ -121,6 +129,7 @@ public class SkiaEditor : SkiaView typeof(float), typeof(SkiaEditor), 1.4f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); /// @@ -132,6 +141,7 @@ public class SkiaEditor : SkiaView typeof(float), typeof(SkiaEditor), 4f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -143,6 +153,7 @@ public class SkiaEditor : SkiaView typeof(float), typeof(SkiaEditor), 12f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); /// @@ -154,6 +165,7 @@ public class SkiaEditor : SkiaView typeof(bool), typeof(SkiaEditor), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// @@ -164,7 +176,8 @@ public class SkiaEditor : SkiaView nameof(MaxLength), typeof(int), typeof(SkiaEditor), - -1); + -1, + BindingMode.TwoWay); /// /// Bindable property for AutoSize. @@ -175,6 +188,7 @@ public class SkiaEditor : SkiaView typeof(bool), typeof(SkiaEditor), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); #endregion diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs index 1c66979..049f2ac 100644 --- a/Views/SkiaEntry.cs +++ b/Views/SkiaEntry.cs @@ -24,7 +24,7 @@ public class SkiaEntry : SkiaView typeof(string), typeof(SkiaEntry), "", - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaEntry)b).OnTextPropertyChanged((string)o, (string)n)); /// diff --git a/Views/SkiaShell.cs b/Views/SkiaShell.cs index 75870c4..4ff629f 100644 --- a/Views/SkiaShell.cs +++ b/Views/SkiaShell.cs @@ -22,7 +22,7 @@ public class SkiaShell : SkiaLayoutView typeof(bool), typeof(SkiaShell), false, - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaShell)b).OnFlyoutIsPresentedChanged((bool)n)); /// @@ -34,6 +34,7 @@ public class SkiaShell : SkiaLayoutView typeof(ShellFlyoutBehavior), typeof(SkiaShell), ShellFlyoutBehavior.Flyout, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); /// @@ -45,6 +46,7 @@ public class SkiaShell : SkiaLayoutView typeof(float), typeof(SkiaShell), 280f, + BindingMode.TwoWay, coerceValue: (b, v) => Math.Max(100f, (float)v), propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); @@ -57,6 +59,19 @@ public class SkiaShell : SkiaLayoutView typeof(SKColor), typeof(SkiaShell), SKColors.White, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + /// + /// Bindable property for FlyoutTextColor. + /// + public static readonly BindableProperty FlyoutTextColorProperty = + BindableProperty.Create( + nameof(FlyoutTextColor), + typeof(SKColor), + typeof(SkiaShell), + new SKColor(33, 33, 33), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); /// @@ -68,6 +83,7 @@ public class SkiaShell : SkiaLayoutView typeof(SKColor), typeof(SkiaShell), new SKColor(33, 150, 243), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); /// @@ -79,6 +95,7 @@ public class SkiaShell : SkiaLayoutView typeof(SKColor), typeof(SkiaShell), SKColors.White, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); /// @@ -90,6 +107,7 @@ public class SkiaShell : SkiaLayoutView typeof(float), typeof(SkiaShell), 56f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); /// @@ -101,6 +119,7 @@ public class SkiaShell : SkiaLayoutView typeof(float), typeof(SkiaShell), 56f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); /// @@ -112,6 +131,7 @@ public class SkiaShell : SkiaLayoutView typeof(bool), typeof(SkiaShell), true, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); /// @@ -123,6 +143,7 @@ public class SkiaShell : SkiaLayoutView typeof(bool), typeof(SkiaShell), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); /// @@ -133,9 +154,22 @@ public class SkiaShell : SkiaLayoutView nameof(ContentPadding), typeof(float), typeof(SkiaShell), - 16f, + 0f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); + /// + /// Bindable property for ContentBackgroundColor. + /// + public static readonly BindableProperty ContentBackgroundColorProperty = + BindableProperty.Create( + nameof(ContentBackgroundColor), + typeof(SKColor), + typeof(SkiaShell), + new SKColor(250, 250, 250), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + /// /// Bindable property for Title. /// @@ -145,6 +179,7 @@ public class SkiaShell : SkiaLayoutView typeof(string), typeof(SkiaShell), string.Empty, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); #endregion @@ -158,6 +193,10 @@ public class SkiaShell : SkiaLayoutView // Navigation stack for push/pop navigation private readonly Stack<(SkiaView Content, string Title)> _navigationStack = new(); + private float _flyoutScrollOffset; + private readonly Dictionary> _registeredRoutes = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _routeTitles = new(StringComparer.OrdinalIgnoreCase); + private void OnFlyoutIsPresentedChanged(bool newValue) { _flyoutAnimationProgress = newValue ? 1f : 0f; @@ -201,6 +240,35 @@ public class SkiaShell : SkiaLayoutView set => SetValue(FlyoutBackgroundColorProperty, value); } + /// + /// Text color in the flyout. + /// + public SKColor FlyoutTextColor + { + get => (SKColor)GetValue(FlyoutTextColorProperty); + set => SetValue(FlyoutTextColorProperty, value); + } + + /// + /// Optional header view in the flyout. + /// + public SkiaView? FlyoutHeaderView { get; set; } + + /// + /// Height of the flyout header. + /// + public float FlyoutHeaderHeight { get; set; } = 140f; + + /// + /// Optional footer text in the flyout. + /// + public string? FlyoutFooterText { get; set; } + + /// + /// Height of the flyout footer. + /// + public float FlyoutFooterHeight { get; set; } = 40f; + /// /// Background color of the navigation bar. /// @@ -257,7 +325,6 @@ public class SkiaShell : SkiaLayoutView /// /// Gets or sets the padding applied to page content. - /// Default is 16 pixels on all sides. /// public float ContentPadding { @@ -265,6 +332,15 @@ public class SkiaShell : SkiaLayoutView set => SetValue(ContentPaddingProperty, value); } + /// + /// Background color of the content area. + /// + public SKColor ContentBackgroundColor + { + get => (SKColor)GetValue(ContentBackgroundColorProperty); + set => SetValue(ContentBackgroundColorProperty, value); + } + /// /// Current title displayed in the navigation bar. /// @@ -404,34 +480,161 @@ public class SkiaShell : SkiaLayoutView /// public void GoToAsync(string route) { - // Simple route parsing - format: "//section/item" + GoToAsync(route, null); + } + + /// + /// Navigates using a URI route with parameters. + /// + public void GoToAsync(string route, IDictionary? parameters) + { if (string.IsNullOrEmpty(route)) return; - var parts = route.TrimStart('/').Split('/'); + string routePath = route; + Dictionary queryParams = new Dictionary(); + int queryIndex = route.IndexOf('?'); + if (queryIndex >= 0) + { + routePath = route.Substring(0, queryIndex); + queryParams = ParseQueryString(route.Substring(queryIndex + 1)); + } + + Dictionary allParams = new Dictionary(); + foreach (var kvp in queryParams) + { + allParams[kvp.Key] = kvp.Value; + } + if (parameters != null) + { + foreach (var kvp in parameters) + { + allParams[kvp.Key] = kvp.Value; + } + } + + var parts = routePath.TrimStart('/').Split('/'); if (parts.Length == 0) return; + // Check registered routes first + if (_registeredRoutes.TryGetValue(routePath.TrimStart('/'), out Func? factory)) + { + var view = factory(); + if (view != null) + { + ApplyQueryParameters(view, allParams); + PushAsync(view, GetRouteTitle(routePath.TrimStart('/'))); + return; + } + } + // Find matching section for (int i = 0; i < _sections.Count; i++) { var section = _sections[i]; - if (section.Route.Equals(parts[0], StringComparison.OrdinalIgnoreCase)) + if (!section.Route.Equals(parts[0], StringComparison.OrdinalIgnoreCase)) + continue; + + if (parts.Length > 1) { - if (parts.Length > 1) + // Find matching item + for (int j = 0; j < section.Items.Count; j++) { - // Find matching item - for (int j = 0; j < section.Items.Count; j++) + if (section.Items[j].Route.Equals(parts[1], StringComparison.OrdinalIgnoreCase)) { - if (section.Items[j].Route.Equals(parts[1], StringComparison.OrdinalIgnoreCase)) + NavigateToSection(i, j); + if (section.Items[j].Content != null && allParams.Count > 0) { - NavigateToSection(i, j); - return; + ApplyQueryParameters(section.Items[j].Content!, allParams); } + return; } } - NavigateToSection(i, 0); - return; + } + NavigateToSection(i); + if (section.Items.Count > 0 && section.Items[0].Content != null && allParams.Count > 0) + { + ApplyQueryParameters(section.Items[0].Content!, allParams); + } + break; + } + } + + private static Dictionary ParseQueryString(string queryString) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrEmpty(queryString)) return result; + + var pairs = queryString.Split('&', StringSplitOptions.RemoveEmptyEntries); + foreach (var pair in pairs) + { + var parts = pair.Split('=', 2); + if (parts.Length == 2) + { + result[Uri.UnescapeDataString(parts[0])] = Uri.UnescapeDataString(parts[1]); + } + else if (parts.Length == 1) + { + result[Uri.UnescapeDataString(parts[0])] = string.Empty; } } + return result; + } + + private static void ApplyQueryParameters(SkiaView content, IDictionary parameters) + { + if (parameters.Count == 0) return; + + if (content is ISkiaQueryAttributable attributable) + { + attributable.ApplyQueryAttributes(parameters); + } + + var type = content.GetType(); + foreach (var param in parameters) + { + var prop = type.GetProperty(param.Key, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + if (prop != null && prop.CanWrite) + { + try + { + var value = Convert.ChangeType(param.Value, prop.PropertyType); + prop.SetValue(content, value); + } + catch { } + } + } + } + + /// + /// Registers a route with a content factory. + /// + public void RegisterRoute(string route, Func contentFactory, string? title = null) + { + var key = route.TrimStart('/'); + _registeredRoutes[key] = contentFactory; + if (!string.IsNullOrEmpty(title)) + { + _routeTitles[key] = title; + } + } + + /// + /// Unregisters a route. + /// + public void UnregisterRoute(string route) + { + var key = route.TrimStart('/'); + _registeredRoutes.Remove(key); + _routeTitles.Remove(key); + } + + private string GetRouteTitle(string route) + { + if (_routeTitles.TryGetValue(route, out string? title)) + { + return title; + } + return route.Split('/').LastOrDefault() ?? route; } /// @@ -884,6 +1087,32 @@ public class SkiaShell : SkiaLayoutView base.OnPointerPressed(e); } + + public override void OnScroll(ScrollEventArgs e) + { + if (FlyoutIsPresented && _flyoutAnimationProgress > 0) + { + float flyoutX = Bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress); + var flyoutBounds = new SKRect(flyoutX, Bounds.Top, flyoutX + FlyoutWidth, Bounds.Bottom); + + if (flyoutBounds.Contains(e.X, e.Y)) + { + float headerHeight = FlyoutHeaderView != null ? FlyoutHeaderHeight : 0f; + float footerHeight = !string.IsNullOrEmpty(FlyoutFooterText) ? FlyoutFooterHeight : 0f; + float itemHeight = 48f; + float totalItemsHeight = _sections.Count * itemHeight; + float viewableHeight = flyoutBounds.Height - headerHeight - footerHeight; + float maxScroll = Math.Max(0f, totalItemsHeight - viewableHeight); + + _flyoutScrollOffset -= e.DeltaY * 30f; + _flyoutScrollOffset = Math.Max(0f, Math.Min(_flyoutScrollOffset, maxScroll)); + Invalidate(); + e.Handled = true; + return; + } + } + base.OnScroll(e); + } } /// diff --git a/Views/SkiaWebView.cs b/Views/SkiaWebView.cs index b8c6860..d4abfe1 100644 --- a/Views/SkiaWebView.cs +++ b/Views/SkiaWebView.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using SkiaSharp; @@ -8,11 +10,64 @@ namespace Microsoft.Maui.Platform; /// /// WebView implementation using WebKitGTK for Linux. -/// Renders web content in a native GTK window and composites to Skia. +/// Renders web content in a native GTK window embedded via X11. /// public class SkiaWebView : SkiaView { - #region Native Interop - GTK + #region Delegates + + private delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData); + 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 delegate void WebKitSettingsSetHardwareAccelerationPolicyDelegate(IntPtr settings, int policy); + private delegate void WebKitSettingsSetEnableWebglDelegate(IntPtr settings, bool enabled); + + #endregion + + #region X11 Structures + + private struct XWindowAttributes + { + public int x; + public int y; + public int width; + public int height; + public int border_width; + public int depth; + public IntPtr visual; + public IntPtr root; + public int c_class; + public int bit_gravity; + public int win_gravity; + public int backing_store; + public ulong backing_planes; + public ulong backing_pixel; + public int save_under; + public IntPtr colormap; + public int map_installed; + public int map_state; + public long all_event_masks; + public long your_event_mask; + public long do_not_propagate_mask; + public int override_redirect; + public IntPtr screen; + } + + #endregion + + #region Constants private const string LibGtk4 = "libgtk-4.so.1"; private const string LibGtk3 = "libgtk-3.so.0"; @@ -20,12 +75,71 @@ public class SkiaWebView : SkiaView 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 const string LibGdk4 = "libgtk-4.so.1"; + private const string LibGdk3 = "libgdk-3.so.0"; + private const string LibX11 = "libX11.so.6"; + private const int RTLD_NOW = 2; + private const int RTLD_GLOBAL = 256; + + #endregion + + #region Static Fields private static bool _useGtk4; private static bool _gtkInitialized; private static string _webkitLib = LibWebKit2Gtk3; + private static LoadChangedCallback? _loadChangedCallback; + private static IntPtr _webkitHandle; + private static IntPtr _mainDisplay; + private static IntPtr _mainWindow; + + 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; + private static WebKitSettingsSetHardwareAccelerationPolicyDelegate? _webkitSetHardwareAcceleration; + private static WebKitSettingsSetEnableWebglDelegate? _webkitSetWebgl; + + private static readonly List _activeWebViews = new(); + private static readonly Dictionary _webViewInstances = new(); + + #endregion + + #region Instance Fields + + private IntPtr _gtkWindow; + private IntPtr _webView; + private IntPtr _gtkX11Window; + private IntPtr _x11Container; + private string _source = ""; + private string _html = ""; + private bool _isInitialized; + private bool _isEmbedded; + private bool _isProperlyReparented; + private bool _javascriptEnabled = true; + private double _loadProgress; + private SKRect _lastBounds; + private int _lastMainX; + private int _lastMainY; + private int _lastPosX; + private int _lastPosY; + private int _lastWidth; + private int _lastHeight; + + #endregion + + #region GTK Native Imports - // GTK functions [DllImport(LibGtk4, EntryPoint = "gtk_init")] private static extern void gtk4_init(); @@ -41,9 +155,15 @@ public class SkiaWebView : SkiaView [DllImport(LibGtk4, EntryPoint = "gtk_window_set_default_size")] private static extern void gtk4_window_set_default_size(IntPtr window, int width, int height); + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_title")] + private static extern void gtk4_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_default_size")] private static extern void gtk3_window_set_default_size(IntPtr window, int width, int height); + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_title")] + private static extern void gtk3_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_child")] private static extern void gtk4_window_set_child(IntPtr window, IntPtr child); @@ -53,6 +173,9 @@ public class SkiaWebView : SkiaView [DllImport(LibGtk4, EntryPoint = "gtk_widget_show")] private static extern void gtk4_widget_show(IntPtr widget); + [DllImport(LibGtk4, EntryPoint = "gtk_window_present")] + private static extern void gtk4_window_present(IntPtr window); + [DllImport(LibGtk3, EntryPoint = "gtk_widget_show_all")] private static extern void gtk3_widget_show_all(IntPtr widget); @@ -68,53 +191,180 @@ public class SkiaWebView : SkiaView [DllImport(LibGtk4, EntryPoint = "gtk_widget_get_height")] private static extern int gtk4_widget_get_height(IntPtr widget); - // GObject - [DllImport(LibGObject, EntryPoint = "g_object_unref")] + [DllImport(LibGObject)] 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); + [DllImport(LibGObject)] + 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")] + [DllImport(LibGtk4, EntryPoint = "gtk_native_get_surface")] + private static extern IntPtr gtk4_native_get_surface(IntPtr native); + + [DllImport(LibGtk4, EntryPoint = "gdk_x11_surface_get_xid")] + private static extern IntPtr gdk4_x11_surface_get_xid(IntPtr surface); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_get_window")] + private static extern IntPtr gtk3_widget_get_window(IntPtr widget); + + [DllImport(LibGdk3, EntryPoint = "gdk_x11_window_get_xid")] + private static extern IntPtr gdk3_x11_window_get_xid(IntPtr gdkWindow); + + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_decorated")] + private static extern void gtk4_window_set_decorated(IntPtr window, bool decorated); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_decorated")] + private static extern void gtk3_window_set_decorated(IntPtr window, bool decorated); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_move")] + private static extern void gtk3_window_move(IntPtr window, int x, int y); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_resize")] + private static extern void gtk3_window_resize(IntPtr window, int width, int height); + + [DllImport(LibGdk3, EntryPoint = "gdk_window_move_resize")] + private static extern void gdk3_window_move_resize(IntPtr window, int x, int y, int width, int height); + + [DllImport(LibGdk3, EntryPoint = "gdk_window_move")] + private static extern void gdk3_gdk_window_move(IntPtr window, int x, int y); + + [DllImport(LibGdk3, EntryPoint = "gdk_window_set_override_redirect")] + private static extern void gdk3_window_set_override_redirect(IntPtr window, bool override_redirect); + + [DllImport(LibGdk3, EntryPoint = "gdk_window_set_transient_for")] + private static extern void gdk3_window_set_transient_for(IntPtr window, IntPtr parent); + + [DllImport(LibGdk3, EntryPoint = "gdk_window_raise")] + private static extern void gdk3_window_raise(IntPtr window); + + [DllImport(LibGdk3, EntryPoint = "gdk_x11_window_foreign_new_for_display")] + private static extern IntPtr gdk3_x11_window_foreign_new_for_display(IntPtr display, IntPtr window); + + [DllImport(LibGdk3, EntryPoint = "gdk_display_get_default")] + private static extern IntPtr gdk3_display_get_default(); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_set_parent_window")] + private static extern void gtk3_widget_set_parent_window(IntPtr widget, IntPtr parentWindow); + + [DllImport(LibGtk3, EntryPoint = "gtk_socket_new")] + private static extern IntPtr gtk3_socket_new(); + + [DllImport(LibGtk3, EntryPoint = "gtk_socket_add_id")] + private static extern void gtk3_socket_add_id(IntPtr socket, IntPtr windowId); + + [DllImport(LibGtk3, EntryPoint = "gtk_socket_get_id")] + private static extern IntPtr gtk3_socket_get_id(IntPtr socket); + + [DllImport(LibGtk3, EntryPoint = "gtk_plug_new")] + private static extern IntPtr gtk3_plug_new(IntPtr socketId); + + [DllImport(LibGtk3, EntryPoint = "gtk_plug_get_id")] + private static extern IntPtr gtk3_plug_get_id(IntPtr plug); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_skip_taskbar_hint")] + private static extern void gtk3_window_set_skip_taskbar_hint(IntPtr window, bool setting); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_skip_pager_hint")] + private static extern void gtk3_window_set_skip_pager_hint(IntPtr window, bool setting); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_type_hint")] + private static extern void gtk3_window_set_type_hint(IntPtr window, int hint); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_present")] + private static extern void gtk3_window_present(IntPtr window); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_queue_draw")] + private static extern void gtk3_widget_queue_draw(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_keep_above")] + private static extern void gtk3_window_set_keep_above(IntPtr window, bool setting); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_set_hexpand")] + private static extern void gtk3_widget_set_hexpand(IntPtr widget, bool expand); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_set_vexpand")] + private static extern void gtk3_widget_set_vexpand(IntPtr widget, bool expand); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_set_size_request")] + private static extern void gtk3_widget_set_size_request(IntPtr widget, int width, int height); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_realize")] + private static extern void gtk3_widget_realize(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_map")] + private static extern void gtk3_widget_map(IntPtr widget); + + [DllImport(LibGLib)] private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); + [DllImport(LibGtk3, EntryPoint = "gtk_events_pending")] + private static extern bool gtk3_events_pending(); + + [DllImport(LibGtk3, EntryPoint = "gtk_main_iteration")] + private static extern void gtk3_main_iteration(); + + [DllImport(LibGtk3, EntryPoint = "gtk_main_iteration_do")] + private static extern bool gtk3_main_iteration_do(bool blocking); + #endregion - #region WebKit Functions + #region X11 Native Imports - // 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); + [DllImport(LibX11)] + private static extern int XReparentWindow(IntPtr display, IntPtr window, IntPtr parent, int x, int y); - 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(LibX11)] + private static extern int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height); + + [DllImport(LibX11)] + private static extern int XMapWindow(IntPtr display, IntPtr window); + + [DllImport(LibX11)] + private static extern int XUnmapWindow(IntPtr display, IntPtr window); + + [DllImport(LibX11)] + private static extern int XFlush(IntPtr display); + + [DllImport(LibX11)] + private static extern int XRaiseWindow(IntPtr display, IntPtr window); + + [DllImport(LibX11)] + private static extern IntPtr XCreateSimpleWindow(IntPtr display, IntPtr parent, int x, int y, uint width, uint height, uint borderWidth, ulong border, ulong background); + + [DllImport(LibX11)] + private static extern int XDestroyWindow(IntPtr display, IntPtr window); + + [DllImport(LibX11)] + private static extern int XSelectInput(IntPtr display, IntPtr window, long eventMask); + + [DllImport(LibX11)] + private static extern int XSync(IntPtr display, bool discard); + + [DllImport(LibX11)] + private static extern bool XQueryTree(IntPtr display, IntPtr window, ref IntPtr root, ref IntPtr parent, ref IntPtr children, ref uint nchildren); + + [DllImport(LibX11)] + private static extern int XFree(IntPtr data); + + [DllImport(LibX11)] + private static extern int XMapRaised(IntPtr display, IntPtr window); + + [DllImport(LibX11)] + private static extern int XGetWindowAttributes(IntPtr display, IntPtr window, out XWindowAttributes attributes); + + [DllImport(LibX11)] + private static extern bool XTranslateCoordinates(IntPtr display, IntPtr src, IntPtr dest, int srcX, int srcY, out int destX, out int destY, out IntPtr child); + + [DllImport(LibX11)] + private static extern IntPtr XDefaultRootWindow(IntPtr display); + + [DllImport(LibX11)] + private static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists); + + [DllImport(LibX11)] + private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, IntPtr data, int nelements); + + [DllImport(LibX11)] + private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, IntPtr[] data, int nelements); [DllImport("libdl.so.2")] private static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string? filename, int flags); @@ -125,111 +375,46 @@ public class SkiaWebView : SkiaView [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; - private static IntPtr _mainDisplay; - private static IntPtr _mainWindow; - private static readonly HashSet _activeWebViews = new(); - - /// - /// Sets the main window for WebView operations. - /// - public static void SetMainWindow(IntPtr display, IntPtr window) - { - _mainDisplay = display; - _mainWindow = window; - Console.WriteLine($"[WebView] Main window set: display={display}, window={window}"); - } - - /// - /// Processes pending GTK events for WebViews. - /// - public static void ProcessGtkEvents() - { - bool hasActiveWebViews; - lock (_activeWebViews) - { - hasActiveWebViews = _activeWebViews.Count > 0; - } - if (hasActiveWebViews && _gtkInitialized) - { - while (g_main_context_iteration(IntPtr.Zero, mayBlock: false)) - { - } - } - } - - #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) + if (_source == value) return; + _source = value; + if (!string.IsNullOrEmpty(value)) { - _source = value; - if (_isInitialized && !string.IsNullOrEmpty(value)) - { - LoadUrl(value); - } - Invalidate(); + if (!_isInitialized) Initialize(); + if (_isInitialized) LoadUrl(value); } + Invalidate(); } } - /// - /// Gets or sets the HTML content to display. - /// public string Html { get => _html; set { - if (_html != value) + if (_html == value) return; + _html = value; + if (!string.IsNullOrEmpty(value)) { - _html = value; - if (_isInitialized && !string.IsNullOrEmpty(value)) - { - LoadHtml(value); - } - Invalidate(); + if (!_isInitialized) Initialize(); + if (_isInitialized) LoadHtml(value); } + Invalidate(); } } - /// - /// Gets whether the WebView can navigate back. - /// - public bool CanGoBack => _webView != IntPtr.Zero && _webkitCanGoBack?.Invoke(_webView) == true; + public bool CanGoBack => _webView != IntPtr.Zero && (_webkitCanGoBack?.Invoke(_webView) ?? false); - /// - /// Gets whether the WebView can navigate forward. - /// - public bool CanGoForward => _webView != IntPtr.Zero && _webkitCanGoForward?.Invoke(_webView) == true; + public bool CanGoForward => _webView != IntPtr.Zero && (_webkitCanGoForward?.Invoke(_webView) ?? false); - /// - /// Gets the current URL. - /// public string? CurrentUrl { get @@ -240,9 +425,6 @@ public class SkiaWebView : SkiaView } } - /// - /// Gets the current page title. - /// public string? Title { get @@ -253,9 +435,6 @@ public class SkiaWebView : SkiaView } } - /// - /// Gets or sets whether JavaScript is enabled. - /// public bool JavaScriptEnabled { get => _javascriptEnabled; @@ -266,14 +445,8 @@ public class SkiaWebView : SkiaView } } - /// - /// 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 @@ -287,47 +460,54 @@ public class SkiaWebView : SkiaView #endregion - #region Constructor + #region Static Methods - public SkiaWebView() + public static void SetMainWindow(IntPtr display, IntPtr window) { - RequestedWidth = 400; - RequestedHeight = 300; - BackgroundColor = SKColors.White; + _mainDisplay = display; + _mainWindow = window; + Console.WriteLine($"[WebView] Main window set: display={display}, window={window}"); } - #endregion - - #region Initialization + public static void ProcessGtkEvents() + { + bool hasActive; + lock (_activeWebViews) + { + hasActive = _activeWebViews.Count > 0; + } + if (hasActive && _gtkInitialized) + { + while (g_main_context_iteration(IntPtr.Zero, mayBlock: false)) { } + } + } private static bool InitializeWebKit() { if (_webkitHandle != IntPtr.Zero) return true; - // Try WebKitGTK 6.0 (GTK4) first - _webkitHandle = dlopen(LibWebKit2Gtk4, RTLD_NOW | RTLD_GLOBAL); + _webkitHandle = dlopen("libwebkit2gtk-4.1.so.0", RTLD_NOW | RTLD_GLOBAL); if (_webkitHandle != IntPtr.Zero) { - _useGtk4 = true; - _webkitLib = LibWebKit2Gtk4; + _useGtk4 = false; + _webkitLib = "libwebkit2gtk-4.1.so.0"; } else { - // Fall back to WebKitGTK 4.1 (GTK3) - _webkitHandle = dlopen(LibWebKit2Gtk3, RTLD_NOW | RTLD_GLOBAL); + _webkitHandle = dlopen("libwebkit2gtk-4.0.so.37", RTLD_NOW | RTLD_GLOBAL); if (_webkitHandle != IntPtr.Zero) { _useGtk4 = false; - _webkitLib = LibWebKit2Gtk3; + _webkitLib = "libwebkit2gtk-4.0.so.37"; } else { - // Try older WebKitGTK 4.0 - _webkitHandle = dlopen("libwebkit2gtk-4.0.so.37", RTLD_NOW | RTLD_GLOBAL); + _webkitHandle = dlopen("libwebkitgtk-6.0.so.4", RTLD_NOW | RTLD_GLOBAL); if (_webkitHandle != IntPtr.Zero) { - _useGtk4 = false; - _webkitLib = "libwebkit2gtk-4.0.so.37"; + _useGtk4 = true; + _webkitLib = "libwebkitgtk-6.0.so.4"; + Console.WriteLine("[WebView] Warning: Using GTK4 WebKitGTK - embedding may be limited"); } } } @@ -338,7 +518,6 @@ public class SkiaWebView : SkiaView 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"); @@ -353,6 +532,8 @@ public class SkiaWebView : SkiaView _webkitGetProgress = LoadFunction("webkit_web_view_get_estimated_load_progress"); _webkitGetSettings = LoadFunction("webkit_web_view_get_settings"); _webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript"); + _webkitSetHardwareAcceleration = LoadFunction("webkit_settings_set_hardware_acceleration_policy"); + _webkitSetWebgl = LoadFunction("webkit_settings_set_enable_webgl"); Console.WriteLine($"[WebView] Using {_webkitLib}"); return _webkitWebViewNew != null; @@ -361,20 +542,70 @@ public class SkiaWebView : SkiaView private static T? LoadFunction(string name) where T : Delegate { var ptr = dlsym(_webkitHandle, name); - if (ptr == IntPtr.Zero) return null; - return Marshal.GetDelegateForFunctionPointer(ptr); + return ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr); } + private static void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData) + { + string[] events = { "STARTED", "REDIRECTED", "COMMITTED", "FINISHED" }; + string eventName = loadEvent >= 0 && loadEvent < events.Length ? events[loadEvent] : loadEvent.ToString(); + Console.WriteLine($"[WebView] Load event: {eventName}"); + + if (!_webViewInstances.TryGetValue(webView, out var instance)) return; + + string url = instance.Source ?? ""; + if (_webkitGetUri != null) + { + var ptr = _webkitGetUri(webView); + if (ptr != IntPtr.Zero) + { + url = Marshal.PtrToStringAnsi(ptr) ?? ""; + } + } + + switch (loadEvent) + { + case 0: // STARTED + instance.Navigating?.Invoke(instance, new WebNavigatingEventArgs(url)); + break; + case 3: // FINISHED + instance.Navigated?.Invoke(instance, new WebNavigatedEventArgs(url, true)); + break; + } + } + + #endregion + + #region Constructor + + public SkiaWebView() + { + RequestedWidth = 400.0; + RequestedHeight = 300.0; + BackgroundColor = SKColors.White; + } + + #endregion + + #region Initialization + private void Initialize() { - if (_isInitialized) return; - if (!InitializeWebKit()) return; + if (_isInitialized || !InitializeWebKit()) return; try { - // Initialize GTK if needed if (!_gtkInitialized) { + Environment.SetEnvironmentVariable("GDK_BACKEND", "x11"); + Environment.SetEnvironmentVariable("LIBGL_ALWAYS_SOFTWARE", "1"); + Environment.SetEnvironmentVariable("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + Console.WriteLine("[WebView] Using X11 backend with software rendering for proper positioning"); + + var waylandDisplay = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"); + Console.WriteLine($"[WebView] XDG_RUNTIME_DIR: {Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR")}"); + Console.WriteLine($"[WebView] Forcing X11: GDK_BACKEND=x11, WAYLAND_DISPLAY={waylandDisplay}, XDG_SESSION_TYPE=x11"); + if (_useGtk4) { gtk4_init(); @@ -383,12 +614,17 @@ public class SkiaWebView : SkiaView { int argc = 0; IntPtr argv = IntPtr.Zero; - gtk3_init_check(ref argc, ref argv); + if (!gtk3_init_check(ref argc, ref argv)) + { + Console.WriteLine("[WebView] gtk3_init_check failed!"); + } } _gtkInitialized = true; + + var gdkDisplay = gdk3_display_get_default(); + Console.WriteLine($"[WebView] GDK display: {gdkDisplay}"); } - // Create WebKit view _webView = _webkitWebViewNew!(); if (_webView == IntPtr.Zero) { @@ -396,24 +632,47 @@ public class SkiaWebView : SkiaView return; } - // Create GTK window to host the WebView + _webViewInstances[_webView] = this; + _loadChangedCallback = OnLoadChanged; + var callbackPtr = Marshal.GetFunctionPointerForDelegate(_loadChangedCallback); + g_signal_connect_data(_webView, "load-changed", callbackPtr, IntPtr.Zero, IntPtr.Zero, 0); + Console.WriteLine("[WebView] Connected to load-changed signal"); + + int width = Math.Max(800, (int)RequestedWidth); + int height = Math.Max(600, (int)RequestedHeight); + if (_useGtk4) { _gtkWindow = gtk4_window_new(); - gtk4_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight); + gtk4_window_set_title(_gtkWindow, "OpenMaui WebView"); + gtk4_window_set_default_size(_gtkWindow, width, height); gtk4_window_set_child(_gtkWindow, _webView); + Console.WriteLine($"[WebView] GTK4 window created: {width}x{height}"); } else { - _gtkWindow = gtk3_window_new(0); // GTK_WINDOW_TOPLEVEL - gtk3_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight); + _gtkWindow = gtk3_window_new(0); + gtk3_window_set_default_size(_gtkWindow, width, height); + gtk3_window_set_title(_gtkWindow, "WebViewDemo"); + gtk3_widget_set_hexpand(_webView, true); + gtk3_widget_set_vexpand(_webView, true); + gtk3_widget_set_size_request(_webView, width, height); gtk3_container_add(_gtkWindow, _webView); + Console.WriteLine($"[WebView] GTK3 TOPLEVEL window created: {width}x{height}"); } + ConfigureWebKitSettings(); UpdateJavaScriptSetting(); _isInitialized = true; - // Load initial content + lock (_activeWebViews) + { + if (!_activeWebViews.Contains(this)) + { + _activeWebViews.Add(this); + } + } + if (!string.IsNullOrEmpty(_source)) { LoadUrl(_source); @@ -431,6 +690,56 @@ public class SkiaWebView : SkiaView } } + private void ConfigureWebKitSettings() + { + if (_webView == IntPtr.Zero) return; + + try + { + if (_webkitGetSettings == null) return; + + var settings = _webkitGetSettings(_webView); + if (settings == IntPtr.Zero) + { + Console.WriteLine("[WebView] Could not get WebKit settings"); + return; + } + + if (_webkitSetHardwareAcceleration != null) + { + _webkitSetHardwareAcceleration(settings, 2); // NEVER + Console.WriteLine("[WebView] Set hardware acceleration to NEVER (software rendering)"); + } + else + { + Console.WriteLine("[WebView] Warning: Could not set hardware acceleration policy"); + } + + if (_webkitSetWebgl != null) + { + _webkitSetWebgl(settings, false); + Console.WriteLine("[WebView] Disabled WebGL"); + } + + Console.WriteLine("[WebView] WebKit settings configured successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[WebView] Failed to configure settings: {ex.Message}"); + } + } + + 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 Navigation @@ -438,18 +747,28 @@ public class SkiaWebView : SkiaView public void LoadUrl(string url) { if (!_isInitialized) Initialize(); - if (_webView == IntPtr.Zero || _webkitLoadUri == null) return; - - Navigating?.Invoke(this, new WebNavigatingEventArgs(url)); - _webkitLoadUri(_webView, url); + if (_webView != IntPtr.Zero && _webkitLoadUri != null) + { + Navigating?.Invoke(this, new WebNavigatingEventArgs(url)); + _webkitLoadUri(_webView, url); + Console.WriteLine($"[WebView] URL loaded: {url}"); + ShowNativeWindow(); + } } public void LoadHtml(string html, string? baseUrl = null) { + Console.WriteLine($"[WebView] LoadHtml called, html length: {html?.Length ?? 0}"); if (!_isInitialized) Initialize(); - if (_webView == IntPtr.Zero || _webkitLoadHtml == null) return; - + if (_webView == IntPtr.Zero || _webkitLoadHtml == null) + { + Console.WriteLine("[WebView] Cannot load HTML - not initialized or no webkit function"); + return; + } + Console.WriteLine("[WebView] Calling webkit_web_view_load_html..."); _webkitLoadHtml(_webView, html, baseUrl); + Console.WriteLine("[WebView] HTML loaded to WebKit"); + ShowNativeWindow(); } public void GoBack() @@ -470,49 +789,27 @@ public class SkiaWebView : SkiaView public void Reload() { - if (_webView != IntPtr.Zero) - { - _webkitReload?.Invoke(_webView); - } + _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); - } + _webkitStopLoading?.Invoke(_webView); } #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); + g_main_context_iteration(IntPtr.Zero, mayBlock: false); - // Update progress if (_webView != IntPtr.Zero && _webkitGetProgress != null) { - var progress = _webkitGetProgress(_webView); + double progress = _webkitGetProgress(_webView); if (Math.Abs(progress - _loadProgress) > 0.01) { _loadProgress = progress; @@ -521,27 +818,398 @@ public class SkiaWebView : SkiaView } } - /// - /// Show the native WebView window (for testing/debugging). - /// + #endregion + + #region Window Management + + private bool CreateX11Container() + { + if (_mainDisplay == IntPtr.Zero || _mainWindow == IntPtr.Zero) + { + Console.WriteLine("[WebView] Cannot create X11 container - main window not set"); + return false; + } + + if (_x11Container != IntPtr.Zero) + { + Console.WriteLine("[WebView] X11 container already exists"); + return true; + } + + try + { + int x = (int)Bounds.Left; + int y = (int)Bounds.Top; + uint width = Math.Max(100u, (uint)Bounds.Width); + uint height = Math.Max(100u, (uint)Bounds.Height); + + if (width < 100) width = 780; + if (height < 100) height = 300; + + Console.WriteLine($"[WebView] Creating X11 container at ({x}, {y}), size ({width}x{height})"); + + _x11Container = XCreateSimpleWindow(_mainDisplay, _mainWindow, x, y, width, height, 0, 0, 0xFFFFFF); + if (_x11Container == IntPtr.Zero) + { + Console.WriteLine("[WebView] Failed to create X11 container window"); + return false; + } + + Console.WriteLine($"[WebView] Created X11 container: {_x11Container.ToInt64()}"); + XMapWindow(_mainDisplay, _x11Container); + XFlush(_mainDisplay); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"[WebView] Error creating X11 container: {ex.Message}"); + return false; + } + } + public void ShowNativeWindow() { if (!_isInitialized) Initialize(); if (_gtkWindow == IntPtr.Zero) return; + Console.WriteLine("[WebView] Showing native GTK window..."); + + if (!_useGtk4) + { + gtk3_window_set_decorated(_gtkWindow, false); + gtk3_window_set_skip_taskbar_hint(_gtkWindow, true); + gtk3_window_set_skip_pager_hint(_gtkWindow, true); + gtk3_window_set_keep_above(_gtkWindow, true); + gtk3_window_set_type_hint(_gtkWindow, 5); // UTILITY + } + if (_useGtk4) { gtk4_widget_show(_gtkWindow); + gtk4_window_present(_gtkWindow); } else { gtk3_widget_show_all(_gtkWindow); } + + for (int i = 0; i < 100; i++) + { + while (gtk3_events_pending()) + { + gtk3_main_iteration_do(false); + } + } + + TryReparentIntoMainWindow(); + _isEmbedded = true; + Console.WriteLine("[WebView] Native window shown"); + } + + private void TryReparentIntoMainWindow() + { + if (_mainDisplay == IntPtr.Zero || _mainWindow == IntPtr.Zero) + { + Console.WriteLine("[WebView] Cannot setup - main window not set"); + return; + } + + var gdkWindow = gtk3_widget_get_window(_gtkWindow); + if (gdkWindow != IntPtr.Zero) + { + _gtkX11Window = gdk3_x11_window_get_xid(gdkWindow); + Console.WriteLine($"[WebView] GTK X11 window: {_gtkX11Window}"); + } + + PositionUsingGtk(); + } + + private void PositionUsingGtk() + { + if (_gtkWindow == IntPtr.Zero || _mainDisplay == IntPtr.Zero) return; + + int destX = 0, destY = 0; + try + { + var root = XDefaultRootWindow(_mainDisplay); + XTranslateCoordinates(_mainDisplay, _mainWindow, root, 0, 0, out destX, out destY, out _); + } + catch + { + destX = 0; + destY = 0; + } + + int screenX = destX + (int)Bounds.Left; + int screenY = destY + (int)Bounds.Top; + int width = Math.Max(100, (int)Bounds.Width); + int height = Math.Max(100, (int)Bounds.Height); + + Console.WriteLine($"[WebView] Position: screen=({screenX}, {screenY}), size ({width}x{height}), bounds=({Bounds.Left},{Bounds.Top})"); + + if (!_useGtk4) + { + gtk3_window_move(_gtkWindow, screenX, screenY); + gtk3_window_resize(_gtkWindow, width, height); + + while (gtk3_events_pending()) + { + gtk3_main_iteration_do(false); + } + + if (_gtkX11Window != IntPtr.Zero) + { + XRaiseWindow(_mainDisplay, _gtkX11Window); + SetWindowAlwaysOnTop(_gtkX11Window); + XFlush(_mainDisplay); + } + } + else + { + gtk4_window_set_default_size(_gtkWindow, width, height); + } + } + + private void PositionWithX11() + { + if (_gtkX11Window == IntPtr.Zero || _mainDisplay == IntPtr.Zero) return; + + int destX = 0, destY = 0; + try + { + var root = XDefaultRootWindow(_mainDisplay); + XTranslateCoordinates(_mainDisplay, _mainWindow, root, 0, 0, out destX, out destY, out _); + } + catch { } + + int x = destX + (int)Bounds.Left; + int y = destY + (int)Bounds.Top; + uint width = (uint)Math.Max(100f, Bounds.Width > 10f ? Bounds.Width : 780f); + uint height = (uint)Math.Max(100f, Bounds.Height > 10f ? Bounds.Height : 300f); + + XMoveResizeWindow(_mainDisplay, _gtkX11Window, x, y, width, height); + XRaiseWindow(_mainDisplay, _gtkX11Window); + SetWindowAlwaysOnTop(_gtkX11Window); + XFlush(_mainDisplay); + + gtk3_widget_queue_draw(_webView); + + for (int i = 0; i < 5; i++) + { + g_main_context_iteration(IntPtr.Zero, mayBlock: false); + } + } + + private void SetWindowAlwaysOnTop(IntPtr window) + { + try + { + var wmState = XInternAtom(_mainDisplay, "_NET_WM_STATE", false); + var wmStateAbove = XInternAtom(_mainDisplay, "_NET_WM_STATE_ABOVE", false); + var atomType = XInternAtom(_mainDisplay, "ATOM", false); + IntPtr[] data = { wmStateAbove }; + XChangeProperty(_mainDisplay, window, wmState, atomType, 32, 0, data, 1); + } + catch { } + } + + private void EnableOverlayMode() + { + if (_gtkWindow == IntPtr.Zero || _useGtk4) return; + + try + { + gtk3_window_set_type_hint(_gtkWindow, 5); // UTILITY + gtk3_window_set_skip_taskbar_hint(_gtkWindow, true); + gtk3_window_set_skip_pager_hint(_gtkWindow, true); + gtk3_window_set_keep_above(_gtkWindow, true); + gtk3_window_set_decorated(_gtkWindow, false); + Console.WriteLine("[WebView] Overlay mode enabled with UTILITY hint"); + } + catch (Exception ex) + { + Console.WriteLine($"[WebView] Failed to enable overlay mode: {ex.Message}"); + } + } + + private void SetupEmbedding() + { + if (_mainDisplay == IntPtr.Zero || _mainWindow == IntPtr.Zero) + { + Console.WriteLine("[WebView] Cannot setup embedding - main window not set"); + return; + } + + GetWindowPosition(_mainDisplay, _mainWindow, out int x, out int y); + int screenX = x + (int)Bounds.Left; + int screenY = y + (int)Bounds.Top; + int width = Math.Max(100, (int)Bounds.Width); + int height = Math.Max(100, (int)Bounds.Height); + + Console.WriteLine($"[WebView] Initial position: ({screenX}, {screenY}), size ({width}x{height})"); + + if (!_useGtk4) + { + gtk3_window_move(_gtkWindow, screenX, screenY); + gtk3_window_resize(_gtkWindow, width, height); + } + else + { + gtk4_window_set_default_size(_gtkWindow, width, height); + } + + _lastBounds = Bounds; + } + + private void PositionAtScreenCoordinates() + { + if (_gtkWindow == IntPtr.Zero || _mainDisplay == IntPtr.Zero) return; + + int destX = 0, destY = 0; + try + { + var root = XDefaultRootWindow(_mainDisplay); + XTranslateCoordinates(_mainDisplay, _mainWindow, root, 0, 0, out destX, out destY, out _); + } + catch { } + + int offsetX = 0, offsetY = 0; + int screenX = destX + (int)Bounds.Left - offsetX; + int screenY = destY + (int)Bounds.Top - offsetY; + int width = Math.Max(100, (int)Bounds.Width); + int height = Math.Max(100, (int)Bounds.Height); + + if (Math.Abs(screenX - _lastPosX) > 2 || Math.Abs(screenY - _lastPosY) > 2 || + Math.Abs(width - _lastWidth) > 2 || Math.Abs(height - _lastHeight) > 2) + { + Console.WriteLine($"[WebView] Move to ({screenX}, {screenY}), size ({width}x{height}), mainWin=({destX},{destY}), bounds=({Bounds.Left},{Bounds.Top})"); + _lastPosX = screenX; + _lastPosY = screenY; + _lastWidth = width; + _lastHeight = height; + _lastBounds = Bounds; + } + + if (!_useGtk4) + { + gtk3_window_move(_gtkWindow, screenX, screenY); + gtk3_window_resize(_gtkWindow, width, height); + + var gdkWindow = gtk3_widget_get_window(_gtkWindow); + if (gdkWindow != IntPtr.Zero) + { + var xid = gdk3_x11_window_get_xid(gdkWindow); + if (xid != IntPtr.Zero) + { + XRaiseWindow(_mainDisplay, xid); + XFlush(_mainDisplay); + } + } + + while (gtk3_events_pending()) + { + gtk3_main_iteration_do(false); + } + } + else + { + gtk4_window_set_default_size(_gtkWindow, width, height); + } + } + + private IntPtr GetGtkX11Window() + { + if (_gtkWindow == IntPtr.Zero) return IntPtr.Zero; + + for (int i = 0; i < 50; i++) + { + g_main_context_iteration(IntPtr.Zero, mayBlock: false); + } + + if (_useGtk4) + { + var surface = gtk4_native_get_surface(_gtkWindow); + if (surface != IntPtr.Zero) + { + try { return gdk4_x11_surface_get_xid(surface); } + catch { } + } + } + else + { + var gdkWindow = gtk3_widget_get_window(_gtkWindow); + if (gdkWindow != IntPtr.Zero) + { + try { return gdk3_x11_window_get_xid(gdkWindow); } + catch { } + } + } + + return IntPtr.Zero; + } + + private void GetWindowPosition(IntPtr display, IntPtr window, out int x, out int y) + { + x = 0; + y = 0; + try + { + var root = XDefaultRootWindow(display); + if (XTranslateCoordinates(display, window, root, 0, 0, out x, out y, out _)) + { + Console.WriteLine($"[WebView] Main window at screen ({x}, {y})"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[WebView] Failed to get window position: {ex.Message}"); + } + } + + public void UpdateEmbeddedPosition() + { + if (_mainDisplay == IntPtr.Zero) return; + if (Bounds.Width < 10f || Bounds.Height < 10f) return; + + bool boundsChanged = Math.Abs(Bounds.Left - _lastBounds.Left) > 1f || + Math.Abs(Bounds.Top - _lastBounds.Top) > 1f || + Math.Abs(Bounds.Width - _lastBounds.Width) > 1f || + Math.Abs(Bounds.Height - _lastBounds.Height) > 1f; + + if (!boundsChanged) return; + + _lastBounds = Bounds; + int x = (int)Bounds.Left; + int y = (int)Bounds.Top; + uint width = (uint)Math.Max(10f, Bounds.Width); + uint height = (uint)Math.Max(10f, Bounds.Height); + + if (_isProperlyReparented && _gtkX11Window != IntPtr.Zero) + { + Console.WriteLine($"[WebView] UpdateEmbedded (reparented): ({x}, {y}), size ({width}x{height})"); + XMoveResizeWindow(_mainDisplay, _gtkX11Window, x, y, width, height); + XFlush(_mainDisplay); + } + else if (_x11Container != IntPtr.Zero) + { + Console.WriteLine($"[WebView] UpdateEmbedded (container): ({x}, {y}), size ({width}x{height})"); + XMoveResizeWindow(_mainDisplay, _x11Container, x, y, width, height); + if (_gtkX11Window != IntPtr.Zero && _isProperlyReparented) + { + XMoveResizeWindow(_mainDisplay, _gtkX11Window, 0, 0, width, height); + } + else if (_gtkWindow != IntPtr.Zero) + { + PositionAtScreenCoordinates(); + } + XFlush(_mainDisplay); + } + else if (_gtkWindow != IntPtr.Zero) + { + PositionAtScreenCoordinates(); + } } - /// - /// Hide the native WebView window. - /// public void HideNativeWindow() { if (_gtkWindow == IntPtr.Zero) return; @@ -563,12 +1231,41 @@ public class SkiaWebView : SkiaView protected override void OnDraw(SKCanvas canvas, SKRect bounds) { base.OnDraw(canvas, bounds); + Bounds = bounds; - // Draw placeholder/loading state + if (_isInitialized) + { + while (gtk3_events_pending()) + { + gtk3_main_iteration_do(false); + } + + if (_gtkWindow != IntPtr.Zero && _mainDisplay != IntPtr.Zero) + { + bool needsUpdate = Math.Abs(bounds.Left - _lastBounds.Left) > 1f || + Math.Abs(bounds.Top - _lastBounds.Top) > 1f || + Math.Abs(bounds.Width - _lastBounds.Width) > 1f || + Math.Abs(bounds.Height - _lastBounds.Height) > 1f; + + if (!needsUpdate && _lastBounds.Width < 150f && bounds.Width > 150f) + { + needsUpdate = true; + } + + if (needsUpdate && bounds.Width > 50f && bounds.Height > 50f) + { + PositionUsingGtk(); + _lastBounds = bounds; + } + } + } + + if (_isInitialized && _gtkWindow != IntPtr.Zero) return; + + // Draw placeholder when not initialized 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), @@ -577,11 +1274,9 @@ public class SkiaWebView : SkiaView }; canvas.DrawRect(bounds, borderPaint); - // Draw web icon and status - var centerX = bounds.MidX; - var centerY = bounds.MidY; + float midX = bounds.MidX; + float midY = bounds.MidY; - // Globe icon using var iconPaint = new SKPaint { Color = new SKColor(100, 100, 100), @@ -589,11 +1284,10 @@ public class SkiaWebView : SkiaView 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); + canvas.DrawCircle(midX, midY - 20, 25, iconPaint); + canvas.DrawLine(midX - 25, midY - 20, midX + 25, midY - 20, iconPaint); + canvas.DrawArc(new SKRect(midX - 15, midY - 45, midX + 15, midY + 5), 0, 180, false, iconPaint); - // Status text using var textPaint = new SKPaint { Color = new SKColor(80, 80, 80), @@ -619,10 +1313,9 @@ public class SkiaWebView : SkiaView statusText = "WebView (click to open)"; } - var textWidth = textPaint.MeasureText(statusText); - canvas.DrawText(statusText, centerX - textWidth / 2, centerY + 30, textPaint); + float textWidth = textPaint.MeasureText(statusText); + canvas.DrawText(statusText, midX - textWidth / 2, midY + 30, textPaint); - // Draw install hint if not supported if (!IsSupported) { using var hintPaint = new SKPaint @@ -631,19 +1324,18 @@ public class SkiaWebView : SkiaView 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); + string hint = "Install: sudo apt install libwebkit2gtk-4.1-0"; + float hintWidth = hintPaint.MeasureText(hint); + canvas.DrawText(hint, midX - hintWidth / 2, midY + 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; + float 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); @@ -673,6 +1365,16 @@ public class SkiaWebView : SkiaView { if (disposing) { + lock (_activeWebViews) + { + _activeWebViews.Remove(this); + } + + if (_webView != IntPtr.Zero) + { + _webViewInstances.Remove(_webView); + } + if (_gtkWindow != IntPtr.Zero) { if (_useGtk4) @@ -686,7 +1388,15 @@ public class SkiaWebView : SkiaView g_object_unref(_gtkWindow); _gtkWindow = IntPtr.Zero; } + + if (_x11Container != IntPtr.Zero && _mainDisplay != IntPtr.Zero) + { + XDestroyWindow(_mainDisplay, _x11Container); + _x11Container = IntPtr.Zero; + } + _webView = IntPtr.Zero; + _gtkX11Window = IntPtr.Zero; _isInitialized = false; }