diff --git a/Handlers/BorderHandler.cs b/Handlers/BorderHandler.cs index cc64b62..468925d 100644 --- a/Handlers/BorderHandler.cs +++ b/Handlers/BorderHandler.cs @@ -30,6 +30,8 @@ public partial class BorderHandler : ViewHandler [nameof(IView.Background)] = MapBackground, ["BackgroundColor"] = MapBackgroundColor, [nameof(IPadding.Padding)] = MapPadding, + ["WidthRequest"] = MapWidthRequest, + ["HeightRequest"] = MapHeightRequest, }; public static CommandMapper CommandMapper = @@ -59,6 +61,27 @@ public partial class BorderHandler : ViewHandler platformView.MauiView = view; } platformView.Tapped += OnPlatformViewTapped; + + // Explicitly map properties since they may be set before handler creation + if (VirtualView is VisualElement ve) + { + if (ve.BackgroundColor != null) + { + platformView.BackgroundColor = ve.BackgroundColor; + } + else if (ve.Background is SolidColorBrush brush && brush.Color != null) + { + platformView.BackgroundColor = brush.Color; + } + if (ve.WidthRequest >= 0) + { + platformView.WidthRequest = ve.WidthRequest; + } + if (ve.HeightRequest >= 0) + { + platformView.HeightRequest = ve.HeightRequest; + } + } } protected override void DisconnectHandler(SkiaBorder platformView) @@ -130,10 +153,15 @@ public partial class BorderHandler : ViewHandler { if (handler.PlatformView is null) return; - if (border is VisualElement ve && ve.BackgroundColor != null) + if (border is VisualElement ve) { - handler.PlatformView.BackgroundColor = ve.BackgroundColor; - handler.PlatformView.Invalidate(); + var bgColor = ve.BackgroundColor; + Console.WriteLine($"[BorderHandler] MapBackgroundColor: {bgColor}"); + if (bgColor != null) + { + handler.PlatformView.BackgroundColor = bgColor; + handler.PlatformView.Invalidate(); + } } } @@ -239,4 +267,26 @@ public partial class BorderHandler : ViewHandler } handler.PlatformView.Invalidate(); } + + public static void MapWidthRequest(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is VisualElement ve && ve.WidthRequest >= 0) + { + handler.PlatformView.WidthRequest = ve.WidthRequest; + handler.PlatformView.InvalidateMeasure(); + } + } + + public static void MapHeightRequest(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is VisualElement ve && ve.HeightRequest >= 0) + { + handler.PlatformView.HeightRequest = ve.HeightRequest; + handler.PlatformView.InvalidateMeasure(); + } + } } diff --git a/LinuxApplication.cs b/LinuxApplication.cs index 9f2f99f..23d7672 100644 --- a/LinuxApplication.cs +++ b/LinuxApplication.cs @@ -688,53 +688,17 @@ public class LinuxApplication : IDisposable } /// - /// Triggers MAUI's internal RequestedThemeChanged event to force AppThemeBinding updates. + /// Called after theme change to refresh views. + /// Note: MAUI's Application.UserAppTheme setter automatically triggers RequestedThemeChanged + /// via WeakEventManager, which AppThemeBinding subscribes to. This method handles + /// any additional platform-specific refresh needed. /// private void TriggerMauiThemeChanged() { - try - { - var app = Application.Current; - if (app == null) return; + 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}"); - } + Console.WriteLine($"[LinuxApplication] Theme is now: {app.UserAppTheme}, RequestedTheme: {app.RequestedTheme}"); } private void RefreshViewTheme(SkiaView view) @@ -749,8 +713,9 @@ public class LinuxApplication : IDisposable // This ensures theme-dependent bindings are re-evaluated try { - // Background/BackgroundColor + // Background/BackgroundColor - both need updating for AppThemeBinding handler.UpdateValue(nameof(IView.Background)); + handler.UpdateValue("BackgroundColor"); // For ImageButton, force Source to be re-mapped if (mauiView is Microsoft.Maui.Controls.ImageButton) @@ -788,8 +753,29 @@ public class LinuxApplication : IDisposable } } + // Special handling for ItemsViews (CollectionView, ListView) + // Their item views are cached separately and need to be refreshed + if (view is SkiaItemsView itemsView) + { + itemsView.RefreshTheme(); + } + + // Special handling for NavigationPage - it stores content in _currentPage + if (view is SkiaNavigationPage navPage && navPage.CurrentPage != null) + { + RefreshViewTheme(navPage.CurrentPage); + } + + // Special handling for ContentPage - it stores content in Content property + if (view is SkiaPage page && page.Content != null) + { + RefreshViewTheme(page.Content); + } + // Recursively process children - foreach (var child in view.Children) + // Note: SkiaLayoutView hides SkiaView.Children with 'new', so we need to cast + IReadOnlyList children = view is SkiaLayoutView layout ? layout.Children : view.Children; + foreach (var child in children) { RefreshViewTheme(child); } diff --git a/Views/SkiaBorder.cs b/Views/SkiaBorder.cs index f72155d..eb491fd 100644 --- a/Views/SkiaBorder.cs +++ b/Views/SkiaBorder.cs @@ -341,6 +341,14 @@ public class SkiaBorder : SkiaLayoutView return path; } + /// + /// Override to skip rectangular background - OnDraw handles it with the correct shape. + /// + protected override void DrawBackground(SKCanvas canvas, SKRect bounds) + { + // Don't draw rectangular background - OnDraw draws background with shape path + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { float strokeThickness = (float)StrokeThickness; diff --git a/Views/SkiaEditor.cs b/Views/SkiaEditor.cs index ad52832..ef33e61 100644 --- a/Views/SkiaEditor.cs +++ b/Views/SkiaEditor.cs @@ -77,7 +77,7 @@ public class SkiaEditor : SkiaView, IInputContext nameof(BorderColor), typeof(Color), typeof(SkiaEditor), - Color.FromRgb(0xBD, 0xBD, 0xBD), + Colors.Transparent, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); @@ -301,7 +301,7 @@ public class SkiaEditor : SkiaView, IInputContext nameof(EditorBackgroundColor), typeof(Color), typeof(SkiaEditor), - Colors.White, + Colors.Transparent, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); @@ -891,15 +891,18 @@ public class SkiaEditor : SkiaView, IInputContext }; canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint); - // Draw border - using var borderPaint = new SKPaint + // Draw border only if BorderColor is not transparent + if (BorderColor != null && BorderColor != Colors.Transparent && BorderColor.Alpha > 0) { - Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor), - Style = SKPaintStyle.Stroke, - StrokeWidth = IsFocused ? 2 : 1, - IsAntialias = true - }; - canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint); + using var borderPaint = new SKPaint + { + Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor), + Style = SKPaintStyle.Stroke, + StrokeWidth = IsFocused ? 2 : 1, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint); + } // Setup text rendering using var font = new SKFont(SKTypeface.Default, fontSize); @@ -1085,6 +1088,13 @@ public class SkiaEditor : SkiaView, IInputContext { if (!IsEnabled) return; + // Handle right-click context menu + if (e.Button == PointerButton.Right) + { + ShowContextMenu(e.X, e.Y); + return; + } + IsFocused = true; // Use screen coordinates for proper hit detection @@ -1491,6 +1501,39 @@ public class SkiaEditor : SkiaView, IInputContext _selectionLength = 0; } + private void ShowContextMenu(float x, float y) + { + Console.WriteLine($"[SkiaEditor] ShowContextMenu at ({x}, {y})"); + bool hasSelection = _selectionLength != 0; + bool hasText = !string.IsNullOrEmpty(Text); + bool hasClipboard = !string.IsNullOrEmpty(SystemClipboard.GetText()); + bool isEditable = !IsReadOnly; + + GtkContextMenuService.ShowContextMenu(new List + { + new GtkMenuItem("Cut", () => + { + CutToClipboard(); + Invalidate(); + }, hasSelection && isEditable), + new GtkMenuItem("Copy", () => + { + CopyToClipboard(); + }, hasSelection), + new GtkMenuItem("Paste", () => + { + PasteFromClipboard(); + Invalidate(); + }, hasClipboard && isEditable), + GtkMenuItem.Separator, + new GtkMenuItem("Select All", () => + { + SelectAll(); + Invalidate(); + }, hasText) + }); + } + #endregion /// diff --git a/Views/SkiaItemsView.cs b/Views/SkiaItemsView.cs index 85cc64c..c17d0b3 100644 --- a/Views/SkiaItemsView.cs +++ b/Views/SkiaItemsView.cs @@ -129,6 +129,18 @@ public class SkiaItemsView : SkiaView _scrollOffset = 0; } + /// + /// Called when theme changes to refresh all cached item views. + /// Clears the item view cache so items are recreated with new theme colors. + /// + public virtual void RefreshTheme() + { + // Clear cached views to force recreation with new AppThemeBinding values + _itemViewCache.Clear(); + _itemHeights.Clear(); + Invalidate(); + } + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { RefreshItems(); diff --git a/Views/SkiaPage.cs b/Views/SkiaPage.cs index d712e24..22b8f14 100644 --- a/Views/SkiaPage.cs +++ b/Views/SkiaPage.cs @@ -433,24 +433,50 @@ public class SkiaContentPage : SkiaPage }; float rightEdge = navBarBounds.Right - 16; + const float iconSize = 24f; + const float itemPadding = 12f; foreach (var item in primaryItems.AsEnumerable().Reverse()) { - var textBounds = new SKRect(); - textPaint.MeasureText(item.Text, ref textBounds); + float itemWidth; + float itemLeft; - var itemWidth = textBounds.Width + 24; // Padding - var itemLeft = rightEdge - itemWidth; + if (item.Icon != null) + { + // Icon-based toolbar item + itemWidth = iconSize + itemPadding * 2; + itemLeft = rightEdge - itemWidth; + + // Store hit area for click handling + item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom); + + // Draw icon centered in the hit area + var iconX = itemLeft + itemPadding; + var iconY = navBarBounds.MidY - iconSize / 2; + var destRect = new SKRect(iconX, iconY, iconX + iconSize, iconY + iconSize); + canvas.DrawBitmap(item.Icon, destRect); + + Console.WriteLine($"[SkiaContentPage] Drew toolbar icon '{item.Text}' at ({iconX}, {iconY})"); + } + else + { + // Text-based toolbar item (fallback) + var textBounds = new SKRect(); + textPaint.MeasureText(item.Text, ref textBounds); + + itemWidth = textBounds.Width + 24; + itemLeft = rightEdge - itemWidth; + + // Store hit area for click handling + item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom); + + // Draw text + var x = itemLeft + 12; + var y = navBarBounds.MidY - textBounds.MidY; + canvas.DrawText(item.Text, x, y, textPaint); + } - // Store hit area for click handling - item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom); Console.WriteLine($"[SkiaContentPage] Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}"); - - // Draw text - var x = itemLeft + 12; - var y = navBarBounds.MidY - textBounds.MidY; - canvas.DrawText(item.Text, x, y, textPaint); - rightEdge = itemLeft - 8; // Gap between items } }