diff --git a/Handlers/EntryHandler.cs b/Handlers/EntryHandler.cs index be1d71f..64af349 100644 --- a/Handlers/EntryHandler.cs +++ b/Handlers/EntryHandler.cs @@ -241,6 +241,8 @@ public partial class EntryHandler : ViewHandler if (entry is Entry ve && ve.BackgroundColor != null) { handler.PlatformView.EntryBackgroundColor = ve.BackgroundColor; + // Also set base BackgroundColor so SkiaView.DrawBackground() respects transparency + handler.PlatformView.BackgroundColor = ve.BackgroundColor; } } } diff --git a/Handlers/GtkWebViewHandler.cs b/Handlers/GtkWebViewHandler.cs index 3e3a16a..b9f365e 100644 --- a/Handlers/GtkWebViewHandler.cs +++ b/Handlers/GtkWebViewHandler.cs @@ -3,6 +3,7 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; using Microsoft.Maui.Platform.Linux.Native; using Microsoft.Maui.Platform.Linux.Services; using SkiaSharp; @@ -52,6 +53,7 @@ public class GtkWebViewHandler : ViewHandler { _platformWebView.NavigationStarted += OnNavigationStarted; _platformWebView.NavigationCompleted += OnNavigationCompleted; + _platformWebView.ScriptDialogRequested += OnScriptDialogRequested; } Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready"); } @@ -62,6 +64,7 @@ public class GtkWebViewHandler : ViewHandler { _platformWebView.NavigationStarted -= OnNavigationStarted; _platformWebView.NavigationCompleted -= OnNavigationCompleted; + _platformWebView.ScriptDialogRequested -= OnScriptDialogRequested; UnregisterFromHost(); _platformWebView.Dispose(); _platformWebView = null; @@ -69,6 +72,35 @@ public class GtkWebViewHandler : ViewHandler base.DisconnectHandler(platformView); } + private async void OnScriptDialogRequested(object? sender, + (ScriptDialogType Type, string Message, Action Callback) e) + { + Console.WriteLine($"[GtkWebViewHandler] Script dialog requested: type={e.Type}, message={e.Message}"); + + string title = e.Type switch + { + ScriptDialogType.Alert => "Alert", + ScriptDialogType.Confirm => "Confirm", + ScriptDialogType.Prompt => "Prompt", + _ => "Message" + }; + + string? acceptButton = e.Type == ScriptDialogType.Alert ? "OK" : "OK"; + string? cancelButton = e.Type == ScriptDialogType.Alert ? null : "Cancel"; + + try + { + bool result = await LinuxDialogService.ShowAlertAsync(title, e.Message, acceptButton, cancelButton); + e.Callback(result); + Console.WriteLine($"[GtkWebViewHandler] Dialog result: {result}"); + } + catch (Exception ex) + { + Console.WriteLine($"[GtkWebViewHandler] Error showing dialog: {ex.Message}"); + e.Callback(false); + } + } + private void OnNavigationStarted(object? sender, string uri) { Console.WriteLine($"[GtkWebViewHandler] Navigation started: {uri}"); diff --git a/Handlers/GtkWebViewPlatformView.cs b/Handlers/GtkWebViewPlatformView.cs index 7b5a875..9501222 100644 --- a/Handlers/GtkWebViewPlatformView.cs +++ b/Handlers/GtkWebViewPlatformView.cs @@ -1,8 +1,20 @@ using System; using Microsoft.Maui.Platform.Linux.Native; +using Microsoft.Maui.Platform.Linux.Services; namespace Microsoft.Maui.Platform.Linux.Handlers; +/// +/// Type of JavaScript dialog. +/// +public enum ScriptDialogType +{ + Alert = 0, + Confirm = 1, + Prompt = 2, + BeforeUnloadConfirm = 3 +} + /// /// GTK-based WebView platform view using WebKitGTK. /// Provides web browsing capabilities within MAUI applications. @@ -13,7 +25,10 @@ public sealed class GtkWebViewPlatformView : IDisposable private bool _disposed; private string? _currentUri; private ulong _loadChangedSignalId; + private ulong _scriptDialogSignalId; private WebKitNative.LoadChangedCallback? _loadChangedCallback; + private WebKitNative.ScriptDialogCallback? _scriptDialogCallback; + private EventHandler? _themeChangedHandler; public IntPtr Widget => _widget; public string? CurrentUri => _currentUri; @@ -21,6 +36,7 @@ public sealed class GtkWebViewPlatformView : IDisposable public event EventHandler? NavigationStarted; public event EventHandler<(string Url, bool Success)>? NavigationCompleted; public event EventHandler? TitleChanged; + public event EventHandler<(ScriptDialogType Type, string Message, Action Callback)>? ScriptDialogRequested; public GtkWebViewPlatformView() { @@ -36,9 +52,231 @@ public sealed class GtkWebViewPlatformView : IDisposable WebKitNative.ConfigureSettings(_widget); _loadChangedCallback = OnLoadChanged; _loadChangedSignalId = WebKitNative.ConnectLoadChanged(_widget, _loadChangedCallback); + + // Connect to script-dialog signal to intercept JavaScript alerts/confirms/prompts + _scriptDialogCallback = OnScriptDialog; + _scriptDialogSignalId = WebKitNative.ConnectScriptDialog(_widget, _scriptDialogCallback); + + // Set initial background color based on theme + UpdateBackgroundForTheme(); + + // Subscribe to theme changes to update background color + _themeChangedHandler = (sender, args) => + { + GLibNative.IdleAdd(() => + { + UpdateBackgroundForTheme(); + return false; + }); + }; + if (Microsoft.Maui.Controls.Application.Current != null) + { + Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged += _themeChangedHandler; + } + Console.WriteLine("[GtkWebViewPlatformView] Created WebKitWebView widget"); } + /// + /// Updates the WebView background color based on the current app theme. + /// + public void UpdateBackgroundForTheme() + { + if (_widget == IntPtr.Zero) return; + + var isDark = Microsoft.Maui.Controls.Application.Current?.RequestedTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark; + if (isDark) + { + // Dark theme: use a dark gray background + WebKitNative.SetBackgroundColor(_widget, 0.12, 0.12, 0.12, 1.0); // #1E1E1E + } + else + { + // Light theme: use white background + WebKitNative.SetBackgroundColor(_widget, 1.0, 1.0, 1.0, 1.0); + } + } + + private bool OnScriptDialog(IntPtr webView, IntPtr dialog, IntPtr userData) + { + try + { + var webkitDialogType = WebKitNative.GetScriptDialogType(dialog); + var dialogType = (ScriptDialogType)(int)webkitDialogType; + var message = WebKitNative.GetScriptDialogMessage(dialog) ?? ""; + + Console.WriteLine($"[GtkWebViewPlatformView] Script dialog: type={dialogType}, message={message}"); + + // Get the parent window for proper modal behavior + IntPtr parentWindow = GtkHostService.Instance.HostWindow?.Window ?? IntPtr.Zero; + + // Handle prompt dialogs specially - they need a text entry + if (dialogType == ScriptDialogType.Prompt) + { + return HandlePromptDialog(dialog, message, parentWindow); + } + + // Determine dialog type and buttons based on JavaScript dialog type + int messageType = GtkNative.GTK_MESSAGE_INFO; + int buttons = GtkNative.GTK_BUTTONS_OK; + + switch (dialogType) + { + case ScriptDialogType.Alert: + messageType = GtkNative.GTK_MESSAGE_INFO; + buttons = GtkNative.GTK_BUTTONS_OK; + break; + case ScriptDialogType.Confirm: + case ScriptDialogType.BeforeUnloadConfirm: + messageType = GtkNative.GTK_MESSAGE_QUESTION; + buttons = GtkNative.GTK_BUTTONS_OK_CANCEL; + break; + } + + // Create and show native GTK message dialog + IntPtr gtkDialog = GtkNative.gtk_message_dialog_new( + parentWindow, + GtkNative.GTK_DIALOG_MODAL | GtkNative.GTK_DIALOG_DESTROY_WITH_PARENT, + messageType, + buttons, + message, + IntPtr.Zero); + + if (gtkDialog != IntPtr.Zero) + { + // Set dialog title based on type + string title = dialogType switch + { + ScriptDialogType.Alert => "Alert", + ScriptDialogType.Confirm => "Confirm", + ScriptDialogType.BeforeUnloadConfirm => "Leave Page?", + _ => "Message" + }; + GtkNative.gtk_window_set_title(gtkDialog, title); + + // Make dialog modal to parent if we have a parent + if (parentWindow != IntPtr.Zero) + { + GtkNative.gtk_window_set_transient_for(gtkDialog, parentWindow); + GtkNative.gtk_window_set_modal(gtkDialog, true); + } + + // Run the dialog synchronously - this blocks until user responds + int response = GtkNative.gtk_dialog_run(gtkDialog); + Console.WriteLine($"[GtkWebViewPlatformView] Dialog response: {response}"); + + // Set the confirmed state for confirm dialogs + if (dialogType == ScriptDialogType.Confirm || dialogType == ScriptDialogType.BeforeUnloadConfirm) + { + bool confirmed = response == GtkNative.GTK_RESPONSE_OK || response == GtkNative.GTK_RESPONSE_YES; + WebKitNative.SetScriptDialogConfirmed(dialog, confirmed); + } + + // Clean up + GtkNative.gtk_widget_destroy(gtkDialog); + } + + // Return true to indicate we handled the dialog (prevents WebKitGTK's default) + return true; + } + catch (Exception ex) + { + Console.WriteLine($"[GtkWebViewPlatformView] Error in OnScriptDialog: {ex.Message}"); + // Return false on error to let WebKitGTK try its default handling + return false; + } + } + + private bool HandlePromptDialog(IntPtr webkitDialog, string message, IntPtr parentWindow) + { + try + { + // Get the default text for the prompt + string? defaultText = WebKitNative.GetScriptDialogPromptDefaultText(webkitDialog) ?? ""; + + // Create a custom dialog with OK/Cancel buttons + IntPtr gtkDialog = GtkNative.gtk_dialog_new_with_buttons( + "Prompt", + parentWindow, + GtkNative.GTK_DIALOG_MODAL | GtkNative.GTK_DIALOG_DESTROY_WITH_PARENT, + "_Cancel", + GtkNative.GTK_RESPONSE_CANCEL, + "_OK", + GtkNative.GTK_RESPONSE_OK, + IntPtr.Zero); + + if (gtkDialog == IntPtr.Zero) + { + Console.WriteLine("[GtkWebViewPlatformView] Failed to create prompt dialog"); + return false; + } + + // Get the content area + IntPtr contentArea = GtkNative.gtk_dialog_get_content_area(gtkDialog); + + // Create a vertical box for the content + IntPtr vbox = GtkNative.gtk_box_new(GtkNative.GTK_ORIENTATION_VERTICAL, 10); + GtkNative.gtk_widget_set_margin_start(vbox, 12); + GtkNative.gtk_widget_set_margin_end(vbox, 12); + GtkNative.gtk_widget_set_margin_top(vbox, 12); + GtkNative.gtk_widget_set_margin_bottom(vbox, 12); + + // Add the message label + IntPtr label = GtkNative.gtk_label_new(message); + GtkNative.gtk_box_pack_start(vbox, label, false, false, 0); + + // Add the text entry + IntPtr entry = GtkNative.gtk_entry_new(); + GtkNative.gtk_entry_set_text(entry, defaultText); + GtkNative.gtk_box_pack_start(vbox, entry, false, false, 0); + + // Add the vbox to content area + GtkNative.gtk_box_pack_start(contentArea, vbox, true, true, 0); + + // Make dialog modal + if (parentWindow != IntPtr.Zero) + { + GtkNative.gtk_window_set_transient_for(gtkDialog, parentWindow); + GtkNative.gtk_window_set_modal(gtkDialog, true); + } + + // Show all widgets + GtkNative.gtk_widget_show_all(gtkDialog); + + // Run the dialog + int response = GtkNative.gtk_dialog_run(gtkDialog); + Console.WriteLine($"[GtkWebViewPlatformView] Prompt dialog response: {response}"); + + if (response == GtkNative.GTK_RESPONSE_OK) + { + // Get the text from the entry + IntPtr textPtr = GtkNative.gtk_entry_get_text(entry); + string? enteredText = textPtr != IntPtr.Zero + ? System.Runtime.InteropServices.Marshal.PtrToStringUTF8(textPtr) + : ""; + + Console.WriteLine($"[GtkWebViewPlatformView] Prompt text: {enteredText}"); + + // Set the prompt response + WebKitNative.SetScriptDialogPromptText(webkitDialog, enteredText ?? ""); + } + else + { + // User cancelled - for prompts, not confirming means returning null + // WebKit handles this by not calling prompt_set_text + } + + // Clean up + GtkNative.gtk_widget_destroy(gtkDialog); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"[GtkWebViewPlatformView] Error in HandlePromptDialog: {ex.Message}"); + return false; + } + } + private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData) { try @@ -153,12 +391,22 @@ public sealed class GtkWebViewPlatformView : IDisposable if (!_disposed) { _disposed = true; + + // Unsubscribe from theme changes + if (_themeChangedHandler != null && Microsoft.Maui.Controls.Application.Current != null) + { + Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged -= _themeChangedHandler; + _themeChangedHandler = null; + } + if (_widget != IntPtr.Zero) { WebKitNative.DisconnectLoadChanged(_widget); + WebKitNative.DisconnectScriptDialog(_widget); } _widget = IntPtr.Zero; _loadChangedCallback = null; + _scriptDialogCallback = null; } } } diff --git a/Handlers/ImageButtonHandler.cs b/Handlers/ImageButtonHandler.cs index 00b25b7..e9d1c6d 100644 --- a/Handlers/ImageButtonHandler.cs +++ b/Handlers/ImageButtonHandler.cs @@ -27,6 +27,8 @@ public partial class ImageButtonHandler : ViewHandler CommandMapper = new(ViewHandler.ViewCommandMapper) @@ -184,6 +186,26 @@ public partial class ImageButtonHandler : ViewHandler protected override SkiaShell CreatePlatformView() { + Console.WriteLine("[ShellHandler] CreatePlatformView - creating SkiaShell"); return new SkiaShell(); } protected override void ConnectHandler(SkiaShell platformView) { + Console.WriteLine("[ShellHandler] ConnectHandler - connecting to SkiaShell"); base.ConnectHandler(platformView); platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged; platformView.Navigated += OnNavigated; diff --git a/Handlers/WindowHandler.cs b/Handlers/WindowHandler.cs index f584158..4789167 100644 --- a/Handlers/WindowHandler.cs +++ b/Handlers/WindowHandler.cs @@ -81,13 +81,20 @@ public partial class WindowHandler : ElementHandler public static void MapContent(WindowHandler handler, IWindow window) { + Console.Error.WriteLine($"[WindowHandler] MapContent - PlatformView={handler.PlatformView != null}"); if (handler.PlatformView is null) return; var content = window.Content; + Console.Error.WriteLine($"[WindowHandler] MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}"); if (content?.Handler?.PlatformView is SkiaView skiaContent) { + Console.Error.WriteLine($"[WindowHandler] MapContent - setting SkiaView content: {skiaContent.GetType().Name}"); handler.PlatformView.Content = skiaContent; } + else + { + Console.Error.WriteLine($"[WindowHandler] MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}"); + } } public static void MapX(WindowHandler handler, IWindow window) diff --git a/Hosting/LinuxViewRenderer.cs b/Hosting/LinuxViewRenderer.cs index cb0a4c2..f230186 100644 --- a/Hosting/LinuxViewRenderer.cs +++ b/Hosting/LinuxViewRenderer.cs @@ -164,18 +164,11 @@ public class LinuxViewRenderer page.Handler?.DisconnectHandler(); var handler = page.ToHandler(_mauiContext); + // The handler's property mappers (e.g., ContentPageHandler.MapContent) + // already set up the content and child handlers - no need to re-render here. + // Re-rendering would disconnect the existing handler hierarchy. if (handler.PlatformView is SkiaView skiaPage) { - // For ContentPage, render the content - if (page is ContentPage contentPage && contentPage.Content != null) - { - var contentView = RenderView(contentPage.Content); - if (skiaPage is SkiaPage sp && contentView != null) - { - sp.Content = contentView; - } - } - return skiaPage; } diff --git a/LinuxApplication.cs b/LinuxApplication.cs index 1cb5a04..97de89b 100644 --- a/LinuxApplication.cs +++ b/LinuxApplication.cs @@ -110,6 +110,7 @@ public class LinuxApplication : IDisposable /// /// Requests a redraw of the application. + /// Thread-safe - will marshal to GTK thread if needed. /// public static void RequestRedraw() { @@ -117,6 +118,27 @@ public class LinuxApplication : IDisposable if (_isRedrawing) return; + // Check if we're on the GTK thread + int currentThread = Environment.CurrentManagedThreadId; + if (_gtkThreadId != 0 && currentThread != _gtkThreadId) + { + // We're on a background thread - use IdleAdd to marshal to GTK thread + GLibNative.IdleAdd(() => + { + RequestRedrawInternal(); + return false; // Don't repeat + }); + return; + } + + RequestRedrawInternal(); + } + + private static void RequestRedrawInternal() + { + if (_isRedrawing) + return; + _isRedrawing = true; try { @@ -197,7 +219,18 @@ public class LinuxApplication : IDisposable Current = this; // Set up dialog service invalidation callback - LinuxDialogService.SetInvalidateCallback(() => _renderingEngine?.InvalidateAll()); + // This callback will work for both GTK and X11 modes + LinuxDialogService.SetInvalidateCallback(() => + { + if (_useGtk) + { + _gtkWindow?.RequestRedraw(); + } + else + { + _renderingEngine?.InvalidateAll(); + } + }); } /// @@ -265,6 +298,20 @@ public class LinuxApplication : IDisposable currentProperty.SetValue(null, mauiApplication); } + // Set initial theme based on system theme + var systemTheme = SystemThemeService.Instance.CurrentTheme; + Console.WriteLine($"[LinuxApplication] System theme detected at startup: {systemTheme}"); + if (systemTheme == SystemTheme.Dark) + { + mauiApplication.UserAppTheme = AppTheme.Dark; + Console.WriteLine("[LinuxApplication] Set initial UserAppTheme to Dark based on system theme"); + } + else + { + mauiApplication.UserAppTheme = AppTheme.Light; + Console.WriteLine("[LinuxApplication] Set initial UserAppTheme to Light based on system theme"); + } + // Handle user-initiated theme changes ((BindableObject)mauiApplication).PropertyChanged += (s, e) => { @@ -272,7 +319,19 @@ public class LinuxApplication : IDisposable { Console.WriteLine($"[LinuxApplication] User theme changed to: {mauiApplication.UserAppTheme}"); LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme(); - linuxApp._renderingEngine?.InvalidateAll(); + + // Force re-render the entire page to pick up theme changes + linuxApp.RefreshPageForThemeChange(); + + // Invalidate to redraw - use correct method based on mode + if (linuxApp._useGtk) + { + linuxApp._gtkWindow?.RequestRedraw(); + } + else + { + linuxApp._renderingEngine?.InvalidateAll(); + } } }; @@ -280,15 +339,73 @@ public class LinuxApplication : IDisposable SystemThemeService.Instance.ThemeChanged += (s, e) => { Console.WriteLine($"[LinuxApplication] System theme changed to: {e.NewTheme}"); - // Notify MAUI framework that system theme changed - // This will cause AppThemeBinding to re-evaluate - LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme(); - linuxApp._renderingEngine?.InvalidateAll(); + + // Update MAUI's UserAppTheme to match system theme + // This will trigger the PropertyChanged handler which does the refresh + var newAppTheme = e.NewTheme == SystemTheme.Dark ? AppTheme.Dark : AppTheme.Light; + if (mauiApplication.UserAppTheme != newAppTheme) + { + Console.WriteLine($"[LinuxApplication] Setting UserAppTheme to {newAppTheme} to match system"); + mauiApplication.UserAppTheme = newAppTheme; + } + else + { + // If UserAppTheme didn't change (user manually set it), still refresh + LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme(); + linuxApp.RefreshPageForThemeChange(); + if (linuxApp._useGtk) + { + linuxApp._gtkWindow?.RequestRedraw(); + } + else + { + linuxApp._renderingEngine?.InvalidateAll(); + } + } }; - if (mauiApplication.MainPage != null) + // Get the main page - prefer CreateWindow() over deprecated MainPage + Page? mainPage = null; + + // Try CreateWindow() first (the modern MAUI pattern) + try { - var mainPage = mauiApplication.MainPage; + // CreateWindow is protected, use reflection + var createWindowMethod = typeof(Application).GetMethod("CreateWindow", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, + null, new[] { typeof(IActivationState) }, null); + + if (createWindowMethod != null) + { + var mauiWindow = createWindowMethod.Invoke(mauiApplication, new object?[] { null }) as Microsoft.Maui.Controls.Window; + if (mauiWindow != null) + { + Console.WriteLine($"[LinuxApplication] Got Window from CreateWindow: {mauiWindow.GetType().Name}"); + mainPage = mauiWindow.Page; + Console.WriteLine($"[LinuxApplication] Window.Page: {mainPage?.GetType().Name}"); + + // Add to windows list + var windowsField = typeof(Application).GetField("_windows", + BindingFlags.NonPublic | BindingFlags.Instance); + var windowsList = windowsField?.GetValue(mauiApplication) as List; + if (windowsList != null && !windowsList.Contains(mauiWindow)) + { + windowsList.Add(mauiWindow); + mauiWindow.Parent = mauiApplication; + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[LinuxApplication] CreateWindow failed: {ex.Message}"); + } + + // Fall back to deprecated MainPage if CreateWindow didn't work + if (mainPage == null && mauiApplication.MainPage != null) + { + Console.WriteLine($"[LinuxApplication] Falling back to MainPage: {mauiApplication.MainPage.GetType().Name}"); + mainPage = mauiApplication.MainPage; var windowsField = typeof(Application).GetField("_windows", BindingFlags.NonPublic | BindingFlags.Instance); @@ -304,7 +421,10 @@ public class LinuxApplication : IDisposable { windowsList[0].Page = mainPage; } + } + if (mainPage != null) + { var renderer = new LinuxViewRenderer(mauiContext); rootView = renderer.RenderPage(mainPage); @@ -547,6 +667,133 @@ public class LinuxApplication : IDisposable } } + /// + /// Forces all views to refresh their theme-dependent properties. + /// This is needed because AppThemeBinding may not automatically trigger + /// property mappers on all platforms. + /// + private void RefreshPageForThemeChange() + { + Console.WriteLine("[LinuxApplication] RefreshPageForThemeChange - forcing property updates"); + + // First, try to trigger MAUI's RequestedThemeChanged event using reflection + // This ensures AppThemeBinding bindings re-evaluate + TriggerMauiThemeChanged(); + + if (_rootView == null) return; + + // Traverse the visual tree and force theme-dependent properties to update + RefreshViewTheme(_rootView); + } + + /// + /// Triggers MAUI's internal RequestedThemeChanged event to force AppThemeBinding updates. + /// + private void TriggerMauiThemeChanged() + { + try + { + var app = Application.Current; + if (app == null) return; + + var currentTheme = app.UserAppTheme; + Console.WriteLine($"[LinuxApplication] Triggering theme changed event for: {currentTheme}"); + + // Try to find and invoke the RequestedThemeChanged event + var eventField = typeof(Application).GetField("RequestedThemeChanged", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + if (eventField != null) + { + var eventDelegate = eventField.GetValue(app) as MulticastDelegate; + if (eventDelegate != null) + { + var args = new AppThemeChangedEventArgs(currentTheme); + foreach (var handler in eventDelegate.GetInvocationList()) + { + handler.DynamicInvoke(app, args); + } + Console.WriteLine("[LinuxApplication] Successfully invoked RequestedThemeChanged handlers"); + } + } + else + { + // Try alternative approach - trigger OnPropertyChanged for RequestedTheme + var onPropertyChangedMethod = typeof(BindableObject).GetMethod("OnPropertyChanged", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, + null, new[] { typeof(string) }, null); + + if (onPropertyChangedMethod != null) + { + onPropertyChangedMethod.Invoke(app, new object[] { "RequestedTheme" }); + Console.WriteLine("[LinuxApplication] Triggered OnPropertyChanged for RequestedTheme"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[LinuxApplication] Error triggering theme changed: {ex.Message}"); + } + } + + private void RefreshViewTheme(SkiaView view) + { + // Get the associated MAUI view and handler + var mauiView = view.MauiView; + var handler = mauiView?.Handler; + + if (handler != null && mauiView != null) + { + // Force key properties to be re-mapped + // This ensures theme-dependent bindings are re-evaluated + try + { + // Background/BackgroundColor + handler.UpdateValue(nameof(IView.Background)); + + // For ImageButton, force Source to be re-mapped + if (mauiView is Microsoft.Maui.Controls.ImageButton) + { + handler.UpdateValue(nameof(IImageSourcePart.Source)); + } + + // For Image, force Source to be re-mapped + if (mauiView is Microsoft.Maui.Controls.Image) + { + handler.UpdateValue(nameof(IImageSourcePart.Source)); + } + + // For views with text colors + if (mauiView is ITextStyle) + { + handler.UpdateValue(nameof(ITextStyle.TextColor)); + } + + // For Entry/Editor placeholder colors + if (mauiView is IPlaceholder) + { + handler.UpdateValue(nameof(IPlaceholder.PlaceholderColor)); + } + + // For Border stroke + if (mauiView is IBorderStroke) + { + handler.UpdateValue(nameof(IBorderStroke.Stroke)); + } + } + catch (Exception ex) + { + Console.WriteLine($"[LinuxApplication] Error refreshing theme for {mauiView.GetType().Name}: {ex.Message}"); + } + } + + // Recursively process children + foreach (var child in view.Children) + { + RefreshViewTheme(child); + } + } + private void UpdateAnimations() { // Update cursor blink for entry controls @@ -802,6 +1049,16 @@ public class LinuxApplication : IDisposable string buttonName = e.Button == 1 ? "Left" : e.Button == 2 ? "Middle" : e.Button == 3 ? "Right" : $"Unknown({e.Button})"; Console.WriteLine($"[LinuxApplication.GTK] PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})"); + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right; + var args = new PointerEventArgs((float)e.X, (float)e.Y, button); + LinuxDialogService.TopDialog?.OnPointerPressed(args); + _gtkWindow?.RequestRedraw(); + return; + } + if (LinuxDialogService.HasContextMenu) { var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right; @@ -842,6 +1099,17 @@ public class LinuxApplication : IDisposable private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e) { Console.WriteLine("[DIAG] >>> OnGtkPointerReleased ENTER"); + + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right; + var args = new PointerEventArgs((float)e.X, (float)e.Y, button); + LinuxDialogService.TopDialog?.OnPointerReleased(args); + _gtkWindow?.RequestRedraw(); + return; + } + if (_rootView == null) return; if (_capturedView != null) @@ -870,6 +1138,15 @@ public class LinuxApplication : IDisposable private void OnGtkPointerMoved(object? sender, (double X, double Y) e) { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + var args = new PointerEventArgs((float)e.X, (float)e.Y); + LinuxDialogService.TopDialog?.OnPointerMoved(args); + _gtkWindow?.RequestRedraw(); + return; + } + if (LinuxDialogService.HasContextMenu) { var args = new PointerEventArgs((float)e.X, (float)e.Y); @@ -907,11 +1184,20 @@ public class LinuxApplication : IDisposable private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e) { + var key = ConvertGdkKey(e.KeyVal); + var modifiers = ConvertGdkModifiers(e.State); + var args = new KeyEventArgs(key, modifiers); + + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnKeyDown(args); + _gtkWindow?.RequestRedraw(); + return; + } + if (_focusedView != null) { - var key = ConvertGdkKey(e.KeyVal); - var modifiers = ConvertGdkModifiers(e.State); - var args = new KeyEventArgs(key, modifiers); _focusedView.OnKeyDown(args); _gtkWindow?.RequestRedraw(); } @@ -919,11 +1205,20 @@ public class LinuxApplication : IDisposable private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e) { + var key = ConvertGdkKey(e.KeyVal); + var modifiers = ConvertGdkModifiers(e.State); + var args = new KeyEventArgs(key, modifiers); + + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnKeyUp(args); + _gtkWindow?.RequestRedraw(); + return; + } + if (_focusedView != null) { - var key = ConvertGdkKey(e.KeyVal); - var modifiers = ConvertGdkModifiers(e.State); - var args = new KeyEventArgs(key, modifiers); _focusedView.OnKeyUp(args); _gtkWindow?.RequestRedraw(); } diff --git a/Native/GtkNative.cs b/Native/GtkNative.cs index ec61316..04067fe 100644 --- a/Native/GtkNative.cs +++ b/Native/GtkNative.cs @@ -189,4 +189,97 @@ internal static class GtkNative [DllImport("libgdk-3.so.0")] public static extern void gdk_event_free(IntPtr eventPtr); + + // Message Dialog support + public const int GTK_DIALOG_MODAL = 1; + public const int GTK_DIALOG_DESTROY_WITH_PARENT = 2; + + public const int GTK_MESSAGE_INFO = 0; + public const int GTK_MESSAGE_WARNING = 1; + public const int GTK_MESSAGE_QUESTION = 2; + public const int GTK_MESSAGE_ERROR = 3; + public const int GTK_MESSAGE_OTHER = 4; + + public const int GTK_BUTTONS_NONE = 0; + public const int GTK_BUTTONS_OK = 1; + public const int GTK_BUTTONS_CLOSE = 2; + public const int GTK_BUTTONS_CANCEL = 3; + public const int GTK_BUTTONS_YES_NO = 4; + public const int GTK_BUTTONS_OK_CANCEL = 5; + + public const int GTK_RESPONSE_NONE = -1; + public const int GTK_RESPONSE_REJECT = -2; + public const int GTK_RESPONSE_ACCEPT = -3; + public const int GTK_RESPONSE_DELETE_EVENT = -4; + public const int GTK_RESPONSE_OK = -5; + public const int GTK_RESPONSE_CANCEL = -6; + public const int GTK_RESPONSE_CLOSE = -7; + public const int GTK_RESPONSE_YES = -8; + public const int GTK_RESPONSE_NO = -9; + + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_message_dialog_new( + IntPtr parent, + int flags, + int type, + int buttons, + string message, + IntPtr args); + + [DllImport("libgtk-3.so.0")] + public static extern int gtk_dialog_run(IntPtr dialog); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_window_set_modal(IntPtr window, bool modal); + + // Dialog with custom content (for prompt dialogs) + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_dialog_new_with_buttons( + string title, + IntPtr parent, + int flags, + string firstButtonText, + int firstButtonResponse, + string secondButtonText, + int secondButtonResponse, + IntPtr terminator); + + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_dialog_get_content_area(IntPtr dialog); + + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_box_new(int orientation, int spacing); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_box_pack_start(IntPtr box, IntPtr child, bool expand, bool fill, uint padding); + + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_label_new(string text); + + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_entry_new(); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_entry_set_text(IntPtr entry, string text); + + [DllImport("libgtk-3.so.0")] + public static extern IntPtr gtk_entry_get_text(IntPtr entry); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_widget_set_margin_start(IntPtr widget, int margin); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_widget_set_margin_end(IntPtr widget, int margin); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_widget_set_margin_top(IntPtr widget, int margin); + + [DllImport("libgtk-3.so.0")] + public static extern void gtk_widget_set_margin_bottom(IntPtr widget, int margin); + + public const int GTK_ORIENTATION_HORIZONTAL = 0; + public const int GTK_ORIENTATION_VERTICAL = 1; } diff --git a/Native/WebKitNative.cs b/Native/WebKitNative.cs index 11e7b80..4d49242 100644 --- a/Native/WebKitNative.cs +++ b/Native/WebKitNative.cs @@ -20,11 +20,31 @@ internal static class WebKitNative private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView); private delegate void WebKitSettingsSetHardwareAccelerationPolicyDelegate(IntPtr settings, int policy); private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled); + private delegate void WebKitWebViewSetBackgroundColorDelegate(IntPtr webView, ref GdkRGBA color); + + [StructLayout(LayoutKind.Sequential)] + public struct GdkRGBA + { + public double Red; + public double Green; + public double Blue; + public double Alpha; + } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData); - private delegate ulong GSignalConnectDataDelegate(IntPtr instance, string signalName, LoadChangedCallback callback, IntPtr userData, IntPtr destroyNotify, int connectFlags); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate bool ScriptDialogCallback(IntPtr webView, IntPtr dialog, IntPtr userData); + + private delegate ulong GSignalConnectDataDelegate(IntPtr instance, string signalName, Delegate callback, IntPtr userData, IntPtr destroyNotify, int connectFlags); + + // WebKitScriptDialog functions + private delegate int WebKitScriptDialogGetDialogTypeDelegate(IntPtr dialog); + private delegate IntPtr WebKitScriptDialogGetMessageDelegate(IntPtr dialog); + private delegate void WebKitScriptDialogConfirmSetConfirmedDelegate(IntPtr dialog, bool confirmed); + private delegate IntPtr WebKitScriptDialogPromptGetDefaultTextDelegate(IntPtr dialog); + private delegate void WebKitScriptDialogPromptSetTextDelegate(IntPtr dialog, string text); public enum WebKitLoadEvent { @@ -34,6 +54,14 @@ internal static class WebKitNative Finished } + public enum WebKitScriptDialogType + { + Alert = 0, + Confirm = 1, + Prompt = 2, + BeforeUnloadConfirm = 3 + } + private static IntPtr _handle; private static bool _initialized; @@ -59,9 +87,21 @@ internal static class WebKitNative private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings; private static WebKitSettingsSetHardwareAccelerationPolicyDelegate? _webkitSetHardwareAccel; private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript; + private static WebKitWebViewSetBackgroundColorDelegate? _webkitSetBackgroundColor; private static GSignalConnectDataDelegate? _gSignalConnectData; + private static WebKitScriptDialogGetDialogTypeDelegate? _webkitScriptDialogGetDialogType; + private static WebKitScriptDialogGetMessageDelegate? _webkitScriptDialogGetMessage; + private static WebKitScriptDialogConfirmSetConfirmedDelegate? _webkitScriptDialogConfirmSetConfirmed; + private static WebKitScriptDialogPromptGetDefaultTextDelegate? _webkitScriptDialogPromptGetDefaultText; + private static WebKitScriptDialogPromptSetTextDelegate? _webkitScriptDialogPromptSetText; private static readonly Dictionary _loadChangedCallbacks = new Dictionary(); + private static readonly Dictionary _scriptDialogCallbacks = new Dictionary(); + + /// + /// Event raised when a JavaScript dialog (alert, confirm, prompt) is requested. + /// + public static event Action? ScriptDialogRequested; private const int RTLD_NOW = 2; private const int RTLD_GLOBAL = 256; @@ -116,6 +156,12 @@ internal static class WebKitNative _webkitGetSettings = LoadFunction("webkit_web_view_get_settings"); _webkitSetHardwareAccel = LoadFunction("webkit_settings_set_hardware_acceleration_policy"); _webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript"); + _webkitSetBackgroundColor = LoadFunction("webkit_web_view_set_background_color"); + _webkitScriptDialogGetDialogType = LoadFunction("webkit_script_dialog_get_dialog_type"); + _webkitScriptDialogGetMessage = LoadFunction("webkit_script_dialog_get_message"); + _webkitScriptDialogConfirmSetConfirmed = LoadFunction("webkit_script_dialog_confirm_set_confirmed"); + _webkitScriptDialogPromptGetDefaultText = LoadFunction("webkit_script_dialog_prompt_get_default_text"); + _webkitScriptDialogPromptSetText = LoadFunction("webkit_script_dialog_prompt_set_text"); _gobjectHandle = dlopen("libgobject-2.0.so.0", 258); if (_gobjectHandle != IntPtr.Zero) @@ -238,6 +284,15 @@ internal static class WebKitNative } } + public static void SetBackgroundColor(IntPtr webView, double r, double g, double b, double a = 1.0) + { + if (_webkitSetBackgroundColor != null && webView != IntPtr.Zero) + { + var color = new GdkRGBA { Red = r, Green = g, Blue = b, Alpha = a }; + _webkitSetBackgroundColor(webView, ref color); + } + } + public static ulong ConnectLoadChanged(IntPtr webView, LoadChangedCallback callback) { if (_gSignalConnectData == null || webView == IntPtr.Zero) @@ -253,4 +308,72 @@ internal static class WebKitNative { _loadChangedCallbacks.Remove(webView); } + + /// + /// Connects to the script-dialog signal to intercept JavaScript alert/confirm/prompt dialogs. + /// Returns true from the callback to prevent the default WebKitGTK dialog. + /// + public static ulong ConnectScriptDialog(IntPtr webView, ScriptDialogCallback callback) + { + if (_gSignalConnectData == null || webView == IntPtr.Zero) + { + Console.WriteLine("[WebKitNative] Cannot connect script-dialog: signal connect not available"); + return 0uL; + } + _scriptDialogCallbacks[webView] = callback; + return _gSignalConnectData(webView, "script-dialog", callback, IntPtr.Zero, IntPtr.Zero, 0); + } + + public static void DisconnectScriptDialog(IntPtr webView) + { + _scriptDialogCallbacks.Remove(webView); + } + + /// + /// Gets the type of a script dialog. + /// + public static WebKitScriptDialogType GetScriptDialogType(IntPtr dialog) + { + if (_webkitScriptDialogGetDialogType == null || dialog == IntPtr.Zero) + return WebKitScriptDialogType.Alert; + return (WebKitScriptDialogType)_webkitScriptDialogGetDialogType(dialog); + } + + /// + /// Gets the message from a script dialog. + /// + public static string? GetScriptDialogMessage(IntPtr dialog) + { + if (_webkitScriptDialogGetMessage == null || dialog == IntPtr.Zero) + return null; + IntPtr msgPtr = _webkitScriptDialogGetMessage(dialog); + return msgPtr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(msgPtr); + } + + /// + /// Sets the confirmed state for a confirm dialog. + /// + public static void SetScriptDialogConfirmed(IntPtr dialog, bool confirmed) + { + _webkitScriptDialogConfirmSetConfirmed?.Invoke(dialog, confirmed); + } + + /// + /// Gets the default text for a prompt dialog. + /// + public static string? GetScriptDialogPromptDefaultText(IntPtr dialog) + { + if (_webkitScriptDialogPromptGetDefaultText == null || dialog == IntPtr.Zero) + return null; + IntPtr textPtr = _webkitScriptDialogPromptGetDefaultText(dialog); + return textPtr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(textPtr); + } + + /// + /// Sets the text response for a prompt dialog. + /// + public static void SetScriptDialogPromptText(IntPtr dialog, string text) + { + _webkitScriptDialogPromptSetText?.Invoke(dialog, text); + } } diff --git a/Services/SystemThemeService.cs b/Services/SystemThemeService.cs index b332ae1..8913c13 100644 --- a/Services/SystemThemeService.cs +++ b/Services/SystemThemeService.cs @@ -71,6 +71,8 @@ public class SystemThemeService }; private FileSystemWatcher? _settingsWatcher; + private Timer? _pollTimer; + private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2); private SystemThemeService() { @@ -78,6 +80,7 @@ public class SystemThemeService DetectTheme(); UpdateColors(); SetupWatcher(); + SetupPolling(); } private void DetectDesktopEnvironment() @@ -373,6 +376,33 @@ public class SystemThemeService catch { } } + private void SetupPolling() + { + // For GNOME and other desktops that use dconf/gsettings, + // file watching doesn't work. Use periodic polling instead. + _pollTimer = new Timer(OnPollTimer, null, PollInterval, PollInterval); + } + + private void OnPollTimer(object? state) + { + try + { + var oldTheme = CurrentTheme; + DetectTheme(); + + if (oldTheme != CurrentTheme) + { + Console.WriteLine($"[SystemThemeService] Theme change detected via polling: {oldTheme} -> {CurrentTheme}"); + UpdateColors(); + ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme)); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SystemThemeService] Error in poll timer: {ex.Message}"); + } + } + private void OnSettingsChanged(object sender, FileSystemEventArgs e) { // Debounce and check relevant files diff --git a/Views/SkiaBorder.cs b/Views/SkiaBorder.cs index 75bc3b9..f72155d 100644 --- a/Views/SkiaBorder.cs +++ b/Views/SkiaBorder.cs @@ -407,7 +407,9 @@ public class SkiaBorder : SkiaLayoutView canvas.DrawPath(shapePath, borderPaint); } - // Draw children + // Clip to shape and draw children + canvas.Save(); + canvas.ClipPath(shapePath); foreach (var child in Children) { if (child.IsVisible) @@ -415,6 +417,7 @@ public class SkiaBorder : SkiaLayoutView child.Draw(canvas); } } + canvas.Restore(); } #endregion diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs index 402da67..50357b8 100644 --- a/Views/SkiaEntry.cs +++ b/Views/SkiaEntry.cs @@ -74,7 +74,7 @@ public class SkiaEntry : SkiaView, IInputContext nameof(EntryBackgroundColor), typeof(Color), typeof(SkiaEntry), - Colors.White, + Colors.Transparent, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// @@ -837,31 +837,52 @@ public class SkiaEntry : SkiaView, IInputContext Invalidate(); } + protected override void DrawBackground(SKCanvas canvas, SKRect bounds) + { + // Skip base background drawing if Entry is transparent + // (transparent Entry is likely inside a Border that handles appearance) + var bgColor = ToSKColor(EntryBackgroundColor); + var baseBgColor = GetEffectiveBackgroundColor(); + if (bgColor.Alpha < 10 && baseBgColor.Alpha < 10) + return; + + // Otherwise let base class draw + base.DrawBackground(canvas, bounds); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - // Draw background - using var bgPaint = new SKPaint + var bgColor = ToSKColor(EntryBackgroundColor); + var isTransparent = bgColor.Alpha < 10; // Consider nearly transparent as transparent + + // Only draw background and border if not transparent + // (transparent means the Entry is likely inside a Border that handles appearance) + if (!isTransparent) { - Color = ToSKColor(EntryBackgroundColor), - IsAntialias = true, - Style = SKPaintStyle.Fill - }; + // Draw background + using var bgPaint = new SKPaint + { + Color = bgColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; - var rect = new SKRoundRect(bounds, (float)CornerRadius); - canvas.DrawRoundRect(rect, bgPaint); + var rect = new SKRoundRect(bounds, (float)CornerRadius); + canvas.DrawRoundRect(rect, bgPaint); - // Draw border - var borderColor = IsFocused ? ToSKColor(FocusedBorderColor) : ToSKColor(BorderColor); - var borderWidth = IsFocused ? (float)BorderWidth + 1 : (float)BorderWidth; + // Draw border + var borderColor = IsFocused ? ToSKColor(FocusedBorderColor) : ToSKColor(BorderColor); + var borderWidth = IsFocused ? (float)BorderWidth + 1 : (float)BorderWidth; - using var borderPaint = new SKPaint - { - Color = borderColor, - IsAntialias = true, - Style = SKPaintStyle.Stroke, - StrokeWidth = borderWidth - }; - canvas.DrawRoundRect(rect, borderPaint); + using var borderPaint = new SKPaint + { + Color = borderColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = borderWidth + }; + canvas.DrawRoundRect(rect, borderPaint); + } // Calculate content bounds var contentBounds = new SKRect( diff --git a/Views/SkiaImage.cs b/Views/SkiaImage.cs index bd0b11e..08ed712 100644 --- a/Views/SkiaImage.cs +++ b/Views/SkiaImage.cs @@ -693,6 +693,8 @@ public class SkiaImage : SkiaView using var canvas = new SKCanvas(newBitmap); canvas.Clear(SKColors.Transparent); canvas.Scale(scale); + // Translate to handle negative viewBox coordinates (e.g., Material icons use 0 -960 960 960) + canvas.Translate(-cullRect.Left, -cullRect.Top); canvas.DrawPicture(svg.Picture, null); } }, cts.Token); diff --git a/Views/SkiaImageButton.cs b/Views/SkiaImageButton.cs index 22bea11..ccbf614 100644 --- a/Views/SkiaImageButton.cs +++ b/Views/SkiaImageButton.cs @@ -239,6 +239,20 @@ public class SkiaImageButton : SkiaView #region Rendering + protected override void DrawBackground(SKCanvas canvas, SKRect bounds) + { + // Skip base background drawing if button is transparent + var baseBgColor = ImageBackgroundColor != null + ? ToSKColor(ImageBackgroundColor) + : GetEffectiveBackgroundColor(); + + if (baseBgColor.Alpha < 10) + return; + + // Otherwise let base class draw + base.DrawBackground(canvas, bounds); + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var padding = Padding; @@ -249,26 +263,29 @@ public class SkiaImageButton : SkiaView bounds.Bottom - (float)padding.Bottom); // Determine background color + var baseBgColor = ImageBackgroundColor != null + ? ToSKColor(ImageBackgroundColor) + : GetEffectiveBackgroundColor(); + var isTransparentButton = baseBgColor.Alpha < 10; + SKColor bgColor; - if (IsPressed) + if (IsPressed && !isTransparentButton) { + // Only show pressed state for non-transparent buttons bgColor = ToSKColor(PressedBackgroundColor); } - else if (IsHovered) + else if (IsHovered && !isTransparentButton) { + // Only show hovered state for non-transparent buttons bgColor = ToSKColor(HoveredBackgroundColor); } - else if (ImageBackgroundColor != null) - { - bgColor = ToSKColor(ImageBackgroundColor); - } else { - bgColor = GetEffectiveBackgroundColor(); + bgColor = baseBgColor; } - // Draw background - if (bgColor != SKColors.Transparent || !IsOpaque) + // Draw background (skip if fully transparent) + if (bgColor.Alpha > 0) { using var bgPaint = new SKPaint { @@ -477,9 +494,11 @@ public class SkiaImageButton : SkiaView using var canvas = new SKCanvas(bitmap); canvas.Clear(SKColors.Transparent); canvas.Scale(scale); + // Translate to handle negative viewBox coordinates (e.g., Material icons use 0 -960 960 960) + canvas.Translate(-cullRect.Left, -cullRect.Top); canvas.DrawPicture(svg.Picture); Bitmap = bitmap; - Console.WriteLine($"[SkiaImageButton] Loaded SVG: {foundPath} ({width}x{height})"); + Console.WriteLine($"[SkiaImageButton] Loaded SVG: {foundPath} ({width}x{height}), cullRect={cullRect}"); } } else @@ -786,9 +805,12 @@ public class SkiaImageButton : SkiaView } // Fill (3) and Start (0) both use y = bounds.Top - return new Rect(x, y, finalWidth, finalHeight); + var result1 = new Rect(x, y, finalWidth, finalHeight); + Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (aligned): Y={result1.Y}, Height={result1.Height}"); + return result1; } + Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (unchanged): Y={bounds.Y}, Height={bounds.Height}"); return bounds; } diff --git a/Views/SkiaLayoutView.cs b/Views/SkiaLayoutView.cs index 2d48ed3..78b4e28 100644 --- a/Views/SkiaLayoutView.cs +++ b/Views/SkiaLayoutView.cs @@ -907,12 +907,46 @@ public class SkiaGrid : SkiaLayoutView // Apply child's margin var margin = child.Margin; - var marginedBounds = new Rect( - x + (float)margin.Left, - y + (float)margin.Top, - width - (float)margin.Left - (float)margin.Right, - height - (float)margin.Top - (float)margin.Bottom); - child.Arrange(marginedBounds); + var cellX = x + (float)margin.Left; + var cellY = y + (float)margin.Top; + var cellWidth = width - (float)margin.Left - (float)margin.Right; + var cellHeight = height - (float)margin.Top - (float)margin.Bottom; + + // Get child's desired size + var childDesiredSize = child.Measure(new Size(cellWidth, cellHeight)); + var childWidth = (float)childDesiredSize.Width; + var childHeight = (float)childDesiredSize.Height; + + var vAlign = (int)child.VerticalOptions.Alignment; + + // Apply HorizontalOptions + // LayoutAlignment: Start=0, Center=1, End=2, Fill=3 + float finalX = cellX; + float finalWidth = cellWidth; + var hAlign = (int)child.HorizontalOptions.Alignment; + if (hAlign != 3 && childWidth < cellWidth && childWidth > 0) // 3 = Fill + { + finalWidth = childWidth; + if (hAlign == 1) // Center + finalX = cellX + (cellWidth - childWidth) / 2; + else if (hAlign == 2) // End + finalX = cellX + cellWidth - childWidth; + } + + // Apply VerticalOptions + float finalY = cellY; + float finalHeight = cellHeight; + // vAlign already calculated above for debug logging + if (vAlign != 3 && childHeight < cellHeight && childHeight > 0) // 3 = Fill + { + finalHeight = childHeight; + if (vAlign == 1) // Center + finalY = cellY + (cellHeight - childHeight) / 2; + else if (vAlign == 2) // End + finalY = cellY + cellHeight - childHeight; + } + + child.Arrange(new Rect(finalX, finalY, finalWidth, finalHeight)); } return bounds; } diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs index faa36c6..ee1079c 100644 --- a/Views/SkiaView.cs +++ b/Views/SkiaView.cs @@ -1534,8 +1534,8 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible canvas.DrawRect(bounds, paint); } } - // Fall back to BackgroundColor - else if (_backgroundColorSK != SKColors.Transparent) + // Fall back to BackgroundColor (skip if transparent) + else if (_backgroundColorSK.Alpha > 0) { using var paint = new SKPaint { Color = _backgroundColorSK }; canvas.DrawRect(bounds, paint); diff --git a/build/OpenMaui.Controls.Linux.targets b/build/OpenMaui.Controls.Linux.targets new file mode 100644 index 0000000..02c70b8 --- /dev/null +++ b/build/OpenMaui.Controls.Linux.targets @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + +